From d548444d3613c2e0fe11db89f2cd27c48cf3f30c Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sat, 7 Mar 2026 09:39:52 +0100 Subject: [PATCH] Implement ChaCha20-Poly1305 AEAD (RFC 8439) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add pure Rust ChaCha20-Poly1305 authenticated encryption in the crypto crate: - ChaCha20 stream cipher: quarter round, block function (20 rounds), counter-mode encryption with 256-bit key, 96-bit nonce, 32-bit counter - Poly1305 one-time MAC: GF(2^130-5) using 5 limbs of 26 bits, clamped r key, constant-time final reduction - AEAD construction: Poly1305 key generation from ChaCha20 block 0, encrypt-then-MAC with RFC 8439 padding and length encoding - Constant-time tag comparison for secure decryption - 13 tests: RFC 8439 test vectors (§2.1.1, §2.3.2, §2.4.2, §2.5.2, §2.6.2, §2.8.2), tag tamper detection, and round-trip tests Co-Authored-By: Claude Opus 4.6 --- crates/crypto/src/chacha20_poly1305.rs | 656 +++++++++++++++++++++++++ crates/crypto/src/lib.rs | 1 + 2 files changed, 657 insertions(+) create mode 100644 crates/crypto/src/chacha20_poly1305.rs diff --git a/crates/crypto/src/chacha20_poly1305.rs b/crates/crypto/src/chacha20_poly1305.rs new file mode 100644 index 0000000..28ed6af --- /dev/null +++ b/crates/crypto/src/chacha20_poly1305.rs @@ -0,0 +1,656 @@ +//! ChaCha20-Poly1305 AEAD (RFC 8439). +//! +//! - ChaCha20 stream cipher: 256-bit key, 96-bit nonce, 32-bit counter +//! - Poly1305 one-time MAC: GF(2^130 - 5) +//! - AEAD construction: encrypt-then-MAC with padding and length encoding + +// --------------------------------------------------------------------------- +// ChaCha20 quarter round (RFC 8439 §2.1) +// --------------------------------------------------------------------------- + +fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) { + state[a] = state[a].wrapping_add(state[b]); + state[d] ^= state[a]; + state[d] = state[d].rotate_left(16); + + state[c] = state[c].wrapping_add(state[d]); + state[b] ^= state[c]; + state[b] = state[b].rotate_left(12); + + state[a] = state[a].wrapping_add(state[b]); + state[d] ^= state[a]; + state[d] = state[d].rotate_left(8); + + state[c] = state[c].wrapping_add(state[d]); + state[b] ^= state[c]; + state[b] = state[b].rotate_left(7); +} + +// --------------------------------------------------------------------------- +// ChaCha20 block function (RFC 8439 §2.3) +// --------------------------------------------------------------------------- + +/// The ChaCha20 constants: "expand 32-byte k" as little-endian u32s. +const CONSTANTS: [u32; 4] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; + +fn chacha20_block(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u8; 64] { + let mut state = [0u32; 16]; + + // Constants + state[0] = CONSTANTS[0]; + state[1] = CONSTANTS[1]; + state[2] = CONSTANTS[2]; + state[3] = CONSTANTS[3]; + + // Key (8 words, little-endian) + for i in 0..8 { + state[4 + i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + + // Counter + state[12] = counter; + + // Nonce (3 words, little-endian) + for i in 0..3 { + state[13 + i] = u32::from_le_bytes(nonce[4 * i..4 * i + 4].try_into().unwrap()); + } + + let initial = state; + + // 20 rounds (10 column rounds + 10 diagonal rounds) + for _ in 0..10 { + // Column rounds + quarter_round(&mut state, 0, 4, 8, 12); + quarter_round(&mut state, 1, 5, 9, 13); + quarter_round(&mut state, 2, 6, 10, 14); + quarter_round(&mut state, 3, 7, 11, 15); + // Diagonal rounds + quarter_round(&mut state, 0, 5, 10, 15); + quarter_round(&mut state, 1, 6, 11, 12); + quarter_round(&mut state, 2, 7, 8, 13); + quarter_round(&mut state, 3, 4, 9, 14); + } + + // Add initial state + for i in 0..16 { + state[i] = state[i].wrapping_add(initial[i]); + } + + // Serialize to bytes (little-endian) + let mut out = [0u8; 64]; + for i in 0..16 { + out[4 * i..4 * i + 4].copy_from_slice(&state[i].to_le_bytes()); + } + out +} + +// --------------------------------------------------------------------------- +// ChaCha20 encryption (RFC 8439 §2.4) +// --------------------------------------------------------------------------- + +fn chacha20_encrypt(key: &[u8; 32], counter: u32, nonce: &[u8; 12], data: &[u8]) -> Vec { + let mut result = Vec::with_capacity(data.len()); + let mut block_counter = counter; + let mut offset = 0; + + while offset < data.len() { + let keystream = chacha20_block(key, block_counter, nonce); + let remaining = (data.len() - offset).min(64); + for i in 0..remaining { + result.push(data[offset + i] ^ keystream[i]); + } + offset += remaining; + block_counter = block_counter.wrapping_add(1); + } + + result +} + +// --------------------------------------------------------------------------- +// Poly1305 MAC (RFC 8439 §2.5) +// --------------------------------------------------------------------------- + +/// Poly1305 one-time authenticator. +/// +/// Computes the MAC over `data` using the one-time key `key` (32 bytes). +/// The key is split into `r` (clamped, first 16 bytes) and `s` (last 16 bytes). +/// +/// All arithmetic is in GF(2^130 - 5) using 5 limbs of 26 bits each. +fn poly1305_mac(key: &[u8; 32], data: &[u8]) -> [u8; 16] { + // Clamp r per RFC 8439 §2.5 + let mut r_bytes = [0u8; 16]; + r_bytes.copy_from_slice(&key[0..16]); + r_bytes[3] &= 15; + r_bytes[7] &= 15; + r_bytes[11] &= 15; + r_bytes[15] &= 15; + r_bytes[4] &= 252; + r_bytes[8] &= 252; + r_bytes[12] &= 252; + + // r as 5 limbs of 26 bits + let r0 = (u32::from_le_bytes(r_bytes[0..4].try_into().unwrap())) & 0x3ffffff; + let r1 = (u32::from_le_bytes(r_bytes[3..7].try_into().unwrap()) >> 2) & 0x3ffffff; + let r2 = (u32::from_le_bytes(r_bytes[6..10].try_into().unwrap()) >> 4) & 0x3ffffff; + let r3 = (u32::from_le_bytes(r_bytes[9..13].try_into().unwrap()) >> 6) & 0x3ffffff; + let r4 = (u32::from_le_bytes(r_bytes[12..16].try_into().unwrap()) >> 8) & 0x3ffffff; + + // s (second half of key) + let s = &key[16..32]; + + // Precompute 5*r[i] for reduction + let s1 = r1 * 5; + let s2 = r2 * 5; + let s3 = r3 * 5; + let s4 = r4 * 5; + + // Accumulator h = 0 + let mut h0: u32 = 0; + let mut h1: u32 = 0; + let mut h2: u32 = 0; + let mut h3: u32 = 0; + let mut h4: u32 = 0; + + // Process message in 16-byte blocks + let mut offset = 0; + while offset < data.len() { + let remaining = data.len() - offset; + let block_len = remaining.min(16); + + // Read block and add padding byte + let mut block = [0u8; 17]; + block[..block_len].copy_from_slice(&data[offset..offset + block_len]); + block[block_len] = 1; // hibit + + // Convert to 5 limbs of 26 bits + let t0 = u32::from_le_bytes(block[0..4].try_into().unwrap()); + let t1 = u32::from_le_bytes(block[4..8].try_into().unwrap()); + let t2 = u32::from_le_bytes(block[8..12].try_into().unwrap()); + let t3 = u32::from_le_bytes(block[12..16].try_into().unwrap()); + let hibit = block[16] as u32; + + h0 = h0.wrapping_add(t0 & 0x3ffffff); + h1 = h1.wrapping_add(((t0 >> 26) | (t1 << 6)) & 0x3ffffff); + h2 = h2.wrapping_add(((t1 >> 20) | (t2 << 12)) & 0x3ffffff); + h3 = h3.wrapping_add(((t2 >> 14) | (t3 << 18)) & 0x3ffffff); + h4 = h4.wrapping_add((t3 >> 8) | (hibit << 24)); + + // h *= r (mod 2^130 - 5) + let d0 = (h0 as u64) * (r0 as u64) + + (h1 as u64) * (s4 as u64) + + (h2 as u64) * (s3 as u64) + + (h3 as u64) * (s2 as u64) + + (h4 as u64) * (s1 as u64); + let d1 = (h0 as u64) * (r1 as u64) + + (h1 as u64) * (r0 as u64) + + (h2 as u64) * (s4 as u64) + + (h3 as u64) * (s3 as u64) + + (h4 as u64) * (s2 as u64); + let d2 = (h0 as u64) * (r2 as u64) + + (h1 as u64) * (r1 as u64) + + (h2 as u64) * (r0 as u64) + + (h3 as u64) * (s4 as u64) + + (h4 as u64) * (s3 as u64); + let d3 = (h0 as u64) * (r3 as u64) + + (h1 as u64) * (r2 as u64) + + (h2 as u64) * (r1 as u64) + + (h3 as u64) * (r0 as u64) + + (h4 as u64) * (s4 as u64); + let d4 = (h0 as u64) * (r4 as u64) + + (h1 as u64) * (r3 as u64) + + (h2 as u64) * (r2 as u64) + + (h3 as u64) * (r1 as u64) + + (h4 as u64) * (r0 as u64); + + // Partial reduction mod 2^130 - 5 + let mut c: u32; + c = (d0 >> 26) as u32; + h0 = d0 as u32 & 0x3ffffff; + let d1 = d1 + c as u64; + c = (d1 >> 26) as u32; + h1 = d1 as u32 & 0x3ffffff; + let d2 = d2 + c as u64; + c = (d2 >> 26) as u32; + h2 = d2 as u32 & 0x3ffffff; + let d3 = d3 + c as u64; + c = (d3 >> 26) as u32; + h3 = d3 as u32 & 0x3ffffff; + let d4 = d4 + c as u64; + c = (d4 >> 26) as u32; + h4 = d4 as u32 & 0x3ffffff; + h0 = h0.wrapping_add(c * 5); + c = h0 >> 26; + h0 &= 0x3ffffff; + h1 = h1.wrapping_add(c); + + offset += block_len; + } + + // Final reduction: fully carry h + let mut c = h1 >> 26; + h1 &= 0x3ffffff; + h2 = h2.wrapping_add(c); + c = h2 >> 26; + h2 &= 0x3ffffff; + h3 = h3.wrapping_add(c); + c = h3 >> 26; + h3 &= 0x3ffffff; + h4 = h4.wrapping_add(c); + c = h4 >> 26; + h4 &= 0x3ffffff; + h0 = h0.wrapping_add(c * 5); + c = h0 >> 26; + h0 &= 0x3ffffff; + h1 = h1.wrapping_add(c); + + // Compute h + -(2^130 - 5) = h - p + let mut g0 = h0.wrapping_add(5); + c = g0 >> 26; + g0 &= 0x3ffffff; + let mut g1 = h1.wrapping_add(c); + c = g1 >> 26; + g1 &= 0x3ffffff; + let mut g2 = h2.wrapping_add(c); + c = g2 >> 26; + g2 &= 0x3ffffff; + let mut g3 = h3.wrapping_add(c); + c = g3 >> 26; + g3 &= 0x3ffffff; + let g4 = h4.wrapping_add(c).wrapping_sub(1 << 26); + + // Select h or g based on whether g overflowed (constant-time) + let mask = (g4 >> 31).wrapping_sub(1); // 0xffffffff if g4 >= 0, 0 if g4 < 0 + g0 &= mask; + g1 &= mask; + g2 &= mask; + g3 &= mask; + let nmask = !mask; + h0 = (h0 & nmask) | g0; + h1 = (h1 & nmask) | g1; + h2 = (h2 & nmask) | g2; + h3 = (h3 & nmask) | g3; + + // Reassemble h as a 128-bit number (mod 2^128) and add s + let h_val: u128 = (h0 as u128) + | ((h1 as u128) << 26) + | ((h2 as u128) << 52) + | ((h3 as u128) << 78) + | ((h4 as u128) << 104); + let s_val = u128::from_le_bytes(s.try_into().unwrap()); + let result = h_val.wrapping_add(s_val); + result.to_le_bytes() +} + +// --------------------------------------------------------------------------- +// Constant-time tag comparison +// --------------------------------------------------------------------------- + +fn ct_eq_16(a: &[u8; 16], b: &[u8; 16]) -> bool { + let mut diff = 0u8; + for i in 0..16 { + diff |= a[i] ^ b[i]; + } + diff == 0 +} + +// --------------------------------------------------------------------------- +// ChaCha20-Poly1305 AEAD (RFC 8439 §2.8) +// --------------------------------------------------------------------------- + +/// Construct the Poly1305 input per RFC 8439 §2.8: +/// AAD ∥ pad(AAD) ∥ ciphertext ∥ pad(ciphertext) ∥ len(AAD) ∥ len(CT) +fn build_mac_data(aad: &[u8], ciphertext: &[u8]) -> Vec { + let mut mac_data = Vec::new(); + + // AAD + padding to 16-byte boundary + mac_data.extend_from_slice(aad); + let pad_aad = (16 - (aad.len() % 16)) % 16; + mac_data.extend(std::iter::repeat_n(0u8, pad_aad)); + + // Ciphertext + padding to 16-byte boundary + mac_data.extend_from_slice(ciphertext); + let pad_ct = (16 - (ciphertext.len() % 16)) % 16; + mac_data.extend(std::iter::repeat_n(0u8, pad_ct)); + + // Lengths as 64-bit little-endian + mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes()); + mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes()); + + mac_data +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// ChaCha20-Poly1305 AEAD encrypt (RFC 8439). +/// +/// Returns `(ciphertext, 128-bit tag)`. +pub fn chacha20_poly1305_encrypt( + key: &[u8; 32], + nonce: &[u8; 12], + plaintext: &[u8], + aad: &[u8], +) -> (Vec, [u8; 16]) { + // Generate Poly1305 one-time key from first ChaCha20 block (counter = 0) + let otk_block = chacha20_block(key, 0, nonce); + let mut otk = [0u8; 32]; + otk.copy_from_slice(&otk_block[0..32]); + + // Encrypt plaintext with ChaCha20 (counter starts at 1) + let ciphertext = chacha20_encrypt(key, 1, nonce, plaintext); + + // Compute tag + let mac_data = build_mac_data(aad, &ciphertext); + let tag = poly1305_mac(&otk, &mac_data); + + (ciphertext, tag) +} + +/// ChaCha20-Poly1305 AEAD decrypt (RFC 8439). +/// +/// Returns `None` if the authentication tag is invalid. +pub fn chacha20_poly1305_decrypt( + key: &[u8; 32], + nonce: &[u8; 12], + ciphertext: &[u8], + aad: &[u8], + tag: &[u8; 16], +) -> Option> { + // Generate Poly1305 one-time key + let otk_block = chacha20_block(key, 0, nonce); + let mut otk = [0u8; 32]; + otk.copy_from_slice(&otk_block[0..32]); + + // Verify tag before decrypting + let mac_data = build_mac_data(aad, ciphertext); + let expected_tag = poly1305_mac(&otk, &mac_data); + + if !ct_eq_16(&expected_tag, tag) { + return None; + } + + // Decrypt ciphertext with ChaCha20 (counter starts at 1) + let plaintext = chacha20_encrypt(key, 1, nonce, ciphertext); + Some(plaintext) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn from_hex(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() + } + + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() + } + + // ----------------------------------------------------------------------- + // RFC 8439 §2.1.1 — Quarter Round Test Vector + // ----------------------------------------------------------------------- + + #[test] + fn quarter_round_test_vector() { + let mut state = [0u32; 16]; + state[0] = 0x11111111; + state[1] = 0x01020304; + state[2] = 0x9b8d6f43; + state[3] = 0x01234567; + // Apply quarter round to indices 0,1,2,3 (need a 16-element state) + quarter_round(&mut state, 0, 1, 2, 3); + assert_eq!(state[0], 0xea2a92f4); + assert_eq!(state[1], 0xcb1cf8ce); + assert_eq!(state[2], 0x4581472e); + assert_eq!(state[3], 0x5881c4bb); + } + + // ----------------------------------------------------------------------- + // RFC 8439 §2.3.2 — ChaCha20 Block Function Test Vector + // ----------------------------------------------------------------------- + + #[test] + fn chacha20_block_test_vector() { + let key: [u8; 32] = from_hex( + "000102030405060708090a0b0c0d0e0f\ + 101112131415161718191a1b1c1d1e1f", + ) + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("000000090000004a00000000").try_into().unwrap(); + let counter: u32 = 1; + + let block = chacha20_block(&key, counter, &nonce); + + let expected = from_hex( + "10f1e7e4d13b5915500fdd1fa32071c4\ + c7d1f4c733c068030422aa9ac3d46c4e\ + d2826446079faa0914c2d705d98b02a2\ + b5129cd1de164eb9cbd083e8a2503c4e", + ); + assert_eq!(block.to_vec(), expected); + } + + // ----------------------------------------------------------------------- + // RFC 8439 §2.4.2 — ChaCha20 Encryption Test Vector + // ----------------------------------------------------------------------- + + #[test] + fn chacha20_encryption_test_vector() { + let key: [u8; 32] = from_hex( + "000102030405060708090a0b0c0d0e0f\ + 101112131415161718191a1b1c1d1e1f", + ) + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("000000000000004a00000000").try_into().unwrap(); + let counter: u32 = 1; + + let plaintext = b"Ladies and Gentlemen of the class of '99: \ +If I could offer you only one tip for the future, sunscreen would be it."; + + let ciphertext = chacha20_encrypt(&key, counter, &nonce, plaintext); + + let expected = from_hex( + "6e2e359a2568f98041ba0728dd0d6981\ + e97e7aec1d4360c20a27afccfd9fae0b\ + f91b65c5524733ab8f593dabcd62b357\ + 1639d624e65152ab8f530c359f0861d8\ + 07ca0dbf500d6a6156a38e088a22b65e\ + 52bc514d16ccf806818ce91ab7793736\ + 5af90bbf74a35be6b40b8eedf2785e42\ + 874d", + ); + assert_eq!(hex(&ciphertext), hex(&expected)); + } + + // ----------------------------------------------------------------------- + // RFC 8439 §2.5.2 — Poly1305 MAC Test Vector + // ----------------------------------------------------------------------- + + #[test] + fn poly1305_mac_test_vector() { + let key: [u8; 32] = from_hex( + "85d6be7857556d337f4452fe42d506a8\ + 0103808afb0db2fd4abff6af4149f51b", + ) + .try_into() + .unwrap(); + let msg = b"Cryptographic Forum Research Group"; + let tag = poly1305_mac(&key, msg); + assert_eq!(hex(&tag), "a8061dc1305136c6c22b8baf0c0127a9"); + } + + // ----------------------------------------------------------------------- + // RFC 8439 §2.6.2 — Poly1305 Key Generation Test Vector + // ----------------------------------------------------------------------- + + #[test] + fn poly1305_key_generation_test_vector() { + let key: [u8; 32] = from_hex( + "808182838485868788898a8b8c8d8e8f\ + 909192939495969798999a9b9c9d9e9f", + ) + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("000000000001020304050607").try_into().unwrap(); + + let otk_block = chacha20_block(&key, 0, &nonce); + let otk = &otk_block[0..32]; + + assert_eq!( + hex(otk), + "8ad5a08b905f81cc815040274ab29471\ + a833b637e3fd0da508dbb8e2fdd1a646" + ); + } + + // ----------------------------------------------------------------------- + // RFC 8439 §2.8.2 — AEAD Test Vector + // ----------------------------------------------------------------------- + + #[test] + fn aead_encrypt_test_vector() { + let key: [u8; 32] = from_hex( + "808182838485868788898a8b8c8d8e8f\ + 909192939495969798999a9b9c9d9e9f", + ) + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("070000004041424344454647").try_into().unwrap(); + let aad = from_hex("50515253c0c1c2c3c4c5c6c7"); + let plaintext = b"Ladies and Gentlemen of the class of '99: \ +If I could offer you only one tip for the future, sunscreen would be it."; + + let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, plaintext, &aad); + + let expected_ct = from_hex( + "d31a8d34648e60db7b86afbc53ef7ec2\ + a4aded51296e08fea9e2b5a736ee62d6\ + 3dbea45e8ca9671282fafb69da92728b\ + 1a71de0a9e060b2905d6a5b67ecd3b36\ + 92ddbd7f2d778b8c9803aee328091b58\ + fab324e4fad675945585808b4831d7bc\ + 3ff4def08e4b7a9de576d26586cec64b\ + 6116", + ); + assert_eq!(hex(&ct), hex(&expected_ct)); + assert_eq!(hex(&tag), "1ae10b594f09e26a7e902ecbd0600691"); + } + + #[test] + fn aead_decrypt_test_vector() { + let key: [u8; 32] = from_hex( + "808182838485868788898a8b8c8d8e8f\ + 909192939495969798999a9b9c9d9e9f", + ) + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("070000004041424344454647").try_into().unwrap(); + let aad = from_hex("50515253c0c1c2c3c4c5c6c7"); + let ct = from_hex( + "d31a8d34648e60db7b86afbc53ef7ec2\ + a4aded51296e08fea9e2b5a736ee62d6\ + 3dbea45e8ca9671282fafb69da92728b\ + 1a71de0a9e060b2905d6a5b67ecd3b36\ + 92ddbd7f2d778b8c9803aee328091b58\ + fab324e4fad675945585808b4831d7bc\ + 3ff4def08e4b7a9de576d26586cec64b\ + 6116", + ); + let tag: [u8; 16] = from_hex("1ae10b594f09e26a7e902ecbd0600691") + .try_into() + .unwrap(); + + let pt = chacha20_poly1305_decrypt(&key, &nonce, &ct, &aad, &tag).unwrap(); + assert_eq!( + pt, + b"Ladies and Gentlemen of the class of '99: \ +If I could offer you only one tip for the future, sunscreen would be it." + .to_vec() + ); + } + + // ----------------------------------------------------------------------- + // Tag tamper detection + // ----------------------------------------------------------------------- + + #[test] + fn decrypt_rejects_tampered_tag() { + let key = [0x42u8; 32]; + let nonce = [0u8; 12]; + let (ct, mut tag) = chacha20_poly1305_encrypt(&key, &nonce, b"hello", &[]); + tag[0] ^= 1; + assert!(chacha20_poly1305_decrypt(&key, &nonce, &ct, &[], &tag).is_none()); + } + + #[test] + fn decrypt_rejects_tampered_ciphertext() { + let key = [0x42u8; 32]; + let nonce = [0u8; 12]; + let (mut ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, b"hello", &[]); + ct[0] ^= 1; + assert!(chacha20_poly1305_decrypt(&key, &nonce, &ct, &[], &tag).is_none()); + } + + #[test] + fn decrypt_rejects_tampered_aad() { + let key = [0x42u8; 32]; + let nonce = [0u8; 12]; + let aad = b"metadata"; + let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, b"hello", aad); + let bad_aad = b"Metadata"; + assert!(chacha20_poly1305_decrypt(&key, &nonce, &ct, bad_aad, &tag).is_none()); + } + + // ----------------------------------------------------------------------- + // Round-trip + // ----------------------------------------------------------------------- + + #[test] + fn roundtrip_empty_plaintext() { + let key = [0xabu8; 32]; + let nonce = [0x01u8; 12]; + let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, &[], &[]); + assert!(ct.is_empty()); + let pt = chacha20_poly1305_decrypt(&key, &nonce, &ct, &[], &tag).unwrap(); + assert!(pt.is_empty()); + } + + #[test] + fn roundtrip_with_aad() { + let key: [u8; 32] = from_hex( + "deadbeefdeadbeefdeadbeefdeadbeef\ + deadbeefdeadbeefdeadbeefdeadbeef", + ) + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("0102030405060708090a0b0c").try_into().unwrap(); + let pt = b"The quick brown fox jumps over the lazy dog"; + let aad = b"additional authenticated data"; + + let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, pt, aad); + let recovered = chacha20_poly1305_decrypt(&key, &nonce, &ct, aad, &tag).unwrap(); + assert_eq!(recovered, pt.to_vec()); + } + + #[test] + fn roundtrip_large_message() { + let key = [0xffu8; 32]; + let nonce = [0u8; 12]; + let pt: Vec = (0..1024).map(|i| (i % 256) as u8).collect(); + let aad = b"large message test"; + + let (ct, tag) = chacha20_poly1305_encrypt(&key, &nonce, &pt, aad); + let recovered = chacha20_poly1305_decrypt(&key, &nonce, &ct, aad, &tag).unwrap(); + assert_eq!(recovered, pt); + } +} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index dc1b7a0..6991c82 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -1,6 +1,7 @@ //! Pure Rust cryptography — AES-GCM, ChaCha20-Poly1305, SHA-2, X25519, RSA, X.509, ASN.1. pub mod aes_gcm; +pub mod chacha20_poly1305; pub mod hkdf; pub mod hmac; pub mod sha2; -- 2.51.2