Skip to main content

oxicrypto_aead/
nonce_seq.rs

1//! Monotonic nonce sequence generator.
2//!
3//! [`NonceSequence<N>`] produces a sequence of unique, collision-resistant
4//! nonces of exactly `N` bytes.  The nonce layout is:
5//!
6//! ```text
7//! ┌──── N-8 bytes ────┬──────── 8 bytes ────────┐
8//! │    fixed prefix   │  counter (big-endian u64) │
9//! └───────────────────┴─────────────────────────┘
10//! ```
11//!
12//! The counter starts at 0 and increments by 1 with each [`generate`] call.
13//! [`generate`] returns [`CryptoError::Internal`] if the counter would overflow
14//! past `u64::MAX`, preventing nonce reuse.
15//!
16//! # Type aliases
17//!
18//! | Alias      | `N` | Suited for                     |
19//! |------------|-----|--------------------------------|
20//! | [`Nonce12`] | 12 | AES-GCM, ChaCha20-Poly1305     |
21//! | [`Nonce24`] | 24 | XChaCha20-Poly1305             |
22//!
23//! [`generate`]: NonceSequence::generate
24
25extern crate alloc;
26
27use oxicrypto_core::CryptoError;
28
29/// A stateful nonce generator that produces sequentially unique nonces.
30///
31/// # Panics
32///
33/// This type does not panic; all errors are returned as [`CryptoError`].
34///
35/// # Example
36///
37/// ```rust
38/// use oxicrypto_aead::nonce_seq::Nonce12;
39///
40/// let prefix = [0u8; 4]; // 4-byte prefix for a 12-byte nonce
41/// let mut seq = Nonce12::new(&prefix).unwrap();
42/// let n0 = seq.generate().unwrap();
43/// let n1 = seq.generate().unwrap();
44/// assert_ne!(n0, n1);
45/// ```
46pub struct NonceSequence<const N: usize> {
47    /// The full nonce buffer: prefix in `[0..N-8]`, counter in `[N-8..N]`.
48    nonce: [u8; N],
49    /// Current counter value (monotonically increasing).
50    counter: u64,
51}
52
53impl<const N: usize> core::fmt::Debug for NonceSequence<N> {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        write!(f, "NonceSequence<{}>(counter={})", N, self.counter)
56    }
57}
58
59impl<const N: usize> NonceSequence<N> {
60    const PREFIX_LEN: usize = N - 8;
61
62    /// Create a new `NonceSequence` with the given prefix.
63    ///
64    /// `prefix` must be exactly `N - 8` bytes long; otherwise
65    /// [`CryptoError::InvalidNonce`] is returned.
66    ///
67    /// The initial counter value is 0.
68    pub fn new(prefix: &[u8]) -> Result<Self, CryptoError> {
69        if N < 8 {
70            return Err(CryptoError::InvalidNonce);
71        }
72        if prefix.len() != Self::PREFIX_LEN {
73            return Err(CryptoError::InvalidNonce);
74        }
75        let mut nonce = [0u8; N];
76        nonce[..Self::PREFIX_LEN].copy_from_slice(prefix);
77        // Counter bytes start at 0; they will be written on first `generate()`.
78        Ok(Self { nonce, counter: 0 })
79    }
80
81    /// Return the next nonce and advance the counter.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`CryptoError::Internal`] if the counter has wrapped around
86    /// `u64::MAX` (i.e., `2^64` nonces have been generated).
87    pub fn generate(&mut self) -> Result<[u8; N], CryptoError> {
88        // Snapshot the current counter value for this nonce.
89        let current = self.counter;
90
91        // Advance, detecting overflow.
92        self.counter = self
93            .counter
94            .checked_add(1)
95            .ok_or(CryptoError::Internal("NonceSequence counter overflow"))?;
96
97        // Write the counter into the last 8 bytes of the nonce (big-endian).
98        let counter_bytes = current.to_be_bytes();
99        self.nonce[Self::PREFIX_LEN..].copy_from_slice(&counter_bytes);
100        Ok(self.nonce)
101    }
102
103    /// Return the current counter value (number of nonces generated so far).
104    #[must_use]
105    pub fn count(&self) -> u64 {
106        self.counter
107    }
108}
109
110/// 12-byte nonce sequence (4-byte prefix + 8-byte counter).
111///
112/// Suitable for AES-GCM and ChaCha20-Poly1305.
113pub type Nonce12 = NonceSequence<12>;
114
115/// 24-byte nonce sequence (16-byte prefix + 8-byte counter).
116///
117/// Suitable for XChaCha20-Poly1305.
118pub type Nonce24 = NonceSequence<24>;
119
120// ── Tests ─────────────────────────────────────────────────────────────────────
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn nonce12_sequential_uniqueness() {
128        let prefix = [0xABu8; 4]; // 4-byte prefix for 12-byte nonces
129        let mut seq = Nonce12::new(&prefix).expect("new");
130
131        let nonces: alloc::vec::Vec<[u8; 12]> =
132            (0..10).map(|_| seq.generate().expect("generate")).collect();
133
134        // All must be unique.
135        for i in 0..nonces.len() {
136            for j in i + 1..nonces.len() {
137                assert_ne!(nonces[i], nonces[j], "nonces[{i}] == nonces[{j}]");
138            }
139        }
140
141        // Prefix is preserved in all nonces.
142        for n in &nonces {
143            assert_eq!(&n[..4], &prefix);
144        }
145
146        // Counter bytes (last 8) must differ monotonically.
147        for (idx, n) in nonces.iter().enumerate() {
148            let counter = u64::from_be_bytes(n[4..].try_into().expect("slice"));
149            assert_eq!(counter, idx as u64, "counter byte mismatch at index {idx}");
150        }
151    }
152
153    #[test]
154    fn nonce24_sequential_uniqueness() {
155        let prefix = [0xCDu8; 16]; // 16-byte prefix for 24-byte nonces
156        let mut seq = Nonce24::new(&prefix).expect("new");
157
158        let n0 = seq.generate().expect("n0");
159        let n1 = seq.generate().expect("n1");
160        assert_ne!(n0, n1);
161        assert_eq!(&n0[..16], &prefix);
162        assert_eq!(&n1[..16], &prefix);
163    }
164
165    #[test]
166    fn nonce12_counter_overflow_detected() {
167        let prefix = [0u8; 4];
168        let mut seq = Nonce12::new(&prefix).expect("new");
169        // Force counter to u64::MAX - 1 to trigger overflow on the second call.
170        seq.counter = u64::MAX - 1;
171
172        // Should succeed (returns the nonce at counter = u64::MAX - 1).
173        seq.generate().expect("penultimate nonce");
174
175        // Counter is now u64::MAX; next generate() must detect overflow.
176        let result = seq.generate();
177        assert!(
178            matches!(result, Err(CryptoError::Internal(_))),
179            "should have detected overflow, got: {:?}",
180            result
181        );
182    }
183
184    #[test]
185    fn nonce12_wrong_prefix_length() {
186        let result = Nonce12::new(&[0u8; 5]);
187        assert!(
188            matches!(result, Err(CryptoError::InvalidNonce)),
189            "expected InvalidNonce, got: {:?}",
190            result.as_ref().err()
191        );
192    }
193
194    #[test]
195    fn nonce24_wrong_prefix_length() {
196        let result = Nonce24::new(&[0u8; 8]);
197        assert!(
198            matches!(result, Err(CryptoError::InvalidNonce)),
199            "expected InvalidNonce, got: {:?}",
200            result.as_ref().err()
201        );
202    }
203
204    #[test]
205    fn nonce12_count_tracks_generated() {
206        let prefix = [0u8; 4];
207        let mut seq = Nonce12::new(&prefix).expect("new");
208        assert_eq!(seq.count(), 0);
209        seq.generate().expect("generate 1");
210        assert_eq!(seq.count(), 1);
211        seq.generate().expect("generate 2");
212        assert_eq!(seq.count(), 2);
213    }
214}