Skip to main content

wacore_libsignal/protocol/
record_components.rs

1//! Owned, validated projections of persisted Signal records.
2//!
3//! These types keep codec-generated structures private while providing a
4//! stable representation for record handoff. Conversion validates fixed-width
5//! key material and emits public keys in their canonical serialized form. The
6//! explicit field mappings are intentional: a generated schema change must
7//! fail to compile here instead of silently dropping protocol state.
8
9use std::fmt;
10
11use buffa::MessageField;
12use bytes::Bytes;
13
14use crate::core::curve::PublicKey;
15use crate::protocol::error::{Result, SignalProtocolError};
16use crate::protocol::ratchet::MessageKeyGenerator;
17use crate::protocol::ratchet::keys::MessageKeys;
18use crate::protocol::stores::{
19    SenderKeyStateStructure, SessionStructure, sender_key_state_structure, session_structure,
20};
21
22const PRIVATE_KEY_BYTES: usize = 32;
23const SYMMETRIC_KEY_BYTES: usize = 32;
24const MESSAGE_IV_BYTES: usize = 16;
25
26/// Complete protocol state carried by a session record.
27#[derive(Debug, Clone, PartialEq, Eq, Default)]
28pub struct SessionRecordComponents {
29    pub current_session: Option<SessionComponents>,
30    pub previous_sessions: Vec<SessionComponents>,
31}
32
33/// One current or archived session state.
34#[derive(Clone, PartialEq, Eq, Default)]
35pub struct SessionComponents {
36    pub session_version: Option<u32>,
37    pub local_identity_public: Option<Vec<u8>>,
38    pub remote_identity_public: Option<Vec<u8>>,
39    pub root_key: Option<Vec<u8>>,
40    pub previous_counter: Option<u32>,
41    pub sender_chain: Option<SessionChainComponents>,
42    pub receiver_chains: Vec<SessionChainComponents>,
43    pub pending_key_exchange: Option<PendingKeyExchangeComponents>,
44    pub pending_pre_key: Option<PendingPreKeyComponents>,
45    pub remote_registration_id: Option<u32>,
46    pub local_registration_id: Option<u32>,
47    pub needs_refresh: Option<bool>,
48    pub alice_base_key: Option<Vec<u8>>,
49}
50
51/// A sender or receiver ratchet chain and its skipped message keys.
52///
53/// Imported sender chains must be structurally complete. Imported receiver
54/// chains must not carry private material; persisted receiver private fields
55/// are ignored for compatibility with the canonical reader.
56#[derive(Clone, PartialEq, Eq, Default)]
57pub struct SessionChainComponents {
58    pub sender_ratchet_key: Option<Vec<u8>>,
59    pub sender_ratchet_key_private: Option<Vec<u8>>,
60    pub chain_key: Option<SessionChainKeyComponents>,
61    pub message_keys: Vec<SessionMessageKeyComponents>,
62}
63
64/// Position and secret material for a session chain.
65#[derive(Clone, PartialEq, Eq, Default)]
66pub struct SessionChainKeyComponents {
67    pub index: Option<u32>,
68    pub key: Option<Vec<u8>>,
69}
70
71/// A skipped message key identified by its chain index.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct SessionMessageKeyComponents {
74    pub index: u32,
75    pub material: SessionMessageKeyMaterial,
76}
77
78/// Secret material used by a skipped session message key.
79///
80/// `Seed` is the compact form: importing it expands the derived keys with the
81/// protocol's canonical derivation, and exporting a record that retained the
82/// seed returns it. `Derived` is what remains when there is no seed to return,
83/// which is the case for keys persisted before the seed was retained.
84#[derive(Clone, Copy, PartialEq, Eq)]
85pub enum SessionMessageKeyMaterial {
86    Seed([u8; SYMMETRIC_KEY_BYTES]),
87    Derived {
88        cipher_key: [u8; SYMMETRIC_KEY_BYTES],
89        mac_key: [u8; SYMMETRIC_KEY_BYTES],
90        iv: [u8; MESSAGE_IV_BYTES],
91    },
92}
93
94/// Pending key-exchange state.
95#[derive(Clone, PartialEq, Eq, Default)]
96pub struct PendingKeyExchangeComponents {
97    pub sequence: Option<u32>,
98    pub local_base_key: Option<Vec<u8>>,
99    pub local_base_key_private: Option<Vec<u8>>,
100    pub local_ratchet_key: Option<Vec<u8>>,
101    pub local_ratchet_key_private: Option<Vec<u8>>,
102    pub local_identity_key: Option<Vec<u8>>,
103    pub local_identity_key_private: Option<Vec<u8>>,
104}
105
106/// Pending pre-key state.
107#[derive(Clone, PartialEq, Eq, Default)]
108pub struct PendingPreKeyComponents {
109    pub pre_key_id: Option<u32>,
110    pub signed_pre_key_id: Option<i32>,
111    pub base_key: Option<Vec<u8>>,
112    pub kyber_pre_key_id: Option<u32>,
113    pub kyber_ciphertext: Option<Vec<u8>>,
114}
115
116/// Complete protocol state carried by a sender-key record.
117#[derive(Debug, Clone, PartialEq, Eq, Default)]
118pub struct SenderKeyRecordComponents {
119    pub states: Vec<SenderKeyStateComponents>,
120}
121
122/// One sender-key state, including skipped message keys.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct SenderKeyStateComponents {
125    pub key_id: u32,
126    pub chain_key: SenderChainKeyComponents,
127    pub signing_key: SenderSigningKeyComponents,
128    pub message_keys: Vec<SenderMessageKeyComponents>,
129}
130
131/// Position and secret material for a sender chain.
132#[derive(Clone, PartialEq, Eq)]
133pub struct SenderChainKeyComponents {
134    pub iteration: u32,
135    pub seed: Vec<u8>,
136}
137
138/// Public and optional private signing-key material.
139#[derive(Clone, PartialEq, Eq)]
140pub struct SenderSigningKeyComponents {
141    pub public: Vec<u8>,
142    pub private: Option<Vec<u8>>,
143}
144
145/// A skipped sender message key identified by its iteration.
146#[derive(Clone, PartialEq, Eq)]
147pub struct SenderMessageKeyComponents {
148    pub iteration: u32,
149    pub seed: Vec<u8>,
150}
151
152struct Redacted;
153
154impl fmt::Debug for Redacted {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        f.write_str("<redacted>")
157    }
158}
159
160macro_rules! impl_redacted_debug {
161    ($type:ident { visible: [$($visible:ident),* $(,)?], secret: [$($secret:ident),* $(,)?] }) => {
162        impl fmt::Debug for $type {
163            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164                let mut debug = f.debug_struct(stringify!($type));
165                $(debug.field(stringify!($visible), &self.$visible);)*
166                $(debug.field(stringify!($secret), &Redacted);)*
167                debug.finish()
168            }
169        }
170    };
171}
172
173impl_redacted_debug!(SessionComponents {
174    visible: [
175        session_version,
176        previous_counter,
177        sender_chain,
178        receiver_chains,
179        pending_key_exchange,
180        pending_pre_key,
181        remote_registration_id,
182        local_registration_id,
183        needs_refresh,
184    ],
185    secret: [
186        local_identity_public,
187        remote_identity_public,
188        root_key,
189        alice_base_key,
190    ]
191});
192impl_redacted_debug!(SessionChainComponents {
193    visible: [chain_key, message_keys],
194    secret: [sender_ratchet_key, sender_ratchet_key_private]
195});
196impl_redacted_debug!(SessionChainKeyComponents {
197    visible: [index],
198    secret: [key]
199});
200impl_redacted_debug!(PendingKeyExchangeComponents {
201    visible: [sequence],
202    secret: [
203        local_base_key,
204        local_base_key_private,
205        local_ratchet_key,
206        local_ratchet_key_private,
207        local_identity_key,
208        local_identity_key_private,
209    ]
210});
211impl_redacted_debug!(PendingPreKeyComponents {
212    visible: [pre_key_id, signed_pre_key_id, kyber_pre_key_id],
213    secret: [base_key, kyber_ciphertext]
214});
215impl_redacted_debug!(SenderChainKeyComponents {
216    visible: [iteration],
217    secret: [seed]
218});
219impl_redacted_debug!(SenderSigningKeyComponents {
220    visible: [],
221    secret: [public, private]
222});
223impl_redacted_debug!(SenderMessageKeyComponents {
224    visible: [iteration],
225    secret: [seed]
226});
227
228impl fmt::Debug for SessionMessageKeyMaterial {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            Self::Seed(_) => f.debug_tuple("Seed").field(&Redacted).finish(),
232            Self::Derived { .. } => f
233                .debug_struct("Derived")
234                .field("cipher_key", &Redacted)
235                .field("mac_key", &Redacted)
236                .field("iv", &Redacted)
237                .finish(),
238        }
239    }
240}
241
242fn invalid(field: &'static str, expectation: &'static str) -> SignalProtocolError {
243    SignalProtocolError::InvalidArgument(format!("{field} must be {expectation}"))
244}
245
246fn normalize_public_key(mut value: Vec<u8>, field: &'static str) -> Result<Vec<u8>> {
247    let key = match value.len() {
248        PublicKey::RAW_KEY_LEN => PublicKey::from_djb_public_key_bytes(&value),
249        PublicKey::SERIALIZED_KEY_LEN => PublicKey::deserialize(&value),
250        _ => {
251            return Err(invalid(field, "a raw or canonically serialized public key"));
252        }
253    }
254    .map_err(|_| invalid(field, "a valid public key"))?;
255
256    value.clear();
257    value.extend_from_slice(&key.serialize());
258    Ok(value)
259}
260
261fn optional_public_key(value: Option<Vec<u8>>, field: &'static str) -> Result<Option<Vec<u8>>> {
262    value
263        .map(|value| normalize_public_key(value, field))
264        .transpose()
265}
266
267fn required_public_key(value: Option<Vec<u8>>, field: &'static str) -> Result<Vec<u8>> {
268    normalize_public_key(value.ok_or_else(|| invalid(field, "present"))?, field)
269}
270
271fn exact_bytes(value: Vec<u8>, length: usize, field: &'static str) -> Result<Vec<u8>> {
272    if value.len() == length {
273        Ok(value)
274    } else {
275        Err(SignalProtocolError::InvalidArgument(format!(
276            "{field} must be {length} bytes"
277        )))
278    }
279}
280
281fn optional_exact_bytes(
282    value: Option<Vec<u8>>,
283    length: usize,
284    field: &'static str,
285) -> Result<Option<Vec<u8>>> {
286    value
287        .map(|value| exact_bytes(value, length, field))
288        .transpose()
289}
290
291fn required_exact_bytes(
292    value: Option<Vec<u8>>,
293    length: usize,
294    field: &'static str,
295) -> Result<Vec<u8>> {
296    exact_bytes(
297        value.ok_or_else(|| invalid(field, "present"))?,
298        length,
299        field,
300    )
301}
302
303/// Fixed-width key material, copied out of the persisted `Bytes` without
304/// allocating.
305fn key_material<const N: usize>(value: Bytes, field: &'static str) -> Result<[u8; N]> {
306    <[u8; N]>::try_from(value.as_ref())
307        .map_err(|_| SignalProtocolError::InvalidArgument(format!("{field} must be {N} bytes")))
308}
309
310fn required_key_material<const N: usize>(
311    value: Option<Bytes>,
312    field: &'static str,
313) -> Result<[u8; N]> {
314    key_material(value.ok_or_else(|| invalid(field, "present"))?, field)
315}
316
317impl SessionMessageKeyComponents {
318    fn into_structure(self) -> session_structure::chain::MessageKey {
319        match self.material {
320            SessionMessageKeyMaterial::Seed(seed) => {
321                MessageKeyGenerator::new_from_seed(&seed, self.index).into_pb()
322            }
323            SessionMessageKeyMaterial::Derived {
324                cipher_key,
325                mac_key,
326                iv,
327            } => session_structure::chain::MessageKey {
328                index: Some(self.index),
329                cipher_key: Some(Bytes::copy_from_slice(&cipher_key)),
330                mac_key: Some(Bytes::copy_from_slice(&mac_key)),
331                iv: Some(Bytes::copy_from_slice(&iv)),
332                seed: None,
333            },
334        }
335    }
336
337    fn from_structure(mut value: session_structure::chain::MessageKey) -> Result<Self> {
338        let index = value
339            .index
340            .ok_or_else(|| invalid("session message-key index", "present"))?;
341        // A persisted seed supersedes the derived triple on export rather than
342        // complementing it, so one that does not reproduce that triple would
343        // hand a consumer a key decrypting nothing, undetectably. Both are
344        // written from the same material; disagreeing means the record is
345        // corrupt.
346        if let Some(seed) = value.seed.take() {
347            let seed = key_material(seed, "session message-key seed")?;
348            let derived = MessageKeys::derive_keys(&seed, None, index);
349            if value.cipher_key.as_deref() != Some(derived.cipher_key())
350                || value.mac_key.as_deref() != Some(derived.mac_key())
351                || value.iv.as_deref() != Some(derived.iv())
352            {
353                return Err(invalid(
354                    "session message-key seed",
355                    "consistent with the derived keys stored beside it",
356                ));
357            }
358            return Ok(Self {
359                index,
360                material: SessionMessageKeyMaterial::Seed(seed),
361            });
362        }
363        Ok(Self {
364            index,
365            material: SessionMessageKeyMaterial::Derived {
366                cipher_key: required_key_material(value.cipher_key, "session message cipher key")?,
367                mac_key: required_key_material(value.mac_key, "session message MAC key")?,
368                iv: required_key_material(value.iv, "session message IV")?,
369            },
370        })
371    }
372}
373
374impl SessionChainKeyComponents {
375    fn into_structure(self) -> Result<session_structure::chain::ChainKey> {
376        Ok(session_structure::chain::ChainKey {
377            index: self.index,
378            key: optional_exact_bytes(self.key, SYMMETRIC_KEY_BYTES, "session chain key")?
379                .map(Bytes::from),
380        })
381    }
382
383    fn from_structure(value: session_structure::chain::ChainKey) -> Result<Self> {
384        Ok(Self {
385            index: value.index,
386            key: optional_exact_bytes(
387                value.key.map(|value| value.to_vec()),
388                SYMMETRIC_KEY_BYTES,
389                "session chain key",
390            )?,
391        })
392    }
393
394    fn into_required_structure(self) -> Result<session_structure::chain::ChainKey> {
395        Ok(session_structure::chain::ChainKey {
396            index: Some(
397                self.index
398                    .ok_or_else(|| invalid("sender chain-key index", "present"))?,
399            ),
400            key: Some(Bytes::from(required_exact_bytes(
401                self.key,
402                SYMMETRIC_KEY_BYTES,
403                "sender chain key",
404            )?)),
405        })
406    }
407
408    fn from_required_structure(value: session_structure::chain::ChainKey) -> Result<Self> {
409        Ok(Self {
410            index: Some(
411                value
412                    .index
413                    .ok_or_else(|| invalid("sender chain-key index", "present"))?,
414            ),
415            key: Some(required_exact_bytes(
416                value.key.map(|value| value.to_vec()),
417                SYMMETRIC_KEY_BYTES,
418                "sender chain key",
419            )?),
420        })
421    }
422}
423
424impl SessionChainComponents {
425    fn into_sender_structure(self) -> Result<session_structure::Chain> {
426        Ok(session_structure::Chain {
427            sender_ratchet_key: Some(required_public_key(
428                self.sender_ratchet_key,
429                "sender ratchet public key",
430            )?),
431            sender_ratchet_key_private: Some(required_exact_bytes(
432                self.sender_ratchet_key_private,
433                PRIVATE_KEY_BYTES,
434                "sender ratchet private key",
435            )?),
436            chain_key: MessageField::some(
437                self.chain_key
438                    .ok_or_else(|| invalid("sender chain key", "present"))?
439                    .into_required_structure()?,
440            ),
441            message_keys: self
442                .message_keys
443                .into_iter()
444                .map(SessionMessageKeyComponents::into_structure)
445                .collect(),
446        })
447    }
448
449    fn into_receiver_structure(self) -> Result<session_structure::Chain> {
450        if self.sender_ratchet_key_private.is_some() {
451            return Err(invalid(
452                "receiver ratchet private key",
453                "absent from receiver chains",
454            ));
455        }
456
457        Ok(session_structure::Chain {
458            sender_ratchet_key: optional_public_key(
459                self.sender_ratchet_key,
460                "receiver ratchet public key",
461            )?,
462            sender_ratchet_key_private: None,
463            chain_key: self
464                .chain_key
465                .map(SessionChainKeyComponents::into_structure)
466                .transpose()?
467                .into(),
468            message_keys: self
469                .message_keys
470                .into_iter()
471                .map(SessionMessageKeyComponents::into_structure)
472                .collect(),
473        })
474    }
475
476    fn from_sender_structure(mut value: session_structure::Chain) -> Result<Self> {
477        Ok(Self {
478            sender_ratchet_key: Some(required_public_key(
479                value.sender_ratchet_key,
480                "sender ratchet public key",
481            )?),
482            sender_ratchet_key_private: Some(required_exact_bytes(
483                value.sender_ratchet_key_private,
484                PRIVATE_KEY_BYTES,
485                "sender ratchet private key",
486            )?),
487            chain_key: Some(SessionChainKeyComponents::from_required_structure(
488                value
489                    .chain_key
490                    .take()
491                    .ok_or_else(|| invalid("sender chain key", "present"))?,
492            )?),
493            message_keys: value
494                .message_keys
495                .into_iter()
496                .map(SessionMessageKeyComponents::from_structure)
497                .collect::<Result<_>>()?,
498        })
499    }
500
501    fn from_receiver_structure(mut value: session_structure::Chain) -> Result<Self> {
502        // The official reader ignores this field for receiver chains, including
503        // historical non-canonical values.
504        value.sender_ratchet_key_private = None;
505        Ok(Self {
506            sender_ratchet_key: optional_public_key(
507                value.sender_ratchet_key,
508                "receiver ratchet public key",
509            )?,
510            sender_ratchet_key_private: None,
511            chain_key: value
512                .chain_key
513                .take()
514                .map(SessionChainKeyComponents::from_structure)
515                .transpose()?,
516            message_keys: value
517                .message_keys
518                .into_iter()
519                .map(SessionMessageKeyComponents::from_structure)
520                .collect::<Result<_>>()?,
521        })
522    }
523}
524
525impl PendingKeyExchangeComponents {
526    fn into_structure(self) -> Result<session_structure::PendingKeyExchange> {
527        Ok(session_structure::PendingKeyExchange {
528            sequence: self.sequence,
529            local_base_key: optional_public_key(self.local_base_key, "local base public key")?,
530            local_base_key_private: optional_exact_bytes(
531                self.local_base_key_private,
532                PRIVATE_KEY_BYTES,
533                "local base private key",
534            )?,
535            local_ratchet_key: optional_public_key(
536                self.local_ratchet_key,
537                "local ratchet public key",
538            )?,
539            local_ratchet_key_private: optional_exact_bytes(
540                self.local_ratchet_key_private,
541                PRIVATE_KEY_BYTES,
542                "local ratchet private key",
543            )?,
544            local_identity_key: optional_public_key(
545                self.local_identity_key,
546                "local identity public key",
547            )?,
548            local_identity_key_private: optional_exact_bytes(
549                self.local_identity_key_private,
550                PRIVATE_KEY_BYTES,
551                "local identity private key",
552            )?,
553        })
554    }
555
556    fn from_structure(value: session_structure::PendingKeyExchange) -> Result<Self> {
557        Ok(Self {
558            sequence: value.sequence,
559            local_base_key: optional_public_key(value.local_base_key, "local base public key")?,
560            local_base_key_private: optional_exact_bytes(
561                value.local_base_key_private,
562                PRIVATE_KEY_BYTES,
563                "local base private key",
564            )?,
565            local_ratchet_key: optional_public_key(
566                value.local_ratchet_key,
567                "local ratchet public key",
568            )?,
569            local_ratchet_key_private: optional_exact_bytes(
570                value.local_ratchet_key_private,
571                PRIVATE_KEY_BYTES,
572                "local ratchet private key",
573            )?,
574            local_identity_key: optional_public_key(
575                value.local_identity_key,
576                "local identity public key",
577            )?,
578            local_identity_key_private: optional_exact_bytes(
579                value.local_identity_key_private,
580                PRIVATE_KEY_BYTES,
581                "local identity private key",
582            )?,
583        })
584    }
585}
586
587impl PendingPreKeyComponents {
588    fn into_structure(self) -> Result<session_structure::PendingPreKey> {
589        Ok(session_structure::PendingPreKey {
590            pre_key_id: self.pre_key_id,
591            signed_pre_key_id: self.signed_pre_key_id,
592            base_key: optional_public_key(self.base_key, "pending pre-key base key")?,
593            kyber_pre_key_id: self.kyber_pre_key_id,
594            kyber_ciphertext: self.kyber_ciphertext,
595        })
596    }
597
598    fn from_structure(value: session_structure::PendingPreKey) -> Result<Self> {
599        Ok(Self {
600            pre_key_id: value.pre_key_id,
601            signed_pre_key_id: value.signed_pre_key_id,
602            base_key: optional_public_key(value.base_key, "pending pre-key base key")?,
603            kyber_pre_key_id: value.kyber_pre_key_id,
604            kyber_ciphertext: value.kyber_ciphertext,
605        })
606    }
607}
608
609pub(crate) fn session_structure_from_components(
610    value: SessionComponents,
611) -> Result<SessionStructure> {
612    Ok(SessionStructure {
613        session_version: value.session_version,
614        local_identity_public: optional_public_key(
615            value.local_identity_public,
616            "local identity public key",
617        )?,
618        remote_identity_public: optional_public_key(
619            value.remote_identity_public,
620            "remote identity public key",
621        )?,
622        root_key: optional_exact_bytes(value.root_key, SYMMETRIC_KEY_BYTES, "session root key")?,
623        previous_counter: value.previous_counter,
624        sender_chain: value
625            .sender_chain
626            .map(SessionChainComponents::into_sender_structure)
627            .transpose()?
628            .into(),
629        receiver_chains: value
630            .receiver_chains
631            .into_iter()
632            .map(SessionChainComponents::into_receiver_structure)
633            .collect::<Result<_>>()?,
634        pending_key_exchange: value
635            .pending_key_exchange
636            .map(PendingKeyExchangeComponents::into_structure)
637            .transpose()?
638            .into(),
639        pending_pre_key: value
640            .pending_pre_key
641            .map(PendingPreKeyComponents::into_structure)
642            .transpose()?
643            .into(),
644        remote_registration_id: value.remote_registration_id,
645        local_registration_id: value.local_registration_id,
646        needs_refresh: value.needs_refresh,
647        alice_base_key: optional_public_key(value.alice_base_key, "session base public key")?,
648    })
649}
650
651pub(crate) fn session_components_from_structure(
652    mut value: SessionStructure,
653) -> Result<SessionComponents> {
654    Ok(SessionComponents {
655        session_version: value.session_version,
656        local_identity_public: optional_public_key(
657            value.local_identity_public,
658            "local identity public key",
659        )?,
660        remote_identity_public: optional_public_key(
661            value.remote_identity_public,
662            "remote identity public key",
663        )?,
664        root_key: optional_exact_bytes(value.root_key, SYMMETRIC_KEY_BYTES, "session root key")?,
665        previous_counter: value.previous_counter,
666        sender_chain: value
667            .sender_chain
668            .take()
669            .map(SessionChainComponents::from_sender_structure)
670            .transpose()?,
671        receiver_chains: value
672            .receiver_chains
673            .into_iter()
674            .map(SessionChainComponents::from_receiver_structure)
675            .collect::<Result<_>>()?,
676        pending_key_exchange: value
677            .pending_key_exchange
678            .take()
679            .map(PendingKeyExchangeComponents::from_structure)
680            .transpose()?,
681        pending_pre_key: value
682            .pending_pre_key
683            .take()
684            .map(PendingPreKeyComponents::from_structure)
685            .transpose()?,
686        remote_registration_id: value.remote_registration_id,
687        local_registration_id: value.local_registration_id,
688        needs_refresh: value.needs_refresh,
689        alice_base_key: optional_public_key(value.alice_base_key, "session base public key")?,
690    })
691}
692
693pub(crate) fn sender_state_structure_from_components(
694    value: SenderKeyStateComponents,
695) -> Result<SenderKeyStateStructure> {
696    let chain_seed = exact_bytes(
697        value.chain_key.seed,
698        SYMMETRIC_KEY_BYTES,
699        "sender chain seed",
700    )?;
701    let signing_public =
702        normalize_public_key(value.signing_key.public, "sender signing public key")?;
703    let signing_private = optional_exact_bytes(
704        value.signing_key.private,
705        PRIVATE_KEY_BYTES,
706        "sender signing private key",
707    )?;
708    Ok(SenderKeyStateStructure {
709        sender_key_id: Some(value.key_id),
710        sender_chain_key: MessageField::some(sender_key_state_structure::SenderChainKey {
711            iteration: Some(value.chain_key.iteration),
712            seed: Some(Bytes::from(chain_seed)),
713        }),
714        sender_signing_key: MessageField::some(sender_key_state_structure::SenderSigningKey {
715            public: Some(Bytes::from(signing_public)),
716            private: signing_private.map(Bytes::from),
717        }),
718        sender_message_keys: value
719            .message_keys
720            .into_iter()
721            .map(|key| {
722                Ok(sender_key_state_structure::SenderMessageKey {
723                    iteration: Some(key.iteration),
724                    seed: Some(Bytes::from(exact_bytes(
725                        key.seed,
726                        SYMMETRIC_KEY_BYTES,
727                        "sender message-key seed",
728                    )?)),
729                })
730            })
731            .collect::<Result<_>>()?,
732    })
733}
734
735pub(crate) fn sender_state_components_from_structure(
736    mut value: SenderKeyStateStructure,
737) -> Result<SenderKeyStateComponents> {
738    let chain = value
739        .sender_chain_key
740        .take()
741        .ok_or_else(|| invalid("sender chain key", "present"))?;
742    let signing = value
743        .sender_signing_key
744        .take()
745        .ok_or_else(|| invalid("sender signing key", "present"))?;
746    Ok(SenderKeyStateComponents {
747        key_id: value
748            .sender_key_id
749            .ok_or_else(|| invalid("sender key id", "present"))?,
750        chain_key: SenderChainKeyComponents {
751            iteration: chain
752                .iteration
753                .ok_or_else(|| invalid("sender chain iteration", "present"))?,
754            seed: exact_bytes(
755                chain
756                    .seed
757                    .ok_or_else(|| invalid("sender chain seed", "present"))?
758                    .to_vec(),
759                SYMMETRIC_KEY_BYTES,
760                "sender chain seed",
761            )?,
762        },
763        signing_key: SenderSigningKeyComponents {
764            public: normalize_public_key(
765                signing
766                    .public
767                    .ok_or_else(|| invalid("sender signing public key", "present"))?
768                    .to_vec(),
769                "sender signing public key",
770            )?,
771            private: optional_exact_bytes(
772                signing.private.map(|value| value.to_vec()),
773                PRIVATE_KEY_BYTES,
774                "sender signing private key",
775            )?,
776        },
777        message_keys: value
778            .sender_message_keys
779            .into_iter()
780            .map(|key| {
781                Ok(SenderMessageKeyComponents {
782                    iteration: key
783                        .iteration
784                        .ok_or_else(|| invalid("sender message-key iteration", "present"))?,
785                    seed: exact_bytes(
786                        key.seed
787                            .ok_or_else(|| invalid("sender message-key seed", "present"))?
788                            .to_vec(),
789                        SYMMETRIC_KEY_BYTES,
790                        "sender message-key seed",
791                    )?,
792                })
793            })
794            .collect::<Result<_>>()?,
795    })
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::protocol::{
802        ChainKey, IdentityKey, KeyPair, RootKey, SenderKeyRecord, SessionRecord, SessionState,
803    };
804
805    #[derive(Debug, Clone, Copy)]
806    enum SenderChainFault {
807        MissingPrivateKey,
808        PrivateKeyLength(usize),
809        MissingPublicKey,
810        MissingChainKey,
811        MissingChainKeyIndex,
812        MissingChainKeySecret,
813    }
814
815    fn public_key(seed: u8) -> Vec<u8> {
816        PublicKey::from_djb_public_key_bytes(&[seed; PublicKey::RAW_KEY_LEN])
817            .expect("valid test key")
818            .serialize()
819            .to_vec()
820    }
821
822    fn canonical_session_state() -> SessionState {
823        let mut rng = rand::make_rng::<rand::rngs::StdRng>();
824        let local_identity = IdentityKey::new(KeyPair::generate(&mut rng).public_key);
825        let remote_identity = IdentityKey::new(KeyPair::generate(&mut rng).public_key);
826        let base_key = KeyPair::generate(&mut rng).public_key;
827        let mut state = SessionState::new(
828            3,
829            &local_identity,
830            &remote_identity,
831            &RootKey::new([3; SYMMETRIC_KEY_BYTES]),
832            &base_key,
833        );
834        state.set_sender_chain(
835            &KeyPair::generate(&mut rng),
836            &ChainKey::new([4; SYMMETRIC_KEY_BYTES], 5),
837        );
838        state.add_receiver_chain(
839            &KeyPair::generate(&mut rng).public_key,
840            &ChainKey::new([6; SYMMETRIC_KEY_BYTES], 7),
841        );
842        state
843    }
844
845    fn canonical_session_components() -> SessionRecordComponents {
846        SessionRecord::new(canonical_session_state())
847            .into_components()
848            .expect("canonical session projects")
849    }
850
851    fn sender_chain_faults() -> [SenderChainFault; 8] {
852        [
853            SenderChainFault::MissingPrivateKey,
854            SenderChainFault::PrivateKeyLength(0),
855            SenderChainFault::PrivateKeyLength(PRIVATE_KEY_BYTES - 1),
856            SenderChainFault::PrivateKeyLength(PRIVATE_KEY_BYTES + 1),
857            SenderChainFault::MissingPublicKey,
858            SenderChainFault::MissingChainKey,
859            SenderChainFault::MissingChainKeyIndex,
860            SenderChainFault::MissingChainKeySecret,
861        ]
862    }
863
864    fn corrupt_sender_components(
865        components: &mut SessionRecordComponents,
866        fault: SenderChainFault,
867    ) {
868        let sender = components
869            .current_session
870            .as_mut()
871            .and_then(|session| session.sender_chain.as_mut())
872            .expect("canonical sender chain");
873        match fault {
874            SenderChainFault::MissingPrivateKey => sender.sender_ratchet_key_private = None,
875            SenderChainFault::PrivateKeyLength(length) => {
876                sender.sender_ratchet_key_private = Some(vec![0; length]);
877            }
878            SenderChainFault::MissingPublicKey => sender.sender_ratchet_key = None,
879            SenderChainFault::MissingChainKey => sender.chain_key = None,
880            SenderChainFault::MissingChainKeyIndex => {
881                sender
882                    .chain_key
883                    .as_mut()
884                    .expect("canonical chain key")
885                    .index = None;
886            }
887            SenderChainFault::MissingChainKeySecret => {
888                sender.chain_key.as_mut().expect("canonical chain key").key = None;
889            }
890        }
891    }
892
893    fn corrupt_persisted_sender(session: &mut SessionStructure, fault: SenderChainFault) {
894        let sender = session
895            .sender_chain
896            .as_option_mut()
897            .expect("canonical sender chain");
898        match fault {
899            SenderChainFault::MissingPrivateKey => sender.sender_ratchet_key_private = None,
900            SenderChainFault::PrivateKeyLength(length) => {
901                sender.sender_ratchet_key_private = Some(vec![0; length]);
902            }
903            SenderChainFault::MissingPublicKey => sender.sender_ratchet_key = None,
904            SenderChainFault::MissingChainKey => sender.chain_key = MessageField::none(),
905            SenderChainFault::MissingChainKeyIndex => {
906                sender
907                    .chain_key
908                    .as_option_mut()
909                    .expect("canonical chain key")
910                    .index = None;
911            }
912            SenderChainFault::MissingChainKeySecret => {
913                sender
914                    .chain_key
915                    .as_option_mut()
916                    .expect("canonical chain key")
917                    .key = None;
918            }
919        }
920    }
921
922    fn session_record() -> SessionRecordComponents {
923        let current_session = SessionComponents {
924            session_version: Some(3),
925            local_identity_public: Some(public_key(1)),
926            remote_identity_public: Some(public_key(2)),
927            root_key: Some(vec![3; 32]),
928            previous_counter: Some(0),
929            sender_chain: Some(SessionChainComponents {
930                sender_ratchet_key: Some(public_key(4)),
931                sender_ratchet_key_private: Some(vec![5; 32]),
932                chain_key: Some(SessionChainKeyComponents {
933                    index: Some(0),
934                    key: Some(vec![6; 32]),
935                }),
936                message_keys: vec![SessionMessageKeyComponents {
937                    index: 1,
938                    material: SessionMessageKeyMaterial::Derived {
939                        cipher_key: [16; 32],
940                        mac_key: [17; 32],
941                        iv: [18; 16],
942                    },
943                }],
944            }),
945            receiver_chains: vec![SessionChainComponents {
946                sender_ratchet_key: Some(public_key(7)),
947                sender_ratchet_key_private: None,
948                chain_key: Some(SessionChainKeyComponents {
949                    index: Some(2),
950                    key: Some(vec![8; 32]),
951                }),
952                message_keys: vec![SessionMessageKeyComponents {
953                    index: 3,
954                    material: SessionMessageKeyMaterial::Derived {
955                        cipher_key: [19; 32],
956                        mac_key: [20; 32],
957                        iv: [21; 16],
958                    },
959                }],
960            }],
961            pending_key_exchange: Some(PendingKeyExchangeComponents {
962                sequence: Some(22),
963                local_base_key: Some(public_key(23)),
964                local_base_key_private: Some(vec![24; 32]),
965                local_ratchet_key: Some(public_key(25)),
966                local_ratchet_key_private: Some(vec![26; 32]),
967                local_identity_key: Some(public_key(27)),
968                local_identity_key_private: Some(vec![28; 32]),
969            }),
970            pending_pre_key: Some(PendingPreKeyComponents {
971                pre_key_id: Some(29),
972                signed_pre_key_id: Some(30),
973                base_key: Some(public_key(31)),
974                kyber_pre_key_id: Some(32),
975                kyber_ciphertext: Some(vec![33; 48]),
976            }),
977            remote_registration_id: Some(9),
978            local_registration_id: Some(10),
979            needs_refresh: Some(false),
980            alice_base_key: Some(public_key(11)),
981        };
982        SessionRecordComponents {
983            current_session: Some(current_session.clone()),
984            previous_sessions: vec![current_session],
985        }
986    }
987
988    fn sender_key_record() -> SenderKeyRecordComponents {
989        SenderKeyRecordComponents {
990            states: vec![
991                SenderKeyStateComponents {
992                    key_id: 17,
993                    chain_key: SenderChainKeyComponents {
994                        iteration: 0,
995                        seed: vec![12; 32],
996                    },
997                    signing_key: SenderSigningKeyComponents {
998                        public: public_key(13),
999                        private: Some(vec![14; 32]),
1000                    },
1001                    message_keys: vec![SenderMessageKeyComponents {
1002                        iteration: 3,
1003                        seed: vec![15; 32],
1004                    }],
1005                },
1006                SenderKeyStateComponents {
1007                    key_id: 18,
1008                    chain_key: SenderChainKeyComponents {
1009                        iteration: 4,
1010                        seed: vec![16; 32],
1011                    },
1012                    signing_key: SenderSigningKeyComponents {
1013                        public: public_key(17),
1014                        private: None,
1015                    },
1016                    message_keys: Vec::new(),
1017                },
1018            ],
1019        }
1020    }
1021
1022    #[test]
1023    fn raw_public_keys_are_normalized_once() {
1024        let normalized = normalize_public_key(vec![7; 32], "test key").expect("valid key");
1025        assert_eq!(normalized.len(), PublicKey::SERIALIZED_KEY_LEN);
1026        assert_eq!(normalized[0], crate::core::curve::KeyType::Djb.value());
1027        assert_eq!(&normalized[1..], &[7; 32]);
1028    }
1029
1030    #[test]
1031    fn invalid_serialized_public_key_type_is_rejected() {
1032        let mut key = public_key(7);
1033        key[0] = u8::MAX;
1034
1035        assert!(normalize_public_key(key, "test key").is_err());
1036    }
1037
1038    #[test]
1039    fn debug_output_redacts_key_material() {
1040        let chain = SessionChainKeyComponents {
1041            index: Some(7),
1042            key: Some(vec![42; 32]),
1043        };
1044        let material = SessionMessageKeyMaterial::Derived {
1045            cipher_key: [1; 32],
1046            mac_key: [2; 32],
1047            iv: [3; 16],
1048        };
1049
1050        assert_eq!(
1051            format!("{chain:?}"),
1052            "SessionChainKeyComponents { index: Some(7), key: <redacted> }"
1053        );
1054        assert_eq!(
1055            format!("{material:?}"),
1056            "Derived { cipher_key: <redacted>, mac_key: <redacted>, iv: <redacted> }"
1057        );
1058    }
1059
1060    #[test]
1061    fn message_key_seed_uses_the_existing_derivation() {
1062        let seed = [9; 32];
1063        let expected = MessageKeyGenerator::new_from_seed(&seed, 17).into_pb();
1064        let actual = SessionMessageKeyComponents {
1065            index: 17,
1066            material: SessionMessageKeyMaterial::Seed(seed),
1067        }
1068        .into_structure();
1069
1070        assert_eq!(actual, expected);
1071    }
1072
1073    /// The symptom this field exists for: a record imported with a seed used
1074    /// to come back as `Derived`, which no seed-based format can express.
1075    #[test]
1076    fn seed_material_survives_the_record_round_trip() {
1077        let expected = SessionMessageKeyComponents {
1078            index: 17,
1079            material: SessionMessageKeyMaterial::Seed([9; SYMMETRIC_KEY_BYTES]),
1080        };
1081        let actual = SessionMessageKeyComponents::from_structure(expected.clone().into_structure())
1082            .expect("valid persisted key");
1083
1084        assert_eq!(actual, expected);
1085    }
1086
1087    /// Keys persisted before the seed was retained carry only the derived
1088    /// triple and must keep projecting as `Derived`.
1089    #[test]
1090    fn a_persisted_key_without_a_seed_projects_as_derived() {
1091        let mut persisted = SessionMessageKeyComponents {
1092            index: 3,
1093            material: SessionMessageKeyMaterial::Seed([9; SYMMETRIC_KEY_BYTES]),
1094        }
1095        .into_structure();
1096        persisted.seed = None;
1097
1098        let actual =
1099            SessionMessageKeyComponents::from_structure(persisted).expect("seedless key projects");
1100
1101        assert!(matches!(
1102            actual.material,
1103            SessionMessageKeyMaterial::Derived { .. }
1104        ));
1105    }
1106
1107    /// The seed supersedes the derived triple, so a corrupt one must fail the
1108    /// projection instead of exporting key material that derives nothing.
1109    #[test]
1110    fn a_malformed_persisted_seed_is_rejected() {
1111        for length in [0, SYMMETRIC_KEY_BYTES - 1, SYMMETRIC_KEY_BYTES + 1] {
1112            let mut persisted = SessionMessageKeyComponents {
1113                index: 3,
1114                material: SessionMessageKeyMaterial::Seed([9; SYMMETRIC_KEY_BYTES]),
1115            }
1116            .into_structure();
1117            persisted.seed = Some(Bytes::from(vec![0; length]));
1118
1119            let error = SessionMessageKeyComponents::from_structure(persisted)
1120                .expect_err("malformed seed must fail");
1121            assert!(
1122                matches!(error, SignalProtocolError::InvalidArgument(_)),
1123                "{length}-byte seed: {error}"
1124            );
1125        }
1126    }
1127
1128    /// A well-formed seed that derives something else is the dangerous case:
1129    /// the export would look fine and the exported key would decrypt nothing.
1130    #[test]
1131    fn a_persisted_seed_that_derives_other_keys_is_rejected() {
1132        let mut persisted = SessionMessageKeyComponents {
1133            index: 3,
1134            material: SessionMessageKeyMaterial::Seed([9; SYMMETRIC_KEY_BYTES]),
1135        }
1136        .into_structure();
1137        persisted.seed = Some(Bytes::copy_from_slice(&[10; SYMMETRIC_KEY_BYTES]));
1138
1139        let error = SessionMessageKeyComponents::from_structure(persisted)
1140            .expect_err("inconsistent seed must fail");
1141        assert!(matches!(error, SignalProtocolError::InvalidArgument(_)));
1142    }
1143
1144    #[test]
1145    fn canonical_session_api_round_trips_through_components() {
1146        let state = canonical_session_state();
1147        let structure = SessionStructure::from(&state);
1148        assert_eq!(
1149            structure.receiver_chains[0].sender_ratchet_key_private,
1150            Some(Vec::new())
1151        );
1152        assert_eq!(
1153            structure
1154                .sender_chain
1155                .as_option()
1156                .and_then(|chain| chain.sender_ratchet_key_private.as_ref())
1157                .map(Vec::len),
1158            Some(PRIVATE_KEY_BYTES)
1159        );
1160
1161        let persisted = SessionRecord::new(state)
1162            .serialize()
1163            .expect("serialize canonical record");
1164        let projected = SessionRecord::deserialize(&persisted)
1165            .expect("deserialize canonical record")
1166            .into_components()
1167            .expect("project canonical record");
1168        let session = projected
1169            .current_session
1170            .as_ref()
1171            .expect("test session is present");
1172        assert_eq!(session.receiver_chains[0].sender_ratchet_key_private, None);
1173        assert_eq!(
1174            session
1175                .sender_chain
1176                .as_ref()
1177                .and_then(|chain| chain.sender_ratchet_key_private.as_ref())
1178                .map(Vec::len),
1179            Some(PRIVATE_KEY_BYTES)
1180        );
1181
1182        let rebuilt = SessionRecord::from_components(projected.clone())
1183            .expect("rebuild canonical record")
1184            .serialize()
1185            .expect("serialize rebuilt record");
1186        let reprojected = SessionRecord::deserialize(&rebuilt)
1187            .expect("deserialize rebuilt record")
1188            .into_components()
1189            .expect("project rebuilt record");
1190        assert_eq!(reprojected, projected);
1191    }
1192
1193    #[test]
1194    fn persisted_receiver_private_material_is_ignored() {
1195        for (case, private_key) in [
1196            ("absent", None),
1197            ("empty", Some(Vec::new())),
1198            ("one byte", Some(vec![0; 1])),
1199            ("short key", Some(vec![0; PRIVATE_KEY_BYTES - 1])),
1200            ("key-sized", Some(vec![0; PRIVATE_KEY_BYTES])),
1201            ("long key", Some(vec![0; PRIVATE_KEY_BYTES + 1])),
1202        ] {
1203            let mut persisted = SessionStructure::from(canonical_session_state());
1204            persisted.receiver_chains[0].sender_ratchet_key_private = private_key;
1205
1206            let projected = session_components_from_structure(persisted).unwrap_or_else(|error| {
1207                panic!("{case} receiver material must be ignored: {error}")
1208            });
1209            assert_eq!(
1210                projected.receiver_chains[0].sender_ratchet_key_private, None,
1211                "{case}"
1212            );
1213        }
1214    }
1215
1216    #[test]
1217    fn receiver_component_import_rejects_private_material() {
1218        for private_key in [Vec::new(), vec![0; PRIVATE_KEY_BYTES]] {
1219            let mut components = canonical_session_components();
1220            components
1221                .current_session
1222                .as_mut()
1223                .expect("canonical session")
1224                .receiver_chains[0]
1225                .sender_ratchet_key_private = Some(private_key);
1226
1227            let error = SessionRecord::from_components(components)
1228                .err()
1229                .expect("receiver components must not carry private material");
1230            assert!(matches!(error, SignalProtocolError::InvalidArgument(_)));
1231        }
1232    }
1233
1234    #[test]
1235    fn sender_component_import_requires_a_complete_chain() {
1236        for fault in sender_chain_faults() {
1237            let mut components = canonical_session_components();
1238            corrupt_sender_components(&mut components, fault);
1239
1240            let error = SessionRecord::from_components(components)
1241                .err()
1242                .unwrap_or_else(|| panic!("sender fault {fault:?} must fail"));
1243            assert!(
1244                matches!(error, SignalProtocolError::InvalidArgument(_)),
1245                "{fault:?}: {error}"
1246            );
1247        }
1248    }
1249
1250    #[test]
1251    fn persisted_sender_projection_requires_a_complete_chain() {
1252        for fault in sender_chain_faults() {
1253            let mut persisted = SessionStructure::from(canonical_session_state());
1254            corrupt_persisted_sender(&mut persisted, fault);
1255
1256            let error = session_components_from_structure(persisted)
1257                .err()
1258                .unwrap_or_else(|| panic!("persisted sender fault {fault:?} must fail"));
1259            assert!(
1260                matches!(error, SignalProtocolError::InvalidArgument(_)),
1261                "{fault:?}: {error}"
1262            );
1263        }
1264    }
1265
1266    #[test]
1267    fn component_import_allows_an_absent_sender_chain() {
1268        let mut components = canonical_session_components();
1269        components
1270            .current_session
1271            .as_mut()
1272            .expect("canonical session")
1273            .sender_chain = None;
1274
1275        SessionRecord::from_components(components)
1276            .expect("intermediate state without sender chain");
1277    }
1278
1279    #[test]
1280    fn session_components_use_the_canonical_record_codec() {
1281        let expected = session_record();
1282        let bytes = SessionRecord::from_components(expected.clone())
1283            .expect("valid components")
1284            .serialize()
1285            .expect("serialize record");
1286        let actual = SessionRecord::deserialize(&bytes)
1287            .expect("deserialize record")
1288            .into_components()
1289            .expect("project record");
1290
1291        assert_eq!(actual, expected);
1292    }
1293
1294    #[test]
1295    fn session_components_bound_archived_state_count() {
1296        let mut components = session_record();
1297        let archived = components
1298            .current_session
1299            .clone()
1300            .expect("test session is present");
1301        components.previous_sessions =
1302            vec![archived; crate::protocol::consts::ARCHIVED_STATES_MAX_LENGTH + 1];
1303
1304        let actual = SessionRecord::from_components(components)
1305            .expect("valid components")
1306            .into_components()
1307            .expect("project record");
1308
1309        assert_eq!(
1310            actual.previous_sessions.len(),
1311            crate::protocol::consts::ARCHIVED_STATES_MAX_LENGTH
1312        );
1313    }
1314
1315    #[test]
1316    fn session_handoff_burns_the_reserved_sender_range_in_all_states() {
1317        let mut record = SessionRecord::from_components(session_record()).expect("valid record");
1318        record.reserve_sender_chain_counters(0);
1319        let ceiling = record.reserved_sender_chain_index();
1320        let components = record.into_components().expect("safe handoff");
1321        let index = components
1322            .current_session
1323            .as_ref()
1324            .and_then(|session| session.sender_chain.as_ref())
1325            .and_then(|chain| chain.chain_key.as_ref())
1326            .and_then(|chain| chain.index);
1327        let archived_indexes: Vec<_> = components
1328            .previous_sessions
1329            .iter()
1330            .map(|session| {
1331                session
1332                    .sender_chain
1333                    .as_ref()
1334                    .and_then(|chain| chain.chain_key.as_ref())
1335                    .and_then(|chain| chain.index)
1336            })
1337            .collect();
1338
1339        assert_eq!(index, Some(ceiling));
1340        assert_eq!(archived_indexes, vec![Some(ceiling)]);
1341    }
1342
1343    #[test]
1344    fn session_handoff_drops_unadvanceable_chains_without_losing_the_record() {
1345        let mut record = SessionRecord::from_components(session_record()).expect("valid record");
1346        record.reserve_sender_chain_counters(
1347            crate::protocol::consts::MAX_RESERVATION_FAST_FORWARD + 1,
1348        );
1349
1350        let components = record
1351            .into_components()
1352            .expect("unadvanceable chains must not consume the record on error");
1353
1354        assert!(
1355            components
1356                .current_session
1357                .as_ref()
1358                .is_some_and(|session| session.sender_chain.is_none())
1359        );
1360        assert_eq!(components.previous_sessions.len(), 1);
1361        assert!(components.previous_sessions[0].sender_chain.is_none());
1362    }
1363
1364    #[test]
1365    fn sender_key_components_use_the_canonical_record_codec() {
1366        let expected = sender_key_record();
1367        let bytes = SenderKeyRecord::from_components(expected.clone())
1368            .expect("valid components")
1369            .serialize()
1370            .expect("serialize record");
1371        let actual = SenderKeyRecord::deserialize(&bytes)
1372            .expect("deserialize record")
1373            .into_components()
1374            .expect("project record");
1375
1376        assert_eq!(actual, expected);
1377    }
1378
1379    #[test]
1380    fn sender_key_components_bound_state_history() {
1381        let mut components = sender_key_record();
1382        let state = components.states[0].clone();
1383        components.states = vec![state; crate::protocol::consts::MAX_SENDER_KEY_STATES + 1];
1384
1385        let actual = SenderKeyRecord::from_components(components)
1386            .expect("valid components")
1387            .into_components()
1388            .expect("project record");
1389
1390        assert_eq!(
1391            actual.states.len(),
1392            crate::protocol::consts::MAX_SENDER_KEY_STATES
1393        );
1394    }
1395
1396    #[test]
1397    fn sender_key_handoff_burns_the_reserved_iteration_range() {
1398        let mut record =
1399            SenderKeyRecord::from_components(sender_key_record()).expect("valid record");
1400        record.reserve_iterations(0);
1401        let ceiling = record.reserved_iteration();
1402        let components = record.into_components().expect("safe handoff");
1403
1404        assert_eq!(components.states[0].chain_key.iteration, ceiling);
1405    }
1406}