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 #[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 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(¤t.private_key)?;
581 if let Ok(decrypted) = nip44::decrypt(¤t_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
612fn 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 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 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 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 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}