Skip to main content

nostro2_nips/nip_104/
group.rs

1//! NIP-104 — Group transport: distributions, outer events, and a manager.
2//!
3//! Builds on [`crate::nip_104_sender_key::SenderKeyState`] (the symmetric
4//! chain) to give the hybrid group model from `mmalmi/nostr-double-ratchet`:
5//!
6//! 1. A sending device mints a per-group sender-key chain and an associated
7//!    **sender-event keypair** (the pubkey that signs its one-to-many events).
8//! 2. The chain seed travels to each member as a [`SenderKeyDistribution`],
9//!    carried inside the authenticated 1:1 ratchet sessions (so only members
10//!    learn it).
11//! 3. Group messages are published **once** as a [`GroupSenderKeyMessage`]
12//!    outer event, encrypted with the next key off the chain.
13//! 4. Members decrypt with the chain state learned from the distribution.
14//!
15//! [`GroupManager`] is the pure, in-memory state machine tying these together:
16//! it owns our sending chain per group, tracks received chains keyed by sender,
17//! and is side-effect free — methods return the wire objects to publish or the
18//! plaintext decrypted, leaving transport to the caller (exactly like
19//! [`crate::nip_104::SessionManager`]).
20
21use 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
32/// Outer Nostr event kind for group messages.
33///
34/// Same kind as 1:1 ratchet messages (the reference's `MESSAGE_EVENT_KIND` =
35/// 1060); members tell the two apart by whether the event `pubkey` maps to a
36/// sender-key chain.
37pub const GROUP_MESSAGE_KIND: u32 = MESSAGE_EVENT_KIND;
38
39/// Inner-rumor kind for a sender-key distribution, delivered pairwise over
40/// each member's 1:1 Double Ratchet session (reference
41/// `GROUP_SENDER_KEY_DISTRIBUTION_KIND`).
42pub const GROUP_SENDER_KEY_DISTRIBUTION_KIND: u32 = 10446;
43
44/// Inner-rumor kind for a group chat message (reference `CHAT_MESSAGE_KIND`,
45/// the NIP-17 private-DM kind). The plaintext inside an outer group event is a
46/// JSON rumor of this kind.
47pub const GROUP_CHAT_MESSAGE_KIND: u32 = 14;
48
49/// Seed for one sender-key chain, distributed to members over their 1:1
50/// sessions.
51///
52/// JSON field names are **camelCase** to match the reference
53/// `SenderKeyDistribution` byte-for-byte on the wire; `chain_key` is
54/// 64-char hex.
55#[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/// A published one-to-many group message. The `sender_event_pubkey`
71/// locates the receiving chain; `key_id` + `message_number` index it.
72/// `ciphertext` is the base64 NIP-44 v2 payload from the chain.
73#[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/// Our own sending side for one group: the chain plus the sender-event keypair
84/// it is published/signed under.
85#[derive(Debug, Clone)]
86struct SendingChain {
87    sender_event_pubkey: String,
88    sender_event_secret: [u8; 32],
89    state: SenderKeyState,
90}
91
92/// Scrub the raw sender-event secret on drop. `state` (a [`SenderKeyState`])
93/// scrubs itself via its own `Drop` impl.
94impl Drop for SendingChain {
95    fn drop(&mut self) {
96        self.sender_event_secret.zeroize();
97    }
98}
99
100/// All state for a single group.
101#[derive(Debug, Clone, Default)]
102struct GroupRecord {
103    /// Our sending chain, once minted.
104    sending: Option<SendingChain>,
105    /// Received chains, keyed by their `sender_event_pubkey`.
106    receiving: BTreeMap<String, SenderKeyState>,
107}
108
109/// A persistence snapshot of our sending side for one group.
110///
111/// Holds the sender-event keypair (pubkey + raw secret) plus the chain
112/// [`SenderKeyState`]. Serialize the state via its getters
113/// ([`SenderKeyState::key_id`], [`SenderKeyState::chain_key_hex`],
114/// [`SenderKeyState::iteration`], [`SenderKeyState::skipped_keys`]) and
115/// revive it with [`SenderKeyState::from_parts`].
116#[derive(Debug, Clone)]
117pub struct SendingChainSnapshot {
118    /// The per-group sender-event pubkey our outer events are signed under.
119    pub sender_event_pubkey: String,
120    /// The raw 32-byte sender-event secret key.
121    pub sender_event_secret: [u8; 32],
122    /// The current sending chain state.
123    pub state: SenderKeyState,
124}
125
126/// Scrub the raw sender-event secret on drop. `state` scrubs itself.
127impl Drop for SendingChainSnapshot {
128    fn drop(&mut self) {
129        self.sender_event_secret.zeroize();
130    }
131}
132
133/// A full persistence snapshot of one group's transport state: our sending
134/// chain (if minted) and every receiving chain keyed by its sender-event
135/// pubkey. The inverse of [`GroupManager::restore_group`].
136#[derive(Debug, Clone)]
137pub struct GroupSnapshot {
138    /// The group id.
139    pub group_id: String,
140    /// Our sending chain, if we have minted one.
141    pub sending: Option<SendingChainSnapshot>,
142    /// Receiving chains: `(sender_event_pubkey, state)`.
143    pub receiving: Vec<(String, SenderKeyState)>,
144}
145
146/// A group message decrypted by [`GroupManager::decrypt`].
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct GroupReceivedMessage {
149    /// The group it belongs to.
150    pub group_id: String,
151    /// The sender-event pubkey that produced it.
152    pub sender_event_pubkey: String,
153    /// The recovered plaintext.
154    pub plaintext: Vec<u8>,
155}
156
157/// Pure, in-memory group transport state machine.
158///
159/// Generic over the in-process keypair `K`, matching [`SenderKeyState`] and
160/// [`crate::nip_104::SessionManager`].
161#[derive(Debug, Clone)]
162pub struct GroupManager<K: NostrKeypair> {
163    our_pubkey: String,
164    groups: BTreeMap<String, GroupRecord>,
165    /// Reverse index: a sender-event pubkey → the `group_id` its chain belongs
166    /// to. Lets us route a bare outer event (which carries no `group_id`) to
167    /// the right chain by its author pubkey alone.
168    sender_to_group: BTreeMap<String, String>,
169    _marker: std::marker::PhantomData<fn() -> K>,
170}
171
172impl<K: NostrKeypair> GroupManager<K> {
173    /// Create a manager owned by `our_pubkey` (our owner/device identity hex).
174    #[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    /// Our identity pubkey.
185    #[must_use]
186    pub fn our_pubkey(&self) -> &str {
187        &self.our_pubkey
188    }
189
190    /// Whether we hold a sending chain for `group_id`.
191    #[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    /// The sender-event pubkeys whose chains we can currently decrypt for
199    /// `group_id`, sorted.
200    #[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    /// **Mint** our sending chain for `group_id`. Generates a fresh sender-event
209    /// keypair and a random chain key, then returns the
210    /// [`SenderKeyDistribution`] to hand to every member over their 1:1
211    /// session. Replaces any prior sending chain (a key rotation).
212    ///
213    /// # Errors
214    /// Propagates key/derivation failures.
215    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    /// Re-derive the current [`SenderKeyDistribution`] for our sending chain —
247    /// e.g. to hand the chain to a member who joined after we minted it. Note
248    /// the `iteration` reflects the chain's *current* position, so a late
249    /// joiner only decrypts messages from here forward.
250    ///
251    /// # Errors
252    /// [`Nip104Error::SessionNotReady`] if we have no sending chain yet.
253    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    /// **Install** a [`SenderKeyDistribution`] received from a member — the
274    /// receiving chain we use to decrypt that sender's group messages. A newer
275    /// distribution for the same sender (same `sender_event_pubkey`) replaces
276    /// the old chain (handles rotation).
277    ///
278    /// # Errors
279    /// [`Nip104Error`] if the chain key is malformed hex.
280    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    /// Frame a distribution as the unsigned kind-10446 session rumor to hand to
294    /// the `SessionManager` for every member; its serialized JSON becomes the
295    /// inner ratchet plaintext. Tags match the reference: l/key/ms; pubkey is
296    /// our device identity; id is the rumor hash.
297    ///
298    /// # Errors
299    /// JSON serialization failure of the distribution payload.
300    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    /// Consume an inbound session rumor. If it is a kind-10446 distribution,
325    /// parse and install it, returning the applied distribution. Returns
326    /// Ok(None) for any other rumor kind, so it composes with a 1:1 loop.
327    ///
328    /// # Errors
329    /// Malformed payload or bad chain-key hex.
330    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    /// **Encrypt** `plaintext` as the next message on our sending chain for
343    /// `group_id`, returning the [`GroupSenderKeyMessage`] to publish once.
344    ///
345    /// # Errors
346    /// [`Nip104Error::SessionNotReady`] if we have no sending chain; plus
347    /// cipher failures.
348    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    /// **Decrypt** an inbound [`GroupSenderKeyMessage`], routing it to the
372    /// receiving chain for its `sender_event_pubkey`.
373    ///
374    /// # Errors
375    /// [`Nip104Error::SessionNotReady`] if we hold no chain for that sender
376    /// (we are missing their distribution); plus key-id/ordering/cipher
377    /// failures from the chain.
378    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    /// **Encrypt and build the publishable outer event** for `group_id`. The
394    /// returned [`nostro2::NostrNote`] is a signed kind-[`GROUP_MESSAGE_KIND`]
395    /// event authored by our per-group sender-event key, with
396    /// `content = base64(key_id_be32 || message_number_be32 || nip44_bytes)`
397    /// and empty tags — byte-compatible with the reference `OneToManyChannel`.
398    /// Publish it **once**; every member decrypts it.
399    ///
400    /// # Errors
401    /// [`Nip104Error::SessionNotReady`] if we have no sending chain; plus
402    /// cipher/signing failures.
403    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    /// **Decrypt an inbound outer event.** Verifies the event, routes by its
433    /// `pubkey` (the sender-event key) to the matching receiving chain, parses
434    /// the compact payload, and decrypts. Returns `Ok(None)` if the author is
435    /// not a sender-key chain we know (e.g. a 1:1 message, or a member whose
436    /// distribution we have not yet received).
437    ///
438    /// # Errors
439    /// [`Nip104Error`] on a bad signature, malformed payload, or chain
440    /// decrypt failure.
441    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        // Route by author: do we hold a receiving chain for this sender?
450        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    /// Snapshot every group's transport state for persistence — the inverse of
476    /// [`Self::restore_group`]. Returns one [`GroupSnapshot`] per group in
477    /// sorted `group_id` order, each carrying our sending chain (if any) and
478    /// all receiving chains. Combined with `our_pubkey()`, this is everything
479    /// needed to revive the manager across a restart.
480    #[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    /// Restore one group's transport state from a [`GroupSnapshot`], replacing
501    /// any existing record for that group and rebuilding the sender-routing
502    /// index for both the sending and receiving chains. The inverse of
503    /// [`Self::snapshot`].
504    pub fn restore_group(&mut self, snap: GroupSnapshot) {
505        let mut record = GroupRecord::default();
506        // Borrow rather than move: `SendingChainSnapshot` scrubs its secret on
507        // `Drop`, which forbids destructuring it by value.
508        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    /// Build the reference's compact outer payload:
526    /// `base64(key_id_be32 || message_number_be32 || raw_nip44_bytes)`.
527    ///
528    /// Our [`SenderKeyState`] ciphertext is the **base64** NIP-44 payload; we
529    /// decode it back to the raw bytes the reference frames.
530    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    /// Inverse of [`encode_outer_content`](Self::encode_outer_content),
546    /// returning `(key_id, message_number, base64 nip44 ciphertext)` ready for
547    /// the chain.
548    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        // Alice mints a chain and distributes it to Bob and Carol.
575        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        // One published message decrypts for every member.
584        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    /// Snapshot a `GroupManager` mid-conversation, rebuild a fresh one purely
590    /// from the snapshot, and confirm both sending and receiving chains resume
591    /// exactly where they left off (no key reuse, no decrypt gaps).
592    #[test]
593    fn snapshot_round_trip_resumes_both_directions() {
594        let mut alice = mgr("alice");
595        let mut bob = mgr("bob");
596
597        // Alice mints, Bob installs; exchange one message each way.
598        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        // Snapshot Alice's sending side and Bob's receiving side mid-stream.
604        let alice_snaps = alice.snapshot();
605        let bob_snaps = bob.snapshot();
606
607        // Revive both from scratch.
608        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        // The restored sender continues the chain (iteration advanced past m1);
618        // the restored receiver decrypts it with no gap.
619        let m2 = alice2.encrypt("g1", b"after restore", 1002).unwrap();
620        assert_eq!(bob2.decrypt(&m2).unwrap().plaintext, b"after restore");
621
622        // And routing-by-author survives: bob2 knows the sender chain.
623        assert!(bob2.known_senders("g1").contains(&dist.sender_event_pubkey));
624    }
625
626    /// A receiving chain with banked skipped keys (out-of-order delivery) must
627    /// survive a snapshot — the gap remains decryptable after restore.
628    #[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        // Alice produces three messages; Bob receives #2 first (banking the
636        // key for #0 and #1 as skipped).
637        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        // Snapshot Bob now, while #0 and #1 are banked skipped keys.
643        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        // The restored receiver still decrypts the earlier, out-of-order msgs.
650        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        // Dave joins now: he gets the chain at its current iteration.
681        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        // New message: both Bob (iter 1) and Dave (iter 1) decrypt.
686        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        // Alice and Bob each have their own chain; Carol decrypts both.
694        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        // Bob never got the distribution.
718        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        // Rotate to a new key id → fresh sender-event pubkey + chain.
730        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        // Alice frames the kind-10446 rumor she'd send Bob over their 1:1 session.
764        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        // Bob receives that rumor (out of his session) and installs the chain.
773        let applied = bob.apply_distribution_rumor(&rumor).unwrap().unwrap();
774        assert_eq!(applied, dist);
775
776        // Now the one-to-many outer event decrypts for Bob.
777        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        // One signed outer event, published once.
806        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        // Every member decrypts the same wire event.
813        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        // Bob has no distribution → unknown author → Ok(None), not an error.
826        let ev = alice.encrypt_to_event("g", b"secret", 1).unwrap();
827        assert!(bob.decrypt_event(&ev).unwrap().is_none());
828    }
829
830    // ── Adversarial / scale ───────────────────────────────────────
831
832    /// One mint, fan out to a large membership: a single published event must
833    /// decrypt identically for every one of N members.
834    #[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        // Publish ONCE.
849        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    /// A busy group: many sequential messages, each decrypting for two members
862    /// who advance in lockstep with no skipped-key residue.
863    #[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    /// A member who joins after thousands of messages, via `current_distribution`,
888    /// decrypts from that point forward — and cannot read the backlog (he never
889    /// had the earlier chain keys, and the message indices are in his past).
890    #[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        // Eve joins now at the current iteration.
907        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        // Forward secrecy for the future: a new message decrypts for Eve.
913        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        // The backlog is in Eve's past with no stored keys → unrecoverable.
920        let old = backlog.last().unwrap();
921        assert!(eve.decrypt_event(old).is_err());
922    }
923
924    /// A tampered outer event (content mutated after signing) breaks the
925    /// Schnorr signature and is rejected — and the receiver chain is left
926    /// untouched so the genuine event still decrypts.
927    #[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'); // invalidates id + signature
937
938        assert!(matches!(
939            bob.decrypt_event(&forged),
940            Err(Nip104Error::InvalidHeader)
941        ));
942        // Untouched chain: the authentic event still decrypts.
943        assert_eq!(
944            bob.decrypt_event(&ev).unwrap().unwrap().plaintext,
945            b"genuine"
946        );
947    }
948
949    /// An event whose signature is valid but whose content is structurally
950    /// malformed (too short to hold the `key_id`/`message_number` prefix) is a
951    /// decode error, not a panic.
952    #[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        // Re-sign a too-short payload under the real sender-event key so the
960        // signature passes and we reach the decoder.
961        let sender_pk = dist.sender_event_pubkey.clone();
962        // Build via the genuine path then swap content + re-sign is not exposed;
963        // instead exercise the pure decoder directly for the structural cases.
964        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        // 4 bytes base64 — present but shorter than the 8-byte header.
973        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    /// Replaying a group message a second time is rejected: the chain has moved
982    /// past that index and kept no skipped key for it.
983    #[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        // Second delivery of the identical event: past index, no stored key.
993        assert!(bob.decrypt_event(&ev).is_err());
994    }
995
996    /// Two groups, one device: a message minted in group A must not be
997    /// decryptable as group B even though the same manager holds both chains —
998    /// routing is strictly by sender-event pubkey.
999    #[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    /// After a key rotation, a message from the *retired* chain no longer
1024    /// decrypts if the receiver has replaced it (rotation supersedes the old
1025    /// sender-event pubkey only when reused; distinct pubkeys coexist). Here we
1026    /// assert a stale message under the old `key_id` against the new chain fails.
1027    #[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        // Rotate to key_id 2 and install; the receiving chain for the old
1036        // sender pubkey is still present, but a message carrying key_id 1 routed
1037        // to a key_id-2 chain is an InvalidHeader.
1038        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        // key_id and message_number are big-endian u32 prefixes.
1051        let content = GroupManager::<K>::encode_outer_content(0x0102_0304, 0x0506_0708, &{
1052            // a minimal valid base64 of some bytes
1053            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}