Skip to main content

pq_msg/messaging/
mod.rs

1use hkdf::Hkdf;
2use rand_core::{OsRng, RngCore};
3use sha2::{Digest, Sha256};
4use zeroize::Zeroizing;
5
6use crate::{
7    errors::CryptoError,
8    exchange::{
9        encryptor::Encryptor,
10        pair::{self, KEMPair},
11        prekey::SignedPrekey,
12    },
13    signatures::keypair::{self, PublicKeyBytes, SignerPair, VerifierPair, ViewOperations},
14};
15
16/// Protocol label hashed into every session transcript
17const PROTOCOL_LABEL: &[u8] = b"pq-msg/v2";
18/// Version byte of the `MessageSession::to_bytes` format
19const SESSION_FORMAT_VERSION: u8 = 2;
20/// Direction tags, signed with every message so it can't be reflected back to its sender
21const INITIATOR_TO_RESPONDER: u8 = 0;
22const RESPONDER_TO_INITIATOR: u8 = 1;
23/// Length of a serialized session
24const SESSION_BYTES: usize = 2
25    + keypair::PUBLIC_KEY_BYTES
26    + keypair::SECRET_KEY_BYTES
27    + keypair::PUBLIC_KEY_BYTES
28    + 32 * 3
29    + 16
30    + 8 * 2;
31
32/// MessageSession manages the cryptographic state for secure message exchange
33/// between two parties using post-quantum cryptographic algorithms.
34///
35/// Each session contains:
36/// - A digital signature keypair for signing our messages
37/// - A verifier for validating messages from the other party
38/// - One chain key and one counter per direction, derived from the ML-KEM shared
39///   secret, so both parties can send at the same time without nonce reuse
40/// - A transcript hash of the handshake that every message signature is bound to
41///
42/// Every message is encrypted with its own key, derived from the chain key, after which
43/// the chain key moves forward and the old one is wiped. Someone who steals the session
44/// (or a stored `to_bytes()` copy) can't decrypt the messages that came before.
45///
46/// Messages must be validated in the order they were crafted.
47pub struct MessageSession {
48    /// The digital signature keypair for this session
49    ds_pair: SignerPair,
50    /// The verifier for the other party's messages
51    target_verifier: VerifierPair,
52    /// Whether we initiated the session, which decides our sending direction
53    is_initiator: bool,
54    /// Hash of the handshake: session id, both identities, KEM public key and ciphertext
55    transcript: [u8; 32],
56    /// Chain key for the next message we send
57    send_chain: Zeroizing<[u8; 32]>,
58    /// Chain key for the next message we receive
59    recv_chain: Zeroizing<[u8; 32]>,
60    /// Session id, the first 16 bytes of every nonce
61    session_id: [u8; 16],
62    /// Counter of the last message we sent
63    send_counter: u64,
64    /// Counter of the last message we received
65    recv_counter: u64,
66}
67
68impl MessageSession {
69    /// Serializes the session to a byte array
70    ///
71    /// # Returns
72    /// The serialized session (wiped when dropped)
73    ///
74    /// # Security Note
75    /// The serialized data contains sensitive cryptographic material including private keys.
76    /// It should be stored securely and only deserialized in a trusted environment.
77    pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
78        let mut bytes = Zeroizing::new(Vec::with_capacity(SESSION_BYTES));
79        bytes.push(SESSION_FORMAT_VERSION);
80        bytes.push(self.is_initiator as u8);
81        bytes.extend_from_slice(&self.ds_pair.to_bytes_uniform());
82        bytes.extend_from_slice(self.target_verifier.pub_key_bytes());
83        bytes.extend_from_slice(&self.transcript);
84        bytes.extend_from_slice(&self.send_chain[..]);
85        bytes.extend_from_slice(&self.recv_chain[..]);
86        bytes.extend_from_slice(&self.session_id);
87        bytes.extend_from_slice(&self.send_counter.to_le_bytes());
88        bytes.extend_from_slice(&self.recv_counter.to_le_bytes());
89        bytes
90    }
91
92    /// Deserializes a session from a byte array
93    ///
94    /// # Arguments
95    /// * `bytes` - The serialized session bytes
96    ///
97    /// # Returns
98    /// - `Result<Self, CryptoError>`: The deserialized session or an error
99    ///
100    /// # Errors
101    /// Returns an error if the byte array is not the correct length, version or format
102    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
103        if bytes.len() != SESSION_BYTES {
104            return Err(CryptoError::IncongruentLength(SESSION_BYTES, bytes.len()));
105        }
106        if bytes[0] != SESSION_FORMAT_VERSION || bytes[1] > 1 {
107            return Err(CryptoError::InvalidSession);
108        }
109
110        let mut rest = &bytes[2..];
111        let mut take = |n: usize| {
112            let (head, tail) = rest.split_at(n);
113            rest = tail;
114            head
115        };
116
117        Ok(Self {
118            is_initiator: bytes[1] == 1,
119            ds_pair: SignerPair::from_bytes_uniform(take(
120                keypair::PUBLIC_KEY_BYTES + keypair::SECRET_KEY_BYTES,
121            ))?,
122            target_verifier: VerifierPair::new(take(keypair::PUBLIC_KEY_BYTES))?,
123            transcript: take(32).try_into()?,
124            send_chain: Zeroizing::new(take(32).try_into()?),
125            recv_chain: Zeroizing::new(take(32).try_into()?),
126            session_id: take(16).try_into()?,
127            send_counter: u64::from_le_bytes(take(8).try_into()?),
128            recv_counter: u64::from_le_bytes(take(8).try_into()?),
129        })
130    }
131
132    /// Creates a new session as the initiator
133    ///
134    /// Send the responder everything it needs to call [`MessageSession::new_responder`]:
135    /// the returned ciphertext, `target_prekey.id()`, the base nonce and your FN-DSA
136    /// public key.
137    ///
138    /// # Arguments
139    /// * `my_signer` - Your own signer pair
140    /// * `base_nonce` - Base nonce (0..16 session id, 16..24 counter), shared with the responder
141    /// * `target_prekey` - A signed prekey of the target
142    /// * `target_verifier` - FN-DSA public key of the target. Get it from a source you
143    ///   trust: it is what proves the prekey really belongs to the target.
144    ///
145    /// # Returns
146    /// - `Result<(Self, [u8; pair::CIPHERTEXT_BYTES]), CryptoError>`:
147    ///   The session and ciphertext for the responder, or `InvalidSignature` if the
148    ///   prekey was not signed by `target_verifier`
149    pub fn new_initiator(
150        my_signer: SignerPair,
151        base_nonce: [u8; 24],
152        target_prekey: &SignedPrekey,
153        target_verifier: &PublicKeyBytes,
154    ) -> Result<(Self, [u8; pair::CIPHERTEXT_BYTES]), CryptoError> {
155        let target_verifier = VerifierPair::new(target_verifier)?;
156        if !target_prekey.verify(target_verifier.pub_key_bytes()) {
157            return Err(CryptoError::InvalidSignature);
158        }
159
160        // We are the initiator, we encapsulate a shared secret for the responder
161        let (shared_secret, ciphertext) = KEMPair::encapsulate(target_prekey.kem_pub_key())?;
162
163        let session = Self::establish(
164            my_signer,
165            target_verifier,
166            true,
167            base_nonce,
168            target_prekey.kem_pub_key(),
169            &ciphertext,
170            &shared_secret,
171        );
172        Ok((session, ciphertext))
173    }
174
175    /// Creates a new session as the responder
176    ///
177    /// If `my_prekey` is a one-time prekey, delete it everywhere you store it once
178    /// this returns. That deletion is what gives the session forward secrecy.
179    ///
180    /// # Arguments
181    /// * `my_prekey` - The prekey the initiator used (look it up by the id they sent)
182    /// * `my_signer` - Your own signer pair
183    /// * `base_nonce` - Base nonce (0..16 session id, 16..24 counter), shared with the initiator
184    /// * `ciphertext` - KEM ciphertext sent by the initiator
185    /// * `sender_verifier` - FN-DSA public key of the initiator
186    ///
187    /// # Returns
188    /// - `Result<Self, CryptoError>`: The session or an error
189    pub fn new_responder(
190        my_prekey: &KEMPair,
191        my_signer: SignerPair,
192        base_nonce: [u8; 24],
193        ciphertext: &[u8; pair::CIPHERTEXT_BYTES],
194        sender_verifier: &PublicKeyBytes,
195    ) -> Result<Self, CryptoError> {
196        let target_verifier = VerifierPair::new(sender_verifier)?;
197        let shared_secret = my_prekey.decapsulate(ciphertext);
198
199        Ok(Self::establish(
200            my_signer,
201            target_verifier,
202            false,
203            base_nonce,
204            &my_prekey.pub_key_bytes(),
205            ciphertext,
206            &shared_secret,
207        ))
208    }
209
210    /// Derives the transcript and one chain key per direction, identically on both sides
211    fn establish(
212        ds_pair: SignerPair,
213        target_verifier: VerifierPair,
214        is_initiator: bool,
215        base_nonce: [u8; 24],
216        responder_kem_pubkey: &[u8; pair::PUBLIC_KEY_BYTES],
217        ciphertext: &[u8; pair::CIPHERTEXT_BYTES],
218        shared_secret: &[u8; pair::SHARED_SECRET_BYTES],
219    ) -> Self {
220        let (initiator, responder) = if is_initiator {
221            (ds_pair.pub_key_bytes(), target_verifier.pub_key_bytes())
222        } else {
223            (target_verifier.pub_key_bytes(), ds_pair.pub_key_bytes())
224        };
225
226        // All fields are fixed-length, so plain concatenation is unambiguous
227        let transcript: [u8; 32] = Sha256::new()
228            .chain_update(PROTOCOL_LABEL)
229            .chain_update(base_nonce)
230            .chain_update(initiator)
231            .chain_update(responder)
232            .chain_update(responder_kem_pubkey)
233            .chain_update(ciphertext)
234            .finalize()
235            .into();
236
237        let hkdf = Hkdf::<Sha256>::new(Some(&transcript), shared_secret);
238        let mut i2r = Zeroizing::new([0u8; 32]);
239        let mut r2i = Zeroizing::new([0u8; 32]);
240        hkdf.expand(b"pq-msg/v2 initiator->responder", &mut i2r[..])
241            .expect("32 bytes is a valid HKDF-SHA256 output length");
242        hkdf.expand(b"pq-msg/v2 responder->initiator", &mut r2i[..])
243            .expect("32 bytes is a valid HKDF-SHA256 output length");
244        let (send_chain, recv_chain) = if is_initiator { (i2r, r2i) } else { (r2i, i2r) };
245
246        let counter = u64::from_le_bytes(base_nonce[16..].try_into().unwrap());
247        Self {
248            ds_pair,
249            target_verifier,
250            is_initiator,
251            transcript,
252            send_chain,
253            recv_chain,
254            session_id: base_nonce[..16].try_into().unwrap(),
255            send_counter: counter,
256            recv_counter: counter,
257        }
258    }
259
260    /// Creates a signed and encrypted message for the other party
261    ///
262    /// # Arguments
263    /// * `message` - The plaintext message to encrypt
264    ///
265    /// # Returns
266    /// - `Result<Vec<u8>, CryptoError>`: The encrypted message or an error
267    ///
268    /// # Security Note
269    /// This method increments the send counter to ensure a unique nonce for each
270    /// message, and fails with `NonceExhausted` instead of ever reusing one.
271    pub fn craft_message(&mut self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
272        let counter = self
273            .send_counter
274            .checked_add(1)
275            .ok_or(CryptoError::NonceExhausted)?;
276        let direction = self.send_direction();
277
278        // Sign the message bound to this session, direction and position
279        let signed_data = self.signed_data(direction, counter, message);
280        let sig = self.ds_pair.sign(&signed_data)?;
281
282        let plaintext = Zeroizing::new([&sig[..], message].concat());
283        let (message_key, next_chain) = ratchet(&self.send_chain);
284        let ciphertext = Encryptor::new(&message_key)
285            .encrypt(&plaintext, &create_nonce(&self.session_id, counter))?;
286
287        // Assigning drops (and wipes) the old chain key
288        self.send_chain = next_chain;
289        self.send_counter = counter;
290        Ok(ciphertext)
291    }
292
293    /// Decrypts and validates a message from the other party
294    ///
295    /// # Arguments
296    /// * `ciphertext` - The encrypted message
297    ///
298    /// # Returns
299    /// - `Result<Vec<u8>, CryptoError>`: The decrypted and validated message or an error
300    ///
301    /// # Security Note
302    /// Messages must arrive in the order they were crafted. The receive counter only
303    /// advances for authentic messages, so replayed or forged input is rejected
304    /// without desynchronizing the session.
305    pub fn validate_message(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
306        let counter = self
307            .recv_counter
308            .checked_add(1)
309            .ok_or(CryptoError::NonceExhausted)?;
310        let direction = 1 - self.send_direction();
311
312        let (message_key, next_chain) = ratchet(&self.recv_chain);
313        let mut plaintext = Encryptor::new(&message_key)
314            .decrypt(ciphertext, &create_nonce(&self.session_id, counter))?;
315        if plaintext.len() < keypair::SIGNATURE_BYTES {
316            return Err(CryptoError::InvalidSignature);
317        }
318
319        let (sig, message) = plaintext.split_at(keypair::SIGNATURE_BYTES);
320        if !self
321            .target_verifier
322            .verify(&self.signed_data(direction, counter, message), sig)
323        {
324            return Err(CryptoError::InvalidSignature);
325        }
326
327        self.recv_chain = next_chain;
328        self.recv_counter = counter;
329        plaintext.drain(..keypair::SIGNATURE_BYTES);
330        Ok(plaintext)
331    }
332
333    /// The direction tag of the messages we send
334    fn send_direction(&self) -> u8 {
335        if self.is_initiator {
336            INITIATOR_TO_RESPONDER
337        } else {
338            RESPONDER_TO_INITIATOR
339        }
340    }
341
342    /// The bytes actually signed for a message: transcript || direction || counter || message
343    fn signed_data(&self, direction: u8, counter: u64, message: &[u8]) -> Vec<u8> {
344        [
345            &self.transcript[..],
346            &[direction],
347            &counter.to_le_bytes(),
348            message,
349        ]
350        .concat()
351    }
352
353    /// Gets the counter of the last message sent
354    pub fn send_counter(&self) -> u64 {
355        self.send_counter
356    }
357
358    /// Gets the counter of the last message received
359    pub fn recv_counter(&self) -> u64 {
360        self.recv_counter
361    }
362}
363
364/// Splits a chain key into the key for one message and the next chain key
365///
366/// This is a one-way step: the next chain key can't be turned back into this one.
367fn ratchet(chain: &[u8; 32]) -> (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>) {
368    let hkdf = Hkdf::<Sha256>::from_prk(chain).expect("32 bytes is a valid HKDF-SHA256 PRK");
369    let mut message_key = Zeroizing::new([0u8; 32]);
370    let mut next_chain = Zeroizing::new([0u8; 32]);
371    hkdf.expand(b"pq-msg/v2 message key", &mut message_key[..])
372        .expect("32 bytes is a valid HKDF-SHA256 output length");
373    hkdf.expand(b"pq-msg/v2 chain key", &mut next_chain[..])
374        .expect("32 bytes is a valid HKDF-SHA256 output length");
375    (message_key, next_chain)
376}
377
378/// Generates a random session ID of 16 bytes, used in nonce creation
379///
380/// # Returns
381/// - `[u8; 16]`: The generated session ID
382///   The random 16-byte array
383pub fn gen_session_id() -> [u8; 16] {
384    let mut session_id = [0u8; 16];
385    OsRng.fill_bytes(&mut session_id);
386
387    session_id
388}
389
390/// Creates a nonce from a session ID and a counter
391///
392/// # Arguments
393/// * `session_id` - The session ID (16 bytes)
394/// * `counter` - The counter value (u64)
395///
396/// # Returns
397/// - `[u8; 24]`: The generated nonce (16 bytes session ID + 8 bytes counter)
398///   The nonce is a combination of the session ID and the counter
399pub fn create_nonce(session_id: &[u8; 16], counter: u64) -> [u8; 24] {
400    let mut nonce = [0u8; 24];
401    nonce[..16].copy_from_slice(session_id);
402    nonce[16..24].copy_from_slice(&counter.to_le_bytes());
403    nonce
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    /// A new prekey for `owner`: the secret part and the signed public part
411    fn signed_prekey(owner: &mut SignerPair) -> (KEMPair, SignedPrekey) {
412        let prekey = KEMPair::create();
413        let signed = SignedPrekey::new(owner, &prekey).unwrap();
414        (prekey, signed)
415    }
416
417    /// Alice (initiator) and Bob (responder) with a fresh session
418    fn session_pair(base_counter: u64) -> (MessageSession, MessageSession) {
419        let alice_ds = SignerPair::create();
420        let mut bob_ds = SignerPair::create();
421        let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
422        let base_nonce = create_nonce(&gen_session_id(), base_counter);
423
424        let (alice, ciphertext) = MessageSession::new_initiator(
425            alice_ds.clone(),
426            base_nonce,
427            &bob_signed_prekey,
428            bob_ds.pub_key_bytes(),
429        )
430        .unwrap();
431        let bob = MessageSession::new_responder(
432            &bob_prekey,
433            bob_ds,
434            base_nonce,
435            &ciphertext,
436            alice_ds.pub_key_bytes(),
437        )
438        .unwrap();
439        (alice, bob)
440    }
441
442    #[test]
443    fn test_message_session_serialization() {
444        let (mut alice, mut bob) = session_pair(0);
445        bob.validate_message(&alice.craft_message(b"before").unwrap())
446            .unwrap();
447
448        // Serialize and deserialize Alice, then keep talking with the restored copy
449        let mut restored = MessageSession::from_bytes(&alice.to_bytes()).unwrap();
450        assert_eq!(alice.to_bytes(), restored.to_bytes());
451        assert_eq!(restored.send_counter(), 1);
452
453        let msg = bob.validate_message(&restored.craft_message(b"after").unwrap());
454        assert_eq!(msg.unwrap(), b"after");
455        let reply = restored.validate_message(&bob.craft_message(b"reply").unwrap());
456        assert_eq!(reply.unwrap(), b"reply");
457    }
458
459    #[test]
460    fn test_session_deserialization_rejects_bad_input() {
461        let (alice, _) = session_pair(0);
462        let bytes = alice.to_bytes();
463
464        assert!(matches!(
465            MessageSession::from_bytes(&bytes[1..]),
466            Err(CryptoError::IncongruentLength(SESSION_BYTES, _))
467        ));
468
469        let mut wrong_version = bytes.to_vec();
470        wrong_version[0] = 1;
471        assert!(matches!(
472            MessageSession::from_bytes(&wrong_version),
473            Err(CryptoError::InvalidSession)
474        ));
475
476        let mut wrong_role = bytes.to_vec();
477        wrong_role[1] = 2;
478        assert!(matches!(
479            MessageSession::from_bytes(&wrong_role),
480            Err(CryptoError::InvalidSession)
481        ));
482
483        // Corrupted key material: our own public key no longer matches our secret key
484        let mut wrong_own_key = bytes.to_vec();
485        wrong_own_key[3] ^= 1;
486        assert!(matches!(
487            MessageSession::from_bytes(&wrong_own_key),
488            Err(CryptoError::InvalidKey)
489        ));
490
491        // Corrupted peer key: it no longer decodes
492        let mut wrong_peer_key = bytes.to_vec();
493        wrong_peer_key[2 + keypair::PUBLIC_KEY_BYTES + keypair::SECRET_KEY_BYTES] ^= 0xFF;
494        assert!(matches!(
495            MessageSession::from_bytes(&wrong_peer_key),
496            Err(CryptoError::InvalidKey)
497        ));
498    }
499
500    #[test]
501    fn test_wrong_peer_identity_rejected() {
502        // Bob believes the session came from Carol; Alice's messages must not pass as hers
503        let alice_ds = SignerPair::create();
504        let carol_ds = SignerPair::create();
505        let mut bob_ds = SignerPair::create();
506        let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
507        let base_nonce = create_nonce(&gen_session_id(), 0);
508
509        let (mut alice, ct) = MessageSession::new_initiator(
510            alice_ds,
511            base_nonce,
512            &bob_signed_prekey,
513            bob_ds.pub_key_bytes(),
514        )
515        .unwrap();
516        let mut bob = MessageSession::new_responder(
517            &bob_prekey,
518            bob_ds,
519            base_nonce,
520            &ct,
521            carol_ds.pub_key_bytes(),
522        )
523        .unwrap();
524
525        assert!(
526            bob.validate_message(&alice.craft_message(b"hi").unwrap())
527                .is_err()
528        );
529    }
530
531    #[test]
532    fn test_invalid_peer_keys_rejected() {
533        let mut bob_ds = SignerPair::create();
534        let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
535        let base_nonce = create_nonce(&gen_session_id(), 0);
536        let mut undecodable = *bob_ds.pub_key_bytes();
537        undecodable[0] ^= 0xFF;
538
539        let initiator = MessageSession::new_initiator(
540            SignerPair::create(),
541            base_nonce,
542            &bob_signed_prekey,
543            &undecodable,
544        );
545        assert!(matches!(initiator, Err(CryptoError::InvalidKey)));
546
547        let responder = MessageSession::new_responder(
548            &bob_prekey,
549            bob_ds,
550            base_nonce,
551            &[0u8; pair::CIPHERTEXT_BYTES],
552            &undecodable,
553        );
554        assert!(matches!(responder, Err(CryptoError::InvalidKey)));
555    }
556
557    #[test]
558    fn test_plaintext_too_short_for_signature() {
559        // Only the peer holds the key, but a buggy or malicious peer could still send this
560        let (alice, mut bob) = session_pair(0);
561        let short = Encryptor::new(&ratchet(&alice.send_chain).0)
562            .encrypt(b"no signature here", &create_nonce(&alice.session_id, 1))
563            .unwrap();
564
565        assert!(matches!(
566            bob.validate_message(&short),
567            Err(CryptoError::InvalidSignature)
568        ));
569        assert_eq!(bob.recv_counter(), 0);
570    }
571
572    #[test]
573    fn test_full_message_exchange() {
574        let (mut alice, mut bob) = session_pair(0);
575
576        // Each side derived the other's chain keys
577        assert_eq!(alice.send_chain, bob.recv_chain);
578        assert_eq!(alice.recv_chain, bob.send_chain);
579        assert_ne!(alice.send_chain, alice.recv_chain);
580        assert_eq!(alice.transcript, bob.transcript);
581
582        // Alice sends a message to Bob
583        let message = b"Hello, Bob! This is a secret message.";
584        let encrypted_message = alice.craft_message(message).unwrap();
585        assert_eq!(alice.send_counter(), 1);
586        assert_eq!(bob.recv_counter(), 0);
587
588        // Bob decrypts and verifies Alice's message
589        let raw_message = bob.validate_message(&encrypted_message).unwrap();
590        assert_eq!(bob.recv_counter(), 1);
591        assert_eq!(raw_message, message);
592
593        // Bob replies to Alice
594        let reply = b"Hello, Alice! I received your message safely.";
595        let encrypted_reply = bob.craft_message(reply).unwrap();
596        let raw_reply = alice.validate_message(&encrypted_reply).unwrap();
597        assert_eq!(raw_reply, reply);
598
599        assert_eq!(alice.send_counter(), bob.recv_counter());
600        assert_eq!(alice.recv_counter(), bob.send_counter());
601    }
602
603    #[test]
604    fn test_concurrent_sends() {
605        // Regression: both parties used to share one key and one counter, so two
606        // messages sent at the same time reused a key-nonce pair and leaked m_A ^ m_B
607        let (mut alice, mut bob) = session_pair(0);
608
609        let (ma, mb) = (
610            b"attack at dawn, bring snacks!!",
611            b"meet me at the north gate 9pm.",
612        );
613        let ca = alice.craft_message(ma).unwrap();
614        let cb = bob.craft_message(mb).unwrap();
615
616        let ct_xor: Vec<u8> = ca.iter().zip(&cb).map(|(x, y)| x ^ y).collect();
617        let pt_xor: Vec<u8> = ma.iter().zip(mb).map(|(x, y)| x ^ y).collect();
618        assert!(!ct_xor.windows(pt_xor.len()).any(|w| w == pt_xor));
619
620        // Both messages still arrive, and the session stays in sync
621        assert_eq!(bob.validate_message(&ca).unwrap(), ma);
622        assert_eq!(alice.validate_message(&cb).unwrap(), mb);
623        let next = alice.craft_message(b"next").unwrap();
624        assert_eq!(bob.validate_message(&next).unwrap(), b"next");
625    }
626
627    #[test]
628    fn test_replay_and_garbage_do_not_desync() {
629        let (mut alice, mut bob) = session_pair(0);
630
631        let first = alice.craft_message(b"first").unwrap();
632        bob.validate_message(&first).unwrap();
633
634        // A replayed message and random garbage are rejected without moving the counter
635        assert!(bob.validate_message(&first).is_err());
636        assert!(bob.validate_message(&[0u8; 2000]).is_err());
637        assert_eq!(bob.recv_counter(), 1);
638
639        let second = alice.craft_message(b"second").unwrap();
640        assert_eq!(bob.validate_message(&second).unwrap(), b"second");
641    }
642
643    #[test]
644    fn test_out_of_order_rejected() {
645        let (mut alice, mut bob) = session_pair(0);
646
647        let first = alice.craft_message(b"first").unwrap();
648        let second = alice.craft_message(b"second").unwrap();
649
650        assert!(bob.validate_message(&second).is_err());
651        assert_eq!(bob.validate_message(&first).unwrap(), b"first");
652        assert_eq!(bob.validate_message(&second).unwrap(), b"second");
653    }
654
655    #[test]
656    fn test_reflected_message_rejected() {
657        let (mut alice, _) = session_pair(0);
658
659        // Alice's own message sent back to her doesn't decrypt under her receive key
660        let msg = alice.craft_message(b"hello").unwrap();
661        assert!(alice.validate_message(&msg).is_err());
662    }
663
664    #[test]
665    fn test_forwarded_message_rejected() {
666        // Bob receives a signed message from Alice, then opens a session to Carol
667        // claiming to be Alice and forwards it. The signature is bound to the
668        // Alice-Bob transcript, so Carol must reject it.
669        let alice_ds = SignerPair::create();
670        let mut bob_ds = SignerPair::create();
671        let (bob_prekey, bob_signed_prekey) = signed_prekey(&mut bob_ds);
672        let mut carol_ds = SignerPair::create();
673        let (carol_prekey, carol_signed_prekey) = signed_prekey(&mut carol_ds);
674        let base_nonce = create_nonce(&gen_session_id(), 0);
675
676        let (mut alice, ct) = MessageSession::new_initiator(
677            alice_ds.clone(),
678            base_nonce,
679            &bob_signed_prekey,
680            bob_ds.pub_key_bytes(),
681        )
682        .unwrap();
683        let bob = MessageSession::new_responder(
684            &bob_prekey,
685            bob_ds.clone(),
686            base_nonce,
687            &ct,
688            alice_ds.pub_key_bytes(),
689        )
690        .unwrap();
691
692        // Bob decrypts Alice's message to get her signature and the message
693        let sent = alice.craft_message(b"I owe Bob 100 euros").unwrap();
694        let signed = Encryptor::new(&ratchet(&bob.recv_chain).0)
695            .decrypt(&sent, &create_nonce(&bob.session_id, 1))
696            .unwrap();
697
698        // Bob opens a session to Carol, telling her he is Alice. He encapsulated the
699        // shared secret and the transcript is public, so he can derive Carol's
700        // receive key; the test simply reads it from her session.
701        let (_, ct) = MessageSession::new_initiator(
702            bob_ds,
703            base_nonce,
704            &carol_signed_prekey,
705            carol_ds.pub_key_bytes(),
706        )
707        .unwrap();
708        let mut carol = MessageSession::new_responder(
709            &carol_prekey,
710            carol_ds,
711            base_nonce,
712            &ct,
713            alice_ds.pub_key_bytes(),
714        )
715        .unwrap();
716        let forged = Encryptor::new(&ratchet(&carol.recv_chain).0)
717            .encrypt(&signed, &create_nonce(&carol.session_id, 1))
718            .unwrap();
719
720        assert!(matches!(
721            carol.validate_message(&forged),
722            Err(CryptoError::InvalidSignature)
723        ));
724    }
725
726    #[test]
727    fn test_stolen_session_cannot_read_past_messages() {
728        let (mut alice, mut bob) = session_pair(0);
729
730        let first = alice.craft_message(b"first").unwrap();
731        bob.validate_message(&first).unwrap();
732
733        // An attacker later steals Bob's stored session and rewinds its counter.
734        // The chain key has already moved on, so the old message stays unreadable.
735        let mut stolen = MessageSession::from_bytes(&bob.to_bytes()).unwrap();
736        stolen.recv_counter = 0;
737        assert!(stolen.validate_message(&first).is_err());
738    }
739
740    #[test]
741    fn test_deleted_prekey_gives_forward_secrecy() {
742        let alice_ds = SignerPair::create();
743        let mut bob_ds = SignerPair::create();
744        let (last_resort, _) = signed_prekey(&mut bob_ds);
745        let (one_time, one_time_signed) = signed_prekey(&mut bob_ds);
746        let base_nonce = create_nonce(&gen_session_id(), 0);
747
748        let (mut alice, ct) = MessageSession::new_initiator(
749            alice_ds.clone(),
750            base_nonce,
751            &one_time_signed,
752            bob_ds.pub_key_bytes(),
753        )
754        .unwrap();
755        let mut bob = MessageSession::new_responder(
756            &one_time,
757            bob_ds.clone(),
758            base_nonce,
759            &ct,
760            alice_ds.pub_key_bytes(),
761        )
762        .unwrap();
763        drop(one_time); // Bob deletes the one-time prekey
764
765        // An attacker records the handshake and the first message
766        let recorded = alice.craft_message(b"secret").unwrap();
767        assert_eq!(bob.validate_message(&recorded).unwrap(), b"secret");
768
769        // Later every key Bob still has leaks; none of them opens the recorded session
770        let mut attacker = MessageSession::new_responder(
771            &last_resort,
772            bob_ds,
773            base_nonce,
774            &ct,
775            alice_ds.pub_key_bytes(),
776        )
777        .unwrap();
778        assert!(attacker.validate_message(&recorded).is_err());
779    }
780
781    #[test]
782    fn test_initiator_rejects_prekey_not_signed_by_target() {
783        // E.g. a malicious server hands out its own prekey for Bob
784        let bob_ds = SignerPair::create();
785        let mut mallory_ds = SignerPair::create();
786        let (_, fake) = signed_prekey(&mut mallory_ds);
787
788        let result = MessageSession::new_initiator(
789            SignerPair::create(),
790            create_nonce(&gen_session_id(), 0),
791            &fake,
792            bob_ds.pub_key_bytes(),
793        );
794        assert!(matches!(result, Err(CryptoError::InvalidSignature)));
795    }
796
797    #[test]
798    fn test_counter_exhaustion() {
799        let (mut alice, mut bob) = session_pair(u64::MAX - 1);
800
801        let last = alice.craft_message(b"last").unwrap();
802        assert_eq!(alice.send_counter(), u64::MAX);
803        assert_eq!(bob.validate_message(&last).unwrap(), b"last");
804
805        // No wrap-around back to 0: the session refuses instead of reusing a nonce
806        assert!(matches!(
807            alice.craft_message(b"one more"),
808            Err(CryptoError::NonceExhausted)
809        ));
810        assert!(matches!(
811            bob.validate_message(&last),
812            Err(CryptoError::NonceExhausted)
813        ));
814    }
815
816    #[test]
817    fn test_empty_message() {
818        let (mut alice, mut bob) = session_pair(0);
819        let encrypted = alice.craft_message(b"").unwrap();
820        assert_eq!(bob.validate_message(&encrypted).unwrap(), b"");
821    }
822
823    #[test]
824    fn test_session_ids_are_random() {
825        assert_ne!(gen_session_id(), gen_session_id());
826    }
827
828    #[test]
829    fn test_nonce_creation() {
830        // Generate a session ID
831        let session_id = gen_session_id();
832
833        // Create a nonce with a counter of 5
834        let counter = 5;
835        let nonce = create_nonce(&session_id, counter);
836
837        // Verify the nonce structure
838        assert_eq!(&nonce[..16], &session_id[..]);
839        assert_eq!(&nonce[16..], &counter.to_le_bytes()[..]);
840    }
841
842    #[test]
843    fn test_base_counter() {
844        let (mut alice, mut bob) = session_pair(42);
845        assert_eq!(alice.send_counter(), 42);
846        assert_eq!(bob.recv_counter(), 42);
847
848        bob.validate_message(&alice.craft_message(b"hi").unwrap())
849            .unwrap();
850        assert_eq!(alice.send_counter(), 43);
851        assert_eq!(bob.recv_counter(), 43);
852    }
853}