Skip to main content

wacore_libsignal/protocol/ratchet/
keys.rs

1//
2// Copyright 2020 Signal Messenger, LLC.
3// SPDX-License-Identifier: AGPL-3.0-only
4//
5
6use std::fmt;
7use std::sync::LazyLock;
8
9use hmac::{Hmac, HmacReset, KeyInit, Mac};
10use sha2::Sha256;
11
12use crate::protocol::{PrivateKey, PublicKey, Result, crypto, stores::session_structure};
13
14/// Lazy message key generator that defers key derivation and avoids re-serialization.
15///
16/// This enum enables two optimizations:
17/// 1. **Lazy derivation**: Keys are only derived from seed when actually needed for encryption
18/// 2. **Zero-cost round-trip**: Keys loaded from protobuf are kept in serialized form and
19///    returned as-is when saving, avoiding unnecessary deserialization and re-serialization
20pub enum MessageKeyGenerator {
21    /// Seed for lazy derivation - keys derived on demand
22    Seed(([u8; 32], u32)),
23    /// Original protobuf - zero-cost pass-through on save
24    Serialized(session_structure::chain::MessageKey),
25}
26
27impl MessageKeyGenerator {
28    #[inline]
29    pub fn new_from_seed(seed: &[u8; 32], counter: u32) -> Self {
30        Self::Seed((*seed, counter))
31    }
32
33    /// Generate the actual MessageKeys, deriving them if necessary.
34    /// This is called when keys are needed for actual encryption/decryption.
35    pub fn generate_keys(self) -> MessageKeys {
36        match self {
37            Self::Seed((seed, counter)) => MessageKeys::derive_keys(&seed, None, counter),
38            Self::Serialized(pb) => {
39                // Parse on demand - only when keys are actually needed.
40                // Note: from_pb() validates field lengths before creating Serialized,
41                // so these conversions should always succeed. The unwrap_or fallbacks
42                // exist only as a defensive measure; in debug builds we assert to
43                // catch any invariant violations.
44                let cipher_key = pb
45                    .cipher_key
46                    .as_deref()
47                    .and_then(|b| <[u8; 32]>::try_from(b).ok());
48                let mac_key = pb
49                    .mac_key
50                    .as_deref()
51                    .and_then(|b| <[u8; 32]>::try_from(b).ok());
52                let iv = pb.iv.as_deref().and_then(|b| <[u8; 16]>::try_from(b).ok());
53
54                debug_assert!(
55                    cipher_key.is_some() && mac_key.is_some() && iv.is_some(),
56                    "Serialized MessageKeyGenerator has invalid field lengths - from_pb should have rejected this"
57                );
58
59                MessageKeys {
60                    cipher_key: cipher_key.unwrap_or([0u8; 32]),
61                    mac_key: mac_key.unwrap_or([0u8; 32]),
62                    iv: iv.unwrap_or([0u8; 16]),
63                    counter: pb.index.unwrap_or(0),
64                }
65            }
66        }
67    }
68
69    /// Convert to protobuf format for storage.
70    /// Zero-cost for Serialized variant (pass-through), allocates for others.
71    ///
72    /// The seed is persisted next to the keys it derives: the derivation is
73    /// one-way, so a record that kept only the derived keys could never be
74    /// projected back into a seed-based external format.
75    pub fn into_pb(self) -> session_structure::chain::MessageKey {
76        match self {
77            // Zero-cost pass-through: return original protobuf unchanged
78            Self::Serialized(pb) => pb,
79            // Need to serialize: derive keys and convert
80            Self::Seed((seed, counter)) => {
81                use bytes::BytesMut;
82                let keys = MessageKeys::derive_keys(&seed, None, counter);
83                // The four fields are written, stored and dropped together, so
84                // they share one buffer: `split_to` hands out refcounted views
85                // instead of copying each field into its own allocation.
86                let mut material = BytesMut::with_capacity(
87                    keys.cipher_key().len() + keys.mac_key().len() + keys.iv().len() + seed.len(),
88                );
89                material.extend_from_slice(keys.cipher_key());
90                material.extend_from_slice(keys.mac_key());
91                material.extend_from_slice(keys.iv());
92                material.extend_from_slice(&seed);
93
94                let mut material = material.freeze();
95                let cipher_key = material.split_to(keys.cipher_key().len());
96                let mac_key = material.split_to(keys.mac_key().len());
97                let iv = material.split_to(keys.iv().len());
98                session_structure::chain::MessageKey {
99                    cipher_key: Some(cipher_key),
100                    mac_key: Some(mac_key),
101                    iv: Some(iv),
102                    index: Some(keys.counter()),
103                    seed: Some(material),
104                }
105            }
106        }
107    }
108
109    /// Load from protobuf without parsing - keeps original bytes for zero-cost round-trip.
110    pub fn from_pb(
111        pb: session_structure::chain::MessageKey,
112    ) -> std::result::Result<Self, &'static str> {
113        // Validate the protobuf has required fields
114        if pb.cipher_key.as_ref().is_some_and(|b| b.len() == 32)
115            && pb.mac_key.as_ref().is_some_and(|b| b.len() == 32)
116            && pb.iv.as_ref().is_some_and(|b| b.len() == 16)
117        {
118            // Keep as Serialized for zero-cost round-trip
119            Ok(Self::Serialized(pb))
120        } else {
121            Err("invalid message key format")
122        }
123    }
124
125    /// Get the counter/index without fully parsing the keys.
126    #[inline]
127    pub fn counter(&self) -> u32 {
128        match self {
129            Self::Seed((_, counter)) => *counter,
130            Self::Serialized(pb) => pb.index.unwrap_or(0),
131        }
132    }
133}
134
135#[derive(Clone, Copy)]
136pub struct MessageKeys {
137    cipher_key: [u8; 32],
138    mac_key: [u8; 32],
139    iv: [u8; 16],
140    counter: u32,
141}
142
143/// Message-key derivation runs HKDF with no salt, so its extract step is always
144/// HMAC keyed by a constant zero block. Caching that keyed state lets each
145/// derivation clone past the ipad/opad key schedule instead of recomputing it.
146static MESSAGE_KEY_EXTRACT_HMAC: LazyLock<Hmac<Sha256>> =
147    LazyLock::new(|| Hmac::<Sha256>::new_from_slice(&[0u8; 32]).expect("32-byte HMAC key"));
148
149impl MessageKeys {
150    pub fn derive_keys(
151        input_key_material: &[u8],
152        optional_salt: Option<&[u8]>,
153        counter: u32,
154    ) -> Self {
155        let mut okm = [0; 80];
156        match optional_salt {
157            None => {
158                let mut extract = MESSAGE_KEY_EXTRACT_HMAC.clone();
159                extract.update(input_key_material);
160                let prk = extract.finalize().into_bytes();
161                hkdf::Hkdf::<Sha256>::from_prk(&prk)
162                    .expect("PRK is hash-sized")
163                    .expand(b"WhisperMessageKeys", &mut okm)
164                    .expect("valid output length");
165            }
166            Some(salt) => {
167                hkdf::Hkdf::<Sha256>::new(Some(salt), input_key_material)
168                    .expand(b"WhisperMessageKeys", &mut okm)
169                    .expect("valid output length");
170            }
171        }
172
173        // `split_first_chunk` carries the window lengths in the type, so the
174        // 32/32/16 split of the 80-byte OKM is checked at compile time and the
175        // `Option` folds away against the fixed-size array.
176        let (cipher_key, rest) = okm.split_first_chunk::<32>().expect("80-byte OKM");
177        let (mac_key, rest) = rest.split_first_chunk::<32>().expect("80-byte OKM");
178        let (iv, _) = rest.split_first_chunk::<16>().expect("80-byte OKM");
179
180        MessageKeys {
181            cipher_key: *cipher_key,
182            mac_key: *mac_key,
183            iv: *iv,
184            counter,
185        }
186    }
187
188    #[inline]
189    pub fn cipher_key(&self) -> &[u8; 32] {
190        &self.cipher_key
191    }
192
193    #[inline]
194    pub fn mac_key(&self) -> &[u8; 32] {
195        &self.mac_key
196    }
197
198    #[inline]
199    pub fn iv(&self) -> &[u8; 16] {
200        &self.iv
201    }
202
203    #[inline]
204    pub fn counter(&self) -> u32 {
205        self.counter
206    }
207}
208
209#[derive(Clone, Copy, Debug)]
210pub struct ChainKey {
211    key: [u8; 32],
212    index: u32,
213}
214
215impl ChainKey {
216    const MESSAGE_KEY_SEED: [u8; 1] = [0x01u8];
217    const CHAIN_KEY_SEED: [u8; 1] = [0x02u8];
218
219    pub fn new(key: [u8; 32], index: u32) -> Self {
220        Self { key, index }
221    }
222
223    #[inline]
224    pub fn key(&self) -> &[u8; 32] {
225        &self.key
226    }
227
228    #[inline]
229    pub fn index(&self) -> u32 {
230        self.index
231    }
232
233    pub fn next_chain_key(&self) -> Result<Self> {
234        Ok(Self {
235            key: self.calculate_base_material(Self::CHAIN_KEY_SEED),
236            index: self.index.checked_add(1).ok_or_else(|| {
237                crate::protocol::SignalProtocolError::InvalidState(
238                    "next_chain_key",
239                    "chain key index overflow (u32::MAX)".to_string(),
240                )
241            })?,
242        })
243    }
244
245    pub fn message_keys(&self) -> MessageKeyGenerator {
246        MessageKeyGenerator::new_from_seed(
247            &self.calculate_base_material(Self::MESSAGE_KEY_SEED),
248            self.index,
249        )
250    }
251
252    /// Compute both message keys and next chain key in one call, reusing HMAC key setup.
253    #[inline]
254    pub fn step_with_message_keys(&self) -> Result<(MessageKeyGenerator, Self)> {
255        let mut hmac = HmacReset::<Sha256>::new_from_slice(&self.key)
256            .expect("HMAC-SHA256 should accept any size key");
257
258        hmac.update(&Self::MESSAGE_KEY_SEED);
259        let message_key_seed: [u8; 32] = hmac.finalize_reset().into_bytes().into();
260
261        hmac.update(&Self::CHAIN_KEY_SEED);
262        let next_key: [u8; 32] = hmac.finalize().into_bytes().into();
263
264        let message_keys = MessageKeyGenerator::new_from_seed(&message_key_seed, self.index);
265        let next_chain = Self {
266            key: next_key,
267            index: self.index.checked_add(1).ok_or_else(|| {
268                crate::protocol::SignalProtocolError::InvalidState(
269                    "step_with_message_keys",
270                    "chain key index overflow (u32::MAX)".to_string(),
271                )
272            })?,
273        };
274
275        Ok((message_keys, next_chain))
276    }
277
278    fn calculate_base_material(&self, seed: [u8; 1]) -> [u8; 32] {
279        crypto::hmac_sha256(&self.key, &seed)
280    }
281}
282
283#[derive(Clone, Debug)]
284pub struct RootKey {
285    key: [u8; 32],
286}
287
288impl RootKey {
289    pub fn new(key: [u8; 32]) -> Self {
290        Self { key }
291    }
292
293    pub fn key(&self) -> &[u8; 32] {
294        &self.key
295    }
296
297    pub fn create_chain(
298        self,
299        their_ratchet_key: &PublicKey,
300        our_ratchet_key: &PrivateKey,
301    ) -> Result<(RootKey, ChainKey)> {
302        let shared_secret = our_ratchet_key.calculate_agreement(their_ratchet_key)?;
303        let mut derived_secret_bytes = [0; 64];
304        hkdf::Hkdf::<Sha256>::new(Some(&self.key), &shared_secret)
305            .expand(b"WhisperRatchet", &mut derived_secret_bytes)
306            .expect("valid output length");
307
308        let (root_key, chain_key) = derived_secret_bytes
309            .split_first_chunk::<32>()
310            .expect("64-byte OKM");
311        let (chain_key, _) = chain_key.split_first_chunk::<32>().expect("64-byte OKM");
312
313        Ok((
314            RootKey { key: *root_key },
315            ChainKey {
316                key: *chain_key,
317                index: 0,
318            },
319        ))
320    }
321}
322
323impl fmt::Display for RootKey {
324    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
325        write!(f, "{}", hex::encode(self.key))
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    /// The no-salt fast path keys HKDF-extract with a cached zero block; it must
334    /// stay byte-identical to `Hkdf::new(None, ikm)`, or every derived Signal
335    /// message key would desync from the peer.
336    #[test]
337    fn derive_keys_no_salt_matches_plain_hkdf() {
338        let mut ikms: Vec<Vec<u8>> = (0..16u8)
339            .map(|i| vec![i.wrapping_mul(31).wrapping_add(7); 32])
340            .collect();
341        ikms.push(Vec::new());
342        ikms.push(vec![0xAB; 3]);
343        ikms.push(vec![0x5Au8; 64]);
344
345        for (i, ikm) in ikms.iter().enumerate() {
346            let keys = MessageKeys::derive_keys(ikm, None, i as u32);
347
348            let mut okm = [0u8; 80];
349            hkdf::Hkdf::<Sha256>::new(None, ikm)
350                .expand(b"WhisperMessageKeys", &mut okm)
351                .expect("valid output length");
352
353            assert_eq!(&keys.cipher_key()[..], &okm[0..32]);
354            assert_eq!(&keys.mac_key()[..], &okm[32..64]);
355            assert_eq!(&keys.iv()[..], &okm[64..80]);
356            assert_eq!(keys.counter(), i as u32);
357        }
358    }
359
360    /// Test that ChainKey properly derives the next chain key
361    /// The chain key advances by HMAC-SHA256 with a constant seed
362    #[test]
363    fn test_chain_key_stepping() {
364        let initial_key = [0x42u8; 32];
365        let chain_key = ChainKey::new(initial_key, 0);
366
367        // Step the chain multiple times
368        let chain1 = chain_key.next_chain_key().expect("chain key step");
369        let chain2 = chain1.next_chain_key().expect("chain key step");
370        let chain3 = chain2.next_chain_key().expect("chain key step");
371
372        // Verify index increments correctly
373        assert_eq!(chain_key.index(), 0);
374        assert_eq!(chain1.index(), 1);
375        assert_eq!(chain2.index(), 2);
376        assert_eq!(chain3.index(), 3);
377
378        // Verify keys are different at each step
379        assert_ne!(chain_key.key(), chain1.key());
380        assert_ne!(chain1.key(), chain2.key());
381        assert_ne!(chain2.key(), chain3.key());
382
383        // Verify determinism: same initial key produces same chain
384        let chain_key_copy = ChainKey::new(initial_key, 0);
385        let chain1_copy = chain_key_copy.next_chain_key().expect("chain key step");
386        assert_eq!(chain1.key(), chain1_copy.key());
387        assert_eq!(chain1.index(), chain1_copy.index());
388    }
389
390    /// Test that MessageKeys derivation is deterministic
391    #[test]
392    fn test_message_keys_derivation() {
393        let chain_key = ChainKey::new([0x55u8; 32], 10);
394        let message_keys_gen = chain_key.message_keys();
395        let message_keys = message_keys_gen.generate_keys();
396
397        // Verify counter is preserved
398        assert_eq!(message_keys.counter(), 10);
399
400        // Verify key sizes
401        assert_eq!(message_keys.cipher_key().len(), 32);
402        assert_eq!(message_keys.mac_key().len(), 32);
403        assert_eq!(message_keys.iv().len(), 16);
404
405        // Verify determinism
406        let chain_key2 = ChainKey::new([0x55u8; 32], 10);
407        let message_keys2 = chain_key2.message_keys().generate_keys();
408        assert_eq!(message_keys.cipher_key(), message_keys2.cipher_key());
409        assert_eq!(message_keys.mac_key(), message_keys2.mac_key());
410        assert_eq!(message_keys.iv(), message_keys2.iv());
411    }
412
413    /// Test that different chain key indices produce different message keys
414    #[test]
415    fn test_message_keys_differ_by_counter() {
416        let key = [0xAAu8; 32];
417        let chain_key_0 = ChainKey::new(key, 0);
418        let chain_key_1 = ChainKey::new(key, 1);
419
420        let mk0 = chain_key_0.message_keys().generate_keys();
421        let mk1 = chain_key_1.message_keys().generate_keys();
422
423        // Same base key but different counters should produce different message keys
424        // because the counter is used in the derivation
425        assert_eq!(mk0.counter(), 0);
426        assert_eq!(mk1.counter(), 1);
427
428        // The keys themselves depend on the chain key (not counter), but counter differs
429        // If we advance the chain, we should get different underlying keys
430        let chain_advanced = chain_key_0.next_chain_key().expect("chain key step");
431        let mk_advanced = chain_advanced.message_keys().generate_keys();
432
433        assert_ne!(mk0.cipher_key(), mk_advanced.cipher_key());
434    }
435
436    /// Test RootKey creation and accessor
437    #[test]
438    fn test_root_key_basic() {
439        let key_bytes = [0x12u8; 32];
440        let root_key = RootKey::new(key_bytes);
441
442        assert_eq!(root_key.key(), &key_bytes);
443
444        // Test Display trait
445        let display = format!("{}", root_key);
446        assert_eq!(display, hex::encode(key_bytes));
447    }
448
449    /// Test stepping the chain 100 times to verify no issues with long chains
450    #[test]
451    fn test_chain_key_long_chain() {
452        let mut chain = ChainKey::new([0x01u8; 32], 0);
453
454        for i in 0..100 {
455            assert_eq!(chain.index(), i);
456            chain = chain.next_chain_key().expect("chain key step");
457        }
458
459        assert_eq!(chain.index(), 100);
460    }
461
462    /// Test MessageKeyGenerator from seed
463    #[test]
464    fn test_message_key_generator_from_seed() {
465        let seed = [0xBBu8; 32];
466        let counter = 42;
467
468        let generator = MessageKeyGenerator::new_from_seed(&seed, counter);
469        let keys = generator.generate_keys();
470
471        assert_eq!(keys.counter(), counter);
472        assert_eq!(keys.cipher_key().len(), 32);
473        assert_eq!(keys.mac_key().len(), 32);
474        assert_eq!(keys.iv().len(), 16);
475
476        // Verify determinism
477        let generator2 = MessageKeyGenerator::new_from_seed(&seed, counter);
478        let keys2 = generator2.generate_keys();
479        assert_eq!(keys.cipher_key(), keys2.cipher_key());
480    }
481
482    /// The seed is one-way, so persisting it alongside the keys it derives is
483    /// the only thing that keeps a skipped key exportable.
484    #[test]
485    fn into_pb_persists_the_seed_next_to_the_derived_keys() {
486        let seed = [0x3Cu8; 32];
487        let pb = MessageKeyGenerator::new_from_seed(&seed, 11).into_pb();
488        let expected = MessageKeys::derive_keys(&seed, None, 11);
489
490        assert_eq!(pb.index, Some(11));
491        assert_eq!(pb.seed.as_deref(), Some(&seed[..]));
492        assert_eq!(pb.cipher_key.as_deref(), Some(&expected.cipher_key()[..]));
493        assert_eq!(pb.mac_key.as_deref(), Some(&expected.mac_key()[..]));
494        assert_eq!(pb.iv.as_deref(), Some(&expected.iv()[..]));
495    }
496
497    /// Reloading a persisted key must keep using the stored derived material,
498    /// not re-derive from the seed: a decrypt that changed keys here would
499    /// silently fail the MAC. Only material the seed does *not* produce can
500    /// tell the two apart, so the fixture stores a deliberately unrelated
501    /// triple next to it.
502    #[test]
503    fn reloaded_keys_come_from_the_persisted_derived_material() {
504        use bytes::Bytes;
505
506        let seed = [0x9Eu8; 32];
507        let mut pb = MessageKeyGenerator::new_from_seed(&seed, 4).into_pb();
508        pb.cipher_key = Some(Bytes::from_static(&[0x11; 32]));
509        pb.mac_key = Some(Bytes::from_static(&[0x22; 32]));
510        pb.iv = Some(Bytes::from_static(&[0x33; 16]));
511        let from_seed = MessageKeys::derive_keys(&seed, None, 4);
512
513        let reloaded = MessageKeyGenerator::from_pb(pb)
514            .expect("key stays loadable")
515            .generate_keys();
516
517        assert_eq!(reloaded.cipher_key(), &[0x11; 32]);
518        assert_eq!(reloaded.mac_key(), &[0x22; 32]);
519        assert_eq!(reloaded.iv(), &[0x33; 16]);
520        assert_eq!(reloaded.counter(), 4);
521        assert_ne!(reloaded.cipher_key(), from_seed.cipher_key());
522    }
523
524    /// Keys persisted before the seed was retained must still load and produce
525    /// exactly what was stored.
526    #[test]
527    fn seedless_persisted_keys_still_load() {
528        let seed = [0x9Eu8; 32];
529        let mut pb = MessageKeyGenerator::new_from_seed(&seed, 4).into_pb();
530        let expected = MessageKeys::derive_keys(&seed, None, 4);
531        pb.seed = None;
532
533        let reloaded = MessageKeyGenerator::from_pb(pb)
534            .expect("seedless key stays loadable")
535            .generate_keys();
536
537        assert_eq!(reloaded.cipher_key(), expected.cipher_key());
538        assert_eq!(reloaded.mac_key(), expected.mac_key());
539        assert_eq!(reloaded.iv(), expected.iv());
540        assert_eq!(reloaded.counter(), 4);
541    }
542
543    /// A seed alone is not a loadable key: `from_pb` still requires the three
544    /// derived fields, so a downgrade that drops the seed cannot fail the
545    /// whole record.
546    #[test]
547    fn from_pb_still_rejects_a_key_without_derived_material() {
548        let mut pb = MessageKeyGenerator::new_from_seed(&[0x2Bu8; 32], 0).into_pb();
549        pb.cipher_key = None;
550
551        assert!(MessageKeyGenerator::from_pb(pb).is_err());
552    }
553
554    /// Test MessageKeys derive_keys with known inputs
555    #[test]
556    fn test_message_keys_derive_with_salt() {
557        let input_key_material = [0x11u8; 32];
558        let salt = [0x22u8; 32];
559        let counter = 5;
560
561        let keys1 = MessageKeys::derive_keys(&input_key_material, Some(&salt), counter);
562        let keys2 = MessageKeys::derive_keys(&input_key_material, None, counter);
563
564        // With salt vs without salt should produce different keys
565        assert_ne!(keys1.cipher_key(), keys2.cipher_key());
566        assert_ne!(keys1.mac_key(), keys2.mac_key());
567        assert_ne!(keys1.iv(), keys2.iv());
568
569        // Both should have same counter
570        assert_eq!(keys1.counter(), counter);
571        assert_eq!(keys2.counter(), counter);
572    }
573
574    /// Test that step_with_message_keys produces the same results as
575    /// calling message_keys() and next_chain_key() separately
576    #[test]
577    fn test_step_with_message_keys_equivalence() {
578        let initial_key = [0x77u8; 32];
579        let chain_key = ChainKey::new(initial_key, 5);
580
581        // Get results using separate calls
582        let message_keys_separate = chain_key.message_keys().generate_keys();
583        let next_chain_separate = chain_key.next_chain_key().expect("chain key step");
584
585        // Get results using optimized combined call
586        let (message_keys_gen_combined, next_chain_combined) = chain_key
587            .step_with_message_keys()
588            .expect("step with message keys");
589        let message_keys_combined = message_keys_gen_combined.generate_keys();
590
591        // Verify message keys are identical
592        assert_eq!(
593            message_keys_separate.cipher_key(),
594            message_keys_combined.cipher_key()
595        );
596        assert_eq!(
597            message_keys_separate.mac_key(),
598            message_keys_combined.mac_key()
599        );
600        assert_eq!(message_keys_separate.iv(), message_keys_combined.iv());
601        assert_eq!(
602            message_keys_separate.counter(),
603            message_keys_combined.counter()
604        );
605
606        // Verify next chain key is identical
607        assert_eq!(next_chain_separate.key(), next_chain_combined.key());
608        assert_eq!(next_chain_separate.index(), next_chain_combined.index());
609    }
610
611    /// Test step_with_message_keys over multiple iterations
612    #[test]
613    fn test_step_with_message_keys_chain() {
614        let initial_key = [0x88u8; 32];
615        let mut chain_separate = ChainKey::new(initial_key, 0);
616        let mut chain_combined = ChainKey::new(initial_key, 0);
617
618        // Step both chains 10 times and verify they stay in sync
619        for i in 0..10 {
620            let msg_keys_sep = chain_separate.message_keys().generate_keys();
621            chain_separate = chain_separate.next_chain_key().expect("chain key step");
622
623            let (msg_keys_gen_comb, next_chain) = chain_combined
624                .step_with_message_keys()
625                .expect("step with message keys");
626            let msg_keys_comb = msg_keys_gen_comb.generate_keys();
627            chain_combined = next_chain;
628
629            // Verify message keys match
630            assert_eq!(
631                msg_keys_sep.cipher_key(),
632                msg_keys_comb.cipher_key(),
633                "cipher_key mismatch at iteration {i}"
634            );
635
636            // Verify chain keys match
637            assert_eq!(
638                chain_separate.key(),
639                chain_combined.key(),
640                "chain key mismatch at iteration {i}"
641            );
642            assert_eq!(chain_separate.index(), chain_combined.index());
643        }
644    }
645
646    /// Verify next_chain_key fails at u32::MAX instead of wrapping.
647    #[test]
648    fn test_chain_key_overflow_regression() {
649        let chain = ChainKey::new([0xFFu8; 32], u32::MAX);
650        assert!(matches!(
651            chain.next_chain_key(),
652            Err(crate::protocol::SignalProtocolError::InvalidState(..))
653        ));
654    }
655
656    /// Verify step_with_message_keys also fails at u32::MAX.
657    #[test]
658    fn test_chain_key_overflow_step_with_message_keys() {
659        let chain = ChainKey::new([0xEEu8; 32], u32::MAX);
660        assert!(matches!(
661            chain.step_with_message_keys(),
662            Err(crate::protocol::SignalProtocolError::InvalidState(..))
663        ));
664    }
665
666    /// u32::MAX-1 advances once (to MAX), then fails on the next step.
667    #[test]
668    fn test_chain_key_overflow_boundary() {
669        let chain_at_max = ChainKey::new([0xDDu8; 32], u32::MAX - 1)
670            .next_chain_key()
671            .expect("advance to u32::MAX");
672        assert_eq!(chain_at_max.index(), u32::MAX);
673        assert!(chain_at_max.next_chain_key().is_err());
674    }
675
676    /// Chained advances from MAX-3 succeed until MAX, then error.
677    #[test]
678    fn test_chain_key_overflow_chained_advances() {
679        let c1 = ChainKey::new([0xCCu8; 32], u32::MAX - 3);
680        let c2 = c1.next_chain_key().expect("to MAX-2");
681        let c3 = c2.next_chain_key().expect("to MAX-1");
682        let c4 = c3.next_chain_key().expect("to MAX");
683        assert_eq!(c4.index(), u32::MAX);
684        assert!(c4.next_chain_key().is_err());
685    }
686}