Skip to main content

nostr_double_ratchet/
session.rs

1use crate::{
2    device_pubkey_from_secret_bytes, kdf2, random_secret_key_bytes, secret_key_from_bytes,
3    DevicePubkey, DomainError, ProtocolContext, Result, UnixSeconds, MAX_SKIP,
4};
5use base64::Engine;
6use nostr::nips::nip44::{self, Version};
7use nostr::PublicKey;
8use rand::rngs::OsRng;
9use rand::{CryptoRng, RngCore};
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "camelCase")]
15pub struct Header {
16    pub number: u32,
17    pub previous_chain_length: u32,
18    pub next_public_key: DevicePubkey,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct SerializableKeyPair {
23    pub public_key: DevicePubkey,
24    #[serde(with = "hex::serde")]
25    pub private_key: [u8; 32],
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
29pub struct SkippedKeysEntry {
30    #[serde(with = "crate::serde_hex::btreemap_u32")]
31    pub message_keys: BTreeMap<u32, [u8; 32]>,
32    /// NIP-44 conversation keys that decrypt this sender's headers. Kept with
33    /// the skipped message keys so a late message stays readable after the
34    /// local ratchet key its header was encrypted to has rotated out.
35    #[serde(
36        default,
37        skip_serializing_if = "Vec::is_empty",
38        with = "crate::serde_hex::vec"
39    )]
40    pub header_keys: Vec<[u8; 32]>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct SessionState {
45    #[serde(default)]
46    pub metadata_privacy_version: u8,
47    #[serde(with = "hex::serde")]
48    pub root_key: [u8; 32],
49    pub their_current_nostr_public_key: Option<DevicePubkey>,
50    pub their_next_nostr_public_key: Option<DevicePubkey>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub our_previous_nostr_key: Option<SerializableKeyPair>,
53    pub our_current_nostr_key: Option<SerializableKeyPair>,
54    pub our_next_nostr_key: SerializableKeyPair,
55    #[serde(default, with = "crate::serde_hex::option")]
56    pub receiving_chain_key: Option<[u8; 32]>,
57    #[serde(default, with = "crate::serde_hex::option")]
58    pub sending_chain_key: Option<[u8; 32]>,
59    pub sending_chain_message_number: u32,
60    pub receiving_chain_message_number: u32,
61    pub previous_sending_chain_message_count: u32,
62    pub skipped_keys: BTreeMap<DevicePubkey, SkippedKeysEntry>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub struct MessageEnvelope {
67    #[serde(default)]
68    pub metadata_privacy_version: u8,
69    pub sender: DevicePubkey,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub recipient: Option<DevicePubkey>,
72    pub signer_secret_key: [u8; 32],
73    pub created_at: UnixSeconds,
74    pub encrypted_header: String,
75    pub ciphertext: String,
76}
77
78#[derive(Debug, Clone)]
79pub struct SendPlan {
80    pub next_state: SessionState,
81    pub envelope: MessageEnvelope,
82    pub payload: Vec<u8>,
83}
84
85#[derive(Debug, Clone)]
86pub struct SendOutcome {
87    pub envelope: MessageEnvelope,
88    pub payload: Vec<u8>,
89}
90
91#[derive(Debug, Clone)]
92pub struct ReceivePlan {
93    pub next_state: SessionState,
94    pub payload: Vec<u8>,
95    pub sender: DevicePubkey,
96}
97
98#[derive(Debug, Clone)]
99pub struct ReceiveOutcome {
100    pub payload: Vec<u8>,
101    pub sender: DevicePubkey,
102}
103
104#[derive(Debug, Clone)]
105pub struct Session {
106    pub state: SessionState,
107    pub name: String,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum HeaderDecryptionTarget {
112    Current,
113    Next,
114    Previous,
115    Skipped,
116}
117
118impl Session {
119    pub fn from_state(state: SessionState) -> Self {
120        Self {
121            state,
122            name: String::new(),
123        }
124    }
125
126    pub fn new(state: SessionState, name: String) -> Self {
127        Self { state, name }
128    }
129
130    pub fn init(
131        their_ephemeral_nostr_public_key: PublicKey,
132        our_ephemeral_nostr_private_key: [u8; 32],
133        is_initiator: bool,
134        shared_secret: [u8; 32],
135        _name: Option<String>,
136    ) -> Result<Self> {
137        let mut rng = OsRng;
138        let mut ctx = ProtocolContext::new(
139            UnixSeconds(
140                std::time::SystemTime::now()
141                    .duration_since(std::time::UNIX_EPOCH)
142                    .unwrap()
143                    .as_secs(),
144            ),
145            &mut rng,
146        );
147        let peer = DevicePubkey::from_bytes(their_ephemeral_nostr_public_key.to_bytes());
148        let mut session = if is_initiator {
149            Self::new_initiator(
150                &mut ctx,
151                peer,
152                our_ephemeral_nostr_private_key,
153                shared_secret,
154            )
155        } else {
156            Self::new_responder(
157                &mut ctx,
158                peer,
159                our_ephemeral_nostr_private_key,
160                shared_secret,
161            )
162        }?;
163        session.name = _name.unwrap_or_default();
164        Ok(session)
165    }
166
167    pub fn new_initiator<R>(
168        ctx: &mut ProtocolContext<'_, R>,
169        their_ephemeral_public_key: DevicePubkey,
170        our_ephemeral_private_key: [u8; 32],
171        shared_secret: [u8; 32],
172    ) -> Result<Self>
173    where
174        R: RngCore + CryptoRng,
175    {
176        Self::init_with_context(
177            ctx,
178            their_ephemeral_public_key,
179            our_ephemeral_private_key,
180            true,
181            shared_secret,
182        )
183    }
184
185    pub fn new_responder<R>(
186        ctx: &mut ProtocolContext<'_, R>,
187        their_ephemeral_public_key: DevicePubkey,
188        our_ephemeral_private_key: [u8; 32],
189        shared_secret: [u8; 32],
190    ) -> Result<Self>
191    where
192        R: RngCore + CryptoRng,
193    {
194        Self::init_with_context(
195            ctx,
196            their_ephemeral_public_key,
197            our_ephemeral_private_key,
198            false,
199            shared_secret,
200        )
201    }
202
203    fn init_with_context<R>(
204        ctx: &mut ProtocolContext<'_, R>,
205        their_ephemeral_public_key: DevicePubkey,
206        our_ephemeral_private_key: [u8; 32],
207        is_initiator: bool,
208        shared_secret: [u8; 32],
209    ) -> Result<Self>
210    where
211        R: RngCore + CryptoRng,
212    {
213        let (their_ephemeral_public_key, our_ephemeral_private_key) = if is_initiator {
214            (
215                crate::metadata_privacy::private_responder_public_key(
216                    our_ephemeral_private_key,
217                    their_ephemeral_public_key,
218                )?,
219                our_ephemeral_private_key,
220            )
221        } else {
222            (
223                their_ephemeral_public_key,
224                crate::metadata_privacy::private_responder_secret_key(
225                    our_ephemeral_private_key,
226                    their_ephemeral_public_key,
227                )?,
228            )
229        };
230        let our_keys = nostr::Keys::new(secret_key_from_bytes(&our_ephemeral_private_key)?);
231        let our_next_private_key = random_secret_key_bytes(ctx.rng)?;
232        let our_next_keys = nostr::Keys::new(secret_key_from_bytes(&our_next_private_key)?);
233
234        let (root_key, sending_chain_key, our_current_nostr_key, our_next_nostr_key) =
235            if is_initiator {
236                let our_current_pubkey = DevicePubkey::from_nostr(our_keys.public_key());
237                let conversation_key = nip44::v2::ConversationKey::derive(
238                    our_next_keys.secret_key(),
239                    &their_ephemeral_public_key.to_nostr()?,
240                )?;
241                let kdf_outputs = kdf2(&shared_secret, conversation_key.as_bytes());
242                (
243                    kdf_outputs[0],
244                    Some(kdf_outputs[1]),
245                    Some(SerializableKeyPair {
246                        public_key: our_current_pubkey,
247                        private_key: our_ephemeral_private_key,
248                    }),
249                    SerializableKeyPair {
250                        public_key: DevicePubkey::from_nostr(our_next_keys.public_key()),
251                        private_key: our_next_private_key,
252                    },
253                )
254            } else {
255                (
256                    shared_secret,
257                    None,
258                    None,
259                    SerializableKeyPair {
260                        public_key: DevicePubkey::from_nostr(our_keys.public_key()),
261                        private_key: our_ephemeral_private_key,
262                    },
263                )
264            };
265
266        Ok(Self {
267            state: SessionState {
268                metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
269                root_key,
270                their_current_nostr_public_key: None,
271                their_next_nostr_public_key: Some(their_ephemeral_public_key),
272                our_previous_nostr_key: None,
273                our_current_nostr_key,
274                our_next_nostr_key,
275                receiving_chain_key: None,
276                sending_chain_key,
277                sending_chain_message_number: 0,
278                receiving_chain_message_number: 0,
279                previous_sending_chain_message_count: 0,
280                skipped_keys: BTreeMap::new(),
281            },
282            name: String::new(),
283        })
284    }
285
286    pub fn can_send(&self) -> bool {
287        self.state.metadata_privacy_version == crate::METADATA_PRIVACY_VERSION
288            && self.state.their_next_nostr_public_key.is_some()
289            && self.state.our_current_nostr_key.is_some()
290    }
291
292    pub fn matches_sender(&self, sender: DevicePubkey) -> bool {
293        self.state.metadata_privacy_version == crate::METADATA_PRIVACY_VERSION
294            && (self.state.their_current_nostr_public_key == Some(sender)
295                || self.state.their_next_nostr_public_key == Some(sender)
296                || self.state.skipped_keys.contains_key(&sender))
297    }
298
299    pub fn plan_send(&self, payload: &[u8], now: UnixSeconds) -> Result<SendPlan> {
300        if !self.can_send() {
301            return Err(DomainError::CannotSendYet.into());
302        }
303
304        let mut next_state = self.state.clone();
305        let (header, ciphertext) = ratchet_encrypt(&mut next_state, payload)?;
306        let our_current = self
307            .state
308            .our_current_nostr_key
309            .as_ref()
310            .ok_or(DomainError::SessionNotReady)?;
311        let our_secret = secret_key_from_bytes(&our_current.private_key)?;
312        let their_next = self
313            .state
314            .their_next_nostr_public_key
315            .ok_or(DomainError::SessionNotReady)?;
316        let encrypted_header = nip44::encrypt(
317            &our_secret,
318            &their_next.to_nostr()?,
319            &serde_json::to_string(&header)?,
320            Version::V2,
321        )?;
322
323        Ok(SendPlan {
324            next_state,
325            envelope: MessageEnvelope {
326                metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
327                sender: our_current.public_key,
328                recipient: None,
329                signer_secret_key: our_current.private_key,
330                created_at: now,
331                encrypted_header,
332                ciphertext,
333            },
334            payload: payload.to_vec(),
335        })
336    }
337
338    pub fn apply_send(&mut self, plan: SendPlan) -> SendOutcome {
339        self.state = plan.next_state;
340        SendOutcome {
341            envelope: plan.envelope,
342            payload: plan.payload,
343        }
344    }
345
346    pub fn plan_receive<R>(
347        &self,
348        ctx: &mut ProtocolContext<'_, R>,
349        envelope: &MessageEnvelope,
350    ) -> Result<ReceivePlan>
351    where
352        R: RngCore + CryptoRng,
353    {
354        if envelope.metadata_privacy_version != crate::METADATA_PRIVACY_VERSION
355            || !self.matches_sender(envelope.sender)
356        {
357            return Err(DomainError::UnexpectedSender.into());
358        }
359
360        let mut next_state = self.state.clone();
361        let previous_chain_sender = next_state
362            .their_current_nostr_public_key
363            .or(next_state.their_next_nostr_public_key);
364        let (header, decryption_target) =
365            decrypt_header(&next_state, &envelope.encrypted_header, envelope.sender)?;
366        let should_ratchet = decryption_target == HeaderDecryptionTarget::Next;
367
368        let expected_next = next_state.their_next_nostr_public_key;
369        if should_ratchet && expected_next != Some(header.next_public_key) {
370            next_state.their_current_nostr_public_key = next_state.their_next_nostr_public_key;
371            next_state.their_next_nostr_public_key = Some(header.next_public_key);
372        }
373
374        if should_ratchet {
375            if next_state.receiving_chain_key.is_some() {
376                let skipped_sender = previous_chain_sender.ok_or(DomainError::SessionNotReady)?;
377                skip_message_keys(
378                    &mut next_state,
379                    header.previous_chain_length,
380                    skipped_sender,
381                )?;
382            }
383            ratchet_step(&mut next_state, ctx.rng)?;
384        }
385
386        let payload = if decryption_target == HeaderDecryptionTarget::Skipped {
387            // Only a retained message key can open a chain whose header key
388            // is no longer one of ours; never advance the current chain for it.
389            try_skipped_message_keys(
390                &mut next_state,
391                &header,
392                &envelope.ciphertext,
393                envelope.sender,
394            )?
395            .ok_or_else(|| crate::Error::Decryption("no skipped message key".to_string()))?
396        } else {
397            ratchet_decrypt(
398                &mut next_state,
399                &header,
400                &envelope.ciphertext,
401                envelope.sender,
402            )?
403        };
404
405        Ok(ReceivePlan {
406            next_state,
407            payload,
408            sender: envelope.sender,
409        })
410    }
411
412    pub fn apply_receive(&mut self, plan: ReceivePlan) -> ReceiveOutcome {
413        self.state = plan.next_state;
414        ReceiveOutcome {
415            payload: plan.payload,
416            sender: plan.sender,
417        }
418    }
419
420    pub fn close(&self) {}
421}
422
423fn ratchet_encrypt(state: &mut SessionState, plaintext: &[u8]) -> Result<(Header, String)> {
424    let sending_chain_key = state
425        .sending_chain_key
426        .ok_or(DomainError::SessionNotReady)?;
427
428    let kdf_outputs = kdf2(&sending_chain_key, &[1u8]);
429    state.sending_chain_key = Some(kdf_outputs[0]);
430    let message_key = kdf_outputs[1];
431
432    let header = Header {
433        number: state.sending_chain_message_number,
434        next_public_key: state.our_next_nostr_key.public_key,
435        previous_chain_length: state.previous_sending_chain_message_count,
436    };
437
438    state.sending_chain_message_number += 1;
439
440    let conversation_key = nip44::v2::ConversationKey::new(message_key);
441    let encrypted_bytes = nip44::v2::encrypt_to_bytes(&conversation_key, plaintext)?;
442    let ciphertext = base64::engine::general_purpose::STANDARD.encode(encrypted_bytes);
443    Ok((header, ciphertext))
444}
445
446fn ratchet_decrypt(
447    state: &mut SessionState,
448    header: &Header,
449    ciphertext: &str,
450    sender: DevicePubkey,
451) -> Result<Vec<u8>> {
452    if let Some(plaintext) = try_skipped_message_keys(state, header, ciphertext, sender)? {
453        return Ok(plaintext);
454    }
455
456    if state.receiving_chain_key.is_none() {
457        return Err(DomainError::SessionNotReady.into());
458    }
459
460    skip_message_keys(state, header.number, sender)?;
461
462    let receiving_chain_key = state
463        .receiving_chain_key
464        .ok_or(DomainError::SessionNotReady)?;
465
466    let kdf_outputs = kdf2(&receiving_chain_key, &[1u8]);
467    state.receiving_chain_key = Some(kdf_outputs[0]);
468    let message_key = kdf_outputs[1];
469    state.receiving_chain_message_number += 1;
470
471    let conversation_key = nip44::v2::ConversationKey::new(message_key);
472    let ciphertext_bytes = base64::engine::general_purpose::STANDARD
473        .decode(ciphertext)
474        .map_err(|e| crate::Error::Decryption(e.to_string()))?;
475
476    nip44::v2::decrypt_to_bytes(&conversation_key, &ciphertext_bytes).map_err(Into::into)
477}
478
479fn ratchet_step<R>(state: &mut SessionState, rng: &mut R) -> Result<()>
480where
481    R: RngCore + CryptoRng,
482{
483    state.previous_sending_chain_message_count = state.sending_chain_message_number;
484    state.sending_chain_message_number = 0;
485    state.receiving_chain_message_number = 0;
486
487    let our_next_sk = secret_key_from_bytes(&state.our_next_nostr_key.private_key)?;
488    let their_next_pk = state
489        .their_next_nostr_public_key
490        .ok_or(DomainError::SessionNotReady)?;
491
492    let conversation_key1 =
493        nip44::v2::ConversationKey::derive(&our_next_sk, &their_next_pk.to_nostr()?)?;
494    let kdf_outputs = kdf2(&state.root_key, conversation_key1.as_bytes());
495    state.receiving_chain_key = Some(kdf_outputs[1]);
496    state.our_previous_nostr_key = state.our_current_nostr_key.clone();
497    state.our_current_nostr_key = Some(state.our_next_nostr_key.clone());
498
499    let our_next_private_key = random_secret_key_bytes(rng)?;
500    state.our_next_nostr_key = SerializableKeyPair {
501        public_key: device_pubkey_from_secret_bytes(&our_next_private_key)?,
502        private_key: our_next_private_key,
503    };
504
505    let our_next_sk2 = secret_key_from_bytes(&our_next_private_key)?;
506    let conversation_key2 =
507        nip44::v2::ConversationKey::derive(&our_next_sk2, &their_next_pk.to_nostr()?)?;
508    let kdf_outputs2 = kdf2(&kdf_outputs[0], conversation_key2.as_bytes());
509    state.root_key = kdf_outputs2[0];
510    state.sending_chain_key = Some(kdf_outputs2[1]);
511    Ok(())
512}
513
514fn skip_message_keys(state: &mut SessionState, until: u32, sender: DevicePubkey) -> Result<()> {
515    if until <= state.receiving_chain_message_number {
516        return Ok(());
517    }
518
519    if (until - state.receiving_chain_message_number) as usize > MAX_SKIP {
520        return Err(DomainError::TooManySkippedMessages.into());
521    }
522
523    let header_key = if state
524        .skipped_keys
525        .get(&sender)
526        .is_none_or(|entry| entry.header_keys.is_empty())
527    {
528        Some(skipped_header_key(state, sender)?)
529    } else {
530        None
531    };
532    let entry = state.skipped_keys.entry(sender).or_default();
533    entry.header_keys.extend(header_key);
534
535    while state.receiving_chain_message_number < until {
536        let receiving_chain_key = state
537            .receiving_chain_key
538            .ok_or(DomainError::SessionNotReady)?;
539        let kdf_outputs = kdf2(&receiving_chain_key, &[1u8]);
540        state.receiving_chain_key = Some(kdf_outputs[0]);
541        entry
542            .message_keys
543            .insert(state.receiving_chain_message_number, kdf_outputs[1]);
544        state.receiving_chain_message_number += 1;
545    }
546
547    prune_skipped_message_keys(&mut entry.message_keys);
548    Ok(())
549}
550
551fn try_skipped_message_keys(
552    state: &mut SessionState,
553    header: &Header,
554    ciphertext: &str,
555    sender: DevicePubkey,
556) -> Result<Option<Vec<u8>>> {
557    if let Some(entry) = state.skipped_keys.get_mut(&sender) {
558        if let Some(message_key) = entry.message_keys.remove(&header.number) {
559            let conversation_key = nip44::v2::ConversationKey::new(message_key);
560            let ciphertext_bytes = base64::engine::general_purpose::STANDARD
561                .decode(ciphertext)
562                .map_err(|e| crate::Error::Decryption(e.to_string()))?;
563            let plaintext = nip44::v2::decrypt_to_bytes(&conversation_key, &ciphertext_bytes)?;
564            if entry.message_keys.is_empty() {
565                state.skipped_keys.remove(&sender);
566            }
567            return Ok(Some(plaintext));
568        }
569    }
570
571    Ok(None)
572}
573
574fn decrypt_header(
575    state: &SessionState,
576    encrypted_header: &str,
577    sender: DevicePubkey,
578) -> Result<(Header, HeaderDecryptionTarget)> {
579    if let Some(current) = &state.our_current_nostr_key {
580        let current_sk = secret_key_from_bytes(&current.private_key)?;
581        if let Ok(decrypted) = nip44::decrypt(&current_sk, &sender.to_nostr()?, encrypted_header) {
582            let header: Header = serde_json::from_str(&decrypted)?;
583            return Ok((header, HeaderDecryptionTarget::Current));
584        }
585    }
586
587    let next_sk = secret_key_from_bytes(&state.our_next_nostr_key.private_key)?;
588    if let Ok(decrypted) = nip44::decrypt(&next_sk, &sender.to_nostr()?, encrypted_header) {
589        let header: Header = serde_json::from_str(&decrypted)?;
590        return Ok((header, HeaderDecryptionTarget::Next));
591    }
592
593    if let Some(previous) = &state.our_previous_nostr_key {
594        let previous_sk = secret_key_from_bytes(&previous.private_key)?;
595        if let Ok(decrypted) = nip44::decrypt(&previous_sk, &sender.to_nostr()?, encrypted_header) {
596            let header: Header = serde_json::from_str(&decrypted)?;
597            return Ok((header, HeaderDecryptionTarget::Previous));
598        }
599    }
600
601    if let Some(entry) = state.skipped_keys.get(&sender) {
602        for header_key in &entry.header_keys {
603            if let Some(header) = decrypt_header_with_key(header_key, encrypted_header) {
604                return Ok((header, HeaderDecryptionTarget::Skipped));
605            }
606        }
607    }
608
609    Err(crate::Error::Parse("invalid header".to_string()))
610}
611
612/// The key a sender chain's headers are encrypted to is the local ratchet key
613/// that is current while that chain is being received, which is when its
614/// message keys get skipped.
615fn skipped_header_key(state: &SessionState, sender: DevicePubkey) -> Result<[u8; 32]> {
616    let our_key = state
617        .our_current_nostr_key
618        .as_ref()
619        .unwrap_or(&state.our_next_nostr_key);
620    let conversation_key = nip44::v2::ConversationKey::derive(
621        &secret_key_from_bytes(&our_key.private_key)?,
622        &sender.to_nostr()?,
623    )?;
624    conversation_key
625        .as_bytes()
626        .try_into()
627        .map_err(|_| crate::Error::Encryption("invalid conversation key".to_string()))
628}
629
630fn decrypt_header_with_key(header_key: &[u8; 32], encrypted_header: &str) -> Option<Header> {
631    let payload = base64::engine::general_purpose::STANDARD
632        .decode(encrypted_header)
633        .ok()?;
634    if payload.first() != Some(&Version::V2.as_u8()) {
635        return None;
636    }
637    let conversation_key = nip44::v2::ConversationKey::new(*header_key);
638    let decrypted = nip44::v2::decrypt_to_bytes(&conversation_key, &payload).ok()?;
639    serde_json::from_slice(&decrypted).ok()
640}
641
642fn prune_skipped_message_keys(map: &mut BTreeMap<u32, [u8; 32]>) {
643    while map.len() > MAX_SKIP {
644        let Some(first) = map.keys().next().copied() else {
645            break;
646        };
647        map.remove(&first);
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use rand::{rngs::StdRng, SeedableRng};
655
656    fn context(seed: u64) -> ProtocolContext<'static, StdRng> {
657        let rng = Box::new(StdRng::seed_from_u64(seed));
658        let rng = Box::leak(rng);
659        ProtocolContext::new(UnixSeconds(1_700_000_000), rng)
660    }
661
662    #[test]
663    fn header_json_uses_camel_case_wire_fields() {
664        let header = Header {
665            number: 3,
666            previous_chain_length: 2,
667            next_public_key: DevicePubkey::from_bytes([9u8; 32]),
668        };
669
670        let json = serde_json::to_value(&header).unwrap();
671        assert_eq!(json["number"], serde_json::json!(3));
672        assert_eq!(json["previousChainLength"], serde_json::json!(2));
673        assert_eq!(
674            json["nextPublicKey"],
675            serde_json::json!(header.next_public_key.to_string())
676        );
677        assert!(json.get("previous_chain_length").is_none());
678        assert!(json.get("next_public_key").is_none());
679
680        let decoded: Header = serde_json::from_value(json).unwrap();
681        assert_eq!(decoded, header);
682    }
683
684    #[test]
685    fn header_json_rejects_snake_case_wire_fields() {
686        let old_header = serde_json::json!({
687            "number": 3,
688            "previous_chain_length": 2,
689            "next_public_key": DevicePubkey::from_bytes([9u8; 32]).to_string(),
690        });
691
692        assert!(serde_json::from_value::<Header>(old_header).is_err());
693    }
694
695    #[test]
696    fn plan_send_and_apply_receive_roundtrip() {
697        let alice_secret = [1u8; 32];
698        let bob_secret = [2u8; 32];
699        let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
700        let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
701        let shared_secret = [7u8; 32];
702
703        let mut init_ctx_alice = context(1);
704        let alice =
705            Session::new_initiator(&mut init_ctx_alice, bob_pub, alice_secret, shared_secret)
706                .unwrap();
707        let mut init_ctx_bob = context(2);
708        let mut bob =
709            Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
710                .unwrap();
711
712        let payload = b"hello".to_vec();
713        let send_plan = alice
714            .plan_send(&payload, UnixSeconds(1_700_000_010))
715            .unwrap();
716        let send_outcome = alice.clone().apply_send(send_plan.clone());
717
718        let mut recv_ctx = context(10);
719        let receive_plan = bob
720            .plan_receive(&mut recv_ctx, &send_outcome.envelope)
721            .unwrap();
722        let outcome = bob.apply_receive(receive_plan);
723        assert_eq!(outcome.payload, payload);
724    }
725
726    #[test]
727    fn plan_receive_does_not_mutate_original_session() {
728        let alice_secret = [3u8; 32];
729        let bob_secret = [4u8; 32];
730        let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
731        let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
732        let shared_secret = [8u8; 32];
733
734        let mut init_ctx_alice = context(3);
735        let alice =
736            Session::new_initiator(&mut init_ctx_alice, bob_pub, alice_secret, shared_secret)
737                .unwrap();
738        let mut init_ctx_bob = context(4);
739        let bob = Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
740            .unwrap();
741        let bob_before = bob.state.clone();
742
743        let payload = b"typing".to_vec();
744        let send_plan = alice
745            .plan_send(&payload, UnixSeconds(1_700_000_011))
746            .unwrap();
747
748        let mut recv_ctx = context(13);
749        let _ = bob
750            .plan_receive(&mut recv_ctx, &send_plan.envelope)
751            .unwrap();
752
753        assert_eq!(bob.state, bob_before);
754    }
755
756    #[test]
757    fn duplicate_receive_fails_without_corrupting_state() {
758        let alice_secret = [5u8; 32];
759        let bob_secret = [6u8; 32];
760        let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
761        let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
762        let shared_secret = [9u8; 32];
763
764        let mut init_ctx_alice = context(5);
765        let alice =
766            Session::new_initiator(&mut init_ctx_alice, bob_pub, alice_secret, shared_secret)
767                .unwrap();
768        let mut init_ctx_bob = context(6);
769        let mut bob =
770            Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
771                .unwrap();
772
773        let payload = b"hello".to_vec();
774        let send_plan = alice
775            .plan_send(&payload, UnixSeconds(1_700_000_012))
776            .unwrap();
777        let envelope = alice.clone().apply_send(send_plan).envelope;
778
779        let mut recv_ctx = context(15);
780        let first_plan = bob.plan_receive(&mut recv_ctx, &envelope).unwrap();
781        let _ = bob.apply_receive(first_plan);
782        let after_first = bob.state.clone();
783
784        let mut replay_ctx = context(16);
785        let replay = bob.plan_receive(&mut replay_ctx, &envelope);
786        assert!(replay.is_err());
787        assert_eq!(bob.state, after_first);
788    }
789
790    fn session_pair(seed: u8) -> (Session, Session) {
791        let alice_secret = [seed; 32];
792        let bob_secret = [seed + 1; 32];
793        let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
794        let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
795        let shared_secret = [seed + 2; 32];
796        let mut alice_ctx = context(u64::from(seed));
797        let alice =
798            Session::new_initiator(&mut alice_ctx, bob_pub, alice_secret, shared_secret).unwrap();
799        let mut bob_ctx = context(u64::from(seed) + 1);
800        let bob =
801            Session::new_responder(&mut bob_ctx, alice_pub, bob_secret, shared_secret).unwrap();
802        (alice, bob)
803    }
804
805    fn send(session: &mut Session, payload: &[u8]) -> MessageEnvelope {
806        let plan = session
807            .plan_send(payload, UnixSeconds(1_700_000_100))
808            .unwrap();
809        session.apply_send(plan).envelope
810    }
811
812    fn receive(session: &mut Session, seed: u64, envelope: &MessageEnvelope) -> Result<Vec<u8>> {
813        let mut ctx = context(seed);
814        let plan = session.plan_receive(&mut ctx, envelope)?;
815        Ok(session.apply_receive(plan).payload)
816    }
817
818    /// Bob misses one message of Alice's first chain, then both sides ratchet
819    /// until Bob no longer holds the key that chain's headers were encrypted to.
820    fn bob_with_late_message(seed: u8) -> (Session, MessageEnvelope) {
821        let (mut alice, mut bob) = session_pair(seed);
822        let first = send(&mut alice, b"first");
823        let late = send(&mut alice, b"late");
824        assert_eq!(receive(&mut bob, 100, &first).unwrap(), b"first");
825
826        for round in 0..3u64 {
827            let reply = send(&mut bob, b"reply");
828            assert_eq!(receive(&mut alice, 200 + round, &reply).unwrap(), b"reply");
829            let next = send(&mut alice, b"next");
830            assert_ne!(next.sender, late.sender);
831            assert_eq!(receive(&mut bob, 300 + round, &next).unwrap(), b"next");
832        }
833        (bob, late)
834    }
835
836    #[test]
837    fn late_message_from_old_chain_decrypts_after_our_keys_rotated_out() {
838        let (mut bob, late) = bob_with_late_message(40);
839        assert!(bob.state.skipped_keys.contains_key(&late.sender));
840
841        assert_eq!(receive(&mut bob, 400, &late).unwrap(), b"late");
842        assert!(!bob.state.skipped_keys.contains_key(&late.sender));
843    }
844
845    #[test]
846    fn replayed_late_message_fails_without_corrupting_state() {
847        let (mut bob, late) = bob_with_late_message(50);
848        let mut other = bob.clone();
849        assert_eq!(receive(&mut bob, 400, &late).unwrap(), b"late");
850        let after = bob.state.clone();
851
852        assert!(receive(&mut bob, 401, &late).is_err());
853        assert_eq!(bob.state, after);
854
855        // A header that decrypts with a retained header key but names a
856        // message whose key is gone must not advance the current chain.
857        let entry = other.state.skipped_keys.get_mut(&late.sender).unwrap();
858        entry.message_keys.clear();
859        entry.message_keys.insert(999, [1u8; 32]);
860        let before = other.state.clone();
861        assert!(receive(&mut other, 402, &late).is_err());
862        assert_eq!(other.state, before);
863    }
864
865    #[test]
866    fn skipped_header_keys_survive_serialization_and_old_state_still_loads() {
867        let (bob, late) = bob_with_late_message(60);
868        let json = serde_json::to_value(&bob.state).unwrap();
869        let mut restored = Session::from_state(serde_json::from_value(json.clone()).unwrap());
870        assert_eq!(restored.state, bob.state);
871        assert_eq!(receive(&mut restored, 400, &late).unwrap(), b"late");
872
873        // State written before header keys were retained has no such field.
874        let mut old = json;
875        for entry in old["skipped_keys"].as_object_mut().unwrap().values_mut() {
876            entry.as_object_mut().unwrap().remove("header_keys");
877        }
878        let mut old = Session::from_state(serde_json::from_value(old).unwrap());
879        assert!(old.state.skipped_keys.contains_key(&late.sender));
880        let before = old.state.clone();
881        assert!(receive(&mut old, 401, &late).is_err());
882        assert_eq!(old.state, before);
883
884        // A session with nothing skipped serializes exactly as before.
885        let (_, fresh) = session_pair(70);
886        let fresh = serde_json::to_string(&fresh.state).unwrap();
887        assert!(!fresh.contains("header_keys"));
888    }
889
890    #[test]
891    fn invalid_sender_is_rejected() {
892        let alice_secret = [7u8; 32];
893        let bob_secret = [8u8; 32];
894        let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
895        let shared_secret = [10u8; 32];
896
897        let mut init_ctx_bob = context(7);
898        let bob = Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
899            .unwrap();
900
901        let mut recv_ctx = context(17);
902        let err = bob
903            .plan_receive(
904                &mut recv_ctx,
905                &MessageEnvelope {
906                    metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
907                    sender: device_pubkey_from_secret_bytes(&bob_secret).unwrap(),
908                    recipient: None,
909                    signer_secret_key: bob_secret,
910                    created_at: UnixSeconds(1),
911                    encrypted_header: "bad".to_string(),
912                    ciphertext: "bad".to_string(),
913                },
914            )
915            .unwrap_err();
916        assert!(matches!(
917            err,
918            crate::Error::Domain(DomainError::UnexpectedSender)
919        ));
920    }
921}