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}