Skip to main content

libsignal_protocol/
session_management.rs

1//
2// Copyright 2026 Signal Messenger, LLC.
3// SPDX-License-Identifier: AGPL-3.0-only
4//
5
6//! Session management and public encrypt/decrypt API for Signal 1:1 messaging.
7//!
8//! This module owns two things:
9//!
10//! 1. **The public API** — [`message_encrypt`], [`message_decrypt`],
11//!    [`message_decrypt_signal`], [`message_decrypt_prekey`]. These are the
12//!    entry points used by the bridge layer and `sealed_sender`.
13//!
14//! 2. **Sesame session management** — the "which session do we use?" logic:
15//!    trial-decryption across current and previous sessions, session promotion
16//!    on success, and session selection for encryption.
17//!
18//! All cryptographic ratchet operations are delegated to
19//! [`TripleRatchet`]. This module has no knowledge of chain keys,
20//! root keys, or SPQR internals.
21
22use std::time::SystemTime;
23
24use displaydoc::Display;
25use libsignal_core::try_scoped;
26use rand::{CryptoRng, Rng};
27
28use crate::consts::MAX_UNACKNOWLEDGED_SESSION_AGE;
29use crate::state::{InvalidSessionError, SessionState};
30use crate::triple_ratchet::{OutgoingTripleRatchet, TripleRatchet};
31use crate::{
32    CiphertextMessage, CiphertextMessageType, Direction, IdentityKeyStore, KyberPayload,
33    KyberPreKeyStore, PreKeySignalMessage, PreKeyStore, ProtocolAddress, Result, SessionNotFound,
34    SessionRecord, SessionStore, SignalMessage, SignalProtocolError, SignedPreKeyStore, session,
35};
36// ── Public API ───────────────────────────────────────────────────────────────
37
38/// Encrypt `ptext` for `remote_address`, loading and storing session state.
39///
40/// If the session is unacknowledged (a locally-initiated session that has not
41/// yet received a response), wraps the [`SignalMessage`] in a
42/// [`PreKeySignalMessage`] containing the original pre-key material.
43pub async fn message_encrypt<R: Rng + CryptoRng>(
44    ptext: &[u8],
45    remote_address: &ProtocolAddress,
46    local_address: &ProtocolAddress,
47    session_store: &mut dyn SessionStore,
48    identity_store: &mut dyn IdentityKeyStore,
49    now: SystemTime,
50    csprng: &mut R,
51) -> Result<CiphertextMessage> {
52    let no_session_error = || {
53        SignalProtocolError::SessionNotFound(SessionNotFound::new(
54            remote_address.clone(),
55            "message_encrypt",
56        ))
57    };
58    let mut session_record = session_store
59        .load_session(remote_address)
60        .await?
61        .ok_or_else(no_session_error)?;
62    let session_state = session_record
63        .session_state_mut()
64        .ok_or_else(no_session_error)?;
65
66    let mut session = OutgoingTripleRatchet::from_session_state(session_state).map_err(|e| {
67        log::error!("session state corrupt for {remote_address}: {e}");
68        e
69    })?;
70
71    let their_identity_key = session_state
72        .remote_identity_key()?
73        .expect("session was valid; must have remote identity key");
74
75    // Pre-key wrapping — session management concern.
76    let message = if let Some(items) = session_state.unacknowledged_pre_key_message_items()? {
77        let timestamp_as_unix_time = items
78            .timestamp()
79            .duration_since(SystemTime::UNIX_EPOCH)
80            .unwrap_or_default()
81            .as_secs();
82        if items.timestamp() + MAX_UNACKNOWLEDGED_SESSION_AGE < now {
83            log::warn!(
84                "stale unacknowledged session for {remote_address} (created at {timestamp_as_unix_time})"
85            );
86            return Err(no_session_error());
87        }
88
89        let local_registration_id = session_state.local_registration_id();
90
91        log::info!(
92            "Building PreKeyWhisperMessage for: {} with preKeyId: {} (session created at {})",
93            remote_address,
94            items
95                .pre_key_id()
96                .map_or_else(|| "<none>".to_string(), |id| id.to_string()),
97            timestamp_as_unix_time,
98        );
99
100        let kyber_payload = items
101            .kyber_pre_key_id()
102            .zip(items.kyber_ciphertext())
103            .map(|(id, ciphertext)| KyberPayload::new(id, ciphertext.into()));
104        let signal_message = session.encrypt(ptext, Some(local_address), remote_address, csprng)?;
105
106        CiphertextMessage::PreKeySignalMessage(PreKeySignalMessage::new(
107            session.session_version(),
108            local_registration_id,
109            items.pre_key_id(),
110            items.signed_pre_key_id(),
111            kyber_payload,
112            *items.base_key(),
113            *session.local_identity_key(),
114            signal_message,
115        )?)
116    } else {
117        let signal_message = session.encrypt(ptext, None, remote_address, csprng)?;
118        CiphertextMessage::SignalMessage(signal_message)
119    };
120
121    // In clients, `is_trusted_identity` for the Sending direction checks
122    // whether the session's identity key matches the stored key AND whether the
123    // user has approved it (safety number changes, verification status). This
124    // prevents sending to a contact whose identity has changed without user
125    // acknowledgment.
126    if !identity_store
127        .is_trusted_identity(remote_address, &their_identity_key, Direction::Sending)
128        .await?
129    {
130        log::warn!(
131            "Identity key {} is not trusted for remote address {}",
132            hex::encode(their_identity_key.public_key().public_key_bytes()),
133            remote_address,
134        );
135        return Err(SignalProtocolError::UntrustedIdentity(
136            remote_address.clone(),
137        ));
138    }
139
140    identity_store
141        .save_identity(remote_address, &their_identity_key)
142        .await?;
143
144    // Commit and save session state changes.
145    session.apply_to_session_state(session_state);
146
147    session_store
148        .store_session(remote_address, &session_record)
149        .await?;
150    Ok(message)
151}
152
153/// Decrypt a [`CiphertextMessage`] from `remote_address`.
154///
155/// Routes to [`message_decrypt_signal`] or [`message_decrypt_prekey`] based
156/// on message type.
157#[allow(clippy::too_many_arguments)]
158pub async fn message_decrypt<R: Rng + CryptoRng>(
159    ciphertext: &CiphertextMessage,
160    remote_address: &ProtocolAddress,
161    local_address: &ProtocolAddress,
162    session_store: &mut dyn SessionStore,
163    identity_store: &mut dyn IdentityKeyStore,
164    pre_key_store: &mut dyn PreKeyStore,
165    signed_pre_key_store: &dyn SignedPreKeyStore,
166    kyber_pre_key_store: &mut dyn KyberPreKeyStore,
167    csprng: &mut R,
168) -> Result<Vec<u8>> {
169    match ciphertext {
170        CiphertextMessage::SignalMessage(m) => {
171            message_decrypt_signal(
172                m,
173                remote_address,
174                local_address,
175                session_store,
176                identity_store,
177                csprng,
178            )
179            .await
180        }
181        CiphertextMessage::PreKeySignalMessage(m) => {
182            message_decrypt_prekey(
183                m,
184                remote_address,
185                local_address,
186                session_store,
187                identity_store,
188                pre_key_store,
189                signed_pre_key_store,
190                kyber_pre_key_store,
191                csprng,
192            )
193            .await
194        }
195        _ => Err(SignalProtocolError::InvalidArgument(format!(
196            "message_decrypt cannot be used to decrypt {:?} messages",
197            ciphertext.message_type()
198        ))),
199    }
200}
201
202/// Decrypt a [`PreKeySignalMessage`] from `remote_address`.
203///
204/// Processes the pre-key material to establish a session (via
205/// [`session::process_prekey`]), then decrypts the inner [`SignalMessage`].
206#[allow(clippy::too_many_arguments)]
207pub async fn message_decrypt_prekey<R: Rng + CryptoRng>(
208    ciphertext: &PreKeySignalMessage,
209    remote_address: &ProtocolAddress,
210    local_address: &ProtocolAddress,
211    session_store: &mut dyn SessionStore,
212    identity_store: &mut dyn IdentityKeyStore,
213    pre_key_store: &mut dyn PreKeyStore,
214    signed_pre_key_store: &dyn SignedPreKeyStore,
215    kyber_pre_key_store: &mut dyn KyberPreKeyStore,
216    csprng: &mut R,
217) -> Result<Vec<u8>> {
218    let mut session_record = session_store
219        .load_session(remote_address)
220        .await?
221        .unwrap_or_else(SessionRecord::new_fresh);
222
223    // Make sure we log the session state if we fail to process the pre-key.
224    let process_prekey_result = session::process_prekey(
225        ciphertext,
226        remote_address,
227        local_address,
228        &mut session_record,
229        identity_store,
230        pre_key_store,
231        signed_pre_key_store,
232        kyber_pre_key_store,
233    )
234    .await;
235
236    let (pre_key_used, identity_to_save) = match process_prekey_result {
237        Ok(result) => result,
238        Err(e) => {
239            let errs = [e];
240            log::error!(
241                "{}",
242                format_decryption_failure_log(
243                    remote_address,
244                    &errs,
245                    &session_record,
246                    ciphertext.message()
247                )?
248            );
249            let [e] = errs;
250            return Err(e);
251        }
252    };
253
254    let ptext = try_decrypt_from_record(
255        &mut session_record,
256        remote_address,
257        local_address,
258        ciphertext.message(),
259        CiphertextMessageType::PreKey,
260        csprng,
261    )?;
262
263    identity_store
264        .save_identity(
265            identity_to_save.remote_address,
266            identity_to_save.their_identity_key,
267        )
268        .await?;
269
270    if let Some(pre_key_used) = pre_key_used {
271        if let Some(kyber_pre_key_id) = pre_key_used.kyber_pre_key_id {
272            kyber_pre_key_store
273                .mark_kyber_pre_key_used(
274                    kyber_pre_key_id,
275                    pre_key_used.signed_ec_pre_key_id,
276                    ciphertext.base_key(),
277                )
278                .await?;
279        }
280
281        if let Some(pre_key_id) = pre_key_used.one_time_ec_pre_key_id {
282            pre_key_store.remove_pre_key(pre_key_id).await?;
283        }
284    }
285
286    session_store
287        .store_session(remote_address, &session_record)
288        .await?;
289
290    Ok(ptext)
291}
292
293/// Decrypt a [`SignalMessage`] from `remote_address`.
294///
295/// Tries all sessions in the session record. Checks identity key trust
296/// after decryption.
297pub async fn message_decrypt_signal<R: Rng + CryptoRng>(
298    ciphertext: &SignalMessage,
299    remote_address: &ProtocolAddress,
300    local_address: &ProtocolAddress,
301    session_store: &mut dyn SessionStore,
302    identity_store: &mut dyn IdentityKeyStore,
303    csprng: &mut R,
304) -> Result<Vec<u8>> {
305    let mut session_record = session_store
306        .load_session(remote_address)
307        .await?
308        .ok_or_else(|| {
309            SignalProtocolError::SessionNotFound(SessionNotFound::new(
310                remote_address.clone(),
311                "message_decrypt_signal",
312            ))
313        })?;
314
315    let ptext = try_decrypt_from_record(
316        &mut session_record,
317        remote_address,
318        local_address,
319        ciphertext,
320        CiphertextMessageType::Whisper,
321        csprng,
322    )?;
323
324    // Why are we performing this check after decryption instead of before?
325    let their_identity_key = session_record
326        .session_state()
327        .expect("successfully decrypted; must have a current state")
328        .remote_identity_key()
329        .expect("successfully decrypted; must have a remote identity key")
330        .expect("successfully decrypted; must have a remote identity key");
331
332    if !identity_store
333        .is_trusted_identity(remote_address, &their_identity_key, Direction::Receiving)
334        .await?
335    {
336        log::warn!(
337            "Identity key {} is not trusted for remote address {}",
338            hex::encode(their_identity_key.public_key().public_key_bytes()),
339            remote_address,
340        );
341        return Err(SignalProtocolError::UntrustedIdentity(
342            remote_address.clone(),
343        ));
344    }
345
346    identity_store
347        .save_identity(remote_address, &their_identity_key)
348        .await?;
349
350    session_store
351        .store_session(remote_address, &session_record)
352        .await?;
353
354    Ok(ptext)
355}
356
357// ── Session management (Sesame) ──────────────────────────────────────────────
358
359/// Try to decrypt `ciphertext` against every session in `record`, in order.
360///
361/// Tries the current session first, then previous sessions. On success from
362/// a previous session, promotes that session to current (Sesame behavior).
363///
364/// `original_message_type` is `Whisper` for normal messages and `PreKey` for
365/// the inner `SignalMessage` of a pre-key message. When it is `PreKey`, we
366/// skip the fallback to previous sessions — a PreKey message establishes a
367/// fresh session and should always match the current one.
368pub(crate) fn try_decrypt_from_record<R: Rng + CryptoRng>(
369    record: &mut SessionRecord,
370    remote_address: &ProtocolAddress,
371    local_address: &ProtocolAddress,
372    ciphertext: &SignalMessage,
373    original_message_type: CiphertextMessageType,
374    csprng: &mut R,
375) -> Result<Vec<u8>> {
376    debug_assert!(matches!(
377        original_message_type,
378        CiphertextMessageType::Whisper | CiphertextMessageType::PreKey
379    ));
380    let ciphertext_version = ciphertext.message_version() as u32;
381
382    let log_failure = |label: &str, state: &SessionState, error: &SignalProtocolError| {
383        log::warn!(
384            "Failed to decrypt {:?} message with ratchet key: {} and counter: {}. \
385             Session loaded for {}. {} session has base key: {} and counter: {}. {}",
386            original_message_type,
387            hex::encode(ciphertext.sender_ratchet_key().public_key_bytes()),
388            ciphertext.counter(),
389            remote_address,
390            label,
391            state
392                .sender_ratchet_key_for_logging()
393                .unwrap_or_else(|e| format!("<error: {e}>")),
394            state.previous_counter(),
395            error
396        );
397    };
398
399    let mut errs = vec![];
400
401    // ── Try current session ──────────────────────────────────────────────────
402
403    if let Some(current_state) = record.session_state() {
404        let mut current_state = current_state.clone();
405
406        if current_state.session_version()? != ciphertext_version {
407            let e = SignalProtocolError::UnrecognizedMessageVersion(ciphertext_version);
408            log_failure("Current", &current_state, &e);
409            errs.push(e);
410        } else {
411            match try_decrypt_with_state(
412                &mut current_state,
413                remote_address,
414                local_address,
415                ciphertext,
416                original_message_type,
417                CurrentOrPrevious::Current,
418                csprng,
419            ) {
420                Ok(ptext) => {
421                    log::info!(
422                        "decrypted {:?} message from {} with current session state (base key {})",
423                        original_message_type,
424                        remote_address,
425                        current_state
426                            .sender_ratchet_key_for_logging()
427                            .expect("successful decrypt always has a valid base key"),
428                    );
429                    record.set_session_state(current_state);
430                    return Ok(ptext);
431                }
432                Err(e @ SignalProtocolError::DuplicatedMessage(_, _)) => return Err(e),
433                Err(e) => {
434                    log_failure("Current", &current_state, &e);
435                    errs.push(e);
436                    match original_message_type {
437                        CiphertextMessageType::PreKey => {
438                            // A PreKey message creates a session and then decrypts a Whisper message
439                            // using that session. No need to check older sessions.
440                            log::error!(
441                                "{}",
442                                format_decryption_failure_log(
443                                    remote_address,
444                                    &errs,
445                                    record,
446                                    ciphertext,
447                                )?
448                            );
449                            // Note that we don't propagate `e` here; we always return InvalidMessage,
450                            // as we would for a Whisper message that tried several sessions.
451                            return Err(SignalProtocolError::InvalidMessage(
452                                original_message_type,
453                                "decryption failed".to_owned(),
454                            ));
455                        }
456                        CiphertextMessageType::Whisper => {}
457                        CiphertextMessageType::SenderKey | CiphertextMessageType::Plaintext => {
458                            unreachable!("should not be using Double Ratchet for these")
459                        }
460                    }
461                }
462            }
463        }
464    }
465
466    // ── Try previous sessions (Whisper only) ─────────────────────────────────
467
468    let mut promoted = None;
469
470    for (idx, previous) in record.previous_session_states().enumerate() {
471        let mut previous = match previous {
472            Ok(previous) => previous,
473            Err(e) => {
474                let e: SignalProtocolError = e.into();
475                log::warn!(
476                    "Skipping corrupt previous session {} for {}: {}",
477                    idx,
478                    remote_address,
479                    e
480                );
481                errs.push(e);
482                continue;
483            }
484        };
485
486        if previous.session_version()? != ciphertext_version {
487            let e = SignalProtocolError::UnrecognizedMessageVersion(ciphertext_version);
488            log_failure("Previous", &previous, &e);
489            errs.push(e);
490            continue;
491        }
492
493        match try_decrypt_with_state(
494            &mut previous,
495            remote_address,
496            local_address,
497            ciphertext,
498            original_message_type,
499            CurrentOrPrevious::Previous,
500            csprng,
501        ) {
502            Ok(ptext) => {
503                log::info!(
504                    "decrypted {:?} message from {} with PREVIOUS session state (base key {})",
505                    original_message_type,
506                    remote_address,
507                    previous
508                        .sender_ratchet_key_for_logging()
509                        .expect("successful decrypt always has a valid base key"),
510                );
511                promoted = Some((ptext, idx, previous));
512                break;
513            }
514            Err(e @ SignalProtocolError::DuplicatedMessage(_, _)) => return Err(e),
515            Err(e) => {
516                log_failure("Previous", &previous, &e);
517                errs.push(e);
518            }
519        }
520    }
521
522    if let Some((ptext, idx, updated)) = promoted {
523        // Sesame: promote the successful previous session to current.
524        // The upcoming session management update will remove this promotion.
525        record.promote_old_session(idx, updated);
526        Ok(ptext)
527    } else {
528        let previous_state_count = || record.previous_session_states().len();
529        if let Some(current_state) = record.session_state() {
530            log::error!(
531                "No valid session for recipient: {}, current session base key {}, \
532                 number of previous states: {}",
533                remote_address,
534                current_state
535                    .sender_ratchet_key_for_logging()
536                    .unwrap_or_else(|e| format!("<error: {e}>")),
537                previous_state_count(),
538            );
539        } else {
540            log::error!(
541                "No valid session for recipient: {}, (no current session state), \
542                 number of previous states: {}",
543                remote_address,
544                previous_state_count(),
545            );
546        }
547        log::error!(
548            "{}",
549            format_decryption_failure_log(remote_address, &errs, record, ciphertext)?
550        );
551        Err(SignalProtocolError::InvalidMessage(
552            original_message_type,
553            "decryption failed".to_owned(),
554        ))
555    }
556}
557
558// ── Per-session decrypt ──────────────────────────────────────────────────────
559
560/// Attempt to decrypt `ciphertext` using the crypto state in `state`.
561///
562/// Caller must only pass version-compatible ciphertext/session pairs.
563///
564/// Constructs a [`TripleRatchet`], delegates the actual decryption, and writes
565/// updated state back on success. On failure, `state` is unchanged.
566pub(crate) fn try_decrypt_with_state<R: Rng + CryptoRng>(
567    state: &mut SessionState,
568    remote_address: &ProtocolAddress,
569    local_address: &ProtocolAddress,
570    ciphertext: &SignalMessage,
571    original_message_type: CiphertextMessageType,
572    curr_or_prev_for_logging: CurrentOrPrevious,
573    csprng: &mut R,
574) -> Result<Vec<u8>> {
575    debug_assert_eq!(
576        state.session_version()?,
577        ciphertext.message_version() as u32
578    );
579
580    let self_session = try_scoped::<bool, InvalidSessionError>(|| {
581        Ok(state.local_identity_key()?.is_same_account(
582            local_address,
583            &state
584                .remote_identity_key()?
585                .ok_or(InvalidSessionError("missing remote identity key"))?,
586            remote_address,
587        ))
588    })
589    .inspect_err(|e| log::warn!("Failed to determine self_session: {}", e))
590    .unwrap_or_default();
591    let mut session = TripleRatchet::from_session_state(state, self_session)?;
592
593    let ptext = session.decrypt(
594        remote_address,
595        local_address,
596        ciphertext,
597        original_message_type,
598        curr_or_prev_for_logging,
599        csprng,
600    )?;
601
602    session.apply_to_session_state(state);
603    state.clear_unacknowledged_pre_key_message();
604
605    Ok(ptext)
606}
607
608// ── Logging helpers ──────────────────────────────────────────────────────────
609
610pub(crate) fn format_decryption_failure_log(
611    remote_address_for_logging: &ProtocolAddress,
612    mut errs: &[SignalProtocolError],
613    record: &SessionRecord,
614    ciphertext: &SignalMessage,
615) -> Result<String> {
616    fn append_session_summary(
617        lines: &mut Vec<String>,
618        idx: usize,
619        state: std::result::Result<&SessionState, InvalidSessionError>,
620        err: Option<&SignalProtocolError>,
621    ) {
622        let chains = state.map(|state| state.all_receiver_chain_logging_info());
623        match (err, &chains) {
624            (Some(err), Ok(chains)) => {
625                lines.push(format!(
626                    "Candidate session {} failed with '{}', had {} receiver chains",
627                    idx,
628                    err,
629                    chains.len()
630                ));
631            }
632            (Some(err), Err(state_err)) => {
633                lines.push(format!(
634                    "Candidate session {idx} failed with '{err}'; \
635                     cannot get receiver chain info ({state_err})",
636                ));
637            }
638            (None, Ok(chains)) => {
639                lines.push(format!(
640                    "Candidate session {} had {} receiver chains",
641                    idx,
642                    chains.len()
643                ));
644            }
645            (None, Err(state_err)) => {
646                lines.push(format!(
647                    "Candidate session {idx}: cannot get receiver chain info ({state_err})",
648                ));
649            }
650        }
651
652        if let Ok(chains) = chains {
653            for chain in chains {
654                let chain_idx = match chain.1 {
655                    Some(i) => i.to_string(),
656                    None => "missing in protobuf".to_string(),
657                };
658                lines.push(format!(
659                    "Receiver chain with sender ratchet public key {} chain key index {}",
660                    hex::encode(chain.0),
661                    chain_idx
662                ));
663            }
664        }
665    }
666
667    let mut lines = vec![];
668    lines.push(format!(
669        "Message from {} failed to decrypt; sender ratchet public key {} message counter {}",
670        remote_address_for_logging,
671        hex::encode(ciphertext.sender_ratchet_key().public_key_bytes()),
672        ciphertext.counter()
673    ));
674
675    if let Some(current_session) = record.session_state() {
676        let err = errs.first();
677        if err.is_some() {
678            errs = &errs[1..];
679        }
680        append_session_summary(&mut lines, 0, Ok(current_session), err);
681    } else {
682        lines.push("No current session".to_string());
683    }
684
685    for (idx, (state, err)) in record
686        .previous_session_states()
687        .zip(errs.iter().map(Some).chain(std::iter::repeat(None)))
688        .enumerate()
689    {
690        let state = match state {
691            Ok(ref state) => Ok(state),
692            Err(err) => Err(err),
693        };
694        append_session_summary(&mut lines, idx + 1, state, err);
695    }
696
697    Ok(lines.join("\n"))
698}
699
700#[derive(Clone, Copy, Display)]
701pub(crate) enum CurrentOrPrevious {
702    /// current
703    Current,
704    /// previous
705    Previous,
706}
707
708// ── Comparison proptest ──────────────────────────────────────────────────────
709//
710// Verifies that the refactored encrypt/decrypt path produces identical results
711// to the legacy snapshot for any message sequence.
712#[cfg(test)]
713mod legacy_interop_tests {
714    use assert_matches::assert_matches;
715    // These tests live next to `session_management` rather than under
716    // `rust/protocol/tests/` because they compare the refactored code against
717    // the private `session_cipher_legacy` implementation and also assert
718    // byte-level equivalence of internal persisted state. That makes them
719    // implementation-regression tests for this refactor, not normal public API
720    // integration tests.
721    //
722    // This harness is temporary. Once we are confident in the refactor, remove
723    // `session_cipher_legacy` and the new-vs-legacy equivalence tests along
724    // with it.
725    use futures_util::FutureExt;
726    use libsignal_protocol_test_support::Event;
727    use proptest::prelude::*;
728    use prost::Message;
729    use rand::SeedableRng;
730    use rand_chacha::ChaCha8Rng;
731
732    use super::*;
733    use crate::proto::storage::RecordStructure;
734    use crate::ratchet::{
735        AliceSignalProtocolParameters, BobSignalProtocolParameters,
736        initialize_alice_session_record, initialize_bob_session_record,
737    };
738    use crate::{
739        DecryptionErrorMessage, DeviceId, GenericSignedPreKey, IdentityKeyPair,
740        InMemSignalProtocolStore, KeyPair, KyberPreKeyId, KyberPreKeyRecord, PlaintextContent,
741        PreKeyBundle, PreKeyId, PreKeyRecord, ProtocolAddress, SessionRecord,
742        SessionUsabilityRequirements, SignalProtocolError, SignedPreKeyId, SignedPreKeyRecord,
743        Timestamp, extract_decryption_error_message_from_serialized_content, process_prekey_bundle,
744        session_cipher_legacy as legacy,
745    };
746
747    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
748    enum MessageStatus {
749        Sent,
750        Dropped,
751        Delivered,
752    }
753
754    #[derive(Clone)]
755    struct DualLocalState {
756        new_store: InMemSignalProtocolStore,
757        legacy_store: InMemSignalProtocolStore,
758        pre_key_count: u32,
759    }
760
761    struct DualParticipant {
762        address: ProtocolAddress,
763        message_queue: Vec<(CiphertextMessage, u64)>,
764        state: DualLocalState,
765        snapshots: Vec<DualLocalState>,
766        message_send_log: Vec<MessageStatus>,
767    }
768
769    /// Build a matched (alice, bob) session pair from a seeded RNG.
770    fn setup_stores(
771        rng: &mut ChaCha8Rng,
772    ) -> (
773        InMemSignalProtocolStore,
774        InMemSignalProtocolStore,
775        ProtocolAddress,
776        ProtocolAddress,
777    ) {
778        let alice_identity = IdentityKeyPair::generate(rng);
779        let bob_identity = IdentityKeyPair::generate(rng);
780
781        let alice_base_key = KeyPair::generate(rng);
782        let bob_signed_pre_key = KeyPair::generate(rng);
783        let bob_kyber_key = crate::kem::KeyPair::generate(crate::kem::KeyType::Kyber1024, rng);
784
785        let alice_params = AliceSignalProtocolParameters::new(
786            alice_identity,
787            alice_base_key,
788            *bob_identity.identity_key(),
789            bob_signed_pre_key.public_key,
790            bob_signed_pre_key.public_key,
791            bob_kyber_key.public_key.clone(),
792            false,
793        );
794
795        let alice_record =
796            initialize_alice_session_record(&alice_params, rng).expect("alice session init");
797        let kyber_ct: Box<[u8]> = alice_record
798            .get_kyber_ciphertext()
799            .expect("session valid")
800            .expect("has kyber ciphertext")
801            .clone()
802            .into_boxed_slice();
803
804        let bob_params = BobSignalProtocolParameters::new(
805            bob_identity,
806            bob_signed_pre_key,
807            None,
808            bob_kyber_key,
809            *alice_identity.identity_key(),
810            alice_base_key.public_key,
811            &kyber_ct,
812            false,
813        );
814
815        let bob_record = initialize_bob_session_record(&bob_params, &bob_signed_pre_key)
816            .expect("bob session init");
817
818        let alice_address = ProtocolAddress::new(
819            "57721566-4901-5328-6060-651209008240".to_owned(),
820            DeviceId::new(1).unwrap(),
821        );
822        let bob_address = ProtocolAddress::new(
823            "26149721-2847-6427-8375-542683860869".to_owned(),
824            DeviceId::new(1).unwrap(),
825        );
826
827        let mut alice_store = InMemSignalProtocolStore::new(alice_identity, 1).unwrap();
828        let mut bob_store = InMemSignalProtocolStore::new(bob_identity, 2).unwrap();
829
830        alice_store
831            .session_store
832            .store_session(&bob_address, &alice_record)
833            .now_or_never()
834            .unwrap()
835            .unwrap();
836        bob_store
837            .session_store
838            .store_session(&alice_address, &bob_record)
839            .now_or_never()
840            .unwrap()
841            .unwrap();
842
843        (alice_store, bob_store, alice_address, bob_address)
844    }
845
846    /// Create a pre-key bundle for Bob, storing new key material in his store.
847    ///
848    /// The `*_id` parameters must not collide with any IDs already in the
849    /// store. Using a monotonically increasing generation counter (1, 2, …)
850    /// is sufficient.
851    fn create_bob_bundle(
852        bob_store: &mut InMemSignalProtocolStore,
853        pre_key_id: u32,
854        signed_pre_key_id: u32,
855        kyber_pre_key_id: u32,
856        rng: &mut ChaCha8Rng,
857    ) -> PreKeyBundle {
858        let identity_key_pair = bob_store
859            .get_identity_key_pair()
860            .now_or_never()
861            .unwrap()
862            .unwrap();
863
864        let pre_key = KeyPair::generate(rng);
865        let signed_pre_key = KeyPair::generate(rng);
866        let kyber_key = crate::kem::KeyPair::generate(crate::kem::KeyType::Kyber1024, rng);
867
868        let pk_id = PreKeyId::from(pre_key_id);
869        let spk_id = SignedPreKeyId::from(signed_pre_key_id);
870        let kpk_id = KyberPreKeyId::from(kyber_pre_key_id);
871
872        let spk_sig = identity_key_pair
873            .private_key()
874            .calculate_signature(&signed_pre_key.public_key.serialize(), rng)
875            .unwrap();
876        let kpk_sig = identity_key_pair
877            .private_key()
878            .calculate_signature(&kyber_key.public_key.serialize(), rng)
879            .unwrap();
880
881        bob_store
882            .save_pre_key(pk_id, &PreKeyRecord::new(pk_id, &pre_key))
883            .now_or_never()
884            .unwrap()
885            .unwrap();
886        bob_store
887            .save_signed_pre_key(
888                spk_id,
889                &SignedPreKeyRecord::new(
890                    spk_id,
891                    Timestamp::from_epoch_millis(42),
892                    &signed_pre_key,
893                    &spk_sig,
894                ),
895            )
896            .now_or_never()
897            .unwrap()
898            .unwrap();
899        bob_store
900            .save_kyber_pre_key(
901                kpk_id,
902                &KyberPreKeyRecord::new(
903                    kpk_id,
904                    Timestamp::from_epoch_millis(43),
905                    &kyber_key,
906                    &kpk_sig,
907                ),
908            )
909            .now_or_never()
910            .unwrap()
911            .unwrap();
912
913        let reg_id = bob_store
914            .get_local_registration_id()
915            .now_or_never()
916            .unwrap()
917            .unwrap();
918
919        PreKeyBundle::new(
920            reg_id,
921            DeviceId::new(1).unwrap(),
922            Some((pk_id, pre_key.public_key)),
923            spk_id,
924            signed_pre_key.public_key,
925            spk_sig.to_vec(),
926            kpk_id,
927            kyber_key.public_key.clone(),
928            kpk_sig.to_vec(),
929            *identity_key_pair.identity_key(),
930        )
931        .unwrap()
932    }
933
934    #[test]
935    fn encrypt_preserves_corruption_error_instead_of_session_not_found() {
936        let mut rng = ChaCha8Rng::seed_from_u64(0xC0FFEE);
937        let (mut alice_store, _bob_store, alice_address, bob_address) = setup_stores(&mut rng);
938        let now = SystemTime::now();
939
940        let good_record = alice_store
941            .session_store
942            .load_session(&bob_address)
943            .now_or_never()
944            .expect("sync")
945            .expect("load succeeded")
946            .expect("session exists");
947
948        let serialized = good_record.serialize().expect("serialize");
949        let mut record_pb = RecordStructure::decode(serialized.as_slice()).expect("decode record");
950        record_pb
951            .current_session
952            .as_mut()
953            .expect("current session")
954            .remote_identity_public = vec![0xFF];
955
956        let corrupted_record = SessionRecord::deserialize(record_pb.encode_to_vec().as_slice())
957            .expect("deserialize corrupted record");
958
959        alice_store
960            .session_store
961            .store_session(&bob_address, &corrupted_record)
962            .now_or_never()
963            .expect("sync")
964            .expect("store succeeded");
965
966        let legacy_err = legacy::legacy_message_encrypt(
967            b"test",
968            &bob_address,
969            &alice_address,
970            &mut alice_store.session_store,
971            &mut alice_store.identity_store,
972            now,
973            &mut rng,
974        )
975        .now_or_never()
976        .expect("sync")
977        .expect_err("legacy encrypt should fail on corrupted state");
978        assert!(
979            matches!(
980                legacy_err,
981                SignalProtocolError::InvalidSessionStructure("invalid remote identity key")
982            ),
983            "unexpected legacy error: {legacy_err:?}"
984        );
985
986        alice_store
987            .session_store
988            .store_session(&bob_address, &corrupted_record)
989            .now_or_never()
990            .expect("sync")
991            .expect("store succeeded");
992
993        let new_err = message_encrypt(
994            b"test",
995            &bob_address,
996            &alice_address,
997            &mut alice_store.session_store,
998            &mut alice_store.identity_store,
999            now,
1000            &mut rng,
1001        )
1002        .now_or_never()
1003        .expect("sync")
1004        .expect_err("new encrypt should fail on corrupted state");
1005        assert!(
1006            matches!(
1007                new_err,
1008                SignalProtocolError::InvalidSessionStructure("invalid remote identity key")
1009            ),
1010            "unexpected new error: {new_err:?}"
1011        );
1012    }
1013
1014    #[test]
1015    fn encrypt_ignores_corrupt_unused_receiver_chain() {
1016        let mut rng = ChaCha8Rng::seed_from_u64(0xACE55);
1017        let (mut alice_store, _bob_store, alice_address, bob_address) = setup_stores(&mut rng);
1018        let now = SystemTime::now();
1019
1020        let good_record = alice_store
1021            .session_store
1022            .load_session(&bob_address)
1023            .now_or_never()
1024            .expect("sync")
1025            .expect("load succeeded")
1026            .expect("session exists");
1027
1028        let serialized = good_record.serialize().expect("serialize");
1029        let mut record_pb = RecordStructure::decode(serialized.as_slice()).expect("decode record");
1030        let current_session = record_pb.current_session.as_mut().expect("current session");
1031        assert!(
1032            !current_session.receiver_chains.is_empty(),
1033            "expected at least one receiver chain"
1034        );
1035        current_session.receiver_chains[0].sender_ratchet_key = vec![0xFF];
1036
1037        let corrupted_record = SessionRecord::deserialize(record_pb.encode_to_vec().as_slice())
1038            .expect("deserialize corrupted record");
1039
1040        alice_store
1041            .session_store
1042            .store_session(&bob_address, &corrupted_record)
1043            .now_or_never()
1044            .expect("sync")
1045            .expect("store succeeded");
1046
1047        let mut legacy_rng = rng.clone();
1048        let legacy_ct = legacy::legacy_message_encrypt(
1049            b"test",
1050            &bob_address,
1051            &alice_address,
1052            &mut alice_store.session_store,
1053            &mut alice_store.identity_store,
1054            now,
1055            &mut legacy_rng,
1056        )
1057        .now_or_never()
1058        .expect("sync")
1059        .expect("legacy encrypt should ignore unused receiver-chain corruption");
1060
1061        alice_store
1062            .session_store
1063            .store_session(&bob_address, &corrupted_record)
1064            .now_or_never()
1065            .expect("sync")
1066            .expect("store succeeded");
1067
1068        let new_ct = message_encrypt(
1069            b"test",
1070            &bob_address,
1071            &alice_address,
1072            &mut alice_store.session_store,
1073            &mut alice_store.identity_store,
1074            now,
1075            &mut rng,
1076        )
1077        .now_or_never()
1078        .expect("sync")
1079        .expect("new encrypt should ignore unused receiver-chain corruption");
1080
1081        let legacy_msg = match legacy_ct {
1082            CiphertextMessage::SignalMessage(m) => m,
1083            other => panic!(
1084                "expected SignalMessage from legacy enc, got {:?}",
1085                other.message_type()
1086            ),
1087        };
1088        let new_msg = match new_ct {
1089            CiphertextMessage::SignalMessage(m) => m,
1090            other => panic!(
1091                "expected SignalMessage from new enc, got {:?}",
1092                other.message_type()
1093            ),
1094        };
1095
1096        assert_eq!(legacy_msg.serialized(), new_msg.serialized());
1097    }
1098
1099    #[test]
1100    fn decrypt_skips_corrupt_previous_session_and_uses_later_valid_previous() {
1101        let mut rng = ChaCha8Rng::seed_from_u64(0xBAD5EED);
1102        let (mut alice_store, mut bob_store, alice_address, bob_address) = setup_stores(&mut rng);
1103        let now = SystemTime::now();
1104
1105        let delayed_plaintext = b"delayed on session A".to_vec();
1106
1107        let delayed_ct = legacy::legacy_message_encrypt(
1108            &delayed_plaintext,
1109            &bob_address,
1110            &alice_address,
1111            &mut alice_store.session_store,
1112            &mut alice_store.identity_store,
1113            now,
1114            &mut rng,
1115        )
1116        .now_or_never()
1117        .expect("sync")
1118        .expect("delayed legacy enc");
1119
1120        let delayed_signal_msg = match delayed_ct {
1121            CiphertextMessage::SignalMessage(m) => m,
1122            other => panic!(
1123                "expected SignalMessage for delayed msg, got {:?}",
1124                other.message_type()
1125            ),
1126        };
1127
1128        let bundle = create_bob_bundle(&mut bob_store, 1, 1, 1, &mut rng);
1129        process_prekey_bundle(
1130            &bob_address,
1131            &alice_address,
1132            &mut alice_store.session_store,
1133            &mut alice_store.identity_store,
1134            &bundle,
1135            now,
1136            &mut rng,
1137        )
1138        .now_or_never()
1139        .expect("sync")
1140        .expect("process_prekey_bundle");
1141
1142        let session_b_init = message_encrypt(
1143            b"session B init",
1144            &bob_address,
1145            &alice_address,
1146            &mut alice_store.session_store,
1147            &mut alice_store.identity_store,
1148            now,
1149            &mut rng,
1150        )
1151        .now_or_never()
1152        .expect("sync")
1153        .expect("session B init enc");
1154
1155        message_decrypt(
1156            &session_b_init,
1157            &alice_address,
1158            &bob_address,
1159            &mut bob_store.session_store,
1160            &mut bob_store.identity_store,
1161            &mut bob_store.pre_key_store,
1162            &bob_store.signed_pre_key_store,
1163            &mut bob_store.kyber_pre_key_store,
1164            &mut rng,
1165        )
1166        .now_or_never()
1167        .expect("sync")
1168        .expect("session B init dec");
1169
1170        let session_b_ack = message_encrypt(
1171            b"session B ack",
1172            &alice_address,
1173            &bob_address,
1174            &mut bob_store.session_store,
1175            &mut bob_store.identity_store,
1176            now,
1177            &mut rng,
1178        )
1179        .now_or_never()
1180        .expect("sync")
1181        .expect("session B ack enc");
1182
1183        let session_b_ack_signal = match &session_b_ack {
1184            CiphertextMessage::SignalMessage(m) => m,
1185            other => panic!(
1186                "expected Whisper for session B ack, got {:?}",
1187                other.message_type()
1188            ),
1189        };
1190        message_decrypt_signal(
1191            session_b_ack_signal,
1192            &bob_address,
1193            &alice_address,
1194            &mut alice_store.session_store,
1195            &mut alice_store.identity_store,
1196            &mut rng,
1197        )
1198        .now_or_never()
1199        .expect("sync")
1200        .expect("session B ack dec");
1201
1202        let bob_record = bob_store
1203            .session_store
1204            .load_session(&alice_address)
1205            .now_or_never()
1206            .expect("sync")
1207            .expect("load succeeded")
1208            .expect("session exists");
1209        let serialized = bob_record.serialize().expect("serialize");
1210        let mut record_pb = RecordStructure::decode(serialized.as_slice()).expect("decode record");
1211        assert_eq!(
1212            record_pb.previous_sessions.len(),
1213            1,
1214            "expected one valid previous session"
1215        );
1216        record_pb.previous_sessions.insert(0, vec![0xFF]);
1217        let corrupted_record = SessionRecord::deserialize(record_pb.encode_to_vec().as_slice())
1218            .expect("deserialize mutated record");
1219        bob_store
1220            .session_store
1221            .store_session(&alice_address, &corrupted_record)
1222            .now_or_never()
1223            .expect("sync")
1224            .expect("store succeeded");
1225
1226        let ptext = message_decrypt_signal(
1227            &delayed_signal_msg,
1228            &alice_address,
1229            &bob_address,
1230            &mut bob_store.session_store,
1231            &mut bob_store.identity_store,
1232            &mut rng,
1233        )
1234        .now_or_never()
1235        .expect("sync")
1236        .expect("delayed msg dec via valid later previous session");
1237
1238        assert_eq!(ptext, delayed_plaintext);
1239    }
1240
1241    fn setup_two_alice_receiver_chains_on_bob(
1242        rng: &mut ChaCha8Rng,
1243    ) -> (
1244        InMemSignalProtocolStore,
1245        InMemSignalProtocolStore,
1246        ProtocolAddress,
1247        ProtocolAddress,
1248        SignalMessage,
1249    ) {
1250        let (mut alice_store, mut bob_store, alice_address, bob_address) = setup_stores(rng);
1251        let now = SystemTime::now();
1252
1253        let delayed_ct = message_encrypt(
1254            b"delayed old",
1255            &bob_address,
1256            &alice_address,
1257            &mut alice_store.session_store,
1258            &mut alice_store.identity_store,
1259            now,
1260            rng,
1261        )
1262        .now_or_never()
1263        .expect("sync")
1264        .expect("delayed old enc");
1265        let delayed_signal_msg = match delayed_ct {
1266            CiphertextMessage::SignalMessage(m) => m,
1267            other => panic!(
1268                "expected delayed SignalMessage, got {:?}",
1269                other.message_type()
1270            ),
1271        };
1272
1273        let trigger_ct = message_encrypt(
1274            b"trigger old chain advancement",
1275            &bob_address,
1276            &alice_address,
1277            &mut alice_store.session_store,
1278            &mut alice_store.identity_store,
1279            now,
1280            rng,
1281        )
1282        .now_or_never()
1283        .expect("sync")
1284        .expect("trigger enc");
1285        let trigger_signal_msg = match trigger_ct {
1286            CiphertextMessage::SignalMessage(m) => m,
1287            other => panic!(
1288                "expected trigger SignalMessage, got {:?}",
1289                other.message_type()
1290            ),
1291        };
1292        message_decrypt_signal(
1293            &trigger_signal_msg,
1294            &alice_address,
1295            &bob_address,
1296            &mut bob_store.session_store,
1297            &mut bob_store.identity_store,
1298            rng,
1299        )
1300        .now_or_never()
1301        .expect("sync")
1302        .expect("trigger dec");
1303
1304        let bob_reply_ct = message_encrypt(
1305            b"bob reply new ratchet",
1306            &alice_address,
1307            &bob_address,
1308            &mut bob_store.session_store,
1309            &mut bob_store.identity_store,
1310            now,
1311            rng,
1312        )
1313        .now_or_never()
1314        .expect("sync")
1315        .expect("bob reply enc");
1316        let bob_reply_signal_msg = match bob_reply_ct {
1317            CiphertextMessage::SignalMessage(m) => m,
1318            other => panic!(
1319                "expected bob reply SignalMessage, got {:?}",
1320                other.message_type()
1321            ),
1322        };
1323        message_decrypt_signal(
1324            &bob_reply_signal_msg,
1325            &bob_address,
1326            &alice_address,
1327            &mut alice_store.session_store,
1328            &mut alice_store.identity_store,
1329            rng,
1330        )
1331        .now_or_never()
1332        .expect("sync")
1333        .expect("bob reply dec");
1334
1335        let alice_new_chain_ct = message_encrypt(
1336            b"alice new chain",
1337            &bob_address,
1338            &alice_address,
1339            &mut alice_store.session_store,
1340            &mut alice_store.identity_store,
1341            now,
1342            rng,
1343        )
1344        .now_or_never()
1345        .expect("sync")
1346        .expect("alice new chain enc");
1347        let alice_new_chain_signal_msg = match alice_new_chain_ct {
1348            CiphertextMessage::SignalMessage(m) => m,
1349            other => panic!(
1350                "expected alice new chain SignalMessage, got {:?}",
1351                other.message_type()
1352            ),
1353        };
1354        message_decrypt_signal(
1355            &alice_new_chain_signal_msg,
1356            &alice_address,
1357            &bob_address,
1358            &mut bob_store.session_store,
1359            &mut bob_store.identity_store,
1360            rng,
1361        )
1362        .now_or_never()
1363        .expect("sync")
1364        .expect("alice new chain dec");
1365
1366        (
1367            alice_store,
1368            bob_store,
1369            alice_address,
1370            bob_address,
1371            delayed_signal_msg,
1372        )
1373    }
1374
1375    #[test]
1376    fn decrypt_ignores_corrupt_unmatched_receiver_chain() {
1377        let mut rng = ChaCha8Rng::seed_from_u64(0xD311A9);
1378        let (_alice_store, mut bob_store, alice_address, bob_address, delayed_signal_msg) =
1379            setup_two_alice_receiver_chains_on_bob(&mut rng);
1380
1381        let bob_record = bob_store
1382            .session_store
1383            .load_session(&alice_address)
1384            .now_or_never()
1385            .expect("sync")
1386            .expect("load succeeded")
1387            .expect("session exists");
1388        let serialized = bob_record.serialize().expect("serialize");
1389        let mut record_pb = RecordStructure::decode(serialized.as_slice()).expect("decode record");
1390        let current_session = record_pb.current_session.as_mut().expect("current session");
1391        assert!(
1392            current_session.receiver_chains.len() >= 2,
1393            "expected at least two receiver chains"
1394        );
1395
1396        let matched_key = delayed_signal_msg.sender_ratchet_key().serialize().to_vec();
1397        let matched_idx = current_session
1398            .receiver_chains
1399            .iter()
1400            .position(|chain| chain.sender_ratchet_key == matched_key)
1401            .expect("matching receiver chain present");
1402        let unmatched_idx = (0..current_session.receiver_chains.len())
1403            .find(|idx| *idx != matched_idx)
1404            .expect("unmatched receiver chain present");
1405
1406        current_session.receiver_chains[unmatched_idx].sender_ratchet_key = vec![0xFF];
1407
1408        let corrupted_record = SessionRecord::deserialize(record_pb.encode_to_vec().as_slice())
1409            .expect("deserialize mutated record");
1410        bob_store
1411            .session_store
1412            .store_session(&alice_address, &corrupted_record)
1413            .now_or_never()
1414            .expect("sync")
1415            .expect("store succeeded");
1416
1417        let ptext = message_decrypt_signal(
1418            &delayed_signal_msg,
1419            &alice_address,
1420            &bob_address,
1421            &mut bob_store.session_store,
1422            &mut bob_store.identity_store,
1423            &mut rng,
1424        )
1425        .now_or_never()
1426        .expect("sync")
1427        .expect("decrypt should ignore unmatched corrupt receiver chain");
1428
1429        assert_eq!(ptext, b"delayed old");
1430    }
1431
1432    #[test]
1433    fn decrypt_fails_on_corrupt_matched_receiver_chain() {
1434        let mut rng = ChaCha8Rng::seed_from_u64(0xD311AA);
1435        let (_alice_store, mut bob_store, alice_address, bob_address, delayed_signal_msg) =
1436            setup_two_alice_receiver_chains_on_bob(&mut rng);
1437
1438        let bob_record = bob_store
1439            .session_store
1440            .load_session(&alice_address)
1441            .now_or_never()
1442            .expect("sync")
1443            .expect("load succeeded")
1444            .expect("session exists");
1445        let serialized = bob_record.serialize().expect("serialize");
1446        let mut record_pb = RecordStructure::decode(serialized.as_slice()).expect("decode record");
1447        let current_session = record_pb.current_session.as_mut().expect("current session");
1448
1449        let matched_key = delayed_signal_msg.sender_ratchet_key().serialize().to_vec();
1450        let matched_idx = current_session
1451            .receiver_chains
1452            .iter()
1453            .position(|chain| chain.sender_ratchet_key == matched_key)
1454            .expect("matching receiver chain present");
1455        current_session.receiver_chains[matched_idx]
1456            .chain_key
1457            .as_mut()
1458            .expect("chain key present")
1459            .key = vec![0xFF];
1460
1461        let corrupted_record = SessionRecord::deserialize(record_pb.encode_to_vec().as_slice())
1462            .expect("deserialize mutated record");
1463        bob_store
1464            .session_store
1465            .store_session(&alice_address, &corrupted_record)
1466            .now_or_never()
1467            .expect("sync")
1468            .expect("store succeeded");
1469
1470        let err = message_decrypt_signal(
1471            &delayed_signal_msg,
1472            &alice_address,
1473            &bob_address,
1474            &mut bob_store.session_store,
1475            &mut bob_store.identity_store,
1476            &mut rng,
1477        )
1478        .now_or_never()
1479        .expect("sync")
1480        .expect_err("decrypt should fail on corrupt matched receiver chain");
1481
1482        assert_matches!(
1483            err,
1484            SignalProtocolError::InvalidMessage(
1485                CiphertextMessageType::Whisper,
1486                msg
1487            )
1488            if msg == "decryption failed"
1489        );
1490    }
1491
1492    // ── Dual-path simulation helpers ────────────────────────────────────
1493    //
1494    // Run every operation on both the refactored and legacy code paths,
1495    // asserting identical outputs (ciphertexts, plaintexts, or error
1496    // variants).  RNG sync follows the same clone-before-each-op pattern
1497    // as proptest_ciphertext_equality.
1498
1499    fn assert_store_state_equivalent(
1500        new_store: &InMemSignalProtocolStore,
1501        leg_store: &InMemSignalProtocolStore,
1502        peer_addr: &ProtocolAddress,
1503        context: &str,
1504    ) {
1505        let new_session = new_store
1506            .session_store
1507            .load_session(peer_addr)
1508            .now_or_never()
1509            .expect("sync")
1510            .expect("new load session");
1511        let leg_session = leg_store
1512            .session_store
1513            .load_session(peer_addr)
1514            .now_or_never()
1515            .expect("sync")
1516            .expect("legacy load session");
1517
1518        let new_session_bytes = new_session.map(|record| record.serialize().expect("serialize"));
1519        let leg_session_bytes = leg_session.map(|record| record.serialize().expect("serialize"));
1520        assert_eq!(
1521            new_session_bytes, leg_session_bytes,
1522            "{context}: session records diverged"
1523        );
1524
1525        let new_identity = new_store
1526            .identity_store
1527            .get_identity(peer_addr)
1528            .now_or_never()
1529            .expect("sync")
1530            .expect("new load identity")
1531            .map(|identity| identity.serialize());
1532        let leg_identity = leg_store
1533            .identity_store
1534            .get_identity(peer_addr)
1535            .now_or_never()
1536            .expect("sync")
1537            .expect("legacy load identity")
1538            .map(|identity| identity.serialize());
1539        assert_eq!(
1540            new_identity, leg_identity,
1541            "{context}: trusted identities diverged"
1542        );
1543    }
1544
1545    /// Encrypt on both paths with cloned RNG. Assert ciphertexts and sender
1546    /// state are byte-identical.
1547    fn dual_encrypt(
1548        plaintext: &[u8],
1549        recv_addr: &ProtocolAddress,
1550        send_addr: &ProtocolAddress,
1551        new_sender: &mut InMemSignalProtocolStore,
1552        leg_sender: &mut InMemSignalProtocolStore,
1553        now: SystemTime,
1554        rng: &mut ChaCha8Rng,
1555    ) -> SignalMessage {
1556        let mut leg_rng = rng.clone();
1557
1558        let new_ct = message_encrypt(
1559            plaintext,
1560            recv_addr,
1561            send_addr,
1562            &mut new_sender.session_store,
1563            &mut new_sender.identity_store,
1564            now,
1565            rng,
1566        )
1567        .now_or_never()
1568        .expect("sync")
1569        .expect("new encrypt");
1570
1571        let leg_ct = legacy::legacy_message_encrypt(
1572            plaintext,
1573            recv_addr,
1574            send_addr,
1575            &mut leg_sender.session_store,
1576            &mut leg_sender.identity_store,
1577            now,
1578            &mut leg_rng,
1579        )
1580        .now_or_never()
1581        .expect("sync")
1582        .expect("legacy encrypt");
1583
1584        assert_eq!(
1585            new_ct.serialize(),
1586            leg_ct.serialize(),
1587            "new and legacy produced different ciphertexts"
1588        );
1589        assert_eq!(
1590            new_ct.message_type(),
1591            leg_ct.message_type(),
1592            "new and legacy produced different ciphertext types"
1593        );
1594        assert_store_state_equivalent(new_sender, leg_sender, recv_addr, "encrypt");
1595
1596        match new_ct {
1597            CiphertextMessage::SignalMessage(m) => m,
1598            other => panic!(
1599                "expected SignalMessage from dual_encrypt, got {:?}",
1600                other.message_type()
1601            ),
1602        }
1603    }
1604
1605    /// Encrypt on both paths with cloned RNG. Assert full ciphertext
1606    /// equivalence, including `PreKeySignalMessage`.
1607    fn dual_encrypt_any(
1608        plaintext: &[u8],
1609        recv_addr: &ProtocolAddress,
1610        send_addr: &ProtocolAddress,
1611        new_sender: &mut InMemSignalProtocolStore,
1612        leg_sender: &mut InMemSignalProtocolStore,
1613        now: SystemTime,
1614        rng: &mut ChaCha8Rng,
1615    ) -> CiphertextMessage {
1616        let mut leg_rng = rng.clone();
1617
1618        let new_ct = message_encrypt(
1619            plaintext,
1620            recv_addr,
1621            send_addr,
1622            &mut new_sender.session_store,
1623            &mut new_sender.identity_store,
1624            now,
1625            rng,
1626        )
1627        .now_or_never()
1628        .expect("sync")
1629        .expect("new encrypt");
1630
1631        let leg_ct = legacy::legacy_message_encrypt(
1632            plaintext,
1633            recv_addr,
1634            send_addr,
1635            &mut leg_sender.session_store,
1636            &mut leg_sender.identity_store,
1637            now,
1638            &mut leg_rng,
1639        )
1640        .now_or_never()
1641        .expect("sync")
1642        .expect("legacy encrypt");
1643
1644        assert_eq!(
1645            new_ct.serialize(),
1646            leg_ct.serialize(),
1647            "new and legacy produced different ciphertexts"
1648        );
1649        assert_eq!(
1650            new_ct.message_type(),
1651            leg_ct.message_type(),
1652            "new and legacy produced different ciphertext types"
1653        );
1654        assert_store_state_equivalent(new_sender, leg_sender, recv_addr, "encrypt");
1655        new_ct
1656    }
1657
1658    /// Decrypt on both paths with cloned RNG. Assert plaintexts and receiver
1659    /// state match.
1660    fn dual_decrypt(
1661        msg: &SignalMessage,
1662        sender_addr: &ProtocolAddress,
1663        recv_addr: &ProtocolAddress,
1664        new_receiver: &mut InMemSignalProtocolStore,
1665        leg_receiver: &mut InMemSignalProtocolStore,
1666        rng: &mut ChaCha8Rng,
1667    ) -> Vec<u8> {
1668        let mut leg_rng = rng.clone();
1669
1670        let new_pt = message_decrypt_signal(
1671            msg,
1672            sender_addr,
1673            recv_addr,
1674            &mut new_receiver.session_store,
1675            &mut new_receiver.identity_store,
1676            rng,
1677        )
1678        .now_or_never()
1679        .expect("sync")
1680        .expect("new decrypt");
1681
1682        let leg_pt = legacy::legacy_message_decrypt_signal(
1683            msg,
1684            sender_addr,
1685            &mut leg_receiver.session_store,
1686            &mut leg_receiver.identity_store,
1687            &mut leg_rng,
1688        )
1689        .now_or_never()
1690        .expect("sync")
1691        .expect("legacy decrypt");
1692
1693        assert_eq!(
1694            new_pt, leg_pt,
1695            "new and legacy produced different plaintexts"
1696        );
1697        assert_store_state_equivalent(new_receiver, leg_receiver, sender_addr, "decrypt");
1698        new_pt
1699    }
1700
1701    /// Decrypt any ciphertext on both paths with cloned RNG. Assert
1702    /// plaintexts and receiver-side state match.
1703    fn dual_decrypt_any(
1704        msg: &CiphertextMessage,
1705        sender_addr: &ProtocolAddress,
1706        receiver_addr: &ProtocolAddress,
1707        new_receiver: &mut InMemSignalProtocolStore,
1708        leg_receiver: &mut InMemSignalProtocolStore,
1709        rng: &mut ChaCha8Rng,
1710    ) -> Vec<u8> {
1711        let mut leg_rng = rng.clone();
1712
1713        let new_pt = message_decrypt(
1714            msg,
1715            sender_addr,
1716            receiver_addr,
1717            &mut new_receiver.session_store,
1718            &mut new_receiver.identity_store,
1719            &mut new_receiver.pre_key_store,
1720            &new_receiver.signed_pre_key_store,
1721            &mut new_receiver.kyber_pre_key_store,
1722            rng,
1723        )
1724        .now_or_never()
1725        .expect("sync")
1726        .expect("new decrypt");
1727
1728        let leg_pt = legacy::legacy_message_decrypt(
1729            msg,
1730            sender_addr,
1731            receiver_addr,
1732            &mut leg_receiver.session_store,
1733            &mut leg_receiver.identity_store,
1734            &mut leg_receiver.pre_key_store,
1735            &leg_receiver.signed_pre_key_store,
1736            &mut leg_receiver.kyber_pre_key_store,
1737            &mut leg_rng,
1738        )
1739        .now_or_never()
1740        .expect("sync")
1741        .expect("legacy decrypt");
1742
1743        assert_eq!(
1744            new_pt, leg_pt,
1745            "new and legacy produced different plaintexts"
1746        );
1747        assert_store_state_equivalent(new_receiver, leg_receiver, sender_addr, "decrypt");
1748        new_pt
1749    }
1750
1751    fn dual_decrypt_any_result(
1752        msg: &CiphertextMessage,
1753        sender_addr: &ProtocolAddress,
1754        receiver_addr: &ProtocolAddress,
1755        new_receiver: &mut InMemSignalProtocolStore,
1756        leg_receiver: &mut InMemSignalProtocolStore,
1757        rng: &mut ChaCha8Rng,
1758    ) -> Result<Vec<u8>> {
1759        let mut leg_rng = rng.clone();
1760
1761        let new_result = message_decrypt(
1762            msg,
1763            sender_addr,
1764            receiver_addr,
1765            &mut new_receiver.session_store,
1766            &mut new_receiver.identity_store,
1767            &mut new_receiver.pre_key_store,
1768            &new_receiver.signed_pre_key_store,
1769            &mut new_receiver.kyber_pre_key_store,
1770            rng,
1771        )
1772        .now_or_never()
1773        .expect("sync");
1774
1775        let leg_result = legacy::legacy_message_decrypt(
1776            msg,
1777            sender_addr,
1778            receiver_addr,
1779            &mut leg_receiver.session_store,
1780            &mut leg_receiver.identity_store,
1781            &mut leg_receiver.pre_key_store,
1782            &leg_receiver.signed_pre_key_store,
1783            &mut leg_receiver.kyber_pre_key_store,
1784            &mut leg_rng,
1785        )
1786        .now_or_never()
1787        .expect("sync");
1788
1789        match (new_result, leg_result) {
1790            (Ok(new_pt), Ok(leg_pt)) => {
1791                assert_eq!(
1792                    new_pt, leg_pt,
1793                    "new and legacy produced different plaintexts"
1794                );
1795                assert_store_state_equivalent(new_receiver, leg_receiver, sender_addr, "decrypt");
1796                Ok(new_pt)
1797            }
1798            (Err(new_err), Err(leg_err)) => {
1799                assert_eq!(
1800                    std::mem::discriminant(&new_err),
1801                    std::mem::discriminant(&leg_err),
1802                    "error variants differ: new={new_err:?}, legacy={leg_err:?}"
1803                );
1804                assert_store_state_equivalent(new_receiver, leg_receiver, sender_addr, "decrypt");
1805                Err(new_err)
1806            }
1807            (new_result, leg_result) => panic!(
1808                "new and legacy disagreed on decrypt result: new={new_result:?}, legacy={leg_result:?}"
1809            ),
1810        }
1811    }
1812
1813    /// Decrypt on both paths, assert both fail with the same error variant.
1814    fn dual_decrypt_expect_err(
1815        msg: &SignalMessage,
1816        sender_addr: &ProtocolAddress,
1817        recv_addr: &ProtocolAddress,
1818        new_receiver: &mut InMemSignalProtocolStore,
1819        leg_receiver: &mut InMemSignalProtocolStore,
1820        rng: &mut ChaCha8Rng,
1821    ) -> SignalProtocolError {
1822        let mut leg_rng = rng.clone();
1823
1824        let new_err = message_decrypt_signal(
1825            msg,
1826            sender_addr,
1827            recv_addr,
1828            &mut new_receiver.session_store,
1829            &mut new_receiver.identity_store,
1830            rng,
1831        )
1832        .now_or_never()
1833        .expect("sync")
1834        .expect_err("expected new decrypt to fail");
1835
1836        let leg_err = legacy::legacy_message_decrypt_signal(
1837            msg,
1838            sender_addr,
1839            &mut leg_receiver.session_store,
1840            &mut leg_receiver.identity_store,
1841            &mut leg_rng,
1842        )
1843        .now_or_never()
1844        .expect("sync")
1845        .expect_err("expected legacy decrypt to fail");
1846
1847        assert_eq!(
1848            std::mem::discriminant(&new_err),
1849            std::mem::discriminant(&leg_err),
1850            "error variants differ: new={new_err:?}, legacy={leg_err:?}"
1851        );
1852        new_err
1853    }
1854
1855    // ── DualSession convenience wrapper ─────────────────────────────────
1856
1857    /// Paired new+legacy session state for readable scenario tests.
1858    struct DualSession {
1859        na: InMemSignalProtocolStore,
1860        nb: InMemSignalProtocolStore,
1861        la: InMemSignalProtocolStore,
1862        lb: InMemSignalProtocolStore,
1863        alice: ProtocolAddress,
1864        bob: ProtocolAddress,
1865        rng: ChaCha8Rng,
1866        now: SystemTime,
1867    }
1868
1869    impl DualSession {
1870        fn new(seed: u64) -> Self {
1871            let mut rng = ChaCha8Rng::seed_from_u64(seed);
1872            let (na, nb, alice, bob) = setup_stores(&mut rng);
1873            let (la, lb) = (na.clone(), nb.clone());
1874            Self {
1875                na,
1876                nb,
1877                la,
1878                lb,
1879                alice,
1880                bob,
1881                rng,
1882                now: SystemTime::now(),
1883            }
1884        }
1885
1886        fn alice_sends(&mut self, plaintext: &[u8]) -> SignalMessage {
1887            dual_encrypt(
1888                plaintext,
1889                &self.bob,
1890                &self.alice,
1891                &mut self.na,
1892                &mut self.la,
1893                self.now,
1894                &mut self.rng,
1895            )
1896        }
1897
1898        fn bob_sends(&mut self, plaintext: &[u8]) -> SignalMessage {
1899            dual_encrypt(
1900                plaintext,
1901                &self.alice,
1902                &self.bob,
1903                &mut self.nb,
1904                &mut self.lb,
1905                self.now,
1906                &mut self.rng,
1907            )
1908        }
1909
1910        fn bob_receives(&mut self, msg: &SignalMessage) -> Vec<u8> {
1911            dual_decrypt(
1912                msg,
1913                &self.alice,
1914                &self.bob,
1915                &mut self.nb,
1916                &mut self.lb,
1917                &mut self.rng,
1918            )
1919        }
1920
1921        fn alice_receives(&mut self, msg: &SignalMessage) -> Vec<u8> {
1922            dual_decrypt(
1923                msg,
1924                &self.bob,
1925                &self.alice,
1926                &mut self.na,
1927                &mut self.la,
1928                &mut self.rng,
1929            )
1930        }
1931
1932        fn bob_receives_err(&mut self, msg: &SignalMessage) -> SignalProtocolError {
1933            dual_decrypt_expect_err(
1934                msg,
1935                &self.alice,
1936                &self.bob,
1937                &mut self.nb,
1938                &mut self.lb,
1939                &mut self.rng,
1940            )
1941        }
1942
1943        #[allow(dead_code)]
1944        fn alice_receives_err(&mut self, msg: &SignalMessage) -> SignalProtocolError {
1945            dual_decrypt_expect_err(
1946                msg,
1947                &self.bob,
1948                &self.alice,
1949                &mut self.na,
1950                &mut self.la,
1951                &mut self.rng,
1952            )
1953        }
1954    }
1955
1956    impl DualParticipant {
1957        fn new(
1958            _name: &'static str,
1959            address: ProtocolAddress,
1960            rng: &mut (impl rand::Rng + rand::CryptoRng),
1961        ) -> Self {
1962            let identity = IdentityKeyPair::generate(rng);
1963            let store = InMemSignalProtocolStore::new(identity, rng.random()).unwrap();
1964            Self {
1965                address,
1966                message_queue: Vec::new(),
1967                state: DualLocalState {
1968                    new_store: store.clone(),
1969                    legacy_store: store,
1970                    pre_key_count: 0,
1971                },
1972                snapshots: Vec::new(),
1973                message_send_log: Vec::new(),
1974            }
1975        }
1976
1977        fn address(&self) -> &ProtocolAddress {
1978            &self.address
1979        }
1980
1981        fn has_pending_incoming_messages(&self) -> bool {
1982            !self.message_queue.is_empty()
1983        }
1984
1985        fn assert_equivalent_with(&self, them: &Self, context: &str) {
1986            assert_store_state_equivalent(
1987                &self.state.new_store,
1988                &self.state.legacy_store,
1989                &them.address,
1990                context,
1991            );
1992        }
1993
1994        async fn process_pre_key(
1995            &mut self,
1996            them: &mut Self,
1997            use_one_time_pre_key: bool,
1998            rng: &mut ChaCha8Rng,
1999        ) {
2000            let their_signed_pre_key_pair = KeyPair::generate(rng);
2001            let their_signed_pre_key_public = their_signed_pre_key_pair.public_key.serialize();
2002            let identity_key_pair = them.state.new_store.get_identity_key_pair().await.unwrap();
2003            let their_signed_pre_key_signature = identity_key_pair
2004                .private_key()
2005                .calculate_signature(&their_signed_pre_key_public, rng)
2006                .unwrap();
2007
2008            them.state.pre_key_count += 1;
2009            let signed_pre_key_id: SignedPreKeyId = them.state.pre_key_count.into();
2010            let signed_pre_key_record = SignedPreKeyRecord::new(
2011                signed_pre_key_id,
2012                Timestamp::from_epoch_millis(42),
2013                &their_signed_pre_key_pair,
2014                &their_signed_pre_key_signature,
2015            );
2016            them.state
2017                .new_store
2018                .save_signed_pre_key(signed_pre_key_id, &signed_pre_key_record)
2019                .await
2020                .unwrap();
2021            them.state
2022                .legacy_store
2023                .save_signed_pre_key(signed_pre_key_id, &signed_pre_key_record)
2024                .await
2025                .unwrap();
2026
2027            them.state.pre_key_count += 1;
2028            let pre_key_id: PreKeyId = them.state.pre_key_count.into();
2029            let pre_key_info = if use_one_time_pre_key {
2030                let one_time_pre_key = KeyPair::generate(rng);
2031                let pre_key_record = PreKeyRecord::new(pre_key_id, &one_time_pre_key);
2032                them.state
2033                    .new_store
2034                    .save_pre_key(pre_key_id, &pre_key_record)
2035                    .await
2036                    .unwrap();
2037                them.state
2038                    .legacy_store
2039                    .save_pre_key(pre_key_id, &pre_key_record)
2040                    .await
2041                    .unwrap();
2042                Some((pre_key_id, one_time_pre_key.public_key))
2043            } else {
2044                None
2045            };
2046
2047            let their_kyber_pre_key_pair =
2048                crate::kem::KeyPair::generate(crate::kem::KeyType::Kyber1024, rng);
2049            let their_kyber_pre_key_public = their_kyber_pre_key_pair.public_key.serialize();
2050            let their_kyber_pre_key_signature = identity_key_pair
2051                .private_key()
2052                .calculate_signature(&their_kyber_pre_key_public, rng)
2053                .unwrap();
2054
2055            them.state.pre_key_count += 1;
2056            let kyber_pre_key_id: KyberPreKeyId = them.state.pre_key_count.into();
2057            let kyber_pre_key_record = KyberPreKeyRecord::new(
2058                kyber_pre_key_id,
2059                Timestamp::from_epoch_millis(42),
2060                &their_kyber_pre_key_pair,
2061                &their_kyber_pre_key_signature,
2062            );
2063            them.state
2064                .new_store
2065                .save_kyber_pre_key(kyber_pre_key_id, &kyber_pre_key_record)
2066                .await
2067                .unwrap();
2068            them.state
2069                .legacy_store
2070                .save_kyber_pre_key(kyber_pre_key_id, &kyber_pre_key_record)
2071                .await
2072                .unwrap();
2073
2074            let their_pre_key_bundle = PreKeyBundle::new(
2075                them.state
2076                    .new_store
2077                    .get_local_registration_id()
2078                    .await
2079                    .unwrap(),
2080                DeviceId::new(1).unwrap(),
2081                pre_key_info,
2082                signed_pre_key_id,
2083                their_signed_pre_key_pair.public_key,
2084                their_signed_pre_key_signature.into_vec(),
2085                kyber_pre_key_id,
2086                their_kyber_pre_key_pair.public_key,
2087                their_kyber_pre_key_signature.into_vec(),
2088                *identity_key_pair.identity_key(),
2089            )
2090            .unwrap();
2091
2092            let mut legacy_rng = rng.clone();
2093            process_prekey_bundle(
2094                &them.address,
2095                &self.address,
2096                &mut self.state.new_store.session_store,
2097                &mut self.state.new_store.identity_store,
2098                &their_pre_key_bundle,
2099                SystemTime::UNIX_EPOCH,
2100                rng,
2101            )
2102            .await
2103            .unwrap();
2104            process_prekey_bundle(
2105                &them.address,
2106                &self.address,
2107                &mut self.state.legacy_store.session_store,
2108                &mut self.state.legacy_store.identity_store,
2109                &their_pre_key_bundle,
2110                SystemTime::UNIX_EPOCH,
2111                &mut legacy_rng,
2112            )
2113            .await
2114            .unwrap();
2115
2116            self.assert_equivalent_with(them, "process_pre_key/self");
2117            them.assert_equivalent_with(self, "process_pre_key/them");
2118            assert!(
2119                self.state
2120                    .new_store
2121                    .load_session(&them.address)
2122                    .await
2123                    .unwrap()
2124                    .expect("just created")
2125                    .has_usable_sender_chain(
2126                        SystemTime::UNIX_EPOCH,
2127                        SessionUsabilityRequirements::all(),
2128                    )
2129                    .unwrap()
2130            );
2131        }
2132
2133        async fn send_message(&mut self, them: &mut Self, rng: &mut ChaCha8Rng) {
2134            self.send_message_with_id(them, self.message_send_log.len().try_into().unwrap(), rng)
2135                .await;
2136            self.message_send_log.push(MessageStatus::Sent);
2137        }
2138
2139        async fn send_message_with_id(&mut self, them: &mut Self, id: u64, rng: &mut ChaCha8Rng) {
2140            let has_usable_sender_chain = self
2141                .state
2142                .new_store
2143                .load_session(&them.address)
2144                .await
2145                .unwrap()
2146                .and_then(|session| {
2147                    session
2148                        .has_usable_sender_chain(
2149                            SystemTime::UNIX_EPOCH,
2150                            SessionUsabilityRequirements::all(),
2151                        )
2152                        .ok()
2153                })
2154                .unwrap_or(false);
2155
2156            if !has_usable_sender_chain {
2157                self.process_pre_key(them, rng.random_bool(0.75), rng).await;
2158            }
2159
2160            let buffer = id.to_le_bytes();
2161            let outgoing_message = dual_encrypt_any(
2162                &buffer,
2163                &them.address,
2164                &self.address,
2165                &mut self.state.new_store,
2166                &mut self.state.legacy_store,
2167                SystemTime::UNIX_EPOCH,
2168                rng,
2169            );
2170
2171            let incoming_message = match outgoing_message.message_type() {
2172                CiphertextMessageType::PreKey => CiphertextMessage::PreKeySignalMessage(
2173                    PreKeySignalMessage::try_from(outgoing_message.serialize()).unwrap(),
2174                ),
2175                CiphertextMessageType::Whisper => CiphertextMessage::SignalMessage(
2176                    SignalMessage::try_from(outgoing_message.serialize()).unwrap(),
2177                ),
2178                other_type => panic!("unexpected type {other_type:?}"),
2179            };
2180
2181            them.message_queue.push((incoming_message, id));
2182            self.assert_equivalent_with(them, "send");
2183        }
2184
2185        async fn receive_messages(&mut self, them: &mut Self, rng: &mut ChaCha8Rng) {
2186            for (incoming_message, expected) in self.message_queue.split_off(0) {
2187                match incoming_message {
2188                    CiphertextMessage::SignalMessage(_)
2189                    | CiphertextMessage::PreKeySignalMessage(_) => {
2190                        match dual_decrypt_any_result(
2191                            &incoming_message,
2192                            &them.address,
2193                            &self.address,
2194                            &mut self.state.new_store,
2195                            &mut self.state.legacy_store,
2196                            rng,
2197                        ) {
2198                            Ok(decrypted) => {
2199                                assert_eq!(expected.to_le_bytes(), &decrypted[..]);
2200                                them.ack(expected);
2201                            }
2202                            Err(_) => {
2203                                let error_msg = DecryptionErrorMessage::for_original(
2204                                    incoming_message.serialize(),
2205                                    incoming_message.message_type(),
2206                                    Timestamp::from_epoch_millis(expected),
2207                                    1,
2208                                )
2209                                .expect("can encode DEM");
2210                                them.message_queue.push((
2211                                    CiphertextMessage::PlaintextContent(error_msg.into()),
2212                                    u64::MAX,
2213                                ));
2214                            }
2215                        }
2216                    }
2217                    CiphertextMessage::SenderKeyMessage(_) => unreachable!(),
2218                    CiphertextMessage::PlaintextContent(content) => {
2219                        self.handle_decryption_error(them, content, rng).await;
2220                    }
2221                }
2222            }
2223            self.assert_equivalent_with(them, "receive");
2224            them.assert_equivalent_with(self, "receive/peer");
2225        }
2226
2227        fn drop_message(&mut self, them: &mut Self) {
2228            match self.message_queue.pop() {
2229                None | Some((CiphertextMessage::PlaintextContent(_), _)) => {}
2230                Some((_, id)) => them.nack(id),
2231            }
2232        }
2233
2234        fn shuffle_messages(&mut self, rng: &mut impl rand::Rng) {
2235            use rand::seq::SliceRandom as _;
2236            self.message_queue.shuffle(rng);
2237        }
2238
2239        async fn handle_decryption_error(
2240            &mut self,
2241            them: &mut Self,
2242            content: PlaintextContent,
2243            rng: &mut ChaCha8Rng,
2244        ) {
2245            let dem = extract_decryption_error_message_from_serialized_content(content.body())
2246                .expect("all PlaintextContent is DEM");
2247            assert_eq!(dem.device_id(), 1);
2248
2249            let id = dem.timestamp().epoch_millis();
2250            let Some(status) = self.message_send_log.get(usize::try_from(id).unwrap()) else {
2251                panic!(
2252                    "failed to decrypt an unsent message {id} ({} total sent)",
2253                    self.message_send_log.len()
2254                )
2255            };
2256            match status {
2257                MessageStatus::Sent => {}
2258                MessageStatus::Dropped => {
2259                    panic!("got a decryption error for dropped message {id}");
2260                }
2261                MessageStatus::Delivered => {
2262                    panic!("got a decryption error for successfully delivered message {id}");
2263                }
2264            }
2265
2266            let ratchet_key = dem
2267                .ratchet_key()
2268                .expect("all DEMs for 1:1 messages have ratchet keys");
2269            if self
2270                .state
2271                .new_store
2272                .load_session(&them.address)
2273                .await
2274                .unwrap()
2275                .is_some_and(|session| {
2276                    session
2277                        .current_ratchet_key_matches(ratchet_key)
2278                        .expect("structurally valid session")
2279                })
2280            {
2281                self.archive_session(&them.address).await;
2282            }
2283
2284            self.send_message_with_id(them, id, rng).await;
2285        }
2286
2287        async fn archive_session(&mut self, their_address: &ProtocolAddress) {
2288            for store in [&mut self.state.new_store, &mut self.state.legacy_store] {
2289                if let Some(mut session) = store.load_session(their_address).await.unwrap() {
2290                    session.archive_current_state().unwrap();
2291                    store.store_session(their_address, &session).await.unwrap();
2292                }
2293            }
2294        }
2295
2296        fn snapshot_state(&mut self) {
2297            self.snapshots.push(self.state.clone());
2298        }
2299
2300        fn restore_from_snapshot_if_exists(&mut self, i: u8) {
2301            let i = usize::from(i);
2302            if i < self.snapshots.len() {
2303                self.state = self.snapshots.remove(i);
2304            }
2305        }
2306
2307        fn ack(&mut self, id: u64) {
2308            self.update_status(id, MessageStatus::Delivered);
2309        }
2310
2311        fn nack(&mut self, id: u64) {
2312            self.update_status(id, MessageStatus::Dropped);
2313        }
2314
2315        fn update_status(&mut self, id: u64, updated_status: MessageStatus) {
2316            let Some(status) = self.message_send_log.get_mut(usize::try_from(id).unwrap()) else {
2317                panic!(
2318                    "tried to update unsent message {id} ({} total sent)",
2319                    self.message_send_log.len()
2320                )
2321            };
2322            match status {
2323                MessageStatus::Sent => *status = updated_status,
2324                MessageStatus::Dropped => panic!("updated dropped message {id}"),
2325                MessageStatus::Delivered => panic!("updated delivered message {id}"),
2326            }
2327        }
2328
2329        async fn run_event(&mut self, them: &mut Self, event: Event, rng: &mut ChaCha8Rng) {
2330            match event {
2331                Event::Archive => self.archive_session(them.address()).await,
2332                Event::Snapshot => self.snapshot_state(),
2333                Event::Restore { index } => self.restore_from_snapshot_if_exists(index),
2334                Event::Receive => self.receive_messages(them, rng).await,
2335                Event::Drop => self.drop_message(them),
2336                Event::Shuffle => self.shuffle_messages(rng),
2337                Event::Send { count_times_eight } => {
2338                    for _ in 0..(count_times_eight / 8) {
2339                        self.send_message(them, rng).await;
2340                    }
2341                }
2342            }
2343            self.assert_equivalent_with(them, "event");
2344            them.assert_equivalent_with(self, "event/peer");
2345        }
2346    }
2347
2348    // ── Scenario tests ──────────────────────────────────────────────────
2349
2350    /// Ordinary skipped-key handling remains interoperable when messages are
2351    /// delivered with gaps, later arrive out of order, and both directions
2352    /// continue sending before the session fully catches up.
2353    #[test]
2354    fn scenario_interleaved_delivery_with_gaps_and_recovery() {
2355        let mut s = DualSession::new(0xBEEF_0001);
2356
2357        // Alice sends a burst to Bob. Bob receives only the first and third,
2358        // leaving a gap that must be recovered later from stored skipped keys.
2359        let a_msgs: Vec<_> = (0u8..4)
2360            .map(|i| (s.alice_sends(&[b'A', i]), vec![b'A', i]))
2361            .collect();
2362        assert_eq!(s.bob_receives(&a_msgs[0].0), a_msgs[0].1, "alice msg 0");
2363        assert_eq!(s.bob_receives(&a_msgs[2].0), a_msgs[2].1, "alice msg 2");
2364
2365        // Before Alice's burst is fully drained, Bob sends his own burst.
2366        // Alice receives only the later message first, exercising the same
2367        // skipped-key path in the opposite direction.
2368        let b_msgs: Vec<_> = (0u8..3)
2369            .map(|i| (s.bob_sends(&[b'B', i]), vec![b'B', i]))
2370            .collect();
2371        assert_eq!(s.alice_receives(&b_msgs[2].0), b_msgs[2].1, "bob msg 2");
2372
2373        // The missing earlier messages now arrive and must still decrypt.
2374        assert_eq!(s.bob_receives(&a_msgs[1].0), a_msgs[1].1, "alice msg 1");
2375        assert_eq!(s.bob_receives(&a_msgs[3].0), a_msgs[3].1, "alice msg 3");
2376        assert_eq!(s.alice_receives(&b_msgs[0].0), b_msgs[0].1, "bob msg 0");
2377        assert_eq!(s.alice_receives(&b_msgs[1].0), b_msgs[1].1, "bob msg 1");
2378
2379        // After recovering the gaps, both directions should continue in steady
2380        // state without any special handling.
2381        let alice_followup = s.alice_sends(b"alice steady");
2382        assert_eq!(s.bob_receives(&alice_followup), b"alice steady");
2383        let bob_followup = s.bob_sends(b"bob steady");
2384        assert_eq!(s.alice_receives(&bob_followup), b"bob steady");
2385    }
2386
2387    /// Skip past MAX_FORWARD_JUMPS — both paths must reject with the same
2388    /// error.  Encrypts on just the new path for performance (25k+ messages);
2389    /// both receivers start from identical untouched state.
2390    #[test]
2391    fn scenario_chain_jump_over_limit() {
2392        let mut rng = ChaCha8Rng::seed_from_u64(0xBEEF_0005);
2393        let (mut na, mut nb, alice, bob) = setup_stores(&mut rng);
2394        let mut lb = nb.clone();
2395        let now = SystemTime::now();
2396
2397        let count = crate::consts::MAX_FORWARD_JUMPS + 2;
2398        let mut last = None;
2399        for _ in 0..count {
2400            let ct = message_encrypt(
2401                b"x",
2402                &bob,
2403                &alice,
2404                &mut na.session_store,
2405                &mut na.identity_store,
2406                now,
2407                &mut rng,
2408            )
2409            .now_or_never()
2410            .expect("sync")
2411            .expect("encrypt");
2412            last = Some(match ct {
2413                CiphertextMessage::SignalMessage(m) => m,
2414                _ => panic!("not SignalMessage"),
2415            });
2416        }
2417        let msg = last.unwrap();
2418
2419        // Bob has received nothing — new and legacy stores are identical.
2420        let mut leg_rng = rng.clone();
2421        let new_err = message_decrypt_signal(
2422            &msg,
2423            &alice,
2424            &bob,
2425            &mut nb.session_store,
2426            &mut nb.identity_store,
2427            &mut rng,
2428        )
2429        .now_or_never()
2430        .expect("sync")
2431        .expect_err("should exceed jump limit");
2432
2433        let leg_err = legacy::legacy_message_decrypt_signal(
2434            &msg,
2435            &alice,
2436            &mut lb.session_store,
2437            &mut lb.identity_store,
2438            &mut leg_rng,
2439        )
2440        .now_or_never()
2441        .expect("sync")
2442        .expect_err("should exceed jump limit");
2443
2444        assert_eq!(
2445            std::mem::discriminant(&new_err),
2446            std::mem::discriminant(&leg_err),
2447            "error variants differ: new={new_err:?}, legacy={leg_err:?}"
2448        );
2449        assert!(
2450            matches!(new_err, SignalProtocolError::InvalidMessage(..)),
2451            "expected InvalidMessage, got {new_err:?}"
2452        );
2453    }
2454
2455    /// The initial unacknowledged send must be bit-identical as a
2456    /// `PreKeySignalMessage`, and both sides must end up with identical
2457    /// session state after the ack round-trip.
2458    #[test]
2459    fn scenario_prekey_session_establishment_equivalence() {
2460        let mut rng = ChaCha8Rng::seed_from_u64(0xBEEF_0008);
2461        let alice_identity = IdentityKeyPair::generate(&mut rng);
2462        let bob_identity = IdentityKeyPair::generate(&mut rng);
2463        let alice = ProtocolAddress::new(
2464            "9d0652a3-dcc3-4d11-975f-74d61598733f".to_owned(),
2465            DeviceId::new(1).unwrap(),
2466        );
2467        let bob = ProtocolAddress::new(
2468            "796abedb-ca4e-4f18-8803-1fde5b921f9f".to_owned(),
2469            DeviceId::new(1).unwrap(),
2470        );
2471        let now = SystemTime::now();
2472
2473        let alice_base = InMemSignalProtocolStore::new(alice_identity, 1).expect("alice store");
2474        let mut bob_base = InMemSignalProtocolStore::new(bob_identity, 2).expect("bob store");
2475        let bundle = create_bob_bundle(&mut bob_base, 1, 1, 1, &mut rng);
2476
2477        let (mut alice_new, mut alice_legacy) = (alice_base.clone(), alice_base.clone());
2478        let (mut bob_new, mut bob_legacy) = (bob_base.clone(), bob_base.clone());
2479
2480        let mut legacy_rng = rng.clone();
2481        process_prekey_bundle(
2482            &bob,
2483            &alice,
2484            &mut alice_new.session_store,
2485            &mut alice_new.identity_store,
2486            &bundle,
2487            now,
2488            &mut rng,
2489        )
2490        .now_or_never()
2491        .expect("sync")
2492        .expect("new process_prekey_bundle");
2493        process_prekey_bundle(
2494            &bob,
2495            &alice,
2496            &mut alice_legacy.session_store,
2497            &mut alice_legacy.identity_store,
2498            &bundle,
2499            now,
2500            &mut legacy_rng,
2501        )
2502        .now_or_never()
2503        .expect("sync")
2504        .expect("legacy process_prekey_bundle");
2505        assert_store_state_equivalent(&alice_new, &alice_legacy, &bob, "post-bundle");
2506
2507        let init = dual_encrypt_any(
2508            b"session init",
2509            &bob,
2510            &alice,
2511            &mut alice_new,
2512            &mut alice_legacy,
2513            now,
2514            &mut rng,
2515        );
2516        assert!(
2517            matches!(init, CiphertextMessage::PreKeySignalMessage(_)),
2518            "expected first message after bundle processing to be PreKey"
2519        );
2520
2521        assert_eq!(
2522            dual_decrypt_any(&init, &alice, &bob, &mut bob_new, &mut bob_legacy, &mut rng),
2523            b"session init"
2524        );
2525
2526        let ack = dual_encrypt_any(
2527            b"session ack",
2528            &alice,
2529            &bob,
2530            &mut bob_new,
2531            &mut bob_legacy,
2532            now,
2533            &mut rng,
2534        );
2535        assert!(
2536            matches!(ack, CiphertextMessage::SignalMessage(_)),
2537            "expected ack to be a SignalMessage"
2538        );
2539        assert_eq!(
2540            dual_decrypt_any(
2541                &ack,
2542                &bob,
2543                &alice,
2544                &mut alice_new,
2545                &mut alice_legacy,
2546                &mut rng
2547            ),
2548            b"session ack"
2549        );
2550
2551        let followup = dual_encrypt_any(
2552            b"steady state",
2553            &bob,
2554            &alice,
2555            &mut alice_new,
2556            &mut alice_legacy,
2557            now,
2558            &mut rng,
2559        );
2560        assert!(
2561            matches!(followup, CiphertextMessage::SignalMessage(_)),
2562            "expected acknowledged session to emit SignalMessage"
2563        );
2564        assert_eq!(
2565            dual_decrypt_any(
2566                &followup,
2567                &alice,
2568                &bob,
2569                &mut bob_new,
2570                &mut bob_legacy,
2571                &mut rng
2572            ),
2573            b"steady state"
2574        );
2575    }
2576
2577    /// Flip a byte in the MAC — both paths must reject identically, and
2578    /// the original message must still decrypt afterward.
2579    #[test]
2580    fn scenario_corrupted_ciphertext() {
2581        let mut s = DualSession::new(0xBEEF_0006);
2582
2583        // Warm up the session with a round-trip
2584        let msg = s.alice_sends(b"hello");
2585        assert_eq!(s.bob_receives(&msg), b"hello");
2586        let msg = s.bob_sends(b"hi");
2587        assert_eq!(s.alice_receives(&msg), b"hi");
2588
2589        // Alice sends a message; corrupt the last byte (in the MAC)
2590        let msg = s.alice_sends(b"secret");
2591        let mut corrupted_bytes = msg.serialized().to_vec();
2592        let len = corrupted_bytes.len();
2593        corrupted_bytes[len - 1] ^= 0xFF;
2594        let corrupted =
2595            SignalMessage::try_from(corrupted_bytes.as_slice()).expect("parse corrupted message");
2596
2597        let err = s.bob_receives_err(&corrupted);
2598        assert!(
2599            matches!(
2600                err,
2601                SignalProtocolError::InvalidMessage(CiphertextMessageType::Whisper, _)
2602            ),
2603            "expected InvalidMessage(Whisper, _), got {err:?}"
2604        );
2605
2606        // The original (uncorrupted) message still decrypts — failed MAC
2607        // check does not persist state changes.
2608        assert_eq!(s.bob_receives(&msg), b"secret");
2609    }
2610
2611    /// Replay an already-decrypted message — both paths must detect the
2612    /// duplicate.
2613    #[test]
2614    fn scenario_replay_message() {
2615        let mut s = DualSession::new(0xBEEF_0007);
2616
2617        let msg = s.alice_sends(b"once");
2618        assert_eq!(s.bob_receives(&msg), b"once");
2619
2620        // Same ciphertext again — should be detected as duplicate
2621        let err = s.bob_receives_err(&msg);
2622        assert!(
2623            matches!(err, SignalProtocolError::DuplicatedMessage(..)),
2624            "expected DuplicatedMessage, got {err:?}"
2625        );
2626    }
2627
2628    proptest! {
2629        /// Reuse the existing session-reset event model from `test-support`,
2630        /// but execute every encrypt/decrypt on both new and legacy codepaths.
2631        #[test]
2632        fn proptest_event_model_matches_legacy(
2633            actions in prop::collection::vec(
2634                (prop::bool::ANY, proptest_arbitrary_interop::arb::<Event>()),
2635                0..40,
2636            ),
2637        ) {
2638            let mut rng = ChaCha8Rng::seed_from_u64(0);
2639            let mut alice = DualParticipant::new(
2640                "alice",
2641                ProtocolAddress::new("9d0652a3-dcc3-4d11-975f-74d61598733f".to_owned(), DeviceId::new(1).unwrap()),
2642                &mut rng,
2643            );
2644            let mut bob = DualParticipant::new(
2645                "bob",
2646                ProtocolAddress::new("796abedb-ca4e-4f18-8803-1fde5b921f9f".to_owned(), DeviceId::new(1).unwrap()),
2647                &mut rng,
2648            );
2649
2650            for (who, event) in actions {
2651                let (me, them) = if who {
2652                    (&mut alice, &mut bob)
2653                } else {
2654                    (&mut bob, &mut alice)
2655                };
2656                me.run_event(them, event, &mut rng)
2657                    .now_or_never()
2658                    .expect("sync");
2659            }
2660
2661            while alice.has_pending_incoming_messages() || bob.has_pending_incoming_messages() {
2662                alice
2663                    .receive_messages(&mut bob, &mut rng)
2664                    .now_or_never()
2665                    .expect("sync");
2666                bob.receive_messages(&mut alice, &mut rng)
2667                    .now_or_never()
2668                    .expect("sync");
2669            }
2670
2671            for _ in 0..8 {
2672                alice
2673                    .send_message(&mut bob, &mut rng)
2674                    .now_or_never()
2675                    .expect("sync");
2676                bob.receive_messages(&mut alice, &mut rng)
2677                    .now_or_never()
2678                    .expect("sync");
2679                bob.send_message(&mut alice, &mut rng)
2680                    .now_or_never()
2681                    .expect("sync");
2682                alice
2683                    .receive_messages(&mut bob, &mut rng)
2684                    .now_or_never()
2685                    .expect("sync");
2686            }
2687
2688            alice.assert_equivalent_with(&bob, "final/alice");
2689            bob.assert_equivalent_with(&alice, "final/bob");
2690        }
2691    }
2692
2693    proptest! {
2694        /// New code can take over a session whose state was last written by
2695        /// legacy code.
2696        ///
2697        /// Runs `legacy_actions` using legacy enc+dec on both sides, then
2698        /// switches both sides to new enc+dec for `new_actions`. The session
2699        /// state — chain keys, ratchet state, SPQR state — was written by the
2700        /// legacy decrypt path; new code must read and advance it correctly.
2701        #[test]
2702        fn proptest_legacy_handover_to_new(
2703            seed in 0u64..u64::MAX,
2704            legacy_actions in prop::collection::vec(
2705                (prop::bool::ANY, prop::collection::vec(any::<u8>(), 0..=64)),
2706                1..=10,
2707            ),
2708            new_actions in prop::collection::vec(
2709                (prop::bool::ANY, prop::collection::vec(any::<u8>(), 0..=64)),
2710                1..=10,
2711            ),
2712        ) {
2713            let mut rng = ChaCha8Rng::seed_from_u64(seed);
2714            let (mut alice_store, mut bob_store, alice_address, bob_address) =
2715                setup_stores(&mut rng);
2716            let now = std::time::SystemTime::now();
2717
2718            // Phase 1: legacy enc + legacy dec advance the session state.
2719            for (alice_sends, plaintext) in &legacy_actions {
2720                let (sender, receiver, recv_addr, send_addr) = if *alice_sends {
2721                    (&mut alice_store, &mut bob_store, &bob_address, &alice_address)
2722                } else {
2723                    (&mut bob_store, &mut alice_store, &alice_address, &bob_address)
2724                };
2725
2726                let ct = legacy::legacy_message_encrypt(
2727                    plaintext,
2728                    recv_addr,
2729                    send_addr,
2730                    &mut sender.session_store,
2731                    &mut sender.identity_store,
2732                    now,
2733                    &mut rng,
2734                )
2735                .now_or_never()
2736                .expect("sync")
2737                .expect("legacy enc");
2738
2739                let signal_msg = match &ct {
2740                    CiphertextMessage::SignalMessage(m) => m,
2741                    other => panic!(
2742                        "expected SignalMessage in legacy phase, got {:?}",
2743                        other.message_type()
2744                    ),
2745                };
2746
2747                let ptext = legacy::legacy_message_decrypt_signal(
2748                    signal_msg,
2749                    send_addr,
2750                    &mut receiver.session_store,
2751                    &mut receiver.identity_store,
2752                    &mut rng,
2753                )
2754                .now_or_never()
2755                .expect("sync")
2756                .expect("legacy dec");
2757
2758                prop_assert_eq!(ptext, plaintext.clone(), "legacy phase: wrong plaintext");
2759            }
2760
2761            // Phase 2: new code takes over the session state left by legacy.
2762            for (alice_sends, plaintext) in &new_actions {
2763                let (sender, receiver, recv_addr, send_addr) = if *alice_sends {
2764                    (&mut alice_store, &mut bob_store, &bob_address, &alice_address)
2765                } else {
2766                    (&mut bob_store, &mut alice_store, &alice_address, &bob_address)
2767                };
2768
2769                let ct = message_encrypt(
2770                    plaintext,
2771                    recv_addr,
2772                    send_addr,
2773                    &mut sender.session_store,
2774                    &mut sender.identity_store,
2775                    now,
2776                    &mut rng,
2777                )
2778                .now_or_never()
2779                .expect("sync")
2780                .expect("new enc");
2781
2782                let signal_msg = match &ct {
2783                    CiphertextMessage::SignalMessage(m) => m,
2784                    other => panic!(
2785                        "expected SignalMessage in new phase, got {:?}",
2786                        other.message_type()
2787                    ),
2788                };
2789
2790                let ptext = message_decrypt_signal(
2791                    signal_msg,
2792                    send_addr,
2793                    recv_addr,
2794                    &mut receiver.session_store,
2795                    &mut receiver.identity_store,
2796                    &mut rng,
2797                )
2798                .now_or_never()
2799                .expect("sync")
2800                .expect("new dec");
2801
2802                prop_assert_eq!(ptext, plaintext.clone(), "new phase: wrong plaintext");
2803            }
2804        }
2805
2806        /// A message encrypted by legacy code on a previous session is correctly
2807        /// decrypted by new code after a session transition.
2808        ///
2809        /// Scenario:
2810        ///   1. `pre_actions` exchanges on session A using legacy enc+dec.
2811        ///   2. Alice encrypts a delayed Whisper on session A using legacy enc
2812        ///      (not yet delivered to Bob).
2813        ///   3. Alice processes a new pre-key bundle from Bob → session B
2814        ///      (session A is archived in Alice's previous_sessions).
2815        ///   4. Alice and Bob establish session B on both sides and exchange
2816        ///      `post_actions` using new enc+dec. Bob's session A' is archived to
2817        ///      his previous_sessions when he receives Alice's first session-B
2818        ///      PreKeySignalMessage.
2819        ///   5. The delayed message from step 2 is delivered to Bob via new
2820        ///      message_decrypt_signal. try_decrypt_from_record must fail on
2821        ///      the current session B' and succeed on the previous session A',
2822        ///      exercising promote_old_session.
2823        #[test]
2824        fn proptest_delayed_message_via_previous_session(
2825            seed in 0u64..u64::MAX,
2826            pre_actions in prop::collection::vec(
2827                (prop::bool::ANY, prop::collection::vec(any::<u8>(), 0..=6)),
2828                0..=6,
2829            ),
2830            post_actions in prop::collection::vec(
2831                (prop::bool::ANY, prop::collection::vec(any::<u8>(), 0..=64)),
2832                0..=6,
2833            ),
2834            delayed_plaintext in prop::collection::vec(any::<u8>(), 1..=64),
2835        ) {
2836            let mut rng = ChaCha8Rng::seed_from_u64(seed);
2837            let (mut alice_store, mut bob_store, alice_address, bob_address) =
2838                setup_stores(&mut rng);
2839            let now = std::time::SystemTime::now();
2840
2841            // ── Phase 1: legacy enc+dec on session A ─────────────────────────
2842
2843            for (alice_sends, plaintext) in &pre_actions {
2844                let (sender, receiver, recv_addr, send_addr) = if *alice_sends {
2845                    (&mut alice_store, &mut bob_store, &bob_address, &alice_address)
2846                } else {
2847                    (&mut bob_store, &mut alice_store, &alice_address, &bob_address)
2848                };
2849
2850                let ct = legacy::legacy_message_encrypt(
2851                    plaintext,
2852                    recv_addr,
2853                    send_addr,
2854                    &mut sender.session_store,
2855                    &mut sender.identity_store,
2856                    now,
2857                    &mut rng,
2858                )
2859                .now_or_never()
2860                .expect("sync")
2861                .expect("pre legacy enc");
2862
2863                let signal_msg = match &ct {
2864                    CiphertextMessage::SignalMessage(m) => m,
2865                    other => panic!(
2866                        "expected SignalMessage in pre phase, got {:?}",
2867                        other.message_type()
2868                    ),
2869                };
2870
2871                let ptext = legacy::legacy_message_decrypt_signal(
2872                    signal_msg,
2873                    send_addr,
2874                    &mut receiver.session_store,
2875                    &mut receiver.identity_store,
2876                    &mut rng,
2877                )
2878                .now_or_never()
2879                .expect("sync")
2880                .expect("pre legacy dec");
2881
2882                prop_assert_eq!(ptext, plaintext.clone(), "pre phase: wrong plaintext");
2883            }
2884
2885            // ── Encrypt delayed message (not yet delivered) ──────────────────
2886
2887            // This Whisper is encrypted by Alice on session A's current chain.
2888            // Bob's session A' is at the same chain index, so it can decrypt
2889            // it later.
2890            let delayed_ct = legacy::legacy_message_encrypt(
2891                &delayed_plaintext,
2892                &bob_address,
2893                &alice_address,
2894                &mut alice_store.session_store,
2895                &mut alice_store.identity_store,
2896                now,
2897                &mut rng,
2898            )
2899            .now_or_never()
2900            .expect("sync")
2901            .expect("delayed legacy enc");
2902
2903            let delayed_signal_msg = match delayed_ct {
2904                CiphertextMessage::SignalMessage(m) => m,
2905                other => panic!(
2906                    "expected SignalMessage for delayed msg, got {:?}",
2907                    other.message_type()
2908                ),
2909            };
2910
2911            // ── Session transition: A → B ─────────────────────────────────────
2912
2913            // Alice processes a new pre-key bundle from Bob. This calls
2914            // promote_state, archiving session A to Alice's previous_sessions.
2915            let bundle = create_bob_bundle(&mut bob_store, 1, 1, 1, &mut rng);
2916            process_prekey_bundle(
2917                &bob_address,
2918                &alice_address,
2919                &mut alice_store.session_store,
2920                &mut alice_store.identity_store,
2921                &bundle,
2922                now,
2923                &mut rng,
2924            )
2925            .now_or_never()
2926            .expect("sync")
2927            .expect("process_prekey_bundle");
2928
2929            // Alice sends her first message on session B (a PreKeySignalMessage
2930            // since B is unacknowledged). When Bob decrypts it, process_prekey
2931            // fires and archives his session A' to previous_sessions.
2932            let session_b_init = message_encrypt(
2933                b"session B init",
2934                &bob_address,
2935                &alice_address,
2936                &mut alice_store.session_store,
2937                &mut alice_store.identity_store,
2938                now,
2939                &mut rng,
2940            )
2941            .now_or_never()
2942            .expect("sync")
2943            .expect("session B init enc");
2944
2945            message_decrypt(
2946                &session_b_init,
2947                &alice_address,
2948                &bob_address,
2949                &mut bob_store.session_store,
2950                &mut bob_store.identity_store,
2951                &mut bob_store.pre_key_store,
2952                &bob_store.signed_pre_key_store,
2953                &mut bob_store.kyber_pre_key_store,
2954                &mut rng,
2955            )
2956            .now_or_never()
2957            .expect("sync")
2958            .expect("session B init dec");
2959            // Bob now has: current = session B', previous_sessions = [session A']
2960
2961            // Bob acknowledges session B on Alice's side. Without this, Alice
2962            // would keep wrapping messages as PreKeySignalMessage, and each
2963            // would trigger another process_prekey on Bob's side, nesting
2964            // sessions further. After this round-trip both sides send Whispers.
2965            let session_b_ack = message_encrypt(
2966                b"session B ack",
2967                &alice_address,
2968                &bob_address,
2969                &mut bob_store.session_store,
2970                &mut bob_store.identity_store,
2971                now,
2972                &mut rng,
2973            )
2974            .now_or_never()
2975            .expect("sync")
2976            .expect("session B ack enc");
2977
2978            let session_b_ack_signal = match &session_b_ack {
2979                CiphertextMessage::SignalMessage(m) => m,
2980                other => panic!(
2981                    "expected Whisper for session B ack, got {:?}",
2982                    other.message_type()
2983                ),
2984            };
2985            message_decrypt_signal(
2986                session_b_ack_signal,
2987                &bob_address,
2988                &alice_address,
2989                &mut alice_store.session_store,
2990                &mut alice_store.identity_store,
2991                &mut rng,
2992            )
2993            .now_or_never()
2994            .expect("sync")
2995            .expect("session B ack dec");
2996            // Alice's session B is now acknowledged; all her sends are Whispers.
2997
2998            // ── Phase 2: new enc+dec on session B ────────────────────────────
2999
3000            for (alice_sends, plaintext) in &post_actions {
3001                let (sender, receiver, recv_addr, send_addr) = if *alice_sends {
3002                    (&mut alice_store, &mut bob_store, &bob_address, &alice_address)
3003                } else {
3004                    (&mut bob_store, &mut alice_store, &alice_address, &bob_address)
3005                };
3006
3007                let ct = message_encrypt(
3008                    plaintext,
3009                    recv_addr,
3010                    send_addr,
3011                    &mut sender.session_store,
3012                    &mut sender.identity_store,
3013                    now,
3014                    &mut rng,
3015                )
3016                .now_or_never()
3017                .expect("sync")
3018                .expect("post new enc");
3019
3020                let signal_msg = match &ct {
3021                    CiphertextMessage::SignalMessage(m) => m,
3022                    other => panic!(
3023                        "expected SignalMessage in post phase, got {:?}",
3024                        other.message_type()
3025                    ),
3026                };
3027
3028                let ptext = message_decrypt_signal(
3029                    signal_msg,
3030                    send_addr,
3031                    recv_addr,
3032                    &mut receiver.session_store,
3033                    &mut receiver.identity_store,
3034                    &mut rng,
3035                )
3036                .now_or_never()
3037                .expect("sync")
3038                .expect("post new dec");
3039
3040                prop_assert_eq!(ptext, plaintext.clone(), "post phase: wrong plaintext");
3041            }
3042
3043            // ── Deliver delayed message ───────────────────────────────────────
3044
3045            // Bob's current session is B'. The delayed message was encrypted
3046            // under session A. try_decrypt_from_record must:
3047            //   1. Try session B' → fail (wrong ratchet key / counter).
3048            //   2. Try session A' from previous_sessions → succeed.
3049            //   3. Call promote_old_session, making A' the current session.
3050            let ptext = message_decrypt_signal(
3051                &delayed_signal_msg,
3052                &alice_address,
3053                &bob_address,
3054                &mut bob_store.session_store,
3055                &mut bob_store.identity_store,
3056                &mut rng,
3057            )
3058            .now_or_never()
3059            .expect("sync")
3060            .expect("delayed msg dec via previous session");
3061
3062            prop_assert_eq!(
3063                ptext,
3064                delayed_plaintext.clone(),
3065                "delayed message: wrong plaintext"
3066            );
3067        }
3068
3069        /// New encrypt and legacy encrypt produce byte-identical ciphertexts
3070        /// when given the same RNG state.
3071        ///
3072        /// Runs two parallel session pairs from the same initial state.
3073        /// Before each encrypt, the RNG is cloned so that both the new and
3074        /// legacy paths start from the same randomness.  If the RNG
3075        /// consumption is identical (one `spqr::send` call per encrypt,
3076        /// one `KeyPair::generate` per DH ratchet step on decrypt), the
3077        /// ciphertexts must be equal.  The receiver sessions are advanced
3078        /// with the same split so that subsequent iterations stay in sync.
3079        #[test]
3080        fn proptest_ciphertext_equality(
3081            seed in 0u64..u64::MAX,
3082            actions in prop::collection::vec(
3083                (prop::bool::ANY, prop::collection::vec(any::<u8>(), 0..=64)),
3084                1..=20,
3085            ),
3086        ) {
3087            let mut rng = ChaCha8Rng::seed_from_u64(seed);
3088            let (mut alice_new, mut bob_new, alice_address, bob_address) =
3089                setup_stores(&mut rng);
3090            // Clone the freshly-initialized stores so both paths start from
3091            // identical state.
3092            let (mut alice_legacy, mut bob_legacy) = (alice_new.clone(), bob_new.clone());
3093            let now = SystemTime::now();
3094
3095            for (alice_sends, plaintext) in &actions {
3096                // Borrow the right stores for sender/receiver on each path.
3097                let (
3098                    (sender_new, receiver_new),
3099                    (sender_legacy, receiver_legacy),
3100                    sender_addr,
3101                    receiver_addr,
3102                ) = if *alice_sends {
3103                    (
3104                        (&mut alice_new, &mut bob_new),
3105                        (&mut alice_legacy, &mut bob_legacy),
3106                        &alice_address,
3107                        &bob_address,
3108                    )
3109                } else {
3110                    (
3111                        (&mut bob_new, &mut alice_new),
3112                        (&mut bob_legacy, &mut alice_legacy),
3113                        &bob_address,
3114                        &alice_address,
3115                    )
3116                };
3117
3118                // Both encrypt calls start from the same RNG position.
3119                let mut enc_rng = rng.clone();
3120
3121                let new_ct = message_encrypt(
3122                    plaintext,
3123                    receiver_addr,
3124                    sender_addr,
3125                    &mut sender_new.session_store,
3126                    &mut sender_new.identity_store,
3127                    now,
3128                    &mut rng,
3129                )
3130                .now_or_never()
3131                .expect("sync")
3132                .expect("new encrypt succeeded");
3133
3134                let legacy_ct = legacy::legacy_message_encrypt(
3135                    plaintext,
3136                    receiver_addr,
3137                    sender_addr,
3138                    &mut sender_legacy.session_store,
3139                    &mut sender_legacy.identity_store,
3140                    now,
3141                    &mut enc_rng,
3142                )
3143                .now_or_never()
3144                .expect("sync")
3145                .expect("legacy encrypt succeeded");
3146
3147                let new_msg = match &new_ct {
3148                    CiphertextMessage::SignalMessage(m) => m,
3149                    other => panic!(
3150                        "expected SignalMessage from new enc, got {:?}",
3151                        other.message_type()
3152                    ),
3153                };
3154                let legacy_msg = match &legacy_ct {
3155                    CiphertextMessage::SignalMessage(m) => m,
3156                    other => panic!(
3157                        "expected SignalMessage from legacy enc, got {:?}",
3158                        other.message_type()
3159                    ),
3160                };
3161
3162                prop_assert_eq!(
3163                    new_msg.serialized(),
3164                    legacy_msg.serialized(),
3165                    "new and legacy produced different ciphertexts from the same RNG state"
3166                );
3167
3168                // Advance both receiver sessions with the same RNG split so
3169                // their states stay in sync for the next iteration.
3170                let mut dec_rng = rng.clone();
3171
3172                let _ = message_decrypt_signal(
3173                    new_msg,
3174                    sender_addr,
3175                    receiver_addr,
3176                    &mut receiver_new.session_store,
3177                    &mut receiver_new.identity_store,
3178                    &mut rng,
3179                )
3180                .now_or_never()
3181                .expect("sync")
3182                .expect("new decrypt succeeded");
3183
3184                let _ = legacy::legacy_message_decrypt_signal(
3185                    legacy_msg,
3186                    sender_addr,
3187                    &mut receiver_legacy.session_store,
3188                    &mut receiver_legacy.identity_store,
3189                    &mut dec_rng,
3190                )
3191                .now_or_never()
3192                .expect("sync")
3193                .expect("legacy decrypt succeeded");
3194            }
3195        }
3196    }
3197}