1use crate::{
2 device_pubkey_from_secret_bytes, kdf, 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}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct SessionState {
36 #[serde(default)]
37 pub metadata_privacy_version: u8,
38 #[serde(with = "hex::serde")]
39 pub root_key: [u8; 32],
40 pub their_current_nostr_public_key: Option<DevicePubkey>,
41 pub their_next_nostr_public_key: Option<DevicePubkey>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub our_previous_nostr_key: Option<SerializableKeyPair>,
44 pub our_current_nostr_key: Option<SerializableKeyPair>,
45 pub our_next_nostr_key: SerializableKeyPair,
46 #[serde(default, with = "crate::serde_hex::option")]
47 pub receiving_chain_key: Option<[u8; 32]>,
48 #[serde(default, with = "crate::serde_hex::option")]
49 pub sending_chain_key: Option<[u8; 32]>,
50 pub sending_chain_message_number: u32,
51 pub receiving_chain_message_number: u32,
52 pub previous_sending_chain_message_count: u32,
53 pub skipped_keys: BTreeMap<DevicePubkey, SkippedKeysEntry>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57pub struct MessageEnvelope {
58 #[serde(default)]
59 pub metadata_privacy_version: u8,
60 pub sender: DevicePubkey,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub recipient: Option<DevicePubkey>,
63 pub signer_secret_key: [u8; 32],
64 pub created_at: UnixSeconds,
65 pub encrypted_header: String,
66 pub ciphertext: String,
67}
68
69#[derive(Debug, Clone)]
70pub struct SendPlan {
71 pub next_state: SessionState,
72 pub envelope: MessageEnvelope,
73 pub payload: Vec<u8>,
74}
75
76#[derive(Debug, Clone)]
77pub struct SendOutcome {
78 pub envelope: MessageEnvelope,
79 pub payload: Vec<u8>,
80}
81
82#[derive(Debug, Clone)]
83pub struct ReceivePlan {
84 pub next_state: SessionState,
85 pub payload: Vec<u8>,
86 pub sender: DevicePubkey,
87}
88
89#[derive(Debug, Clone)]
90pub struct ReceiveOutcome {
91 pub payload: Vec<u8>,
92 pub sender: DevicePubkey,
93}
94
95#[derive(Debug, Clone)]
96pub struct Session {
97 pub state: SessionState,
98 pub name: String,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102enum HeaderDecryptionTarget {
103 Current,
104 Next,
105 Previous,
106}
107
108impl Session {
109 pub fn from_state(state: SessionState) -> Self {
110 Self {
111 state,
112 name: String::new(),
113 }
114 }
115
116 pub fn new(state: SessionState, name: String) -> Self {
117 Self { state, name }
118 }
119
120 pub fn init(
121 their_ephemeral_nostr_public_key: PublicKey,
122 our_ephemeral_nostr_private_key: [u8; 32],
123 is_initiator: bool,
124 shared_secret: [u8; 32],
125 _name: Option<String>,
126 ) -> Result<Self> {
127 let mut rng = OsRng;
128 let mut ctx = ProtocolContext::new(
129 UnixSeconds(
130 std::time::SystemTime::now()
131 .duration_since(std::time::UNIX_EPOCH)
132 .unwrap()
133 .as_secs(),
134 ),
135 &mut rng,
136 );
137 let peer = DevicePubkey::from_bytes(their_ephemeral_nostr_public_key.to_bytes());
138 let mut session = if is_initiator {
139 Self::new_initiator(
140 &mut ctx,
141 peer,
142 our_ephemeral_nostr_private_key,
143 shared_secret,
144 )
145 } else {
146 Self::new_responder(
147 &mut ctx,
148 peer,
149 our_ephemeral_nostr_private_key,
150 shared_secret,
151 )
152 }?;
153 session.name = _name.unwrap_or_default();
154 Ok(session)
155 }
156
157 pub fn new_initiator<R>(
158 ctx: &mut ProtocolContext<'_, R>,
159 their_ephemeral_public_key: DevicePubkey,
160 our_ephemeral_private_key: [u8; 32],
161 shared_secret: [u8; 32],
162 ) -> Result<Self>
163 where
164 R: RngCore + CryptoRng,
165 {
166 Self::init_with_context(
167 ctx,
168 their_ephemeral_public_key,
169 our_ephemeral_private_key,
170 true,
171 shared_secret,
172 )
173 }
174
175 pub fn new_responder<R>(
176 ctx: &mut ProtocolContext<'_, R>,
177 their_ephemeral_public_key: DevicePubkey,
178 our_ephemeral_private_key: [u8; 32],
179 shared_secret: [u8; 32],
180 ) -> Result<Self>
181 where
182 R: RngCore + CryptoRng,
183 {
184 Self::init_with_context(
185 ctx,
186 their_ephemeral_public_key,
187 our_ephemeral_private_key,
188 false,
189 shared_secret,
190 )
191 }
192
193 fn init_with_context<R>(
194 ctx: &mut ProtocolContext<'_, R>,
195 their_ephemeral_public_key: DevicePubkey,
196 our_ephemeral_private_key: [u8; 32],
197 is_initiator: bool,
198 shared_secret: [u8; 32],
199 ) -> Result<Self>
200 where
201 R: RngCore + CryptoRng,
202 {
203 let (their_ephemeral_public_key, our_ephemeral_private_key) = if is_initiator {
204 (
205 crate::metadata_privacy::private_responder_public_key(
206 our_ephemeral_private_key,
207 their_ephemeral_public_key,
208 )?,
209 our_ephemeral_private_key,
210 )
211 } else {
212 (
213 their_ephemeral_public_key,
214 crate::metadata_privacy::private_responder_secret_key(
215 our_ephemeral_private_key,
216 their_ephemeral_public_key,
217 )?,
218 )
219 };
220 let our_keys = nostr::Keys::new(secret_key_from_bytes(&our_ephemeral_private_key)?);
221 let our_next_private_key = random_secret_key_bytes(ctx.rng)?;
222 let our_next_keys = nostr::Keys::new(secret_key_from_bytes(&our_next_private_key)?);
223
224 let (root_key, sending_chain_key, our_current_nostr_key, our_next_nostr_key) =
225 if is_initiator {
226 let our_current_pubkey = DevicePubkey::from_nostr(our_keys.public_key());
227 let conversation_key = nip44::v2::ConversationKey::derive(
228 our_next_keys.secret_key(),
229 &their_ephemeral_public_key.to_nostr()?,
230 )?;
231 let kdf_outputs = kdf(&shared_secret, conversation_key.as_bytes(), 2);
232 (
233 kdf_outputs[0],
234 Some(kdf_outputs[1]),
235 Some(SerializableKeyPair {
236 public_key: our_current_pubkey,
237 private_key: our_ephemeral_private_key,
238 }),
239 SerializableKeyPair {
240 public_key: DevicePubkey::from_nostr(our_next_keys.public_key()),
241 private_key: our_next_private_key,
242 },
243 )
244 } else {
245 (
246 shared_secret,
247 None,
248 None,
249 SerializableKeyPair {
250 public_key: DevicePubkey::from_nostr(our_keys.public_key()),
251 private_key: our_ephemeral_private_key,
252 },
253 )
254 };
255
256 Ok(Self {
257 state: SessionState {
258 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
259 root_key,
260 their_current_nostr_public_key: None,
261 their_next_nostr_public_key: Some(their_ephemeral_public_key),
262 our_previous_nostr_key: None,
263 our_current_nostr_key,
264 our_next_nostr_key,
265 receiving_chain_key: None,
266 sending_chain_key,
267 sending_chain_message_number: 0,
268 receiving_chain_message_number: 0,
269 previous_sending_chain_message_count: 0,
270 skipped_keys: BTreeMap::new(),
271 },
272 name: String::new(),
273 })
274 }
275
276 pub fn can_send(&self) -> bool {
277 self.state.metadata_privacy_version == crate::METADATA_PRIVACY_VERSION
278 && self.state.their_next_nostr_public_key.is_some()
279 && self.state.our_current_nostr_key.is_some()
280 }
281
282 pub fn matches_sender(&self, sender: DevicePubkey) -> bool {
283 self.state.metadata_privacy_version == crate::METADATA_PRIVACY_VERSION
284 && (self.state.their_current_nostr_public_key == Some(sender)
285 || self.state.their_next_nostr_public_key == Some(sender)
286 || self.state.skipped_keys.contains_key(&sender))
287 }
288
289 pub fn plan_send(&self, payload: &[u8], now: UnixSeconds) -> Result<SendPlan> {
290 if !self.can_send() {
291 return Err(DomainError::CannotSendYet.into());
292 }
293
294 let mut next_state = self.state.clone();
295 let (header, ciphertext) = ratchet_encrypt(&mut next_state, payload)?;
296 let our_current = self
297 .state
298 .our_current_nostr_key
299 .as_ref()
300 .ok_or(DomainError::SessionNotReady)?;
301 let our_secret = secret_key_from_bytes(&our_current.private_key)?;
302 let their_next = self
303 .state
304 .their_next_nostr_public_key
305 .ok_or(DomainError::SessionNotReady)?;
306 let encrypted_header = nip44::encrypt(
307 &our_secret,
308 &their_next.to_nostr()?,
309 &serde_json::to_string(&header)?,
310 Version::V2,
311 )?;
312
313 Ok(SendPlan {
314 next_state,
315 envelope: MessageEnvelope {
316 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
317 sender: our_current.public_key,
318 recipient: None,
319 signer_secret_key: our_current.private_key,
320 created_at: now,
321 encrypted_header,
322 ciphertext,
323 },
324 payload: payload.to_vec(),
325 })
326 }
327
328 pub fn apply_send(&mut self, plan: SendPlan) -> SendOutcome {
329 self.state = plan.next_state;
330 SendOutcome {
331 envelope: plan.envelope,
332 payload: plan.payload,
333 }
334 }
335
336 pub fn plan_receive<R>(
337 &self,
338 ctx: &mut ProtocolContext<'_, R>,
339 envelope: &MessageEnvelope,
340 ) -> Result<ReceivePlan>
341 where
342 R: RngCore + CryptoRng,
343 {
344 if envelope.metadata_privacy_version != crate::METADATA_PRIVACY_VERSION
345 || !self.matches_sender(envelope.sender)
346 {
347 return Err(DomainError::UnexpectedSender.into());
348 }
349
350 let mut next_state = self.state.clone();
351 let previous_chain_sender = next_state
352 .their_current_nostr_public_key
353 .or(next_state.their_next_nostr_public_key);
354 let (header, decryption_target) =
355 decrypt_header(&next_state, &envelope.encrypted_header, envelope.sender)?;
356 let should_ratchet = decryption_target == HeaderDecryptionTarget::Next;
357
358 let expected_next = next_state.their_next_nostr_public_key;
359 if should_ratchet && expected_next != Some(header.next_public_key) {
360 next_state.their_current_nostr_public_key = next_state.their_next_nostr_public_key;
361 next_state.their_next_nostr_public_key = Some(header.next_public_key);
362 }
363
364 if should_ratchet {
365 if next_state.receiving_chain_key.is_some() {
366 let skipped_sender = previous_chain_sender.ok_or(DomainError::SessionNotReady)?;
367 skip_message_keys(
368 &mut next_state,
369 header.previous_chain_length,
370 skipped_sender,
371 )?;
372 }
373 ratchet_step(&mut next_state, ctx.rng)?;
374 }
375
376 let payload = ratchet_decrypt(
377 &mut next_state,
378 &header,
379 &envelope.ciphertext,
380 envelope.sender,
381 )?;
382
383 Ok(ReceivePlan {
384 next_state,
385 payload,
386 sender: envelope.sender,
387 })
388 }
389
390 pub fn apply_receive(&mut self, plan: ReceivePlan) -> ReceiveOutcome {
391 self.state = plan.next_state;
392 ReceiveOutcome {
393 payload: plan.payload,
394 sender: plan.sender,
395 }
396 }
397
398 pub fn close(&self) {}
399}
400
401fn ratchet_encrypt(state: &mut SessionState, plaintext: &[u8]) -> Result<(Header, String)> {
402 let sending_chain_key = state
403 .sending_chain_key
404 .ok_or(DomainError::SessionNotReady)?;
405
406 let kdf_outputs = kdf(&sending_chain_key, &[1u8], 2);
407 state.sending_chain_key = Some(kdf_outputs[0]);
408 let message_key = kdf_outputs[1];
409
410 let header = Header {
411 number: state.sending_chain_message_number,
412 next_public_key: state.our_next_nostr_key.public_key,
413 previous_chain_length: state.previous_sending_chain_message_count,
414 };
415
416 state.sending_chain_message_number += 1;
417
418 let conversation_key = nip44::v2::ConversationKey::new(message_key);
419 let encrypted_bytes = nip44::v2::encrypt_to_bytes(&conversation_key, plaintext)?;
420 let ciphertext = base64::engine::general_purpose::STANDARD.encode(encrypted_bytes);
421 Ok((header, ciphertext))
422}
423
424fn ratchet_decrypt(
425 state: &mut SessionState,
426 header: &Header,
427 ciphertext: &str,
428 sender: DevicePubkey,
429) -> Result<Vec<u8>> {
430 if let Some(plaintext) = try_skipped_message_keys(state, header, ciphertext, sender)? {
431 return Ok(plaintext);
432 }
433
434 if state.receiving_chain_key.is_none() {
435 return Err(DomainError::SessionNotReady.into());
436 }
437
438 skip_message_keys(state, header.number, sender)?;
439
440 let receiving_chain_key = state
441 .receiving_chain_key
442 .ok_or(DomainError::SessionNotReady)?;
443
444 let kdf_outputs = kdf(&receiving_chain_key, &[1u8], 2);
445 state.receiving_chain_key = Some(kdf_outputs[0]);
446 let message_key = kdf_outputs[1];
447 state.receiving_chain_message_number += 1;
448
449 let conversation_key = nip44::v2::ConversationKey::new(message_key);
450 let ciphertext_bytes = base64::engine::general_purpose::STANDARD
451 .decode(ciphertext)
452 .map_err(|e| crate::Error::Decryption(e.to_string()))?;
453
454 nip44::v2::decrypt_to_bytes(&conversation_key, &ciphertext_bytes).map_err(Into::into)
455}
456
457fn ratchet_step<R>(state: &mut SessionState, rng: &mut R) -> Result<()>
458where
459 R: RngCore + CryptoRng,
460{
461 state.previous_sending_chain_message_count = state.sending_chain_message_number;
462 state.sending_chain_message_number = 0;
463 state.receiving_chain_message_number = 0;
464
465 let our_next_sk = secret_key_from_bytes(&state.our_next_nostr_key.private_key)?;
466 let their_next_pk = state
467 .their_next_nostr_public_key
468 .ok_or(DomainError::SessionNotReady)?;
469
470 let conversation_key1 =
471 nip44::v2::ConversationKey::derive(&our_next_sk, &their_next_pk.to_nostr()?)?;
472 let kdf_outputs = kdf(&state.root_key, conversation_key1.as_bytes(), 2);
473 state.receiving_chain_key = Some(kdf_outputs[1]);
474 state.our_previous_nostr_key = state.our_current_nostr_key.clone();
475 state.our_current_nostr_key = Some(state.our_next_nostr_key.clone());
476
477 let our_next_private_key = random_secret_key_bytes(rng)?;
478 state.our_next_nostr_key = SerializableKeyPair {
479 public_key: device_pubkey_from_secret_bytes(&our_next_private_key)?,
480 private_key: our_next_private_key,
481 };
482
483 let our_next_sk2 = secret_key_from_bytes(&our_next_private_key)?;
484 let conversation_key2 =
485 nip44::v2::ConversationKey::derive(&our_next_sk2, &their_next_pk.to_nostr()?)?;
486 let kdf_outputs2 = kdf(&kdf_outputs[0], conversation_key2.as_bytes(), 2);
487 state.root_key = kdf_outputs2[0];
488 state.sending_chain_key = Some(kdf_outputs2[1]);
489 Ok(())
490}
491
492fn skip_message_keys(state: &mut SessionState, until: u32, sender: DevicePubkey) -> Result<()> {
493 if until <= state.receiving_chain_message_number {
494 return Ok(());
495 }
496
497 if (until - state.receiving_chain_message_number) as usize > MAX_SKIP {
498 return Err(DomainError::TooManySkippedMessages.into());
499 }
500
501 let entry = state.skipped_keys.entry(sender).or_default();
502
503 while state.receiving_chain_message_number < until {
504 let receiving_chain_key = state
505 .receiving_chain_key
506 .ok_or(DomainError::SessionNotReady)?;
507 let kdf_outputs = kdf(&receiving_chain_key, &[1u8], 2);
508 state.receiving_chain_key = Some(kdf_outputs[0]);
509 entry
510 .message_keys
511 .insert(state.receiving_chain_message_number, kdf_outputs[1]);
512 state.receiving_chain_message_number += 1;
513 }
514
515 prune_skipped_message_keys(&mut entry.message_keys);
516 Ok(())
517}
518
519fn try_skipped_message_keys(
520 state: &mut SessionState,
521 header: &Header,
522 ciphertext: &str,
523 sender: DevicePubkey,
524) -> Result<Option<Vec<u8>>> {
525 if let Some(entry) = state.skipped_keys.get_mut(&sender) {
526 if let Some(message_key) = entry.message_keys.remove(&header.number) {
527 let conversation_key = nip44::v2::ConversationKey::new(message_key);
528 let ciphertext_bytes = base64::engine::general_purpose::STANDARD
529 .decode(ciphertext)
530 .map_err(|e| crate::Error::Decryption(e.to_string()))?;
531 let plaintext = nip44::v2::decrypt_to_bytes(&conversation_key, &ciphertext_bytes)?;
532 if entry.message_keys.is_empty() {
533 state.skipped_keys.remove(&sender);
534 }
535 return Ok(Some(plaintext));
536 }
537 }
538
539 Ok(None)
540}
541
542fn decrypt_header(
543 state: &SessionState,
544 encrypted_header: &str,
545 sender: DevicePubkey,
546) -> Result<(Header, HeaderDecryptionTarget)> {
547 if let Some(current) = &state.our_current_nostr_key {
548 let current_sk = secret_key_from_bytes(¤t.private_key)?;
549 if let Ok(decrypted) = nip44::decrypt(¤t_sk, &sender.to_nostr()?, encrypted_header) {
550 let header: Header = serde_json::from_str(&decrypted)?;
551 return Ok((header, HeaderDecryptionTarget::Current));
552 }
553 }
554
555 let next_sk = secret_key_from_bytes(&state.our_next_nostr_key.private_key)?;
556 if let Ok(decrypted) = nip44::decrypt(&next_sk, &sender.to_nostr()?, encrypted_header) {
557 let header: Header = serde_json::from_str(&decrypted)?;
558 return Ok((header, HeaderDecryptionTarget::Next));
559 }
560
561 if let Some(previous) = &state.our_previous_nostr_key {
562 let previous_sk = secret_key_from_bytes(&previous.private_key)?;
563 if let Ok(decrypted) = nip44::decrypt(&previous_sk, &sender.to_nostr()?, encrypted_header) {
564 let header: Header = serde_json::from_str(&decrypted)?;
565 return Ok((header, HeaderDecryptionTarget::Previous));
566 }
567 }
568
569 Err(crate::Error::Parse("invalid header".to_string()))
570}
571
572fn prune_skipped_message_keys(map: &mut BTreeMap<u32, [u8; 32]>) {
573 while map.len() > MAX_SKIP {
574 let Some(first) = map.keys().next().copied() else {
575 break;
576 };
577 map.remove(&first);
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use rand::{rngs::StdRng, SeedableRng};
585
586 fn context(seed: u64) -> ProtocolContext<'static, StdRng> {
587 let rng = Box::new(StdRng::seed_from_u64(seed));
588 let rng = Box::leak(rng);
589 ProtocolContext::new(UnixSeconds(1_700_000_000), rng)
590 }
591
592 #[test]
593 fn header_json_uses_camel_case_wire_fields() {
594 let header = Header {
595 number: 3,
596 previous_chain_length: 2,
597 next_public_key: DevicePubkey::from_bytes([9u8; 32]),
598 };
599
600 let json = serde_json::to_value(&header).unwrap();
601 assert_eq!(json["number"], serde_json::json!(3));
602 assert_eq!(json["previousChainLength"], serde_json::json!(2));
603 assert_eq!(
604 json["nextPublicKey"],
605 serde_json::json!(header.next_public_key.to_string())
606 );
607 assert!(json.get("previous_chain_length").is_none());
608 assert!(json.get("next_public_key").is_none());
609
610 let decoded: Header = serde_json::from_value(json).unwrap();
611 assert_eq!(decoded, header);
612 }
613
614 #[test]
615 fn header_json_rejects_snake_case_wire_fields() {
616 let old_header = serde_json::json!({
617 "number": 3,
618 "previous_chain_length": 2,
619 "next_public_key": DevicePubkey::from_bytes([9u8; 32]).to_string(),
620 });
621
622 assert!(serde_json::from_value::<Header>(old_header).is_err());
623 }
624
625 #[test]
626 fn plan_send_and_apply_receive_roundtrip() {
627 let alice_secret = [1u8; 32];
628 let bob_secret = [2u8; 32];
629 let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
630 let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
631 let shared_secret = [7u8; 32];
632
633 let mut init_ctx_alice = context(1);
634 let alice =
635 Session::new_initiator(&mut init_ctx_alice, bob_pub, alice_secret, shared_secret)
636 .unwrap();
637 let mut init_ctx_bob = context(2);
638 let mut bob =
639 Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
640 .unwrap();
641
642 let payload = b"hello".to_vec();
643 let send_plan = alice
644 .plan_send(&payload, UnixSeconds(1_700_000_010))
645 .unwrap();
646 let send_outcome = alice.clone().apply_send(send_plan.clone());
647
648 let mut recv_ctx = context(10);
649 let receive_plan = bob
650 .plan_receive(&mut recv_ctx, &send_outcome.envelope)
651 .unwrap();
652 let outcome = bob.apply_receive(receive_plan);
653 assert_eq!(outcome.payload, payload);
654 }
655
656 #[test]
657 fn plan_receive_does_not_mutate_original_session() {
658 let alice_secret = [3u8; 32];
659 let bob_secret = [4u8; 32];
660 let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
661 let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
662 let shared_secret = [8u8; 32];
663
664 let mut init_ctx_alice = context(3);
665 let alice =
666 Session::new_initiator(&mut init_ctx_alice, bob_pub, alice_secret, shared_secret)
667 .unwrap();
668 let mut init_ctx_bob = context(4);
669 let bob = Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
670 .unwrap();
671 let bob_before = bob.state.clone();
672
673 let payload = b"typing".to_vec();
674 let send_plan = alice
675 .plan_send(&payload, UnixSeconds(1_700_000_011))
676 .unwrap();
677
678 let mut recv_ctx = context(13);
679 let _ = bob
680 .plan_receive(&mut recv_ctx, &send_plan.envelope)
681 .unwrap();
682
683 assert_eq!(bob.state, bob_before);
684 }
685
686 #[test]
687 fn duplicate_receive_fails_without_corrupting_state() {
688 let alice_secret = [5u8; 32];
689 let bob_secret = [6u8; 32];
690 let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
691 let bob_pub = device_pubkey_from_secret_bytes(&bob_secret).unwrap();
692 let shared_secret = [9u8; 32];
693
694 let mut init_ctx_alice = context(5);
695 let alice =
696 Session::new_initiator(&mut init_ctx_alice, bob_pub, alice_secret, shared_secret)
697 .unwrap();
698 let mut init_ctx_bob = context(6);
699 let mut bob =
700 Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
701 .unwrap();
702
703 let payload = b"hello".to_vec();
704 let send_plan = alice
705 .plan_send(&payload, UnixSeconds(1_700_000_012))
706 .unwrap();
707 let envelope = alice.clone().apply_send(send_plan).envelope;
708
709 let mut recv_ctx = context(15);
710 let first_plan = bob.plan_receive(&mut recv_ctx, &envelope).unwrap();
711 let _ = bob.apply_receive(first_plan);
712 let after_first = bob.state.clone();
713
714 let mut replay_ctx = context(16);
715 let replay = bob.plan_receive(&mut replay_ctx, &envelope);
716 assert!(replay.is_err());
717 assert_eq!(bob.state, after_first);
718 }
719
720 #[test]
721 fn invalid_sender_is_rejected() {
722 let alice_secret = [7u8; 32];
723 let bob_secret = [8u8; 32];
724 let alice_pub = device_pubkey_from_secret_bytes(&alice_secret).unwrap();
725 let shared_secret = [10u8; 32];
726
727 let mut init_ctx_bob = context(7);
728 let bob = Session::new_responder(&mut init_ctx_bob, alice_pub, bob_secret, shared_secret)
729 .unwrap();
730
731 let mut recv_ctx = context(17);
732 let err = bob
733 .plan_receive(
734 &mut recv_ctx,
735 &MessageEnvelope {
736 metadata_privacy_version: crate::METADATA_PRIVACY_VERSION,
737 sender: device_pubkey_from_secret_bytes(&bob_secret).unwrap(),
738 recipient: None,
739 signer_secret_key: bob_secret,
740 created_at: UnixSeconds(1),
741 encrypted_header: "bad".to_string(),
742 ciphertext: "bad".to_string(),
743 },
744 )
745 .unwrap_err();
746 assert!(matches!(
747 err,
748 crate::Error::Domain(DomainError::UnexpectedSender)
749 ));
750 }
751}