Skip to main content

nostro2_nips/nip_104/
sender_key.rs

1//! NIP-104 — Group **sender keys** (one-to-many ratchet).
2//!
3//! 1:1 [`crate::nip_104::Session`]s give every pair of devices its own
4//! double ratchet. That does not scale to groups: encrypting one message to
5//! `N` members costs `N` ciphertexts. The reference
6//! (`mmalmi/nostr-double-ratchet`) solves this with **sender keys**, the same
7//! construction Signal uses for groups:
8//!
9//! * Each sending device owns a per-group **sender-key chain** — a symmetric
10//!   KDF ratchet seeded by a random chain key.
11//! * The chain key (plus its `key_id` and current `iteration`) is distributed
12//!   **once per member** over the authenticated 1:1 sessions — a
13//!   [`SenderKeyDistribution`].
14//! * Thereafter every group message is published **once** as a single
15//!   one-to-many event, encrypted with the next message key pulled off the
16//!   chain. Members who hold the distribution ratchet their copy forward and
17//!   decrypt.
18//!
19//! This module is the pure state machine for that chain — the interop-critical
20//! core. It is byte-compatible with the reference `SenderKeyState`:
21//!
22//! * KDF salt `ndr-sender-key-v1`,
23//! * `kdf(chain_key, salt, 2)` → `[next_chain_key, message_key]` per step
24//!   (reusing [`crate::nip_104::kdf`], shared with the 1:1 ratchet),
25//! * message keys feed NIP-44 v2 as conversation keys,
26//! * out-of-order delivery handled by storing skipped message keys by index,
27//! * bounded skip (`SENDER_KEY_MAX_SKIP`) and stored-key pruning
28//!   (`SENDER_KEY_MAX_STORED_SKIPPED_KEYS`).
29//!
30//! Like the rest of the crate it follows the **plan / apply** transaction
31//! model: [`plan_encrypt`](SenderKeyState::plan_encrypt) /
32//! [`plan_decrypt`](SenderKeyState::plan_decrypt) are pure and return a
33//! `next_state`; nothing mutates until you `apply`.
34
35use std::collections::BTreeMap;
36
37use nostro2_traits::NostrKeypair;
38use nostro2_traits::hex::Hexable;
39use zeroize::Zeroize;
40
41use super::{Nip104Crypto, Nip104Error};
42
43type Result<T> = std::result::Result<T, Nip104Error>;
44
45/// Maximum number of message keys we will skip ahead to decrypt an
46/// out-of-order message. Matches the reference.
47pub const SENDER_KEY_MAX_SKIP: u32 = 10_000;
48
49/// Maximum number of skipped message keys retained at once (oldest pruned).
50pub const SENDER_KEY_MAX_STORED_SKIPPED_KEYS: usize = 2_000;
51
52/// HKDF salt domain-separating the sender-key chain from the 1:1 ratchet.
53const SENDER_KEY_KDF_SALT: &[u8] = b"ndr-sender-key-v1";
54
55/// One sender-key chain: a symmetric KDF ratchet identified by `key_id`.
56///
57/// Both the sender and every receiver hold a copy seeded from the same
58/// distribution; the sender advances it on encrypt, receivers on decrypt.
59/// Chain keys are stored as 64-char hex to match the crate's `SessionState`
60/// convention (the reference stores raw bytes; the wire `SenderKeyDistribution`
61/// is where the hex/byte boundary lives).
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct SenderKeyState {
64    /// Identifies which chain this is; rotates on membership/key change.
65    pub key_id: u32,
66    /// Current chain key (64-char hex).
67    chain_key: String,
68    /// Next message index this chain will produce/expect.
69    iteration: u32,
70    /// Skipped message keys (index → key hex) for out-of-order delivery.
71    skipped_message_keys: BTreeMap<u32, String>,
72}
73
74/// Scrub the chain key and every banked skipped message key on drop.
75impl Drop for SenderKeyState {
76    fn drop(&mut self) {
77        self.chain_key.zeroize();
78        for key in self.skipped_message_keys.values_mut() {
79            key.zeroize();
80        }
81    }
82}
83
84/// Result of [`SenderKeyState::plan_encrypt`] — apply to commit.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct SenderKeyEncryptPlan {
87    /// The state after this encryption.
88    pub next_state: SenderKeyState,
89    /// Chain id the message was encrypted under.
90    pub key_id: u32,
91    /// Index of this message on the chain.
92    pub message_number: u32,
93    /// Base64 NIP-44 v2 ciphertext.
94    pub ciphertext: String,
95}
96
97/// Result of [`SenderKeyState::plan_decrypt`] — apply to commit.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct SenderKeyDecryptPlan {
100    /// The state after this decryption.
101    pub next_state: SenderKeyState,
102    /// Recovered plaintext.
103    pub plaintext: Vec<u8>,
104}
105
106impl SenderKeyState {
107    /// Create a fresh chain from a 32-byte `chain_key`, starting at
108    /// `iteration`.
109    #[must_use]
110    pub fn new(key_id: u32, chain_key: &[u8; 32], iteration: u32) -> Self {
111        Self {
112            key_id,
113            chain_key: chain_key.to_hex(),
114            iteration,
115            skipped_message_keys: BTreeMap::new(),
116        }
117    }
118
119    /// This chain's id.
120    #[must_use]
121    pub const fn key_id(&self) -> u32 {
122        self.key_id
123    }
124
125    /// The next message index this chain will produce/expect.
126    #[must_use]
127    pub const fn iteration(&self) -> u32 {
128        self.iteration
129    }
130
131    /// The current chain key as 64-char hex (used to re-derive a
132    /// [`crate::nip_104::SenderKeyDistribution`] for late joiners).
133    #[must_use]
134    pub fn chain_key_hex(&self) -> String {
135        self.chain_key.clone()
136    }
137
138    /// Number of skipped message keys currently stored.
139    #[must_use]
140    pub fn skipped_len(&self) -> usize {
141        self.skipped_message_keys.len()
142    }
143
144    /// The skipped message keys currently banked for out-of-order delivery,
145    /// as `message_index → message_key_hex`. Exposed for persistence: a
146    /// chain with banked skipped keys cannot be faithfully restored from
147    /// [`Self::new`] alone (which starts empty), so snapshots must carry these
148    /// and rebuild with [`Self::from_parts`].
149    #[must_use]
150    pub const fn skipped_keys(&self) -> &BTreeMap<u32, String> {
151        &self.skipped_message_keys
152    }
153
154    /// Reconstruct a chain from a full persisted snapshot — the inverse of the
155    /// getters ([`Self::key_id`], [`Self::chain_key_hex`], [`Self::iteration`],
156    /// [`Self::skipped_keys`]). Unlike [`Self::new`], this preserves banked
157    /// skipped message keys, so an out-of-order-delivery chain round-trips
158    /// losslessly through storage.
159    #[must_use]
160    pub const fn from_parts(
161        key_id: u32,
162        chain_key_hex: String,
163        iteration: u32,
164        skipped_message_keys: BTreeMap<u32, String>,
165    ) -> Self {
166        Self {
167            key_id,
168            chain_key: chain_key_hex,
169            iteration,
170            skipped_message_keys,
171        }
172    }
173
174    /// Plan encryption of `plaintext` as the next message on the chain. Pure:
175    /// the returned plan carries the advanced `next_state`; `self` is
176    /// unchanged until [`apply_encrypt`](Self::apply_encrypt).
177    ///
178    /// # Errors
179    /// [`Nip104Error`] on KDF/cipher failure or iteration overflow.
180    pub fn plan_encrypt<K: NostrKeypair>(&self, plaintext: &[u8]) -> Result<SenderKeyEncryptPlan> {
181        let mut next_state = self.clone();
182        let message_number = next_state.iteration;
183        let (next_chain_key, message_key) =
184            Self::derive_message_key::<K>(&K::decode_hex_32(&next_state.chain_key)?);
185        next_state.chain_key = next_chain_key.to_hex();
186        next_state.iteration = next_state
187            .iteration
188            .checked_add(1)
189            .ok_or(Nip104Error::SessionNotReady)?;
190
191        let ciphertext = K::encrypt_with_message_key(&message_key, plaintext)?;
192        Ok(SenderKeyEncryptPlan {
193            next_state,
194            key_id: self.key_id,
195            message_number,
196            ciphertext,
197        })
198    }
199
200    /// Commit an [`SenderKeyEncryptPlan`].
201    pub fn apply_encrypt(&mut self, plan: SenderKeyEncryptPlan) {
202        *self = plan.next_state;
203    }
204
205    /// Encrypt `plaintext`, returning `(message_number, base64 ciphertext)` and
206    /// advancing the chain. Convenience over plan/apply.
207    ///
208    /// # Errors
209    /// As [`plan_encrypt`](Self::plan_encrypt).
210    pub fn encrypt<K: NostrKeypair>(&mut self, plaintext: &[u8]) -> Result<(u32, String)> {
211        let plan = self.plan_encrypt::<K>(plaintext)?;
212        let out = (plan.message_number, plan.ciphertext.clone());
213        self.apply_encrypt(plan);
214        Ok(out)
215    }
216
217    /// Plan decryption of a message at `message_number` carrying `key_id`.
218    /// Handles in-order, future (skip-ahead) and past (skipped-key) messages.
219    /// Pure: `self` is unchanged until [`apply_decrypt`](Self::apply_decrypt).
220    ///
221    /// # Errors
222    /// [`Nip104Error`] on key-id mismatch, too many skipped messages, a
223    /// duplicate/missing past message, or cipher failure.
224    pub fn plan_decrypt<K: NostrKeypair>(
225        &self,
226        key_id: u32,
227        message_number: u32,
228        ciphertext_b64: &str,
229    ) -> Result<SenderKeyDecryptPlan> {
230        if key_id != self.key_id {
231            return Err(Nip104Error::InvalidHeader);
232        }
233        let mut next_state = self.clone();
234        let plaintext = next_state.decrypt_in_place::<K>(message_number, ciphertext_b64)?;
235        Ok(SenderKeyDecryptPlan {
236            next_state,
237            plaintext,
238        })
239    }
240
241    /// Commit a [`SenderKeyDecryptPlan`].
242    pub fn apply_decrypt(&mut self, plan: SenderKeyDecryptPlan) -> Vec<u8> {
243        *self = plan.next_state;
244        plan.plaintext
245    }
246
247    /// Decrypt a message at `message_number`, advancing the chain. Convenience
248    /// over plan/apply.
249    ///
250    /// # Errors
251    /// As [`plan_decrypt`](Self::plan_decrypt).
252    pub fn decrypt<K: NostrKeypair>(
253        &mut self,
254        message_number: u32,
255        ciphertext_b64: &str,
256    ) -> Result<Vec<u8>> {
257        let plan = self.plan_decrypt::<K>(self.key_id, message_number, ciphertext_b64)?;
258        Ok(self.apply_decrypt(plan))
259    }
260
261    /// The in-place ratchet: pull a past skipped key, or step forward (storing
262    /// skipped keys) until `message_number`, then decrypt.
263    fn decrypt_in_place<K: NostrKeypair>(
264        &mut self,
265        message_number: u32,
266        ciphertext_b64: &str,
267    ) -> Result<Vec<u8>> {
268        // Past message: must have a stored skipped key.
269        if message_number < self.iteration {
270            let key = self
271                .skipped_message_keys
272                .remove(&message_number)
273                .ok_or(Nip104Error::InvalidHeader)?;
274            return K::decrypt_with_message_key(&K::decode_hex_32(&key)?, ciphertext_b64);
275        }
276
277        // Future message: bounded skip.
278        let delta = message_number - self.iteration;
279        if delta > SENDER_KEY_MAX_SKIP {
280            return Err(Nip104Error::TooManySkippedMessages);
281        }
282
283        // Step forward, banking skipped keys.
284        while self.iteration < message_number {
285            let (next_chain_key, message_key) =
286                Self::derive_message_key::<K>(&K::decode_hex_32(&self.chain_key)?);
287            self.chain_key = next_chain_key.to_hex();
288            self.skipped_message_keys
289                .insert(self.iteration, message_key.to_hex());
290            self.iteration = self
291                .iteration
292                .checked_add(1)
293                .ok_or(Nip104Error::SessionNotReady)?;
294        }
295
296        // Now at message_number: derive its key and advance once more.
297        let (next_chain_key, message_key) =
298            Self::derive_message_key::<K>(&K::decode_hex_32(&self.chain_key)?);
299        self.chain_key = next_chain_key.to_hex();
300        self.iteration = self
301            .iteration
302            .checked_add(1)
303            .ok_or(Nip104Error::SessionNotReady)?;
304        Self::prune_skipped(&mut self.skipped_message_keys);
305
306        K::decrypt_with_message_key(&message_key, ciphertext_b64)
307    }
308
309    /// `kdf(chain_key, "ndr-sender-key-v1", 2)` → `(next_chain_key, message_key)`.
310    fn derive_message_key<K: NostrKeypair>(chain_key: &[u8; 32]) -> ([u8; 32], [u8; 32]) {
311        let outs = K::kdf(chain_key, SENDER_KEY_KDF_SALT, 2);
312        (outs[0], outs[1])
313    }
314
315    /// Bound the stored skipped-key map, dropping the oldest indices first.
316    fn prune_skipped(map: &mut BTreeMap<u32, String>) {
317        while map.len() > SENDER_KEY_MAX_STORED_SKIPPED_KEYS {
318            let Some(first) = map.keys().next().copied() else {
319                break;
320            };
321            map.remove(&first);
322        }
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    type K = crate::tests::NipTester;
331
332    #[test]
333    fn roundtrip_single_message() {
334        let ck = [7_u8; 32];
335        let mut sender = SenderKeyState::new(1, &ck, 0);
336        let mut receiver = SenderKeyState::new(1, &ck, 0);
337
338        let (n, ct) = sender.encrypt::<K>(b"hello").unwrap();
339        assert_eq!(n, 0);
340        assert_eq!(receiver.decrypt::<K>(n, &ct).unwrap(), b"hello");
341        assert_eq!(sender.iteration(), receiver.iteration());
342    }
343
344    #[test]
345    fn decrypt_out_of_order() {
346        let ck = [9_u8; 32];
347        let mut sender = SenderKeyState::new(1, &ck, 0);
348        let mut receiver = SenderKeyState::new(1, &ck, 0);
349
350        let (n0, c0) = sender.encrypt::<K>(b"m0").unwrap();
351        let (n1, c1) = sender.encrypt::<K>(b"m1").unwrap();
352
353        assert_eq!(receiver.decrypt::<K>(n1, &c1).unwrap(), b"m1");
354        assert_eq!(receiver.decrypt::<K>(n0, &c0).unwrap(), b"m0");
355    }
356
357    #[test]
358    fn rejects_duplicate_message() {
359        let ck = [11_u8; 32];
360        let mut sender = SenderKeyState::new(1, &ck, 0);
361        let mut receiver = SenderKeyState::new(1, &ck, 0);
362        let (n, c) = sender.encrypt::<K>(b"once").unwrap();
363
364        assert_eq!(receiver.decrypt::<K>(n, &c).unwrap(), b"once");
365        assert!(receiver.decrypt::<K>(n, &c).is_err());
366    }
367
368    #[test]
369    fn wrong_key_id_does_not_mutate_receiver() {
370        let ck = [13_u8; 32];
371        let mut sender = SenderKeyState::new(1, &ck, 0);
372        let receiver = SenderKeyState::new(1, &ck, 0);
373        let (n, c) = sender.encrypt::<K>(b"x").unwrap();
374        let before = receiver.clone();
375
376        assert!(receiver.plan_decrypt::<K>(2, n, &c).is_err());
377        assert_eq!(receiver, before);
378    }
379
380    #[test]
381    fn plan_encrypt_is_pure_until_apply() {
382        let ck = [4_u8; 32];
383        let mut sender = SenderKeyState::new(7, &ck, 0);
384        let before = sender.clone();
385
386        let plan = sender.plan_encrypt::<K>(b"deferred").unwrap();
387        assert_eq!(sender, before);
388        assert_eq!(plan.key_id, 7);
389        assert_eq!(plan.message_number, 0);
390
391        sender.apply_encrypt(plan);
392        assert_eq!(sender.iteration(), 1);
393        assert_ne!(sender, before);
394    }
395
396    #[test]
397    fn skip_ahead_then_backfill() {
398        let ck = [19_u8; 32];
399        let mut sender = SenderKeyState::new(1, &ck, 0);
400        let mut receiver = SenderKeyState::new(1, &ck, 0);
401        let (n0, c0) = sender.encrypt::<K>(b"m0").unwrap();
402        let (n1, c1) = sender.encrypt::<K>(b"m1").unwrap();
403        let (n2, c2) = sender.encrypt::<K>(b"m2").unwrap();
404
405        // Receive the latest first → banks two skipped keys.
406        assert_eq!(receiver.decrypt::<K>(n2, &c2).unwrap(), b"m2");
407        assert_eq!(receiver.skipped_len(), 2);
408        // Backfill the earlier two from stored keys.
409        assert_eq!(receiver.decrypt::<K>(n0, &c0).unwrap(), b"m0");
410        assert_eq!(receiver.decrypt::<K>(n1, &c1).unwrap(), b"m1");
411        assert_eq!(receiver.skipped_len(), 0);
412    }
413
414    #[test]
415    fn rejects_too_many_skipped() {
416        let ck = [3_u8; 32];
417        let receiver = SenderKeyState::new(1, &ck, 0);
418        let err = receiver.plan_decrypt::<K>(1, SENDER_KEY_MAX_SKIP + 1, "AA");
419        assert!(matches!(err, Err(Nip104Error::TooManySkippedMessages)));
420    }
421
422    // ── Adversarial / scale ──────────────────────────────────────
423
424    /// Skipping ahead past the stored-key cap evicts the *oldest* skipped keys
425    /// (bounded memory): the banked map never exceeds the cap, the recent tail
426    /// still backfills, but the evicted head is gone forever.
427    #[test]
428    #[allow(clippy::cast_possible_truncation)] // cap is 2000, fits u32
429    fn skip_ahead_prunes_oldest_skipped_keys() {
430        // Produce a long run; remember the first and a recent ciphertext.
431        const AHEAD: u32 = SENDER_KEY_MAX_STORED_SKIPPED_KEYS as u32 + 500;
432        let ck = [21_u8; 32];
433        let mut sender = SenderKeyState::new(1, &ck, 0);
434        let mut receiver = SenderKeyState::new(1, &ck, 0);
435        let mut first = None;
436        let mut recent = None;
437        for i in 0..=AHEAD {
438            let (n, c) = sender.encrypt::<K>(format!("m{i}").as_bytes()).unwrap();
439            if i == 0 {
440                first = Some((n, c.clone()));
441            }
442            if i == AHEAD - 1 {
443                recent = Some((n, c.clone()));
444            }
445            if i == AHEAD {
446                // Receive the very latest first → banks AHEAD skipped keys,
447                // then prunes down to the cap.
448                assert_eq!(
449                    receiver.decrypt::<K>(n, &c).unwrap(),
450                    format!("m{i}").as_bytes()
451                );
452            }
453        }
454        assert_eq!(
455            receiver.skipped_len(),
456            SENDER_KEY_MAX_STORED_SKIPPED_KEYS,
457            "stored skipped keys must be capped"
458        );
459
460        // A recent message (index AHEAD-1) still backfills from a stored key…
461        let (rn, rc) = recent.unwrap();
462        assert_eq!(
463            receiver.decrypt::<K>(rn, &rc).unwrap(),
464            format!("m{}", AHEAD - 1).as_bytes()
465        );
466
467        // …but the long-evicted message 0 is unrecoverable (its key was pruned).
468        let (fn0, fc0) = first.unwrap();
469        assert!(matches!(
470            receiver.plan_decrypt::<K>(1, fn0, &fc0),
471            Err(Nip104Error::InvalidHeader)
472        ));
473    }
474
475    /// A future message exactly `SENDER_KEY_MAX_SKIP` ahead is accepted; one
476    /// more is refused. Guards the off-by-one at the skip ceiling.
477    #[test]
478    fn skip_ceiling_is_inclusive() {
479        let ck = [23_u8; 32];
480        // Receiver at iteration 0; a message at exactly MAX_SKIP has delta ==
481        // MAX_SKIP, which is allowed. We don't have its ciphertext, but the
482        // bound is checked before any key derivation, so a cipher failure (not
483        // TooManySkippedMessages) proves we passed the gate.
484        let receiver = SenderKeyState::new(1, &ck, 0);
485        let at_ceiling = receiver.plan_decrypt::<K>(1, SENDER_KEY_MAX_SKIP, "not-base64!!");
486        assert!(
487            !matches!(at_ceiling, Err(Nip104Error::TooManySkippedMessages)),
488            "delta == MAX_SKIP must pass the skip gate"
489        );
490        let over = receiver.plan_decrypt::<K>(1, SENDER_KEY_MAX_SKIP + 1, "not-base64!!");
491        assert!(matches!(over, Err(Nip104Error::TooManySkippedMessages)));
492    }
493
494    /// Encrypting on a chain already at `u32::MAX` overflows the iteration
495    /// counter — the `checked_add` must surface an error, never wrap to 0
496    /// (which would silently reuse a message key).
497    #[test]
498    fn iteration_overflow_on_encrypt_is_rejected() {
499        let ck = [25_u8; 32];
500        let sender = SenderKeyState::new(1, &ck, u32::MAX);
501        assert!(matches!(
502            sender.plan_encrypt::<K>(b"boom"),
503            Err(Nip104Error::SessionNotReady)
504        ));
505    }
506
507    /// The chain inherits NIP-44 v2's plaintext bounds: a single byte and the
508    /// 65 535-byte maximum both round-trip, but empty and over-max are refused
509    /// (the cipher rejects them before the chain advances).
510    #[test]
511    fn payload_size_bounds_match_nip44() {
512        let ck = [27_u8; 32];
513        let mut sender = SenderKeyState::new(1, &ck, 0);
514        let mut receiver = SenderKeyState::new(1, &ck, 0);
515
516        // One byte: the smallest legal payload.
517        let (n0, c0) = sender.encrypt::<K>(b"x").unwrap();
518        assert_eq!(receiver.decrypt::<K>(n0, &c0).unwrap(), b"x");
519
520        // Exactly the NIP-44 v2 maximum (65 535 bytes).
521        let max = vec![0x5A_u8; 65_535];
522        let (n1, c1) = sender.encrypt::<K>(&max).unwrap();
523        assert_eq!(receiver.decrypt::<K>(n1, &c1).unwrap(), max);
524
525        // Empty plaintext and one-over-max are both rejected by the cipher,
526        // and a rejected encrypt must not advance the (pure) sender plan.
527        assert!(sender.plan_encrypt::<K>(b"").is_err());
528        let over = vec![0_u8; 65_536];
529        assert!(sender.plan_encrypt::<K>(&over).is_err());
530        // Sender still at iteration 2 after the two good messages.
531        assert_eq!(sender.iteration(), 2);
532    }
533
534    /// A tampered ciphertext (valid base64, broken MAC) fails to decrypt and —
535    /// crucially — leaves the receiver chain untouched, so the legitimate
536    /// message at that index still decrypts afterwards.
537    #[test]
538    fn tampered_ciphertext_fails_without_advancing() {
539        use base64::engine::{Engine as _, general_purpose};
540        let ck = [29_u8; 32];
541        let mut sender = SenderKeyState::new(1, &ck, 0);
542        let receiver = SenderKeyState::new(1, &ck, 0);
543        let (n, c) = sender.encrypt::<K>(b"authentic").unwrap();
544
545        // Flip a byte in the middle of the base64-decoded payload.
546        let mut raw = general_purpose::STANDARD.decode(&c).unwrap();
547        let mid = raw.len() / 2;
548        raw[mid] ^= 0xFF;
549        let tampered = general_purpose::STANDARD.encode(&raw);
550
551        let before = receiver.clone();
552        assert!(receiver.plan_decrypt::<K>(1, n, &tampered).is_err());
553        assert_eq!(receiver, before, "failed decrypt must not mutate state");
554
555        // The genuine ciphertext at the same index still works.
556        let mut receiver = receiver;
557        assert_eq!(receiver.decrypt::<K>(n, &c).unwrap(), b"authentic");
558    }
559
560    /// A sustained chain of many in-order messages keeps both ends in lockstep
561    /// with zero skipped-key residue.
562    #[test]
563    fn sustained_in_order_volume() {
564        const N: u32 = 5_000;
565        let ck = [31_u8; 32];
566        let mut sender = SenderKeyState::new(1, &ck, 0);
567        let mut receiver = SenderKeyState::new(1, &ck, 0);
568        for i in 0..N {
569            let (n, c) = sender.encrypt::<K>(format!("msg-{i}").as_bytes()).unwrap();
570            assert_eq!(n, i);
571            assert_eq!(
572                receiver.decrypt::<K>(n, &c).unwrap(),
573                format!("msg-{i}").as_bytes()
574            );
575        }
576        assert_eq!(sender.iteration(), N);
577        assert_eq!(receiver.iteration(), N);
578        assert_eq!(receiver.skipped_len(), 0);
579    }
580}