we (web engine): Experimental web browser project to understand the limits of Claude
1//! ChaCha20-Poly1305 AEAD (RFC 8439).
2//!
3//! - ChaCha20 stream cipher: 256-bit key, 96-bit nonce, 32-bit counter
4//! - Poly1305 one-time MAC: GF(2^130 - 5)
5//! - AEAD construction: encrypt-then-MAC with padding and length encoding
6
7// ---------------------------------------------------------------------------
8// ChaCha20 quarter round (RFC 8439 §2.1)
9// ---------------------------------------------------------------------------
10
11fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
12 state[a] = state[a].wrapping_add(state[b]);
13 state[d] ^= state[a];
14 state[d] = state[d].rotate_left(16);
15
16 state[c] = state[c].wrapping_add(state[d]);
17 state[b] ^= state[c];
18 state[b] = state[b].rotate_left(12);
19
20 state[a] = state[a].wrapping_add(state[b]);
21 state[d] ^= state[a];
22 state[d] = state[d].rotate_left(8);
23
24 state[c] = state[c].wrapping_add(state[d]);
25 state[b] ^= state[c];
26 state[b] = state[b].rotate_left(7);
27}
28
29// ---------------------------------------------------------------------------
30// ChaCha20 block function (RFC 8439 §2.3)
31// ---------------------------------------------------------------------------
32
33/// The ChaCha20 constants: "expand 32-byte k" as little-endian u32s.
34const CONSTANTS: [u32; 4] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574];
35
36fn chacha20_block(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u8; 64] {
37 let mut state = [0u32; 16];
38
39 // Constants
40 state[0] = CONSTANTS[0];
41 state[1] = CONSTANTS[1];
42 state[2] = CONSTANTS[2];
43 state[3] = CONSTANTS[3];
44
45 // Key (8 words, little-endian)
46 for i in 0..8 {
47 state[4 + i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap());
48 }
49
50 // Counter
51 state[12] = counter;
52
53 // Nonce (3 words, little-endian)
54 for i in 0..3 {
55 state[13 + i] = u32::from_le_bytes(nonce[4 * i..4 * i + 4].try_into().unwrap());
56 }
57
58 let initial = state;
59
60 // 20 rounds (10 column rounds + 10 diagonal rounds)
61 for _ in 0..10 {
62 // Column rounds
63 quarter_round(&mut state, 0, 4, 8, 12);
64 quarter_round(&mut state, 1, 5, 9, 13);
65 quarter_round(&mut state, 2, 6, 10, 14);
66 quarter_round(&mut state, 3, 7, 11, 15);
67 // Diagonal rounds
68 quarter_round(&mut state, 0, 5, 10, 15);
69 quarter_round(&mut state, 1, 6, 11, 12);
70 quarter_round(&mut state, 2, 7, 8, 13);
71 quarter_round(&mut state, 3, 4, 9, 14);
72 }
73
74 // Add initial state
75 for i in 0..16 {
76 state[i] = state[i].wrapping_add(initial[i]);
77 }
78
79 // Serialize to bytes (little-endian)
80 let mut out = [0u8; 64];
81 for i in 0..16 {
82 out[4 * i..4 * i + 4].copy_from_slice(&state[i].to_le_bytes());
83 }
84 out
85}
86
87// ---------------------------------------------------------------------------
88// ChaCha20 encryption (RFC 8439 §2.4)
89// ---------------------------------------------------------------------------
90
91fn chacha20_encrypt(key: &[u8; 32], counter: u32, nonce: &[u8; 12], data: &[u8]) -> Vec<u8> {
92 let mut result = Vec::with_capacity(data.len());
93 let mut block_counter = counter;
94 let mut offset = 0;
95
96 while offset < data.len() {
97 let keystream = chacha20_block(key, block_counter, nonce);
98 let remaining = (data.len() - offset).min(64);
99 for i in 0..remaining {
100 result.push(data[offset + i] ^ keystream[i]);
101 }
102 offset += remaining;
103 block_counter = block_counter.wrapping_add(1);
104 }
105
106 result
107}
108
109// ---------------------------------------------------------------------------
110// Poly1305 MAC (RFC 8439 §2.5)
111// ---------------------------------------------------------------------------
112
113/// Poly1305 one-time authenticator.
114///
115/// Computes the MAC over `data` using the one-time key `key` (32 bytes).
116/// The key is split into `r` (clamped, first 16 bytes) and `s` (last 16 bytes).
117///
118/// All arithmetic is in GF(2^130 - 5) using 5 limbs of 26 bits each.
119fn poly1305_mac(key: &[u8; 32], data: &[u8]) -> [u8; 16] {
120 // Clamp r per RFC 8439 §2.5
121 let mut r_bytes = [0u8; 16];
122 r_bytes.copy_from_slice(&key[0..16]);
123 r_bytes[3] &= 15;
124 r_bytes[7] &= 15;
125 r_bytes[11] &= 15;
126 r_bytes[15] &= 15;
127 r_bytes[4] &= 252;
128 r_bytes[8] &= 252;
129 r_bytes[12] &= 252;
130
131 // r as 5 limbs of 26 bits
132 let r0 = (u32::from_le_bytes(r_bytes[0..4].try_into().unwrap())) & 0x3ffffff;
133 let r1 = (u32::from_le_bytes(r_bytes[3..7].try_into().unwrap()) >> 2) & 0x3ffffff;
134 let r2 = (u32::from_le_bytes(r_bytes[6..10].try_into().unwrap()) >> 4) & 0x3ffffff;
135 let r3 = (u32::from_le_bytes(r_bytes[9..13].try_into().unwrap()) >> 6) & 0x3ffffff;
136 let r4 = (u32::from_le_bytes(r_bytes[12..16].try_into().unwrap()) >> 8) & 0x3ffffff;
137
138 // s (second half of key)
139 let s = &key[16..32];
140
141 // Precompute 5*r[i] for reduction
142 let s1 = r1 * 5;
143 let s2 = r2 * 5;
144 let s3 = r3 * 5;
145 let s4 = r4 * 5;
146
147 // Accumulator h = 0
148 let mut h0: u32 = 0;
149 let mut h1: u32 = 0;
150 let mut h2: u32 = 0;
151 let mut h3: u32 = 0;
152 let mut h4: u32 = 0;
153
154 // Process message in 16-byte blocks
155 let mut offset = 0;
156 while offset < data.len() {
157 let remaining = data.len() - offset;
158 let block_len = remaining.min(16);
159
160 // Read block and add padding byte
161 let mut block = [0u8; 17];
162 block[..block_len].copy_from_slice(&data[offset..offset + block_len]);
163 block[block_len] = 1; // hibit
164
165 // Convert to 5 limbs of 26 bits
166 let t0 = u32::from_le_bytes(block[0..4].try_into().unwrap());
167 let t1 = u32::from_le_bytes(block[4..8].try_into().unwrap());
168 let t2 = u32::from_le_bytes(block[8..12].try_into().unwrap());
169 let t3 = u32::from_le_bytes(block[12..16].try_into().unwrap());
170 let hibit = block[16] as u32;
171
172 h0 = h0.wrapping_add(t0 & 0x3ffffff);
173 h1 = h1.wrapping_add(((t0 >> 26) | (t1 << 6)) & 0x3ffffff);
174 h2 = h2.wrapping_add(((t1 >> 20) | (t2 << 12)) & 0x3ffffff);
175 h3 = h3.wrapping_add(((t2 >> 14) | (t3 << 18)) & 0x3ffffff);
176 h4 = h4.wrapping_add((t3 >> 8) | (hibit << 24));
177
178 // h *= r (mod 2^130 - 5)
179 let d0 = (h0 as u64) * (r0 as u64)
180 + (h1 as u64) * (s4 as u64)
181 + (h2 as u64) * (s3 as u64)
182 + (h3 as u64) * (s2 as u64)
183 + (h4 as u64) * (s1 as u64);
184 let d1 = (h0 as u64) * (r1 as u64)
185 + (h1 as u64) * (r0 as u64)
186 + (h2 as u64) * (s4 as u64)
187 + (h3 as u64) * (s3 as u64)
188 + (h4 as u64) * (s2 as u64);
189 let d2 = (h0 as u64) * (r2 as u64)
190 + (h1 as u64) * (r1 as u64)
191 + (h2 as u64) * (r0 as u64)
192 + (h3 as u64) * (s4 as u64)
193 + (h4 as u64) * (s3 as u64);
194 let d3 = (h0 as u64) * (r3 as u64)
195 + (h1 as u64) * (r2 as u64)
196 + (h2 as u64) * (r1 as u64)
197 + (h3 as u64) * (r0 as u64)
198 + (h4 as u64) * (s4 as u64);
199 let d4 = (h0 as u64) * (r4 as u64)
200 + (h1 as u64) * (r3 as u64)
201 + (h2 as u64) * (r2 as u64)
202 + (h3 as u64) * (r1 as u64)
203 + (h4 as u64) * (r0 as u64);
204
205 // Partial reduction mod 2^130 - 5
206 let mut c: u32;
207 c = (d0 >> 26) as u32;
208 h0 = d0 as u32 & 0x3ffffff;
209 let d1 = d1 + c as u64;
210 c = (d1 >> 26) as u32;
211 h1 = d1 as u32 & 0x3ffffff;
212 let d2 = d2 + c as u64;
213 c = (d2 >> 26) as u32;
214 h2 = d2 as u32 & 0x3ffffff;
215 let d3 = d3 + c as u64;
216 c = (d3 >> 26) as u32;
217 h3 = d3 as u32 & 0x3ffffff;
218 let d4 = d4 + c as u64;
219 c = (d4 >> 26) as u32;
220 h4 = d4 as u32 & 0x3ffffff;
221 h0 = h0.wrapping_add(c * 5);
222 c = h0 >> 26;
223 h0 &= 0x3ffffff;
224 h1 = h1.wrapping_add(c);
225
226 offset += block_len;
227 }
228
229 // Final reduction: fully carry h
230 let mut c = h1 >> 26;
231 h1 &= 0x3ffffff;
232 h2 = h2.wrapping_add(c);
233 c = h2 >> 26;
234 h2 &= 0x3ffffff;
235 h3 = h3.wrapping_add(c);
236 c = h3 >> 26;
237 h3 &= 0x3ffffff;
238 h4 = h4.wrapping_add(c);
239 c = h4 >> 26;
240 h4 &= 0x3ffffff;
241 h0 = h0.wrapping_add(c * 5);
242 c = h0 >> 26;
243 h0 &= 0x3ffffff;
244 h1 = h1.wrapping_add(c);
245
246 // Compute h + -(2^130 - 5) = h - p
247 let mut g0 = h0.wrapping_add(5);
248 c = g0 >> 26;
249 g0 &= 0x3ffffff;
250 let mut g1 = h1.wrapping_add(c);
251 c = g1 >> 26;
252 g1 &= 0x3ffffff;
253 let mut g2 = h2.wrapping_add(c);
254 c = g2 >> 26;
255 g2 &= 0x3ffffff;
256 let mut g3 = h3.wrapping_add(c);
257 c = g3 >> 26;
258 g3 &= 0x3ffffff;
259 let g4 = h4.wrapping_add(c).wrapping_sub(1 << 26);
260
261 // Select h or g based on whether g overflowed (constant-time)
262 let mask = (g4 >> 31).wrapping_sub(1); // 0xffffffff if g4 >= 0, 0 if g4 < 0
263 g0 &= mask;
264 g1 &= mask;
265 g2 &= mask;
266 g3 &= mask;
267 let g4 = g4 & mask;
268 let nmask = !mask;
269 h0 = (h0 & nmask) | g0;
270 h1 = (h1 & nmask) | g1;
271 h2 = (h2 & nmask) | g2;
272 h3 = (h3 & nmask) | g3;
273 h4 = (h4 & nmask) | g4;
274
275 // Reassemble h as a 128-bit number (mod 2^128) and add s
276 let h_val: u128 = (h0 as u128)
277 | ((h1 as u128) << 26)
278 | ((h2 as u128) << 52)
279 | ((h3 as u128) << 78)
280 | ((h4 as u128) << 104);
281 let s_val = u128::from_le_bytes(s.try_into().unwrap());
282 let result = h_val.wrapping_add(s_val);
283 result.to_le_bytes()
284}
285
286// ---------------------------------------------------------------------------
287// Constant-time tag comparison
288// ---------------------------------------------------------------------------
289
290fn ct_eq_16(a: &[u8; 16], b: &[u8; 16]) -> bool {
291 let mut diff = 0u8;
292 for i in 0..16 {
293 diff |= a[i] ^ b[i];
294 }
295 diff == 0
296}
297
298// ---------------------------------------------------------------------------
299// ChaCha20-Poly1305 AEAD (RFC 8439 §2.8)
300// ---------------------------------------------------------------------------
301
302/// Construct the Poly1305 input per RFC 8439 §2.8:
303/// AAD ∥ pad(AAD) ∥ ciphertext ∥ pad(ciphertext) ∥ len(AAD) ∥ len(CT)
304fn build_mac_data(aad: &[u8], ciphertext: &[u8]) -> Vec<u8> {
305 let mut mac_data = Vec::new();
306
307 // AAD + padding to 16-byte boundary
308 mac_data.extend_from_slice(aad);
309 let pad_aad = (16 - (aad.len() % 16)) % 16;
310 mac_data.extend(std::iter::repeat_n(0u8, pad_aad));
311
312 // Ciphertext + padding to 16-byte boundary
313 mac_data.extend_from_slice(ciphertext);
314 let pad_ct = (16 - (ciphertext.len() % 16)) % 16;
315 mac_data.extend(std::iter::repeat_n(0u8, pad_ct));
316
317 // Lengths as 64-bit little-endian
318 mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
319 mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
320
321 mac_data
322}
323
324// ---------------------------------------------------------------------------
325// Public API
326// ---------------------------------------------------------------------------
327
328/// ChaCha20-Poly1305 AEAD encrypt (RFC 8439).
329///
330/// Returns `(ciphertext, 128-bit tag)`.
331pub fn chacha20_poly1305_encrypt(
332 key: &[u8; 32],
333 nonce: &[u8; 12],
334 plaintext: &[u8],
335 aad: &[u8],
336) -> (Vec<u8>, [u8; 16]) {
337 // Generate Poly1305 one-time key from first ChaCha20 block (counter = 0)
338 let otk_block = chacha20_block(key, 0, nonce);
339 let mut otk = [0u8; 32];
340 otk.copy_from_slice(&otk_block[0..32]);
341
342 // Encrypt plaintext with ChaCha20 (counter starts at 1)
343 let ciphertext = chacha20_encrypt(key, 1, nonce, plaintext);
344
345 // Compute tag
346 let mac_data = build_mac_data(aad, &ciphertext);
347 let tag = poly1305_mac(&otk, &mac_data);
348
349 (ciphertext, tag)
350}
351
352/// ChaCha20-Poly1305 AEAD decrypt (RFC 8439).
353///
354/// Returns `None` if the authentication tag is invalid.
355pub fn chacha20_poly1305_decrypt(
356 key: &[u8; 32],
357 nonce: &[u8; 12],
358 ciphertext: &[u8],
359 aad: &[u8],
360 tag: &[u8; 16],
361) -> Option<Vec<u8>> {
362 // Generate Poly1305 one-time key
363 let otk_block = chacha20_block(key, 0, nonce);
364 let mut otk = [0u8; 32];
365 otk.copy_from_slice(&otk_block[0..32]);
366
367 // Verify tag before decrypting
368 let mac_data = build_mac_data(aad, ciphertext);
369 let expected_tag = poly1305_mac(&otk, &mac_data);
370
371 if !ct_eq_16(&expected_tag, tag) {
372 return None;
373 }
374
375 // Decrypt ciphertext with ChaCha20 (counter starts at 1)
376 let plaintext = chacha20_encrypt(key, 1, nonce, ciphertext);
377 Some(plaintext)
378}
379
380// ---------------------------------------------------------------------------
381// Tests
382// ---------------------------------------------------------------------------
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 fn from_hex(s: &str) -> Vec<u8> {
389 (0..s.len())
390 .step_by(2)
391 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
392 .collect()
393 }
394
395 fn hex(bytes: &[u8]) -> String {
396 bytes.iter().map(|b| format!("{b:02x}")).collect()
397 }
398
399 // -----------------------------------------------------------------------
400 // RFC 8439 §2.1.1 — Quarter Round Test Vector
401 // -----------------------------------------------------------------------
402
403 #[test]
404 fn quarter_round_test_vector() {
405 let mut state = [0u32; 16];
406 state[0] = 0x11111111;
407 state[1] = 0x01020304;
408 state[2] = 0x9b8d6f43;
409 state[3] = 0x01234567;
410 // Apply quarter round to indices 0,1,2,3 (need a 16-element state)
411 quarter_round(&mut state, 0, 1, 2, 3);
412 assert_eq!(state[0], 0xea2a92f4);
413 assert_eq!(state[1], 0xcb1cf8ce);
414 assert_eq!(state[2], 0x4581472e);
415 assert_eq!(state[3], 0x5881c4bb);
416 }
417
418 // -----------------------------------------------------------------------
419 // RFC 8439 §2.3.2 — ChaCha20 Block Function Test Vector
420 // -----------------------------------------------------------------------
421
422 #[test]
423 fn chacha20_block_test_vector() {
424 let key: [u8; 32] = from_hex(
425 "000102030405060708090a0b0c0d0e0f\
426 101112131415161718191a1b1c1d1e1f",
427 )
428 .try_into()
429 .unwrap();
430 let nonce: [u8; 12] = from_hex("000000090000004a00000000").try_into().unwrap();
431 let counter: u32 = 1;
432
433 let block = chacha20_block(&key, counter, &nonce);
434
435 let expected = from_hex(
436 "10f1e7e4d13b5915500fdd1fa32071c4\
437 c7d1f4c733c068030422aa9ac3d46c4e\
438 d2826446079faa0914c2d705d98b02a2\
439 b5129cd1de164eb9cbd083e8a2503c4e",
440 );
441 assert_eq!(block.to_vec(), expected);
442 }
443
444 // -----------------------------------------------------------------------
445 // RFC 8439 §2.4.2 — ChaCha20 Encryption Test Vector
446 // -----------------------------------------------------------------------
447
448 #[test]
449 fn chacha20_encryption_test_vector() {
450 let key: [u8; 32] = from_hex(
451 "000102030405060708090a0b0c0d0e0f\
452 101112131415161718191a1b1c1d1e1f",
453 )
454 .try_into()
455 .unwrap();
456 let nonce: [u8; 12] = from_hex("000000000000004a00000000").try_into().unwrap();
457 let counter: u32 = 1;
458
459 let plaintext = b"Ladies and Gentlemen of the class of '99: \
460If I could offer you only one tip for the future, sunscreen would be it.";
461
462 let ciphertext = chacha20_encrypt(&key, counter, &nonce, plaintext);
463
464 let expected = from_hex(
465 "6e2e359a2568f98041ba0728dd0d6981\
466 e97e7aec1d4360c20a27afccfd9fae0b\
467 f91b65c5524733ab8f593dabcd62b357\
468 1639d624e65152ab8f530c359f0861d8\
469 07ca0dbf500d6a6156a38e088a22b65e\
470 52bc514d16ccf806818ce91ab7793736\
471 5af90bbf74a35be6b40b8eedf2785e42\
472 874d",
473 );
474 assert_eq!(hex(&ciphertext), hex(&expected));
475 }
476
477 // -----------------------------------------------------------------------
478 // RFC 8439 §2.5.2 — Poly1305 MAC Test Vector
479 // -----------------------------------------------------------------------
480
481 #[test]
482 fn poly1305_mac_test_vector() {
483 let key: [u8; 32] = from_hex(
484 "85d6be7857556d337f4452fe42d506a8\
485 0103808afb0db2fd4abff6af4149f51b",
486 )
487 .try_into()
488 .unwrap();
489 let msg = b"Cryptographic Forum Research Group";
490 let tag = poly1305_mac(&key, msg);
491 assert_eq!(hex(&tag), "a8061dc1305136c6c22b8baf0c0127a9");
492 }
493
494 // -----------------------------------------------------------------------
495 // RFC 8439 §2.6.2 — Poly1305 Key Generation Test Vector
496 // -----------------------------------------------------------------------
497
498 #[test]
499 fn poly1305_key_generation_test_vector() {
500 let key: [u8; 32] = from_hex(
501 "808182838485868788898a8b8c8d8e8f\
502 909192939495969798999a9b9c9d9e9f",
503 )
504 .try_into()
505 .unwrap();
506 let nonce: [u8; 12] = from_hex("000000000001020304050607").try_into().unwrap();
507
508 let otk_block = chacha20_block(&key, 0, &nonce);
509 let otk = &otk_block[0..32];
510
511 assert_eq!(
512 hex(otk),
513 "8ad5a08b905f81cc815040274ab29471\
514 a833b637e3fd0da508dbb8e2fdd1a646"
515 );
516 }
517
518 // -----------------------------------------------------------------------
519 // RFC 8439 §2.8.2 — AEAD Test Vector
520 // -----------------------------------------------------------------------
521
522 #[test]
523 fn aead_encrypt_test_vector() {
524 let key: [u8; 32] = from_hex(
525 "808182838485868788898a8b8c8d8e8f\
526 909192939495969798999a9b9c9d9e9f",
527 )
528 .try_into()
529 .unwrap();
530 let nonce: [u8; 12] = from_hex("070000004041424344454647").try_into().unwrap();
531 let aad = from_hex("50515253c0c1c2c3c4c5c6c7");
532 let plaintext = b"Ladies and Gentlemen of the class of '99: \
533If I could offer you only one tip for the future, sunscreen would be it.";
534
535 let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, plaintext, &aad);
536
537 let expected_ct = from_hex(
538 "d31a8d34648e60db7b86afbc53ef7ec2\
539 a4aded51296e08fea9e2b5a736ee62d6\
540 3dbea45e8ca9671282fafb69da92728b\
541 1a71de0a9e060b2905d6a5b67ecd3b36\
542 92ddbd7f2d778b8c9803aee328091b58\
543 fab324e4fad675945585808b4831d7bc\
544 3ff4def08e4b7a9de576d26586cec64b\
545 6116",
546 );
547 assert_eq!(hex(&ct), hex(&expected_ct));
548 assert_eq!(hex(&tag), "1ae10b594f09e26a7e902ecbd0600691");
549 }
550
551 #[test]
552 fn aead_decrypt_test_vector() {
553 let key: [u8; 32] = from_hex(
554 "808182838485868788898a8b8c8d8e8f\
555 909192939495969798999a9b9c9d9e9f",
556 )
557 .try_into()
558 .unwrap();
559 let nonce: [u8; 12] = from_hex("070000004041424344454647").try_into().unwrap();
560 let aad = from_hex("50515253c0c1c2c3c4c5c6c7");
561 let ct = from_hex(
562 "d31a8d34648e60db7b86afbc53ef7ec2\
563 a4aded51296e08fea9e2b5a736ee62d6\
564 3dbea45e8ca9671282fafb69da92728b\
565 1a71de0a9e060b2905d6a5b67ecd3b36\
566 92ddbd7f2d778b8c9803aee328091b58\
567 fab324e4fad675945585808b4831d7bc\
568 3ff4def08e4b7a9de576d26586cec64b\
569 6116",
570 );
571 let tag: [u8; 16] = from_hex("1ae10b594f09e26a7e902ecbd0600691")
572 .try_into()
573 .unwrap();
574
575 let pt = chacha20_poly1305_decrypt(&key, &nonce, &ct, &aad, &tag).unwrap();
576 assert_eq!(
577 pt,
578 b"Ladies and Gentlemen of the class of '99: \
579If I could offer you only one tip for the future, sunscreen would be it."
580 .to_vec()
581 );
582 }
583
584 // -----------------------------------------------------------------------
585 // Tag tamper detection
586 // -----------------------------------------------------------------------
587
588 #[test]
589 fn decrypt_rejects_tampered_tag() {
590 let key = [0x42u8; 32];
591 let nonce = [0u8; 12];
592 let (ct, mut tag) = chacha20_poly1305_encrypt(&key, &nonce, b"hello", &[]);
593 tag[0] ^= 1;
594 assert!(chacha20_poly1305_decrypt(&key, &nonce, &ct, &[], &tag).is_none());
595 }
596
597 #[test]
598 fn decrypt_rejects_tampered_ciphertext() {
599 let key = [0x42u8; 32];
600 let nonce = [0u8; 12];
601 let (mut ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, b"hello", &[]);
602 ct[0] ^= 1;
603 assert!(chacha20_poly1305_decrypt(&key, &nonce, &ct, &[], &tag).is_none());
604 }
605
606 #[test]
607 fn decrypt_rejects_tampered_aad() {
608 let key = [0x42u8; 32];
609 let nonce = [0u8; 12];
610 let aad = b"metadata";
611 let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, b"hello", aad);
612 let bad_aad = b"Metadata";
613 assert!(chacha20_poly1305_decrypt(&key, &nonce, &ct, bad_aad, &tag).is_none());
614 }
615
616 // -----------------------------------------------------------------------
617 // Round-trip
618 // -----------------------------------------------------------------------
619
620 #[test]
621 fn roundtrip_empty_plaintext() {
622 let key = [0xabu8; 32];
623 let nonce = [0x01u8; 12];
624 let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, &[], &[]);
625 assert!(ct.is_empty());
626 let pt = chacha20_poly1305_decrypt(&key, &nonce, &ct, &[], &tag).unwrap();
627 assert!(pt.is_empty());
628 }
629
630 #[test]
631 fn roundtrip_with_aad() {
632 let key: [u8; 32] = from_hex(
633 "deadbeefdeadbeefdeadbeefdeadbeef\
634 deadbeefdeadbeefdeadbeefdeadbeef",
635 )
636 .try_into()
637 .unwrap();
638 let nonce: [u8; 12] = from_hex("0102030405060708090a0b0c").try_into().unwrap();
639 let pt = b"The quick brown fox jumps over the lazy dog";
640 let aad = b"additional authenticated data";
641
642 let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, pt, aad);
643 let recovered = chacha20_poly1305_decrypt(&key, &nonce, &ct, aad, &tag).unwrap();
644 assert_eq!(recovered, pt.to_vec());
645 }
646
647 #[test]
648 fn roundtrip_large_message() {
649 let key = [0xffu8; 32];
650 let nonce = [0u8; 12];
651 let pt: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
652 let aad = b"large message test";
653
654 let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, &pt, aad);
655 let recovered = chacha20_poly1305_decrypt(&key, &nonce, &ct, aad, &tag).unwrap();
656 assert_eq!(recovered, pt);
657 }
658}