1mod group;
28mod invite;
29mod manager;
30mod sender_key;
31
32pub use group::*;
33pub use invite::*;
34pub use manager::*;
35pub use sender_key::*;
36
37use crate::Nip44;
38use base64::engine::{Engine as _, general_purpose};
39use nostro2_traits::{NostrKeypair, SignerError, hex::Hexable as _};
40use std::collections::BTreeMap;
41use zeroize::Zeroize;
42
43pub const MAX_SKIP: usize = 1000;
46
47pub const MESSAGE_EVENT_KIND: u32 = 1060;
54
55const HEADER_TAG: &str = "header";
57
58#[derive(Debug)]
60pub enum Nip104Error {
61 CannotSendYet,
63 SessionNotReady,
65 UnexpectedSender,
67 TooManySkippedMessages,
69 InvalidHeader,
71 InvalidInvite(String),
74 UnknownPeer(String),
76 Signer(SignerError),
78 Nip44(crate::Nip44Error),
80 Json(String),
82 Base64(base64::DecodeError),
84}
85
86impl std::fmt::Display for Nip104Error {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Self::CannotSendYet => f.write_str("session cannot send yet"),
90 Self::SessionNotReady => f.write_str("session not ready: missing key material"),
91 Self::UnexpectedSender => f.write_str("envelope sender matches no known chain"),
92 Self::TooManySkippedMessages => f.write_str("too many skipped messages"),
93 Self::InvalidHeader => f.write_str("could not decrypt message header"),
94 Self::InvalidInvite(e) => write!(f, "invalid invite: {e}"),
95 Self::UnknownPeer(p) => write!(f, "no session with peer {p}"),
96 Self::Signer(e) => write!(f, "signer error: {e}"),
97 Self::Nip44(e) => write!(f, "nip-44 error: {e}"),
98 Self::Json(e) => write!(f, "json error: {e}"),
99 Self::Base64(e) => write!(f, "base64 error: {e}"),
100 }
101 }
102}
103
104impl std::error::Error for Nip104Error {}
105
106impl From<SignerError> for Nip104Error {
107 fn from(e: SignerError) -> Self {
108 Self::Signer(e)
109 }
110}
111impl From<crate::Nip44Error> for Nip104Error {
112 fn from(e: crate::Nip44Error) -> Self {
113 Self::Nip44(e)
114 }
115}
116impl From<json_bourne::Error> for Nip104Error {
117 fn from(e: json_bourne::Error) -> Self {
118 Self::Json(format!("{e:?}"))
119 }
120}
121impl From<base64::DecodeError> for Nip104Error {
122 fn from(e: base64::DecodeError) -> Self {
123 Self::Base64(e)
124 }
125}
126
127type Result<T> = std::result::Result<T, Nip104Error>;
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct KeyPairBytes {
132 pub public_key: String,
134 pub private_key: String,
136}
137
138impl KeyPairBytes {
139 fn secret_bytes(&self) -> Result<[u8; 32]> {
140 let mut buf = [0_u8; 32];
141 nostro2_traits::hex::FromHex::decode_hex_to_slice(self.private_key.as_str(), &mut buf)
142 .map_err(|_| Nip104Error::Signer(SignerError::InvalidPublicKey))?;
143 Ok(buf)
144 }
145 fn from_secret<K: NostrKeypair>(secret: &[u8; 32]) -> Result<Self> {
146 let kp = K::from_secret_bytes(secret)?;
147 Ok(Self {
148 public_key: kp.public_key(),
149 private_key: secret.to_hex(),
150 })
151 }
152}
153
154impl Drop for KeyPairBytes {
156 fn drop(&mut self) {
157 self.private_key.zeroize();
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, json_bourne::FromJson, json_bourne::ToJson)]
165pub struct Header {
166 number: u32,
167 #[bourne(rename = "previousChainLength")]
168 previous_chain_length: u32,
169 #[bourne(rename = "nextPublicKey")]
170 next_public_key: String,
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct SkippedKeysEntry {
176 pub message_keys: BTreeMap<u32, String>,
178}
179
180impl Drop for SkippedKeysEntry {
182 fn drop(&mut self) {
183 for key in self.message_keys.values_mut() {
184 key.zeroize();
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct SessionState {
196 pub root_key: String,
198 pub their_current_nostr_public_key: Option<String>,
200 pub their_next_nostr_public_key: Option<String>,
202 pub our_previous_nostr_key: Option<KeyPairBytes>,
204 pub our_current_nostr_key: Option<KeyPairBytes>,
206 pub our_next_nostr_key: KeyPairBytes,
208 pub receiving_chain_key: Option<String>,
210 pub sending_chain_key: Option<String>,
212 pub sending_chain_message_number: u32,
214 pub receiving_chain_message_number: u32,
216 pub previous_sending_chain_message_count: u32,
218 pub skipped_keys: BTreeMap<String, SkippedKeysEntry>,
220}
221
222impl Drop for SessionState {
226 fn drop(&mut self) {
227 self.root_key.zeroize();
228 if let Some(k) = self.receiving_chain_key.as_mut() {
229 k.zeroize();
230 }
231 if let Some(k) = self.sending_chain_key.as_mut() {
232 k.zeroize();
233 }
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
241pub struct MessageEnvelope {
242 pub sender: String,
244 pub encrypted_header: String,
246 pub ciphertext: String,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251#[allow(clippy::redundant_pub_crate)]
252pub(crate) enum HeaderTarget {
253 Current,
254 Next,
255 Previous,
256}
257
258#[derive(Debug, Clone)]
261pub struct Session<K: NostrKeypair> {
262 pub state: SessionState,
264 _marker: std::marker::PhantomData<fn() -> K>,
265}
266
267impl<K: NostrKeypair> Session<K> {
268 #[must_use]
270 pub fn from_state(state: SessionState) -> Self {
271 Self {
272 state,
273 _marker: std::marker::PhantomData,
274 }
275 }
276
277 pub fn new_initiator(
285 their_ephemeral_pubkey: &[u8; 32],
286 our_secret: &[u8; 32],
287 shared_secret: &[u8; 32],
288 ) -> Result<Self> {
289 Self::init(their_ephemeral_pubkey, our_secret, true, shared_secret)
290 }
291
292 pub fn new_responder(
297 their_ephemeral_pubkey: &[u8; 32],
298 our_secret: &[u8; 32],
299 shared_secret: &[u8; 32],
300 ) -> Result<Self> {
301 Self::init(their_ephemeral_pubkey, our_secret, false, shared_secret)
302 }
303
304 fn init(
305 their_ephemeral_pubkey: &[u8; 32],
306 our_secret: &[u8; 32],
307 is_initiator: bool,
308 shared_secret: &[u8; 32],
309 ) -> Result<Self> {
310 let our_current = KeyPairBytes::from_secret::<K>(our_secret)?;
311 let their_next_hex = their_ephemeral_pubkey.to_hex();
312
313 let state = if is_initiator {
314 let our_next_secret = K::generate().secret_bytes();
315 let our_next = KeyPairBytes::from_secret::<K>(&our_next_secret)?;
316 let conv = K::derive_conv_key(&our_next_secret, their_ephemeral_pubkey)?;
319 let outs = K::kdf(shared_secret, &conv, 2);
320 SessionState {
321 root_key: outs[0].to_hex(),
322 their_current_nostr_public_key: None,
323 their_next_nostr_public_key: Some(their_next_hex),
324 our_previous_nostr_key: None,
325 our_current_nostr_key: Some(our_current),
326 our_next_nostr_key: our_next,
327 receiving_chain_key: None,
328 sending_chain_key: Some(outs[1].to_hex()),
329 sending_chain_message_number: 0,
330 receiving_chain_message_number: 0,
331 previous_sending_chain_message_count: 0,
332 skipped_keys: BTreeMap::new(),
333 }
334 } else {
335 SessionState {
336 root_key: shared_secret.to_hex(),
337 their_current_nostr_public_key: None,
338 their_next_nostr_public_key: Some(their_next_hex),
339 our_previous_nostr_key: None,
340 our_current_nostr_key: None,
341 our_next_nostr_key: our_current,
342 receiving_chain_key: None,
343 sending_chain_key: None,
344 sending_chain_message_number: 0,
345 receiving_chain_message_number: 0,
346 previous_sending_chain_message_count: 0,
347 skipped_keys: BTreeMap::new(),
348 }
349 };
350
351 Ok(Self::from_state(state))
352 }
353
354 #[must_use]
356 pub const fn can_send(&self) -> bool {
357 self.state.their_next_nostr_public_key.is_some()
358 && self.state.our_current_nostr_key.is_some()
359 }
360
361 fn matches_sender(&self, sender: &str) -> bool {
362 self.state.their_current_nostr_public_key.as_deref() == Some(sender)
363 || self.state.their_next_nostr_public_key.as_deref() == Some(sender)
364 || self.state.skipped_keys.contains_key(sender)
365 }
366
367 #[must_use]
378 pub fn accepted_senders(&self) -> Vec<String> {
379 let mut out = Vec::with_capacity(2 + self.state.skipped_keys.len());
380 if let Some(cur) = self.state.their_current_nostr_public_key.as_deref() {
381 out.push(cur.to_owned());
382 }
383 if let Some(next) = self.state.their_next_nostr_public_key.as_deref()
384 && Some(next) != self.state.their_current_nostr_public_key.as_deref()
385 {
386 out.push(next.to_owned());
387 }
388 for k in self.state.skipped_keys.keys() {
389 if Some(k.as_str()) != self.state.their_current_nostr_public_key.as_deref()
390 && Some(k.as_str()) != self.state.their_next_nostr_public_key.as_deref()
391 {
392 out.push(k.clone());
393 }
394 }
395 out
396 }
397
398 pub fn plan_send(&self, payload: &[u8]) -> Result<(SessionState, MessageEnvelope)> {
407 if !self.can_send() {
408 return Err(Nip104Error::CannotSendYet);
409 }
410 let mut next = self.state.clone();
411 let (header, ciphertext) = K::ratchet_encrypt(&mut next, payload)?;
412
413 let our_current = self
414 .state
415 .our_current_nostr_key
416 .as_ref()
417 .ok_or(Nip104Error::SessionNotReady)?;
418 let their_next = self
419 .state
420 .their_next_nostr_public_key
421 .as_deref()
422 .ok_or(Nip104Error::SessionNotReady)?;
423
424 let our_kp = K::from_secret_bytes(&our_current.secret_bytes()?)?;
425 let header_json = json_bourne::to_string(&header)?;
426 let encrypted_header = our_kp
427 .nip_44_encrypt(&header_json, their_next)?
428 .into_owned();
429
430 Ok((
431 next,
432 MessageEnvelope {
433 sender: our_current.public_key.clone(),
434 encrypted_header,
435 ciphertext,
436 },
437 ))
438 }
439
440 pub fn plan_receive(&self, envelope: &MessageEnvelope) -> Result<(SessionState, Vec<u8>)> {
447 if !self.matches_sender(&envelope.sender) {
448 return Err(Nip104Error::UnexpectedSender);
449 }
450 let mut next = self.state.clone();
451 let previous_chain_sender = next
452 .their_current_nostr_public_key
453 .clone()
454 .or_else(|| next.their_next_nostr_public_key.clone());
455
456 let (header, target) =
457 K::decrypt_header(&next, &envelope.encrypted_header, &envelope.sender)?;
458 let should_ratchet = target == HeaderTarget::Next;
459
460 if should_ratchet
461 && next.their_next_nostr_public_key.as_ref() != Some(&header.next_public_key)
462 {
463 next.their_current_nostr_public_key = next.their_next_nostr_public_key.take();
464 next.their_next_nostr_public_key = Some(header.next_public_key.clone());
465 }
466
467 if should_ratchet {
468 if next.receiving_chain_key.is_some() {
469 let skipped_sender = previous_chain_sender.ok_or(Nip104Error::SessionNotReady)?;
470 K::skip_message_keys(&mut next, header.previous_chain_length, &skipped_sender)?;
471 }
472 K::ratchet_step(&mut next)?;
473 }
474
475 let payload =
476 K::ratchet_decrypt(&mut next, &header, &envelope.ciphertext, &envelope.sender)?;
477 Ok((next, payload))
478 }
479
480 pub fn apply(&mut self, next: SessionState) {
483 self.state = next;
484 }
485
486 pub fn plan_send_event(
500 &self,
501 payload: &[u8],
502 created_at: i64,
503 ) -> Result<(SessionState, nostro2::NostrNote)> {
504 let (next, envelope) = self.plan_send(payload)?;
505 let our_current = self
506 .state
507 .our_current_nostr_key
508 .as_ref()
509 .ok_or(Nip104Error::SessionNotReady)?;
510 let signer = K::from_secret_bytes(&our_current.secret_bytes()?)?;
511 let event = envelope.to_event(&signer, created_at)?;
512 Ok((next, event))
513 }
514
515 pub fn plan_receive_event(
523 &self,
524 event: &nostro2::NostrNote,
525 ) -> Result<(SessionState, Vec<u8>)> {
526 let envelope = MessageEnvelope::from_event(event)?;
527 self.plan_receive(&envelope)
528 }
529}
530
531impl MessageEnvelope {
532 pub fn to_event<S: nostro2::NostrSigner>(
542 &self,
543 signer: &S,
544 created_at: i64,
545 ) -> Result<nostro2::NostrNote> {
546 let mut tags = nostro2::NostrTags::new();
547 tags.add_custom_tag(HEADER_TAG, &self.encrypted_header);
548 let mut note = nostro2::NostrNote {
549 kind: MESSAGE_EVENT_KIND,
550 content: self.ciphertext.clone(),
551 created_at,
552 tags,
553 ..Default::default()
554 };
555 note.sign_with(signer)
556 .map_err(|_| Nip104Error::Signer(SignerError::InvalidSignature))?;
557 Ok(note)
558 }
559
560 pub fn from_event(event: &nostro2::NostrNote) -> Result<Self> {
569 use nostro2::NostrEvent as _;
570 if event.kind != MESSAGE_EVENT_KIND {
571 return Err(Nip104Error::InvalidHeader);
572 }
573 if !event.verify() {
574 return Err(Nip104Error::InvalidHeader);
575 }
576 let encrypted_header = event
577 .tags
578 .find_tags_ref(HEADER_TAG)
579 .into_iter()
580 .next()
581 .ok_or(Nip104Error::InvalidHeader)?
582 .to_owned();
583 Ok(Self {
584 sender: event.pubkey.clone(),
585 encrypted_header,
586 ciphertext: event.content.clone(),
587 })
588 }
589}
590
591#[allow(clippy::redundant_pub_crate)]
599pub(crate) trait Nip104Crypto: NostrKeypair + Sized {
600 fn kdf(input1: &[u8], input2: &[u8], num_outputs: usize) -> Vec<[u8; 32]> {
606 let hk = hkdf::Hkdf::<sha2::Sha256>::new(Some(input2), input1);
607 let mut outputs = Vec::with_capacity(num_outputs);
608 for i in 1..=num_outputs {
609 let mut okm = [0_u8; 32];
610 hk.expand(&[u8::try_from(i).unwrap_or(u8::MAX)], &mut okm)
611 .expect("32 bytes is a valid HKDF length");
612 outputs.push(okm);
613 }
614 outputs
615 }
616
617 fn decode_hex_32(s: &str) -> Result<[u8; 32]> {
619 let mut buf = [0_u8; 32];
620 nostro2_traits::hex::FromHex::decode_hex_to_slice(s, &mut buf)
621 .map_err(|_| Nip104Error::Signer(SignerError::InvalidPublicKey))?;
622 Ok(buf)
623 }
624
625 fn derive_conv_key(sk: &[u8; 32], pk: &[u8; 32]) -> Result<[u8; 32]> {
628 let kp = Self::from_secret_bytes(sk)?;
629 let shared = kp.ecdh_x(pk)?;
630 let conv = <Self as Nip44>::conversation_key_v2(zeroize::Zeroizing::new(shared))?;
631 Ok(*conv)
632 }
633
634 fn encrypt_with_message_key(message_key: &[u8; 32], plaintext: &[u8]) -> Result<String> {
638 let nonce = Self::generate_nonce_32();
639 Ok(<Self as Nip44>::encrypt_v2(message_key, &nonce, plaintext)?)
640 }
641
642 fn decrypt_with_message_key(message_key: &[u8; 32], ciphertext_b64: &str) -> Result<Vec<u8>> {
645 let decoded = general_purpose::STANDARD.decode(ciphertext_b64)?;
646 let s = <Self as Nip44>::decrypt_v2_bytes(message_key, &decoded)?;
647 Ok(s)
648 }
649
650 fn ratchet_encrypt(state: &mut SessionState, plaintext: &[u8]) -> Result<(Header, String)> {
653 let sending_chain_key = Self::decode_hex_32(
654 state
655 .sending_chain_key
656 .as_deref()
657 .ok_or(Nip104Error::SessionNotReady)?,
658 )?;
659 let outs = Self::kdf(&sending_chain_key, &[1_u8], 2);
660 state.sending_chain_key = Some(outs[0].to_hex());
661 let message_key = outs[1];
662
663 let header = Header {
664 number: state.sending_chain_message_number,
665 next_public_key: state.our_next_nostr_key.public_key.clone(),
666 previous_chain_length: state.previous_sending_chain_message_count,
667 };
668 state.sending_chain_message_number += 1;
669
670 let ciphertext = Self::encrypt_with_message_key(&message_key, plaintext)?;
671 Ok((header, ciphertext))
672 }
673
674 fn ratchet_decrypt(
677 state: &mut SessionState,
678 header: &Header,
679 ciphertext: &str,
680 sender: &str,
681 ) -> Result<Vec<u8>> {
682 if let Some(pt) = Self::try_skipped_message_keys(state, header, ciphertext, sender)? {
683 return Ok(pt);
684 }
685 if state.receiving_chain_key.is_none() {
686 return Err(Nip104Error::SessionNotReady);
687 }
688 Self::skip_message_keys(state, header.number, sender)?;
689
690 let receiving_chain_key = Self::decode_hex_32(
691 state
692 .receiving_chain_key
693 .as_deref()
694 .ok_or(Nip104Error::SessionNotReady)?,
695 )?;
696 let outs = Self::kdf(&receiving_chain_key, &[1_u8], 2);
697 state.receiving_chain_key = Some(outs[0].to_hex());
698 let message_key = outs[1];
699 state.receiving_chain_message_number += 1;
700
701 Self::decrypt_with_message_key(&message_key, ciphertext)
702 }
703
704 fn ratchet_step(state: &mut SessionState) -> Result<()> {
707 state.previous_sending_chain_message_count = state.sending_chain_message_number;
708 state.sending_chain_message_number = 0;
709 state.receiving_chain_message_number = 0;
710
711 let their_next = state
712 .their_next_nostr_public_key
713 .as_deref()
714 .ok_or(Nip104Error::SessionNotReady)?;
715 let their_next_bytes = Self::decode_hex_32(their_next)?;
716 let root_key = Self::decode_hex_32(&state.root_key)?;
717
718 let conv1 =
720 Self::derive_conv_key(&state.our_next_nostr_key.secret_bytes()?, &their_next_bytes)?;
721 let outs1 = Self::kdf(&root_key, &conv1, 2);
722 state.receiving_chain_key = Some(outs1[1].to_hex());
723 state.our_previous_nostr_key = state.our_current_nostr_key.take();
724 state.our_current_nostr_key = Some(state.our_next_nostr_key.clone());
725
726 let our_next_secret = Self::generate().secret_bytes();
728 state.our_next_nostr_key = KeyPairBytes::from_secret::<Self>(&our_next_secret)?;
729 let conv2 = Self::derive_conv_key(&our_next_secret, &their_next_bytes)?;
730 let outs2 = Self::kdf(&outs1[0], &conv2, 2);
731 state.root_key = outs2[0].to_hex();
732 state.sending_chain_key = Some(outs2[1].to_hex());
733 Ok(())
734 }
735
736 fn skip_message_keys(state: &mut SessionState, until: u32, sender: &str) -> Result<()> {
739 if until <= state.receiving_chain_message_number {
740 return Ok(());
741 }
742 if (until - state.receiving_chain_message_number) as usize > MAX_SKIP {
743 return Err(Nip104Error::TooManySkippedMessages);
744 }
745 let entry = state.skipped_keys.entry(sender.to_owned()).or_default();
746 while state.receiving_chain_message_number < until {
747 let rck = Self::decode_hex_32(
748 state
749 .receiving_chain_key
750 .as_deref()
751 .ok_or(Nip104Error::SessionNotReady)?,
752 )?;
753 let outs = Self::kdf(&rck, &[1_u8], 2);
754 state.receiving_chain_key = Some(outs[0].to_hex());
755 entry
756 .message_keys
757 .insert(state.receiving_chain_message_number, outs[1].to_hex());
758 state.receiving_chain_message_number += 1;
759 }
760 Self::prune_skipped(&mut entry.message_keys);
761 Ok(())
762 }
763
764 fn try_skipped_message_keys(
767 state: &mut SessionState,
768 header: &Header,
769 ciphertext: &str,
770 sender: &str,
771 ) -> Result<Option<Vec<u8>>> {
772 let Some(entry) = state.skipped_keys.get_mut(sender) else {
773 return Ok(None);
774 };
775 let Some(mk_hex) = entry.message_keys.remove(&header.number) else {
776 return Ok(None);
777 };
778 let message_key = Self::decode_hex_32(&mk_hex)?;
779 let pt = Self::decrypt_with_message_key(&message_key, ciphertext)?;
780 if entry.message_keys.is_empty() {
781 state.skipped_keys.remove(sender);
782 }
783 Ok(Some(pt))
784 }
785
786 fn decrypt_header(
789 state: &SessionState,
790 encrypted_header: &str,
791 sender: &str,
792 ) -> Result<(Header, HeaderTarget)> {
793 if let Some(current) = &state.our_current_nostr_key
794 && let Ok(h) =
795 Self::try_decrypt_header(¤t.secret_bytes()?, sender, encrypted_header)
796 {
797 return Ok((h, HeaderTarget::Current));
798 }
799 if let Ok(h) = Self::try_decrypt_header(
800 &state.our_next_nostr_key.secret_bytes()?,
801 sender,
802 encrypted_header,
803 ) {
804 return Ok((h, HeaderTarget::Next));
805 }
806 if let Some(previous) = &state.our_previous_nostr_key
807 && let Ok(h) =
808 Self::try_decrypt_header(&previous.secret_bytes()?, sender, encrypted_header)
809 {
810 return Ok((h, HeaderTarget::Previous));
811 }
812 Err(Nip104Error::InvalidHeader)
813 }
814
815 fn try_decrypt_header(
817 our_secret: &[u8; 32],
818 sender: &str,
819 encrypted_header: &str,
820 ) -> Result<Header> {
821 let kp = Self::from_secret_bytes(our_secret)?;
822 let json = kp.nip_44_decrypt(encrypted_header, sender)?;
823 Ok(json_bourne::parse_str(&json)?)
824 }
825
826 fn prune_skipped(map: &mut BTreeMap<u32, String>) {
828 while map.len() > MAX_SKIP {
829 let Some(first) = map.keys().next().copied() else {
830 break;
831 };
832 map.remove(&first);
833 }
834 }
835}
836
837impl<K: NostrKeypair> Nip104Crypto for K {}
838
839#[cfg(test)]
840mod tests {
841 use super::*;
842
843 use nostro2_traits::NostrSigner as _;
844
845 type K = crate::tests::NipTester;
846
847 fn shared_secret() -> [u8; 32] {
848 [7_u8; 32]
849 }
850
851 #[test]
852 fn kdf_matches_reference_shape() {
853 let a = K::kdf(&[1_u8; 32], &[2_u8; 32], 2);
855 let b = K::kdf(&[1_u8; 32], &[2_u8; 32], 2);
856 assert_eq!(a, b);
857 assert_eq!(a.len(), 2);
858 assert_ne!(a[0], a[1]);
859 }
860
861 #[test]
862 fn header_json_is_camel_case_and_round_trips() {
863 let h = Header {
864 number: 3,
865 previous_chain_length: 2,
866 next_public_key: "ab".repeat(32),
867 };
868 let s = json_bourne::to_string(&h).unwrap();
869 assert!(s.contains("\"previousChainLength\":2"), "got {s}");
871 assert!(s.contains("\"nextPublicKey\":"), "got {s}");
872 assert!(!s.contains("previous_chain_length"), "got {s}");
873 let back: Header = json_bourne::parse_str(&s).unwrap();
874 assert_eq!(back, h);
875 }
876
877 #[test]
878 fn round_trip_single_message() {
879 let alice_secret = [1_u8; 32];
880 let bob_secret = [2_u8; 32];
881 let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
882 let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
883
884 let alice = Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
885 let mut bob =
886 Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
887
888 let (alice_next, envelope) = alice.plan_send(b"hello bob").unwrap();
889 let _ = alice_next; let (bob_next, plaintext) = bob.plan_receive(&envelope).unwrap();
892 bob.apply(bob_next);
893 assert_eq!(plaintext, b"hello bob");
894 }
895
896 #[test]
900 fn bidirectional_conversation_ratchets() {
901 let alice_secret = [1_u8; 32];
902 let bob_secret = [2_u8; 32];
903 let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
904 let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
905
906 let mut alice =
907 Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
908 let mut bob =
909 Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
910
911 for msg in [b"a1".as_slice(), b"a2".as_slice()] {
913 let (an, env) = alice.plan_send(msg).unwrap();
914 alice.apply(an);
915 let (bn, pt) = bob.plan_receive(&env).unwrap();
916 bob.apply(bn);
917 assert_eq!(pt, msg);
918 }
919
920 for msg in [b"b1".as_slice(), b"b2".as_slice(), b"b3".as_slice()] {
922 let (bn, env) = bob.plan_send(msg).unwrap();
923 bob.apply(bn);
924 let (an, pt) = alice.plan_receive(&env).unwrap();
925 alice.apply(an);
926 assert_eq!(pt, msg);
927 }
928
929 let (an, env) = alice.plan_send(b"a3").unwrap();
931 alice.apply(an);
932 let (bn, pt) = bob.plan_receive(&env).unwrap();
933 bob.apply(bn);
934 assert_eq!(pt, b"a3");
935 }
936
937 #[test]
939 fn out_of_order_delivery_uses_skipped_keys() {
940 let alice_secret = [3_u8; 32];
941 let bob_secret = [4_u8; 32];
942 let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
943 let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
944
945 let mut alice =
946 Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
947 let mut bob =
948 Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
949
950 let (a1, env1) = alice.plan_send(b"first").unwrap();
951 alice.apply(a1);
952 let (a2, env2) = alice.plan_send(b"second").unwrap();
953 alice.apply(a2);
954 let (a3, env3) = alice.plan_send(b"third").unwrap();
955 alice.apply(a3);
956
957 let (bn, pt1) = bob.plan_receive(&env1).unwrap();
959 bob.apply(bn);
960 assert_eq!(pt1, b"first");
961 let (bn, pt3) = bob.plan_receive(&env3).unwrap();
962 bob.apply(bn);
963 assert_eq!(pt3, b"third");
964 let (bn, pt2) = bob.plan_receive(&env2).unwrap();
965 bob.apply(bn);
966 assert_eq!(pt2, b"second");
967 }
968
969 #[test]
978 fn rust_reference_vector_msg1_decrypts() {
979 let vec_json = include_str!("../../test-vectors/nip104-rust-generated.json");
980
981 let field = |key: &str| -> String {
984 let needle = format!("\"{key}\":");
985 let start = vec_json.find(&needle).expect("key present") + needle.len();
986 let rest = &vec_json[start..];
987 let q1 = rest.find('"').unwrap() + 1;
988 let q2 = rest[q1..].find('"').unwrap();
989 rest[q1..q1 + q2].to_string()
990 };
991
992 let bob_sk = K::decode_hex_32(&field("bob_ephemeral_sk")).unwrap();
993 let alice_pk = K::decode_hex_32(&field("alice_ephemeral_pk")).unwrap();
994 let shared = K::decode_hex_32(&field("shared_secret")).unwrap();
995 let plaintext = field("plaintext");
996 let sender = field("pubkey");
997 let header = field("header");
998 let content = field("content");
999
1000 let mut bob = Session::<K>::new_responder(&alice_pk, &bob_sk, &shared).unwrap();
1001 let envelope = MessageEnvelope {
1002 sender,
1003 encrypted_header: header,
1004 ciphertext: content,
1005 };
1006
1007 let (next, payload) = bob
1008 .plan_receive(&envelope)
1009 .expect("reference msg1 must decrypt under native ratchet");
1010 bob.apply(next);
1011
1012 let decoded = String::from_utf8(payload).expect("payload is UTF-8 JSON");
1015 assert!(
1016 decoded.contains(&plaintext),
1017 "decrypted rumor {decoded:?} must contain plaintext {plaintext:?}"
1018 );
1019 }
1020
1021 #[test]
1027 fn rust_reference_msg1_event_decrypts_via_codec() {
1028 let vec_json = include_str!("../../test-vectors/nip104-rust-generated.json");
1029
1030 let field = |key: &str| -> String {
1031 let needle = format!("\"{key}\":");
1032 let start = vec_json.find(&needle).expect("key present") + needle.len();
1033 let rest = &vec_json[start..];
1034 let q1 = rest.find('"').unwrap() + 1;
1035 let q2 = rest[q1..].find('"').unwrap();
1036 rest[q1..q1 + q2].to_string()
1037 };
1038
1039 let bob_sk = K::decode_hex_32(&field("bob_ephemeral_sk")).unwrap();
1040 let alice_pk = K::decode_hex_32(&field("alice_ephemeral_pk")).unwrap();
1041 let shared = K::decode_hex_32(&field("shared_secret")).unwrap();
1042 let plaintext = field("plaintext");
1043
1044 let ev_start = vec_json.find("\"msg1_event\":").unwrap() + "\"msg1_event\":".len();
1046 let obj_start = vec_json[ev_start..].find('{').unwrap() + ev_start;
1047 let obj_end = vec_json[obj_start..].find('}').unwrap() + obj_start + 1;
1048 let event: nostro2::NostrNote = vec_json[obj_start..obj_end].parse().unwrap();
1049
1050 let mut bob = Session::<K>::new_responder(&alice_pk, &bob_sk, &shared).unwrap();
1051 let (next, payload) = bob
1052 .plan_receive_event(&event)
1053 .expect("reference event must decrypt via the native codec");
1054 bob.apply(next);
1055
1056 let decoded = String::from_utf8(payload).unwrap();
1057 assert!(decoded.contains(&plaintext), "got {decoded:?}");
1058 }
1059
1060 #[test]
1065 fn message_event_codec_round_trips() {
1066 use nostro2::NostrEvent as _;
1067
1068 let alice_secret = [1_u8; 32];
1069 let bob_secret = [2_u8; 32];
1070 let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
1071 let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
1072
1073 let mut alice =
1074 Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
1075 let mut bob =
1076 Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
1077
1078 let (anext, event) = alice
1079 .plan_send_event(b"over the wire", 1_700_000_000)
1080 .unwrap();
1081 alice.apply(anext);
1082
1083 assert_eq!(event.kind, MESSAGE_EVENT_KIND);
1085 assert!(event.verify(), "event must be self-consistently signed");
1086 assert_eq!(event.created_at, 1_700_000_000);
1087 assert_eq!(event.tags.find_tags_ref(HEADER_TAG).len(), 1);
1088 assert_eq!(event.pubkey, alice_pub.to_hex());
1090
1091 let (bnext, pt) = bob.plan_receive_event(&event).unwrap();
1093 bob.apply(bnext);
1094 assert_eq!(pt, b"over the wire");
1095 }
1096
1097 #[test]
1100 fn message_event_rejects_tampering() {
1101 let alice_secret = [1_u8; 32];
1102 let bob_secret = [2_u8; 32];
1103 let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
1104 let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
1105
1106 let alice = Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
1107 let bob = Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
1108
1109 let (_anext, mut event) = alice.plan_send_event(b"tamper me", 1_700_000_000).unwrap();
1110 event.content.push('A'); assert!(matches!(
1113 bob.plan_receive_event(&event),
1114 Err(Nip104Error::InvalidHeader)
1115 ));
1116 }
1117
1118 #[test]
1119 fn unknown_sender_rejected() {
1120 let bob_secret = [2_u8; 32];
1121 let alice_pub = K::from_secret_bytes(&[1_u8; 32]).unwrap().pubkey_bytes();
1122 let bob = Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
1123
1124 let envelope = MessageEnvelope {
1125 sender: "cd".repeat(32),
1126 encrypted_header: "x".into(),
1127 ciphertext: "x".into(),
1128 };
1129 assert!(matches!(
1130 bob.plan_receive(&envelope),
1131 Err(Nip104Error::UnexpectedSender)
1132 ));
1133 }
1134
1135 #[test]
1136 fn error_display_covers_all_variants() {
1137 use std::error::Error as _;
1138
1139 let cases: Vec<Nip104Error> = vec![
1140 Nip104Error::CannotSendYet,
1141 Nip104Error::SessionNotReady,
1142 Nip104Error::UnexpectedSender,
1143 Nip104Error::TooManySkippedMessages,
1144 Nip104Error::InvalidHeader,
1145 Nip104Error::InvalidInvite("bad layer".into()),
1146 Nip104Error::UnknownPeer("npub1xyz".into()),
1147 Nip104Error::Signer(SignerError::InvalidSignature),
1148 Nip104Error::Nip44(crate::Nip44Error::MacMismatch),
1149 Nip104Error::Json("unexpected token".into()),
1150 Nip104Error::Base64(general_purpose::STANDARD.decode("!!!").unwrap_err()),
1151 ];
1152 for err in &cases {
1153 assert!(!format!("{err}").is_empty(), "Display empty for {err:?}");
1154 let _ = err.source();
1156 }
1157 }
1158}