Skip to main content

ic_cipher/
sealer.rs

1//! An AEAD that chooses its own nonces.
2//!
3//! Every [`Aead`] takes a nonce from its caller, and under AES-GCM or
4//! ChaCha20-Poly1305 a nonce used twice under one key is catastrophic: GCM
5//! leaks its authentication subkey, ChaCha20 XORs the two plaintexts. The rule
6//! is easy to state and easy to break, because nothing in `seal_detached`'s
7//! signature tells you that the bytes you pass it are the dangerous ones.
8//!
9//! [`Sealer`] takes the nonce out of the caller's hands. It builds each one as
10//! SP 800-38D section 8.2.1's deterministic construction does: a 4-byte fixed
11//! field naming the sender, then a 64-bit invocation counter, big-endian. It
12//! returns the nonce it used, so the receiver can be sent it. It refuses rather
13//! than wrap when the counter runs out.
14//!
15//! [`Opener`] is the receiving side. It accepts only the sender's fixed field
16//! and only counters it has not passed, so a replayed or reordered message is
17//! refused before the AEAD runs.
18//!
19//! ```
20//! use ic_cipher::{Aes256Gcm, sealer::{Opener, Sealer}};
21//!
22//! // A key for this session only: from a key exchange and a KDF, not a constant.
23//! let key = [0x2a; 32];
24//! let mut tx = Sealer::<Aes256Gcm>::new(&key, *b"c->s")?;
25//! let mut rx = Opener::<Aes256Gcm>::new(&key, *b"c->s")?;
26//!
27//! let mut msg = *b"ship it";
28//! let mut tag = [0u8; 16];
29//! let nonce = tx.seal(b"header", &mut msg, &mut tag)?;
30//! rx.open(&nonce, b"header", &mut msg, &tag)?;
31//! assert_eq!(&msg, b"ship it");
32//!
33//! // The same message again is a replay, and is refused.
34//! assert!(rx.open(&nonce, b"header", &mut msg, &tag).is_err());
35//! # Ok::<(), ic_core::Error>(())
36//! ```
37//!
38//! ## What the counter cannot know
39//!
40//! A counter is unique only within the object that holds it. Two `Sealer`s on
41//! the same key and fixed field both start at zero and reuse every nonce, and a
42//! process that restarts and builds a new one does the same. So:
43//!
44//! - **Use a key that lives no longer than its `Sealer`**: one derived for the
45//!   session, as TLS 1.3 and HPKE do. A long-lived key needs the counter
46//!   persisted and restored with [`Sealer::resume`], or a nonce-misuse-resistant
47//!   AEAD such as AES-256-GCM-SIV.
48//! - **Give every sender under one key its own fixed field**, such as one per
49//!   direction. Better still, give each direction its own key.
50//!
51//! The 12-byte nonce is required: it is the length every AEAD here takes and
52//! the one SP 800-38D's construction is defined for.
53
54use core::fmt;
55use ic_core::traits::Aead;
56use ic_core::{ensure, Result};
57
58/// The nonce length a sealer builds: a 4-byte fixed field and an 8-byte counter.
59pub const NONCE_LEN: usize = 12;
60
61/// The fixed field's length.
62pub const FIXED_LEN: usize = 4;
63
64/// Assemble `fixed || counter`, refusing the last counter value so that a
65/// sealer which has used it cannot wrap.
66fn nonce(fixed: &[u8; FIXED_LEN], counter: u64) -> Result<[u8; NONCE_LEN]> {
67    ensure!(
68        counter != u64::MAX,
69        CounterExhausted,
70        "sealer counter exhausted; it never reuses a nonce"
71    );
72    let mut n = [0u8; NONCE_LEN];
73    n[..FIXED_LEN].copy_from_slice(fixed);
74    n[FIXED_LEN..].copy_from_slice(&counter.to_be_bytes());
75    Ok(n)
76}
77
78/// Check that `A` takes the 12-byte nonce this construction builds.
79fn check_nonce_len<A: Aead>() -> Result<()> {
80    ensure!(
81        A::NONCE_LEN == NONCE_LEN,
82        Unsupported,
83        "sealer needs an AEAD with a 12-byte nonce"
84    );
85    Ok(())
86}
87
88/// Seals messages under nonces it builds itself, each used once.
89pub struct Sealer<A: Aead> {
90    aead: A,
91    fixed: [u8; FIXED_LEN],
92    counter: u64,
93}
94
95impl<A: Aead> Sealer<A> {
96    /// A sealer whose first nonce is `fixed || 0`.
97    ///
98    /// `fixed` names this sender. Any other party sealing under `key` must use
99    /// a different one.
100    pub fn new(key: &[u8], fixed: [u8; FIXED_LEN]) -> Result<Self> {
101        Self::resume(key, fixed, 0)
102    }
103
104    /// A sealer that continues from `counter`, the value a previous sealer on
105    /// this key and fixed field reported from [`Sealer::counter`] when it was
106    /// last used.
107    ///
108    /// Restoring a counter lower than one already used reuses nonces. Persist
109    /// the counter before sending what it sealed, not after.
110    pub fn resume(key: &[u8], fixed: [u8; FIXED_LEN], counter: u64) -> Result<Self> {
111        check_nonce_len::<A>()?;
112        Ok(Self {
113            aead: A::new(key)?,
114            fixed,
115            counter,
116        })
117    }
118
119    /// The counter the next seal will use.
120    pub fn counter(&self) -> u64 {
121        self.counter
122    }
123
124    /// Encrypt `in_out` in place, write the tag, and return the nonce used.
125    ///
126    /// The receiver needs the nonce, or the counter, to open the message. On
127    /// failure the counter does not advance.
128    pub fn seal(
129        &mut self,
130        aad: &[u8],
131        in_out: &mut [u8],
132        tag: &mut [u8],
133    ) -> Result<[u8; NONCE_LEN]> {
134        let n = nonce(&self.fixed, self.counter)?;
135        self.aead.seal_detached(&n, aad, in_out, tag)?;
136        self.counter += 1;
137        Ok(n)
138    }
139}
140
141impl<A: Aead> fmt::Debug for Sealer<A> {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.debug_struct("Sealer")
144            .field("fixed", &self.fixed)
145            .field("counter", &self.counter)
146            .finish_non_exhaustive()
147    }
148}
149
150/// Opens messages from one [`Sealer`], refusing replays and reordering.
151pub struct Opener<A: Aead> {
152    aead: A,
153    fixed: [u8; FIXED_LEN],
154    next: u64,
155}
156
157impl<A: Aead> Opener<A> {
158    /// An opener for the sender whose fixed field is `fixed`.
159    pub fn new(key: &[u8], fixed: [u8; FIXED_LEN]) -> Result<Self> {
160        check_nonce_len::<A>()?;
161        Ok(Self {
162            aead: A::new(key)?,
163            fixed,
164            next: 0,
165        })
166    }
167
168    /// The lowest counter the next open will accept.
169    pub fn next_counter(&self) -> u64 {
170        self.next
171    }
172
173    /// Verify and decrypt `in_out` in place.
174    ///
175    /// Refuses a nonce with another sender's fixed field, or a counter lower
176    /// than the last one opened plus one, before the AEAD runs. Counters may
177    /// skip forward, so a lost message does not stall the stream, but never go
178    /// back. On any failure the accepted counter does not advance, and
179    /// `in_out` holds no unauthenticated plaintext.
180    pub fn open(&mut self, nonce: &[u8], aad: &[u8], in_out: &mut [u8], tag: &[u8]) -> Result<()> {
181        ensure!(
182            nonce.len() == NONCE_LEN,
183            InvalidLength,
184            "sealer nonce must be 12 bytes"
185        );
186        // Neither the fixed field nor the counter is secret: both travel in
187        // the clear beside the ciphertext.
188        ensure!(
189            nonce[..FIXED_LEN] == self.fixed,
190            AuthenticationFailed,
191            "sealer nonce is from another sender"
192        );
193        let mut c = [0u8; 8];
194        c.copy_from_slice(&nonce[FIXED_LEN..]);
195        let counter = u64::from_be_bytes(c);
196        ensure!(
197            counter >= self.next && counter != u64::MAX,
198            AuthenticationFailed,
199            "sealer nonce replayed or out of order"
200        );
201        self.aead.open_detached(nonce, aad, in_out, tag)?;
202        self.next = counter + 1;
203        Ok(())
204    }
205}
206
207impl<A: Aead> fmt::Debug for Opener<A> {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.debug_struct("Opener")
210            .field("fixed", &self.fixed)
211            .field("next", &self.next)
212            .finish_non_exhaustive()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219    use crate::{Aes128Gcm, Aes256Gcm, Aes256GcmSiv, ChaCha20Poly1305};
220    use ic_core::ErrorKind;
221
222    const KEY: [u8; 32] = [7; 32];
223
224    fn seal_one<A: Aead>(tx: &mut Sealer<A>, msg: &[u8]) -> ([u8; NONCE_LEN], [u8; 64], [u8; 16]) {
225        let mut buf = [0u8; 64];
226        buf[..msg.len()].copy_from_slice(msg);
227        let mut tag = [0u8; 16];
228        let n = tx.seal(b"aad", &mut buf[..msg.len()], &mut tag).unwrap();
229        (n, buf, tag)
230    }
231
232    /// The nonce is `fixed || counter` big-endian, and the counter advances
233    /// by one per seal: SP 800-38D 8.2.1's deterministic construction.
234    #[test]
235    fn nonces_are_the_fixed_field_then_a_counter() {
236        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
237        for i in 0..3u64 {
238            let (n, _, _) = seal_one(&mut tx, b"m");
239            assert_eq!(&n[..4], b"abcd");
240            assert_eq!(n[4..], i.to_be_bytes());
241        }
242        assert_eq!(tx.counter(), 3);
243    }
244
245    /// What a sealer produces is what the bare AEAD produces under the nonce
246    /// it reports, so it adds no format of its own.
247    #[test]
248    fn a_seal_is_the_bare_aead_under_the_reported_nonce() {
249        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
250        seal_one(&mut tx, b"first");
251        let (n, ct, tag) = seal_one(&mut tx, b"second");
252        let bare = Aes256Gcm::new(&KEY).unwrap();
253        let mut buf = *b"second";
254        let mut t = [0u8; 16];
255        bare.seal_detached(&n, b"aad", &mut buf, &mut t).unwrap();
256        assert_eq!(&ct[..6], &buf);
257        assert_eq!(tag, t);
258    }
259
260    #[test]
261    fn every_twelve_byte_aead_round_trips() {
262        fn round<A: Aead>() {
263            let key = [9u8; 32];
264            let key = &key[..A::KEY_LEN];
265            let mut tx = Sealer::<A>::new(key, *b"wxyz").unwrap();
266            let mut rx = Opener::<A>::new(key, *b"wxyz").unwrap();
267            for _ in 0..3 {
268                let (n, mut ct, tag) = seal_one(&mut tx, b"hello");
269                rx.open(&n, b"aad", &mut ct[..5], &tag).unwrap();
270                assert_eq!(&ct[..5], b"hello");
271            }
272        }
273        round::<Aes128Gcm>();
274        round::<Aes256Gcm>();
275        round::<ChaCha20Poly1305>();
276        round::<Aes256GcmSiv>();
277    }
278
279    #[test]
280    fn a_replay_or_an_earlier_counter_is_refused() {
281        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
282        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
283        let (n0, ct0, t0) = seal_one(&mut tx, b"zero");
284        let (n1, ct1, t1) = seal_one(&mut tx, b"one!");
285        let mut buf = ct1;
286        rx.open(&n1, b"aad", &mut buf[..4], &t1).unwrap();
287        assert_eq!(rx.next_counter(), 2);
288        // The same message again: a replay.
289        let mut buf = ct1;
290        let e = rx.open(&n1, b"aad", &mut buf[..4], &t1).unwrap_err();
291        assert_eq!(e.kind(), ErrorKind::AuthenticationFailed);
292        // Counter 0 arriving late: refused, though authentic.
293        let mut buf = ct0;
294        let e = rx.open(&n0, b"aad", &mut buf[..4], &t0).unwrap_err();
295        assert_eq!(e.kind(), ErrorKind::AuthenticationFailed);
296        assert_eq!(rx.next_counter(), 2, "a refusal does not move the window");
297    }
298
299    #[test]
300    fn a_skipped_counter_is_accepted_so_a_lost_message_does_not_stall() {
301        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
302        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
303        seal_one(&mut tx, b"lost");
304        let (n, mut ct, t) = seal_one(&mut tx, b"kept");
305        rx.open(&n, b"aad", &mut ct[..4], &t).unwrap();
306        assert_eq!(&ct[..4], b"kept");
307    }
308
309    #[test]
310    fn another_senders_fixed_field_is_refused() {
311        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"s->c").unwrap();
312        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"c->s").unwrap();
313        let (n, mut ct, t) = seal_one(&mut tx, b"hi");
314        assert!(rx.open(&n, b"aad", &mut ct[..2], &t).is_err());
315        assert_eq!(rx.next_counter(), 0);
316    }
317
318    #[test]
319    fn a_forgery_does_not_advance_the_opener() {
320        let mut tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
321        let mut rx = Opener::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
322        let (n, mut ct, mut t) = seal_one(&mut tx, b"hi");
323        t[0] ^= 1;
324        assert!(rx.open(&n, b"aad", &mut ct[..2], &t).is_err());
325        assert_eq!(rx.next_counter(), 0);
326    }
327
328    /// The last counter value is never used, so a sealer cannot wrap to zero.
329    #[test]
330    fn an_exhausted_sealer_refuses_rather_than_wrapping() {
331        let mut tx = Sealer::<Aes256Gcm>::resume(&KEY, *b"abcd", u64::MAX - 1).unwrap();
332        seal_one(&mut tx, b"last");
333        let mut buf = *b"over";
334        let mut tag = [0u8; 16];
335        let e = tx.seal(b"", &mut buf, &mut tag).unwrap_err();
336        assert_eq!(e.kind(), ErrorKind::CounterExhausted);
337        assert_eq!(tx.counter(), u64::MAX);
338    }
339
340    #[test]
341    fn resume_continues_where_the_last_sealer_stopped() {
342        let mut a = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
343        seal_one(&mut a, b"0");
344        seal_one(&mut a, b"1");
345        let mut b = Sealer::<Aes256Gcm>::resume(&KEY, *b"abcd", a.counter()).unwrap();
346        let (n, _, _) = seal_one(&mut b, b"2");
347        assert_eq!(n[4..], 2u64.to_be_bytes());
348    }
349
350    #[test]
351    fn debug_shows_no_key() {
352        let tx = Sealer::<Aes256Gcm>::new(&KEY, *b"abcd").unwrap();
353        let s = format!("{tx:?}");
354        assert!(s.contains("counter") && !s.contains("aead"), "{s}");
355    }
356}