1use std::collections::BTreeMap;
22
23use base64::engine::{Engine as _, general_purpose};
24use nostro2_traits::NostrKeypair;
25use nostro2_traits::hex::Hexable;
26use zeroize::Zeroize;
27
28use super::{MESSAGE_EVENT_KIND, Nip104Crypto, Nip104Error, SenderKeyState};
29
30type Result<T> = std::result::Result<T, Nip104Error>;
31
32pub const GROUP_MESSAGE_KIND: u32 = MESSAGE_EVENT_KIND;
38
39pub const GROUP_SENDER_KEY_DISTRIBUTION_KIND: u32 = 10446;
43
44pub const GROUP_CHAT_MESSAGE_KIND: u32 = 14;
48
49#[derive(Debug, Clone, PartialEq, Eq, json_bourne::FromJson, json_bourne::ToJson)]
56pub struct SenderKeyDistribution {
57 #[bourne(rename = "groupId")]
58 pub group_id: String,
59 #[bourne(rename = "keyId")]
60 pub key_id: u32,
61 #[bourne(rename = "senderEventPubkey")]
62 pub sender_event_pubkey: String,
63 #[bourne(rename = "chainKey")]
64 pub chain_key: String,
65 pub iteration: u32,
66 #[bourne(rename = "createdAt")]
67 pub created_at: i64,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, json_bourne::FromJson, json_bourne::ToJson)]
74pub struct GroupSenderKeyMessage {
75 pub group_id: String,
76 pub sender_event_pubkey: String,
77 pub key_id: u32,
78 pub message_number: u32,
79 pub created_at: i64,
80 pub ciphertext: String,
81}
82
83#[derive(Debug, Clone)]
86struct SendingChain {
87 sender_event_pubkey: String,
88 sender_event_secret: [u8; 32],
89 state: SenderKeyState,
90}
91
92impl Drop for SendingChain {
95 fn drop(&mut self) {
96 self.sender_event_secret.zeroize();
97 }
98}
99
100#[derive(Debug, Clone, Default)]
102struct GroupRecord {
103 sending: Option<SendingChain>,
105 receiving: BTreeMap<String, SenderKeyState>,
107}
108
109#[derive(Debug, Clone)]
117pub struct SendingChainSnapshot {
118 pub sender_event_pubkey: String,
120 pub sender_event_secret: [u8; 32],
122 pub state: SenderKeyState,
124}
125
126impl Drop for SendingChainSnapshot {
128 fn drop(&mut self) {
129 self.sender_event_secret.zeroize();
130 }
131}
132
133#[derive(Debug, Clone)]
137pub struct GroupSnapshot {
138 pub group_id: String,
140 pub sending: Option<SendingChainSnapshot>,
142 pub receiving: Vec<(String, SenderKeyState)>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct GroupReceivedMessage {
149 pub group_id: String,
151 pub sender_event_pubkey: String,
153 pub plaintext: Vec<u8>,
155}
156
157#[derive(Debug, Clone)]
162pub struct GroupManager<K: NostrKeypair> {
163 our_pubkey: String,
164 groups: BTreeMap<String, GroupRecord>,
165 sender_to_group: BTreeMap<String, String>,
169 _marker: std::marker::PhantomData<fn() -> K>,
170}
171
172impl<K: NostrKeypair> GroupManager<K> {
173 #[must_use]
175 pub fn new(our_pubkey: impl Into<String>) -> Self {
176 Self {
177 our_pubkey: our_pubkey.into(),
178 groups: BTreeMap::new(),
179 sender_to_group: BTreeMap::new(),
180 _marker: std::marker::PhantomData,
181 }
182 }
183
184 #[must_use]
186 pub fn our_pubkey(&self) -> &str {
187 &self.our_pubkey
188 }
189
190 #[must_use]
192 pub fn has_sending_chain(&self, group_id: &str) -> bool {
193 self.groups
194 .get(group_id)
195 .is_some_and(|g| g.sending.is_some())
196 }
197
198 #[must_use]
201 pub fn known_senders(&self, group_id: &str) -> Vec<String> {
202 self.groups
203 .get(group_id)
204 .map(|g| g.receiving.keys().cloned().collect())
205 .unwrap_or_default()
206 }
207
208 pub fn rotate_sending_chain(
216 &mut self,
217 group_id: &str,
218 key_id: u32,
219 created_at: i64,
220 ) -> Result<SenderKeyDistribution> {
221 let sender_event = K::generate();
222 let sender_event_pubkey = sender_event.public_key();
223 let sender_event_secret = sender_event.secret_bytes();
224 let chain_key = K::generate().secret_bytes();
225 let state = SenderKeyState::new(key_id, &chain_key, 0);
226
227 let dist = SenderKeyDistribution {
228 group_id: group_id.to_owned(),
229 key_id,
230 sender_event_pubkey: sender_event_pubkey.clone(),
231 chain_key: chain_key.to_hex(),
232 iteration: 0,
233 created_at,
234 };
235
236 self.sender_to_group
237 .insert(sender_event_pubkey.clone(), group_id.to_owned());
238 self.groups.entry(group_id.to_owned()).or_default().sending = Some(SendingChain {
239 sender_event_pubkey,
240 sender_event_secret,
241 state,
242 });
243 Ok(dist)
244 }
245
246 pub fn current_distribution(
254 &self,
255 group_id: &str,
256 created_at: i64,
257 ) -> Result<SenderKeyDistribution> {
258 let send = self
259 .groups
260 .get(group_id)
261 .and_then(|g| g.sending.as_ref())
262 .ok_or(Nip104Error::SessionNotReady)?;
263 Ok(SenderKeyDistribution {
264 group_id: group_id.to_owned(),
265 key_id: send.state.key_id(),
266 sender_event_pubkey: send.sender_event_pubkey.clone(),
267 chain_key: send.state.chain_key_hex(),
268 iteration: send.state.iteration(),
269 created_at,
270 })
271 }
272
273 pub fn apply_distribution(&mut self, dist: &SenderKeyDistribution) -> Result<()> {
281 let chain_key = K::decode_hex_32(&dist.chain_key)?;
282 let state = SenderKeyState::new(dist.key_id, &chain_key, dist.iteration);
283 self.sender_to_group
284 .insert(dist.sender_event_pubkey.clone(), dist.group_id.clone());
285 self.groups
286 .entry(dist.group_id.clone())
287 .or_default()
288 .receiving
289 .insert(dist.sender_event_pubkey.clone(), state);
290 Ok(())
291 }
292
293 pub fn distribution_to_rumor(
301 &self,
302 dist: &SenderKeyDistribution,
303 created_at: i64,
304 now_ms: i64,
305 ) -> Result<nostro2::NostrNote> {
306 let mut tags = nostro2::NostrTags::new();
307 tags.add_custom_tag("l", &dist.group_id);
308 tags.add_custom_tag("key", &dist.key_id.to_string());
309 tags.add_custom_tag("ms", &now_ms.to_string());
310 let mut rumor = nostro2::NostrNote {
311 pubkey: self.our_pubkey.clone(),
312 kind: GROUP_SENDER_KEY_DISTRIBUTION_KIND,
313 content: json_bourne::to_string(dist)?,
314 created_at,
315 tags,
316 ..Default::default()
317 };
318 rumor
319 .serialize_id()
320 .map_err(|e| Nip104Error::Json(e.to_string()))?;
321 Ok(rumor)
322 }
323
324 pub fn apply_distribution_rumor(
331 &mut self,
332 rumor: &nostro2::NostrNote,
333 ) -> Result<Option<SenderKeyDistribution>> {
334 if rumor.kind != GROUP_SENDER_KEY_DISTRIBUTION_KIND {
335 return Ok(None);
336 }
337 let dist: SenderKeyDistribution = json_bourne::parse_str(&rumor.content)?;
338 self.apply_distribution(&dist)?;
339 Ok(Some(dist))
340 }
341
342 pub fn encrypt(
349 &mut self,
350 group_id: &str,
351 plaintext: &[u8],
352 created_at: i64,
353 ) -> Result<GroupSenderKeyMessage> {
354 let send = self
355 .groups
356 .get_mut(group_id)
357 .and_then(|g| g.sending.as_mut())
358 .ok_or(Nip104Error::SessionNotReady)?;
359 let key_id = send.state.key_id();
360 let (message_number, ciphertext) = send.state.encrypt::<K>(plaintext)?;
361 Ok(GroupSenderKeyMessage {
362 group_id: group_id.to_owned(),
363 sender_event_pubkey: send.sender_event_pubkey.clone(),
364 key_id,
365 message_number,
366 created_at,
367 ciphertext,
368 })
369 }
370
371 pub fn decrypt(&mut self, msg: &GroupSenderKeyMessage) -> Result<GroupReceivedMessage> {
379 let chain = self
380 .groups
381 .get_mut(&msg.group_id)
382 .and_then(|g| g.receiving.get_mut(&msg.sender_event_pubkey))
383 .ok_or(Nip104Error::SessionNotReady)?;
384 let plan = chain.plan_decrypt::<K>(msg.key_id, msg.message_number, &msg.ciphertext)?;
385 let plaintext = chain.apply_decrypt(plan);
386 Ok(GroupReceivedMessage {
387 group_id: msg.group_id.clone(),
388 sender_event_pubkey: msg.sender_event_pubkey.clone(),
389 plaintext,
390 })
391 }
392
393 pub fn encrypt_to_event(
404 &mut self,
405 group_id: &str,
406 plaintext: &[u8],
407 created_at: i64,
408 ) -> Result<nostro2::NostrNote> {
409 let send = self
410 .groups
411 .get_mut(group_id)
412 .and_then(|g| g.sending.as_mut())
413 .ok_or(Nip104Error::SessionNotReady)?;
414 let key_id = send.state.key_id();
415 let (message_number, ciphertext_b64) = send.state.encrypt::<K>(plaintext)?;
416 let content = Self::encode_outer_content(key_id, message_number, &ciphertext_b64)?;
417
418 let signer =
419 K::from_secret_bytes(&send.sender_event_secret).map_err(Nip104Error::Signer)?;
420 let mut note = nostro2::NostrNote {
421 kind: GROUP_MESSAGE_KIND,
422 content,
423 created_at,
424 tags: nostro2::NostrTags::new(),
425 ..Default::default()
426 };
427 note.sign_with(&signer)
428 .map_err(|_| Nip104Error::Signer(nostro2_traits::SignerError::InvalidSignature))?;
429 Ok(note)
430 }
431
432 pub fn decrypt_event(
442 &mut self,
443 event: &nostro2::NostrNote,
444 ) -> Result<Option<GroupReceivedMessage>> {
445 use nostro2::NostrEvent as _;
446 if event.kind != GROUP_MESSAGE_KIND {
447 return Ok(None);
448 }
449 let Some(group_id) = self.sender_to_group.get(&event.pubkey).cloned() else {
451 return Ok(None);
452 };
453 let known = self
454 .groups
455 .get(&group_id)
456 .is_some_and(|g| g.receiving.contains_key(&event.pubkey));
457 if !known {
458 return Ok(None);
459 }
460 if !event.verify() {
461 return Err(Nip104Error::InvalidHeader);
462 }
463 let (key_id, message_number, ciphertext_b64) = Self::decode_outer_content(&event.content)?;
464 let msg = GroupSenderKeyMessage {
465 group_id,
466 sender_event_pubkey: event.pubkey.clone(),
467 key_id,
468 message_number,
469 created_at: event.created_at,
470 ciphertext: ciphertext_b64,
471 };
472 Ok(Some(self.decrypt(&msg)?))
473 }
474
475 #[must_use]
481 pub fn snapshot(&self) -> Vec<GroupSnapshot> {
482 self.groups
483 .iter()
484 .map(|(group_id, record)| GroupSnapshot {
485 group_id: group_id.clone(),
486 sending: record.sending.as_ref().map(|s| SendingChainSnapshot {
487 sender_event_pubkey: s.sender_event_pubkey.clone(),
488 sender_event_secret: s.sender_event_secret,
489 state: s.state.clone(),
490 }),
491 receiving: record
492 .receiving
493 .iter()
494 .map(|(k, v)| (k.clone(), v.clone()))
495 .collect(),
496 })
497 .collect()
498 }
499
500 pub fn restore_group(&mut self, snap: GroupSnapshot) {
505 let mut record = GroupRecord::default();
506 if let Some(s) = snap.sending.as_ref() {
509 self.sender_to_group
510 .insert(s.sender_event_pubkey.clone(), snap.group_id.clone());
511 record.sending = Some(SendingChain {
512 sender_event_pubkey: s.sender_event_pubkey.clone(),
513 sender_event_secret: s.sender_event_secret,
514 state: s.state.clone(),
515 });
516 }
517 for (sender_event_pubkey, state) in snap.receiving {
518 self.sender_to_group
519 .insert(sender_event_pubkey.clone(), snap.group_id.clone());
520 record.receiving.insert(sender_event_pubkey, state);
521 }
522 self.groups.insert(snap.group_id, record);
523 }
524
525 fn encode_outer_content(
531 key_id: u32,
532 message_number: u32,
533 ciphertext_b64: &str,
534 ) -> Result<String> {
535 let nip44_bytes = general_purpose::STANDARD
536 .decode(ciphertext_b64)
537 .map_err(|_| Nip104Error::InvalidHeader)?;
538 let mut payload = Vec::with_capacity(8 + nip44_bytes.len());
539 payload.extend_from_slice(&key_id.to_be_bytes());
540 payload.extend_from_slice(&message_number.to_be_bytes());
541 payload.extend_from_slice(&nip44_bytes);
542 Ok(general_purpose::STANDARD.encode(&payload))
543 }
544
545 fn decode_outer_content(content: &str) -> Result<(u32, u32, String)> {
549 let bytes = general_purpose::STANDARD
550 .decode(content)
551 .map_err(|_| Nip104Error::InvalidHeader)?;
552 if bytes.len() < 8 {
553 return Err(Nip104Error::InvalidHeader);
554 }
555 let key_id = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
556 let message_number = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
557 let ciphertext_b64 = general_purpose::STANDARD.encode(&bytes[8..]);
558 Ok((key_id, message_number, ciphertext_b64))
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 type K = crate::tests::NipTester;
567
568 fn mgr(id: &str) -> GroupManager<K> {
569 GroupManager::<K>::new(id.to_owned())
570 }
571
572 #[test]
573 fn one_to_many_two_members() {
574 let mut alice = mgr("alice");
576 let mut bob = mgr("bob");
577 let mut carol = mgr("carol");
578
579 let dist = alice.rotate_sending_chain("g1", 1, 1000).unwrap();
580 bob.apply_distribution(&dist).unwrap();
581 carol.apply_distribution(&dist).unwrap();
582
583 let msg = alice.encrypt("g1", b"gm everyone", 1001).unwrap();
585 assert_eq!(bob.decrypt(&msg).unwrap().plaintext, b"gm everyone");
586 assert_eq!(carol.decrypt(&msg).unwrap().plaintext, b"gm everyone");
587 }
588
589 #[test]
593 fn snapshot_round_trip_resumes_both_directions() {
594 let mut alice = mgr("alice");
595 let mut bob = mgr("bob");
596
597 let dist = alice.rotate_sending_chain("g1", 1, 1000).unwrap();
599 bob.apply_distribution(&dist).unwrap();
600 let m1 = alice.encrypt("g1", b"first", 1001).unwrap();
601 assert_eq!(bob.decrypt(&m1).unwrap().plaintext, b"first");
602
603 let alice_snaps = alice.snapshot();
605 let bob_snaps = bob.snapshot();
606
607 let mut alice2 = GroupManager::<K>::new(alice.our_pubkey().to_owned());
609 for s in alice_snaps {
610 alice2.restore_group(s);
611 }
612 let mut bob2 = GroupManager::<K>::new(bob.our_pubkey().to_owned());
613 for s in bob_snaps {
614 bob2.restore_group(s);
615 }
616
617 let m2 = alice2.encrypt("g1", b"after restore", 1002).unwrap();
620 assert_eq!(bob2.decrypt(&m2).unwrap().plaintext, b"after restore");
621
622 assert!(bob2.known_senders("g1").contains(&dist.sender_event_pubkey));
624 }
625
626 #[test]
629 fn snapshot_preserves_skipped_keys() {
630 let mut alice = mgr("alice");
631 let mut bob = mgr("bob");
632 let dist = alice.rotate_sending_chain("g1", 7, 2000).unwrap();
633 bob.apply_distribution(&dist).unwrap();
634
635 let a0 = alice.encrypt("g1", b"msg0", 2001).unwrap();
638 let a1 = alice.encrypt("g1", b"msg1", 2002).unwrap();
639 let a2 = alice.encrypt("g1", b"msg2", 2003).unwrap();
640 assert_eq!(bob.decrypt(&a2).unwrap().plaintext, b"msg2");
641
642 let bob_snaps = bob.snapshot();
644 let mut bob2 = GroupManager::<K>::new(bob.our_pubkey().to_owned());
645 for s in bob_snaps {
646 bob2.restore_group(s);
647 }
648
649 assert_eq!(bob2.decrypt(&a0).unwrap().plaintext, b"msg0");
651 assert_eq!(bob2.decrypt(&a1).unwrap().plaintext, b"msg1");
652 }
653
654 #[test]
655 fn sequential_messages_advance_all() {
656 let mut alice = mgr("alice");
657 let mut bob = mgr("bob");
658 let dist = alice.rotate_sending_chain("g", 1, 0).unwrap();
659 bob.apply_distribution(&dist).unwrap();
660
661 for i in 0..5 {
662 let m = alice.encrypt("g", format!("m{i}").as_bytes(), i).unwrap();
663 assert_eq!(
664 bob.decrypt(&m).unwrap().plaintext,
665 format!("m{i}").as_bytes()
666 );
667 }
668 }
669
670 #[test]
671 fn late_joiner_gets_current_distribution() {
672 let mut alice = mgr("alice");
673 let mut bob = mgr("bob");
674 let mut dave = mgr("dave");
675
676 let dist0 = alice.rotate_sending_chain("g", 1, 0).unwrap();
677 bob.apply_distribution(&dist0).unwrap();
678 let _m0 = alice.encrypt("g", b"before dave", 1).unwrap();
679
680 let dist_now = alice.current_distribution("g", 2).unwrap();
682 assert_eq!(dist_now.iteration, 1);
683 dave.apply_distribution(&dist_now).unwrap();
684
685 let m1 = alice.encrypt("g", b"after dave", 3).unwrap();
687 assert_eq!(bob.decrypt(&m1).unwrap().plaintext, b"after dave");
688 assert_eq!(dave.decrypt(&m1).unwrap().plaintext, b"after dave");
689 }
690
691 #[test]
692 fn two_senders_route_independently() {
693 let mut alice = mgr("alice");
695 let mut bob = mgr("bob");
696 let mut carol = mgr("carol");
697
698 let da = alice.rotate_sending_chain("g", 1, 0).unwrap();
699 let db = bob.rotate_sending_chain("g", 1, 0).unwrap();
700 assert_ne!(da.sender_event_pubkey, db.sender_event_pubkey);
701 carol.apply_distribution(&da).unwrap();
702 carol.apply_distribution(&db).unwrap();
703
704 let ma = alice.encrypt("g", b"from alice", 1).unwrap();
705 let mb = bob.encrypt("g", b"from bob", 1).unwrap();
706 assert_eq!(carol.decrypt(&mb).unwrap().plaintext, b"from bob");
707 assert_eq!(carol.decrypt(&ma).unwrap().plaintext, b"from alice");
708 assert_eq!(carol.known_senders("g").len(), 2);
709 }
710
711 #[test]
712 fn decrypt_without_distribution_fails() {
713 let mut alice = mgr("alice");
714 let mut bob = mgr("bob");
715 alice.rotate_sending_chain("g", 1, 0).unwrap();
716 let m = alice.encrypt("g", b"secret", 1).unwrap();
717 assert!(matches!(bob.decrypt(&m), Err(Nip104Error::SessionNotReady)));
719 }
720
721 #[test]
722 fn rotation_replaces_sending_chain() {
723 let mut alice = mgr("alice");
724 let mut bob = mgr("bob");
725 let d1 = alice.rotate_sending_chain("g", 1, 0).unwrap();
726 bob.apply_distribution(&d1).unwrap();
727 let _ = alice.encrypt("g", b"old", 1).unwrap();
728
729 let d2 = alice.rotate_sending_chain("g", 2, 2).unwrap();
731 assert_ne!(d1.sender_event_pubkey, d2.sender_event_pubkey);
732 bob.apply_distribution(&d2).unwrap();
733 let m = alice.encrypt("g", b"new", 3).unwrap();
734 assert_eq!(m.key_id, 2);
735 assert_eq!(bob.decrypt(&m).unwrap().plaintext, b"new");
736 }
737
738 #[test]
739 fn distribution_json_roundtrips() {
740 let mut alice = mgr("alice");
741 let dist = alice.rotate_sending_chain("g", 1, 1234).unwrap();
742 let json = json_bourne::to_string(&dist).unwrap();
743 let back: SenderKeyDistribution = json_bourne::parse_str(&json).unwrap();
744 assert_eq!(dist, back);
745 }
746
747 #[test]
748 fn message_json_roundtrips() {
749 let mut alice = mgr("alice");
750 alice.rotate_sending_chain("g", 1, 0).unwrap();
751 let msg = alice.encrypt("g", b"hi", 7).unwrap();
752 let json = json_bourne::to_string(&msg).unwrap();
753 let back: GroupSenderKeyMessage = json_bourne::parse_str(&json).unwrap();
754 assert_eq!(msg, back);
755 }
756
757 #[test]
758 fn distribution_rumor_roundtrips_over_session() {
759 let mut alice = mgr("alice");
760 let mut bob = mgr("bob");
761
762 let dist = alice.rotate_sending_chain("g7", 1, 1000).unwrap();
763 let rumor = alice.distribution_to_rumor(&dist, 1000, 1_000_000).unwrap();
765 assert_eq!(rumor.kind, GROUP_SENDER_KEY_DISTRIBUTION_KIND);
766 assert_eq!(rumor.pubkey, "alice");
767 assert!(rumor.id.is_some());
768 assert_eq!(rumor.tags.find_tags("l"), vec!["g7".to_owned()]);
769 assert_eq!(rumor.tags.find_tags("key"), vec!["1".to_owned()]);
770 assert_eq!(rumor.tags.find_tags("ms"), vec!["1000000".to_owned()]);
771
772 let applied = bob.apply_distribution_rumor(&rumor).unwrap().unwrap();
774 assert_eq!(applied, dist);
775
776 let ev = alice.encrypt_to_event("g7", b"after distro", 1001).unwrap();
778 let got = bob.decrypt_event(&ev).unwrap().unwrap();
779 assert_eq!(got.plaintext, b"after distro");
780 }
781
782 #[test]
783 fn apply_distribution_rumor_ignores_other_kinds() {
784 let mut bob = mgr("bob");
785 let mut other = nostro2::NostrNote {
786 kind: GROUP_CHAT_MESSAGE_KIND,
787 content: "hi".to_owned(),
788 ..Default::default()
789 };
790 let _ = other.serialize_id();
791 assert!(bob.apply_distribution_rumor(&other).unwrap().is_none());
792 }
793
794 #[test]
795 fn outer_event_roundtrip_one_to_many() {
796 use nostro2::NostrEvent as _;
797 let mut alice = mgr("alice");
798 let mut bob = mgr("bob");
799 let mut carol = mgr("carol");
800
801 let dist = alice.rotate_sending_chain("g1", 1, 1000).unwrap();
802 bob.apply_distribution(&dist).unwrap();
803 carol.apply_distribution(&dist).unwrap();
804
805 let ev = alice.encrypt_to_event("g1", b"gm group", 1001).unwrap();
807 assert_eq!(ev.kind, GROUP_MESSAGE_KIND);
808 assert_eq!(ev.pubkey, dist.sender_event_pubkey);
809 assert!(ev.verify());
810 assert_eq!(ev.tags.iter().count(), 0);
811
812 let b = bob.decrypt_event(&ev).unwrap().unwrap();
814 let c = carol.decrypt_event(&ev).unwrap().unwrap();
815 assert_eq!(b.plaintext, b"gm group");
816 assert_eq!(c.plaintext, b"gm group");
817 assert_eq!(b.group_id, "g1");
818 }
819
820 #[test]
821 fn decrypt_event_ignores_unknown_author() {
822 let mut alice = mgr("alice");
823 let mut bob = mgr("bob");
824 alice.rotate_sending_chain("g", 1, 0).unwrap();
825 let ev = alice.encrypt_to_event("g", b"secret", 1).unwrap();
827 assert!(bob.decrypt_event(&ev).unwrap().is_none());
828 }
829
830 #[test]
835 fn large_group_fan_out() {
836 const MEMBERS: usize = 256;
837 let mut alice = mgr("alice");
838 let dist = alice.rotate_sending_chain("big", 1, 1000).unwrap();
839
840 let mut members: Vec<GroupManager<K>> = (0..MEMBERS)
841 .map(|i| {
842 let mut m = mgr(&format!("member-{i}"));
843 m.apply_distribution(&dist).unwrap();
844 m
845 })
846 .collect();
847
848 let ev = alice
850 .encrypt_to_event("big", b"hello everyone", 1001)
851 .unwrap();
852 for (i, m) in members.iter_mut().enumerate() {
853 let got = m
854 .decrypt_event(&ev)
855 .unwrap()
856 .unwrap_or_else(|| panic!("member {i} failed to decrypt"));
857 assert_eq!(got.plaintext, b"hello everyone");
858 }
859 }
860
861 #[test]
864 fn high_message_volume_group() {
865 const N: i64 = 2_000;
866 let mut alice = mgr("alice");
867 let mut bob = mgr("bob");
868 let mut carol = mgr("carol");
869 let dist = alice.rotate_sending_chain("busy", 1, 0).unwrap();
870 bob.apply_distribution(&dist).unwrap();
871 carol.apply_distribution(&dist).unwrap();
872
873 for i in 0..N {
874 let body = format!("event #{i}");
875 let ev = alice.encrypt_to_event("busy", body.as_bytes(), i).unwrap();
876 assert_eq!(
877 bob.decrypt_event(&ev).unwrap().unwrap().plaintext,
878 body.as_bytes()
879 );
880 assert_eq!(
881 carol.decrypt_event(&ev).unwrap().unwrap().plaintext,
882 body.as_bytes()
883 );
884 }
885 }
886
887 #[test]
891 fn late_joiner_after_heavy_traffic() {
892 let mut alice = mgr("alice");
893 let mut bob = mgr("bob");
894 let dist0 = alice.rotate_sending_chain("g", 1, 0).unwrap();
895 bob.apply_distribution(&dist0).unwrap();
896
897 let mut backlog = Vec::new();
898 for i in 0..1_000 {
899 backlog.push(
900 alice
901 .encrypt_to_event("g", format!("old-{i}").as_bytes(), i)
902 .unwrap(),
903 );
904 }
905
906 let mut eve = mgr("eve");
908 let dist_now = alice.current_distribution("g", 2000).unwrap();
909 assert_eq!(dist_now.iteration, 1000);
910 eve.apply_distribution(&dist_now).unwrap();
911
912 let fresh = alice.encrypt_to_event("g", b"after eve", 3000).unwrap();
914 assert_eq!(
915 eve.decrypt_event(&fresh).unwrap().unwrap().plaintext,
916 b"after eve"
917 );
918
919 let old = backlog.last().unwrap();
921 assert!(eve.decrypt_event(old).is_err());
922 }
923
924 #[test]
928 fn tampered_outer_event_rejected_without_advancing() {
929 let mut alice = mgr("alice");
930 let mut bob = mgr("bob");
931 let dist = alice.rotate_sending_chain("g", 1, 0).unwrap();
932 bob.apply_distribution(&dist).unwrap();
933
934 let ev = alice.encrypt_to_event("g", b"genuine", 1).unwrap();
935 let mut forged = ev.clone();
936 forged.content.push('A'); assert!(matches!(
939 bob.decrypt_event(&forged),
940 Err(Nip104Error::InvalidHeader)
941 ));
942 assert_eq!(
944 bob.decrypt_event(&ev).unwrap().unwrap().plaintext,
945 b"genuine"
946 );
947 }
948
949 #[test]
953 fn malformed_outer_content_is_an_error() {
954 let mut alice = mgr("alice");
955 let mut bob = mgr("bob");
956 let dist = alice.rotate_sending_chain("g", 1, 0).unwrap();
957 bob.apply_distribution(&dist).unwrap();
958
959 let sender_pk = dist.sender_event_pubkey.clone();
962 assert!(matches!(
965 GroupManager::<K>::decode_outer_content(""),
966 Err(Nip104Error::InvalidHeader)
967 ));
968 assert!(matches!(
969 GroupManager::<K>::decode_outer_content("!!!not base64!!!"),
970 Err(Nip104Error::InvalidHeader)
971 ));
972 let short = general_purpose::STANDARD.encode([0_u8; 4]);
974 assert!(matches!(
975 GroupManager::<K>::decode_outer_content(&short),
976 Err(Nip104Error::InvalidHeader)
977 ));
978 let _ = sender_pk;
979 }
980
981 #[test]
984 fn replayed_group_message_rejected() {
985 let mut alice = mgr("alice");
986 let mut bob = mgr("bob");
987 let dist = alice.rotate_sending_chain("g", 1, 0).unwrap();
988 bob.apply_distribution(&dist).unwrap();
989
990 let ev = alice.encrypt_to_event("g", b"once", 1).unwrap();
991 assert_eq!(bob.decrypt_event(&ev).unwrap().unwrap().plaintext, b"once");
992 assert!(bob.decrypt_event(&ev).is_err());
994 }
995
996 #[test]
1000 fn cross_group_messages_do_not_leak() {
1001 let mut alice = mgr("alice");
1002 let mut bob = mgr("bob");
1003 let da = alice.rotate_sending_chain("groupA", 1, 0).unwrap();
1004 let db = alice.rotate_sending_chain("groupB", 1, 0).unwrap();
1005 assert_ne!(da.sender_event_pubkey, db.sender_event_pubkey);
1006 bob.apply_distribution(&da).unwrap();
1007 bob.apply_distribution(&db).unwrap();
1008
1009 let ev_a = alice.encrypt_to_event("groupA", b"secret A", 1).unwrap();
1010 let got = bob.decrypt_event(&ev_a).unwrap().unwrap();
1011 assert_eq!(got.group_id, "groupA");
1012 assert_eq!(got.plaintext, b"secret A");
1013 assert!(
1014 bob.known_senders("groupA")
1015 .contains(&da.sender_event_pubkey)
1016 );
1017 assert!(
1018 !bob.known_senders("groupB")
1019 .contains(&da.sender_event_pubkey)
1020 );
1021 }
1022
1023 #[test]
1028 fn stale_key_id_after_rotation_rejected() {
1029 let mut alice = mgr("alice");
1030 let mut bob = mgr("bob");
1031 let d1 = alice.rotate_sending_chain("g", 1, 0).unwrap();
1032 bob.apply_distribution(&d1).unwrap();
1033 let stale = alice.encrypt("g", b"old-key", 1).unwrap();
1034
1035 let d2 = alice.rotate_sending_chain("g", 2, 2).unwrap();
1039 bob.apply_distribution(&d2).unwrap();
1040 let mut wrong = stale;
1041 wrong.sender_event_pubkey = d2.sender_event_pubkey.clone();
1042 assert!(matches!(
1043 bob.decrypt(&wrong),
1044 Err(Nip104Error::InvalidHeader)
1045 ));
1046 }
1047
1048 #[test]
1049 fn outer_content_frames_match_reference_layout() {
1050 let content = GroupManager::<K>::encode_outer_content(0x0102_0304, 0x0506_0708, &{
1052 general_purpose::STANDARD.encode([0xAA_u8; 40])
1054 })
1055 .unwrap();
1056 let raw = general_purpose::STANDARD.decode(&content).unwrap();
1057 assert_eq!(&raw[..4], &[0x01, 0x02, 0x03, 0x04]);
1058 assert_eq!(&raw[4..8], &[0x05, 0x06, 0x07, 0x08]);
1059 assert_eq!(&raw[8..], &[0xAA_u8; 40]);
1060
1061 let (k, n, ct) = GroupManager::<K>::decode_outer_content(&content).unwrap();
1062 assert_eq!(k, 0x0102_0304);
1063 assert_eq!(n, 0x0506_0708);
1064 assert_eq!(general_purpose::STANDARD.decode(ct).unwrap(), [0xAA_u8; 40]);
1065 }
1066}