arcana/cipher/chacha20.rs
1//! ChaCha20 stream cipher (RFC 8439).
2//!
3//! This is the IETF / TLS 1.3 variant of ChaCha20: 256-bit key,
4//! 96-bit nonce, 32-bit block counter, 64-byte block size, 20 rounds.
5//!
6//! It is the second AEAD primitive shipped by `arcana`
7//! alongside AES-GCM. Used by TLS 1.3, Noise, Signal, WireGuard,
8//! QUIC, OpenSSH, and most modern protocols that prefer a constant-
9//! time stream cipher with no S-box dependencies (no cache-timing
10//! surface, in contrast to table-based AES).
11//!
12//! # Layout
13//!
14//! ```text
15//! state (4x4 u32 little-endian):
16//!
17//! constants constants constants constants "expand 32-byte k"
18//! key key key key
19//! key key key key
20//! counter nonce nonce nonce
21//! ```
22//!
23//! Each 64-byte block is computed as `serialize(rounds(state) + state)`.
24//! Successive blocks increment `counter`.
25//!
26//! # API
27//!
28//! ```rust,ignore
29//! use arcana::cipher::chacha20::ChaCha20;
30//!
31//! let mut cipher = ChaCha20::new(&key, &nonce, 1); // initial counter = 1
32//! let mut buf = b"plaintext".to_vec();
33//! cipher.apply_keystream(&mut buf); // encrypt or decrypt
34//! ```
35//!
36//! Stream ciphers are symmetric: `apply_keystream` does both
37//! encryption and decryption since the keystream is XOR'd with
38//! whatever is passed in.
39//!
40//! # Tests
41//!
42//! Pinned against RFC 8439 §2.3.2 (block test vector) and §2.4.2
43//! (encryption test vector).
44
45// ============================================================================
46// Quarter-round (RFC 8439 §2.1)
47// ============================================================================
48
49/// Single ChaCha20 quarter-round on 4 state words. Operates in place.
50///
51/// ```text
52/// a += b; d ^= a; d <<<= 16
53/// c += d; b ^= c; b <<<= 12
54/// a += b; d ^= a; d <<<= 8
55/// c += d; b ^= c; b <<<= 7
56/// ```
57#[inline(always)]
58pub(crate) fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
59 state[a] = state[a].wrapping_add(state[b]);
60 state[d] ^= state[a];
61 state[d] = state[d].rotate_left(16);
62
63 state[c] = state[c].wrapping_add(state[d]);
64 state[b] ^= state[c];
65 state[b] = state[b].rotate_left(12);
66
67 state[a] = state[a].wrapping_add(state[b]);
68 state[d] ^= state[a];
69 state[d] = state[d].rotate_left(8);
70
71 state[c] = state[c].wrapping_add(state[d]);
72 state[b] ^= state[c];
73 state[b] = state[b].rotate_left(7);
74}
75
76// ============================================================================
77// Block function (RFC 8439 §2.3)
78// ============================================================================
79
80/// Compute one 64-byte ChaCha20 keystream block from the initial state.
81///
82/// 20 rounds = 10 double-rounds; each double-round is 4 column rounds
83/// followed by 4 diagonal rounds. The output is `(working ⊞ initial)`
84/// serialized little-endian, where `⊞` is wrapping word-wise add.
85fn block(state: &[u32; 16]) -> [u8; 64] {
86 let mut working = *state;
87
88 for _ in 0..10 {
89 // Column round.
90 quarter_round(&mut working, 0, 4, 8, 12);
91 quarter_round(&mut working, 1, 5, 9, 13);
92 quarter_round(&mut working, 2, 6, 10, 14);
93 quarter_round(&mut working, 3, 7, 11, 15);
94 // Diagonal round.
95 quarter_round(&mut working, 0, 5, 10, 15);
96 quarter_round(&mut working, 1, 6, 11, 12);
97 quarter_round(&mut working, 2, 7, 8, 13);
98 quarter_round(&mut working, 3, 4, 9, 14);
99 }
100
101 // Output = working + state, serialized little-endian.
102 let mut out = [0u8; 64];
103 for i in 0..16 {
104 let word = working[i].wrapping_add(state[i]);
105 out[4 * i..4 * i + 4].copy_from_slice(&word.to_le_bytes());
106 }
107 out
108}
109
110// ============================================================================
111// Public API
112// ============================================================================
113
114/// ChaCha20 stream cipher state (RFC 8439).
115///
116/// Holds the initial state (key + nonce + counter) and the current
117/// counter value. Buffers the leftover bytes from the previous
118/// keystream block so partial calls to `apply_keystream` work
119/// correctly.
120#[derive(Clone)]
121pub struct ChaCha20 {
122 /// Initial 16-word state. The counter at index 12 is mutated
123 /// in place between blocks.
124 state: [u32; 16],
125 /// Current 64-byte keystream block, possibly partially consumed.
126 buffer: [u8; 64],
127 /// Number of bytes left in `buffer` (0..=64).
128 buf_pos: usize,
129}
130
131impl ChaCha20 {
132 /// Initialise a ChaCha20 cipher with a 32-byte key, a 12-byte
133 /// nonce, and an initial 32-bit block counter.
134 ///
135 /// Per RFC 8439 §2.4 the AEAD construction starts the cipher
136 /// at counter = 1 (counter = 0 produces the one-time Poly1305
137 /// key). Standalone ChaCha20 users typically start at counter
138 /// = 0 or 1 -- whatever their protocol specifies.
139 pub fn new(key: &[u8; 32], nonce: &[u8; 12], counter: u32) -> Self {
140 let mut state = [0u32; 16];
141 // Constants: "expand 32-byte k" as four little-endian u32.
142 state[0] = 0x6170_7865;
143 state[1] = 0x3320_646e;
144 state[2] = 0x7962_2d32;
145 state[3] = 0x6b20_6574;
146 // Key (8 words, LE).
147 for i in 0..8 {
148 state[4 + i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap());
149 }
150 // Counter (1 word).
151 state[12] = counter;
152 // Nonce (3 words, LE).
153 for i in 0..3 {
154 state[13 + i] = u32::from_le_bytes(nonce[4 * i..4 * i + 4].try_into().unwrap());
155 }
156
157 Self {
158 state,
159 buffer: [0u8; 64],
160 buf_pos: 64,
161 } // buf_pos = 64 means "empty"
162 }
163
164 /// XOR the keystream into `data` in place. Encrypts or decrypts
165 /// indifferently (the cipher is symmetric).
166 ///
167 /// Handles arbitrary lengths and partial blocks: the cipher
168 /// remembers leftover keystream bytes between calls, so
169 /// `cipher.apply_keystream(b"hi"); cipher.apply_keystream(b"!")`
170 /// produces the same output as one call with `b"hi!"`.
171 pub fn apply_keystream(&mut self, data: &mut [u8]) {
172 let mut pos = 0;
173 while pos < data.len() {
174 // Refill the buffer if it is empty.
175 if self.buf_pos == 64 {
176 self.buffer = block(&self.state);
177 // Advance the 32-bit block counter (RFC 8439 §2.3).
178 self.state[12] = self.state[12].wrapping_add(1);
179 self.buf_pos = 0;
180 }
181 let take = (64 - self.buf_pos).min(data.len() - pos);
182 for i in 0..take {
183 data[pos + i] ^= self.buffer[self.buf_pos + i];
184 }
185 self.buf_pos += take;
186 pos += take;
187 }
188 }
189}
190
191// ============================================================================
192// Tests (RFC 8439 pinned vectors)
193// ============================================================================
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn hex(s: &str) -> Vec<u8> {
200 assert!(s.len() % 2 == 0);
201 (0..s.len())
202 .step_by(2)
203 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
204 .collect()
205 }
206
207 fn hex_arr<const N: usize>(s: &str) -> [u8; N] {
208 let v = hex(s);
209 assert_eq!(v.len(), N);
210 let mut out = [0u8; N];
211 out.copy_from_slice(&v);
212 out
213 }
214
215 /// RFC 8439 §2.1.1 quarter-round example.
216 ///
217 /// Input: a=0x11111111, b=0x01020304, c=0x9b8d6f43, d=0x01234567
218 /// Output: a=0xea2a92f4, b=0xcb1cf8ce, c=0x4581472e, d=0x5881c4bb
219 #[test]
220 fn rfc8439_2_1_1_quarter_round() {
221 let mut s = [0u32; 16];
222 s[0] = 0x11111111;
223 s[1] = 0x01020304;
224 s[2] = 0x9b8d6f43;
225 s[3] = 0x01234567;
226 quarter_round(&mut s, 0, 1, 2, 3);
227 assert_eq!(s[0], 0xea2a92f4);
228 assert_eq!(s[1], 0xcb1cf8ce);
229 assert_eq!(s[2], 0x4581472e);
230 assert_eq!(s[3], 0x5881c4bb);
231 }
232
233 /// RFC 8439 §2.3.2 block-function test vector.
234 ///
235 /// Key: 00:01:02:..:1f
236 /// Nonce: 00:00:00:09:00:00:00:4a:00:00:00:00
237 /// Counter: 1
238 /// Output: 10f1e7e4d13b5915500fdd1fa32071c4
239 /// c7d1f4c733c068030422aa9ac3d46c4e
240 /// d2826446079faa0914c2d705d98b02a2
241 /// b5129cd1de164eb9cbd083e8a2503c4e
242 #[test]
243 fn rfc8439_2_3_2_block_vector() {
244 let key: [u8; 32] = hex_arr("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
245 let nonce: [u8; 12] = hex_arr("000000090000004a00000000");
246 let cipher = ChaCha20::new(&key, &nonce, 1);
247 let out = block(&cipher.state);
248 let expected = hex("10f1e7e4d13b5915500fdd1fa32071c4\
249 c7d1f4c733c068030422aa9ac3d46c4e\
250 d2826446079faa0914c2d705d98b02a2\
251 b5129cd1de164eb9cbd083e8a2503c4e");
252 assert_eq!(out.to_vec(), expected);
253 }
254
255 /// RFC 8439 §2.4.2 encryption test vector.
256 ///
257 /// Key: 00:01:02:..:1f
258 /// Nonce: 00:00:00:00:00:00:00:4a:00:00:00:00
259 /// Counter: 1
260 /// Plaintext: "Ladies and Gentlemen of the class of '99: ...
261 /// ...if I could offer you only one tip for the
262 /// future, sunscreen would be it."
263 /// Ciphertext: 6e2e359a2568f98041ba0728dd0d6981...
264 /// ...874d
265 #[test]
266 fn rfc8439_2_4_2_encryption_vector() {
267 let key: [u8; 32] = hex_arr("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
268 let nonce: [u8; 12] = hex_arr("000000000000004a00000000");
269 let plaintext = b"Ladies and Gentlemen of the class of '99: \
270 If I could offer you only one tip for the future, sunscreen \
271 would be it.";
272
273 let mut buf = plaintext.to_vec();
274 let mut cipher = ChaCha20::new(&key, &nonce, 1);
275 cipher.apply_keystream(&mut buf);
276
277 let expected = hex("6e2e359a2568f98041ba0728dd0d6981\
278 e97e7aec1d4360c20a27afccfd9fae0b\
279 f91b65c5524733ab8f593dabcd62b357\
280 1639d624e65152ab8f530c359f0861d8\
281 07ca0dbf500d6a6156a38e088a22b65e\
282 52bc514d16ccf806818ce91ab7793736\
283 5af90bbf74a35be6b40b8eedf2785e42\
284 874d");
285 assert_eq!(buf, expected);
286 }
287
288 /// Decryption is the same operation as encryption (XOR keystream).
289 /// Re-applying the keystream must recover the plaintext.
290 #[test]
291 fn chacha20_encrypt_decrypt_roundtrip() {
292 let key = [0x42u8; 32];
293 let nonce = [0xa5u8; 12];
294 let plaintext = b"the quick brown fox jumps over the lazy dog";
295 let mut buf = plaintext.to_vec();
296
297 let mut enc = ChaCha20::new(&key, &nonce, 1);
298 enc.apply_keystream(&mut buf);
299 assert_ne!(buf.as_slice(), plaintext);
300
301 let mut dec = ChaCha20::new(&key, &nonce, 1);
302 dec.apply_keystream(&mut buf);
303 assert_eq!(buf.as_slice(), plaintext);
304 }
305
306 /// Calling `apply_keystream` in chunks must give the same result
307 /// as one big call (proves the buffered partial-block path is
308 /// stateful and correct).
309 #[test]
310 fn chacha20_chunked_apply_matches_single() {
311 let key = [0x77u8; 32];
312 let nonce = [0x11u8; 12];
313 let plaintext: Vec<u8> = (0..200).map(|i| i as u8).collect();
314
315 // Single call.
316 let mut single = plaintext.clone();
317 ChaCha20::new(&key, &nonce, 1).apply_keystream(&mut single);
318
319 // Chunked at boundaries that span block edges (64).
320 let mut chunked = plaintext.clone();
321 let mut c = ChaCha20::new(&key, &nonce, 1);
322 c.apply_keystream(&mut chunked[..30]);
323 c.apply_keystream(&mut chunked[30..70]); // crosses block boundary
324 c.apply_keystream(&mut chunked[70..130]); // crosses again
325 c.apply_keystream(&mut chunked[130..]);
326
327 assert_eq!(chunked, single);
328 }
329
330 /// All-zero key + all-zero nonce + counter 0 yields a known
331 /// fixed first block (RFC 8439 §2.3.2 sanity).
332 #[test]
333 fn chacha20_zero_key_zero_nonce_block_zero() {
334 let key = [0u8; 32];
335 let nonce = [0u8; 12];
336 let cipher = ChaCha20::new(&key, &nonce, 0);
337 let out = block(&cipher.state);
338 // First 32 bytes from RFC 8439 Appendix A.1 test vector 1.
339 let expected_prefix = hex("76b8e0ada0f13d90405d6ae55386bd28\
340 bdd219b8a08ded1aa836efcc8b770dc7");
341 assert_eq!(&out[..32], expected_prefix.as_slice());
342 }
343}