From d3541ab82f06a321bf37f2187858529073789248 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sat, 7 Mar 2026 09:03:50 +0100 Subject: [PATCH] Implement AES-128-GCM and AES-256-GCM (NIST SP 800-38D) Add pure Rust AES-GCM authenticated encryption in the crypto crate: - AES block cipher (FIPS 197): key expansion and encrypt for 128/256-bit keys - GHASH: constant-time GF(2^128) multiplication using bit masking - GCM encrypt/decrypt with 96-bit nonce, 128-bit authentication tag, and AAD - Constant-time tag comparison for secure decryption - 25 tests including NIST SP 800-38D test vectors (cases 1-4, 13-16), tag tamper detection, and encrypt/decrypt round-trips Co-Authored-By: Claude Opus 4.6 --- crates/crypto/src/aes_gcm.rs | 856 +++++++++++++++++++++++++++++++++++ crates/crypto/src/lib.rs | 1 + 2 files changed, 857 insertions(+) create mode 100644 crates/crypto/src/aes_gcm.rs diff --git a/crates/crypto/src/aes_gcm.rs b/crates/crypto/src/aes_gcm.rs new file mode 100644 index 0000000..ae44275 --- /dev/null +++ b/crates/crypto/src/aes_gcm.rs @@ -0,0 +1,856 @@ +//! AES-128-GCM and AES-256-GCM authenticated encryption (NIST SP 800-38D). +//! +//! - AES block cipher (FIPS 197) with 128-bit and 256-bit keys +//! - GHASH: Galois field multiplication in GF(2^128) +//! - GCM encrypt/decrypt with 96-bit nonce and 128-bit authentication tag + +// --------------------------------------------------------------------------- +// AES S-box (FIPS 197, §5.1.1) +// --------------------------------------------------------------------------- + +const SBOX: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, +]; + +/// Round constants: x^(i-1) in GF(2^8) for i = 1..10. +const RCON: [u8; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + +// --------------------------------------------------------------------------- +// AES key helpers +// --------------------------------------------------------------------------- + +fn rot_word(w: u32) -> u32 { + w.rotate_left(8) +} + +fn sub_word(w: u32) -> u32 { + let b = w.to_be_bytes(); + u32::from_be_bytes([ + SBOX[b[0] as usize], + SBOX[b[1] as usize], + SBOX[b[2] as usize], + SBOX[b[3] as usize], + ]) +} + +// --------------------------------------------------------------------------- +// AES key expansion (FIPS 197, §5.2) +// --------------------------------------------------------------------------- + +fn aes128_expand_key(key: &[u8; 16]) -> Vec<[u8; 16]> { + let mut w = [0u32; 44]; + for i in 0..4 { + w[i] = u32::from_be_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + for i in 4..44 { + let mut temp = w[i - 1]; + if i % 4 == 0 { + temp = sub_word(rot_word(temp)) ^ ((RCON[i / 4 - 1] as u32) << 24); + } + w[i] = w[i - 4] ^ temp; + } + (0..11) + .map(|r| { + let mut rk = [0u8; 16]; + for j in 0..4 { + rk[4 * j..4 * j + 4].copy_from_slice(&w[4 * r + j].to_be_bytes()); + } + rk + }) + .collect() +} + +fn aes256_expand_key(key: &[u8; 32]) -> Vec<[u8; 16]> { + let mut w = [0u32; 60]; + for i in 0..8 { + w[i] = u32::from_be_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + for i in 8..60 { + let mut temp = w[i - 1]; + if i % 8 == 0 { + temp = sub_word(rot_word(temp)) ^ ((RCON[i / 8 - 1] as u32) << 24); + } else if i % 8 == 4 { + temp = sub_word(temp); + } + w[i] = w[i - 8] ^ temp; + } + (0..15) + .map(|r| { + let mut rk = [0u8; 16]; + for j in 0..4 { + rk[4 * j..4 * j + 4].copy_from_slice(&w[4 * r + j].to_be_bytes()); + } + rk + }) + .collect() +} + +// --------------------------------------------------------------------------- +// AES round operations (FIPS 197, §5.1) +// --------------------------------------------------------------------------- + +/// Multiply by x in GF(2^8) with irreducible polynomial x^8+x^4+x^3+x+1. +fn xtime(a: u8) -> u8 { + let shifted = (a as u16) << 1; + let reduce = if a & 0x80 != 0 { 0x1b } else { 0x00 }; + (shifted as u8) ^ reduce +} + +fn sub_bytes(state: &mut [u8; 16]) { + for b in state.iter_mut() { + *b = SBOX[*b as usize]; + } +} + +/// ShiftRows: cyclic left-shift of row r by r positions. +/// State layout: state[row + 4*col] (column-major, per FIPS 197). +fn shift_rows(state: &mut [u8; 16]) { + // Row 1: rotate left by 1 + let t = state[1]; + state[1] = state[5]; + state[5] = state[9]; + state[9] = state[13]; + state[13] = t; + + // Row 2: rotate left by 2 + let (t0, t1) = (state[2], state[6]); + state[2] = state[10]; + state[6] = state[14]; + state[10] = t0; + state[14] = t1; + + // Row 3: rotate left by 3 (= right by 1) + let t = state[15]; + state[15] = state[11]; + state[11] = state[7]; + state[7] = state[3]; + state[3] = t; +} + +/// MixColumns: matrix multiply each column in GF(2^8). +fn mix_columns(state: &mut [u8; 16]) { + for c in 0..4 { + let i = 4 * c; + let (s0, s1, s2, s3) = (state[i], state[i + 1], state[i + 2], state[i + 3]); + state[i] = xtime(s0) ^ (xtime(s1) ^ s1) ^ s2 ^ s3; + state[i + 1] = s0 ^ xtime(s1) ^ (xtime(s2) ^ s2) ^ s3; + state[i + 2] = s0 ^ s1 ^ xtime(s2) ^ (xtime(s3) ^ s3); + state[i + 3] = (xtime(s0) ^ s0) ^ s1 ^ s2 ^ xtime(s3); + } +} + +fn add_round_key(state: &mut [u8; 16], rk: &[u8; 16]) { + for i in 0..16 { + state[i] ^= rk[i]; + } +} + +// --------------------------------------------------------------------------- +// AES block encrypt +// --------------------------------------------------------------------------- + +fn aes_encrypt_block(block: &[u8; 16], round_keys: &[[u8; 16]]) -> [u8; 16] { + let nr = round_keys.len() - 1; // 10 for AES-128, 14 for AES-256 + let mut state = *block; + + add_round_key(&mut state, &round_keys[0]); + + for rk in round_keys.iter().take(nr).skip(1) { + sub_bytes(&mut state); + shift_rows(&mut state); + mix_columns(&mut state); + add_round_key(&mut state, rk); + } + + // Final round (no MixColumns) + sub_bytes(&mut state); + shift_rows(&mut state); + add_round_key(&mut state, &round_keys[nr]); + + state +} + +// --------------------------------------------------------------------------- +// GHASH: GF(2^128) multiplication (NIST SP 800-38D, §6.3) +// --------------------------------------------------------------------------- + +/// Constant-time multiplication in GF(2^128). +/// +/// Uses the irreducible polynomial P = x^128 + x^7 + x^2 + x + 1. +/// Bit ordering follows GCM convention: MSB of byte 0 = coefficient of x^0. +fn gf128_mul(x: &[u8; 16], y: &[u8; 16]) -> [u8; 16] { + let mut z_hi: u64 = 0; + let mut z_lo: u64 = 0; + + let mut v_hi = u64::from_be_bytes(x[0..8].try_into().unwrap()); + let mut v_lo = u64::from_be_bytes(x[8..16].try_into().unwrap()); + + for i in 0..128 { + // y_bit = Y_i (coefficient of x^i in GCM convention) + let y_bit = ((y[i / 8] >> (7 - (i % 8))) & 1) as u64; + let mask = 0u64.wrapping_sub(y_bit); + + z_hi ^= v_hi & mask; + z_lo ^= v_lo & mask; + + // Shift V right by 1 (toward higher powers); reduce if carry + let carry = v_lo & 1; + let carry_mask = 0u64.wrapping_sub(carry); + v_lo = (v_lo >> 1) | ((v_hi & 1) << 63); + v_hi = (v_hi >> 1) ^ (0xe100000000000000u64 & carry_mask); + } + + let mut result = [0u8; 16]; + result[0..8].copy_from_slice(&z_hi.to_be_bytes()); + result[8..16].copy_from_slice(&z_lo.to_be_bytes()); + result +} + +fn xor_block(a: &mut [u8; 16], b: &[u8; 16]) { + for i in 0..16 { + a[i] ^= b[i]; + } +} + +/// Feed data blocks (with zero-padding of the final partial block) into GHASH. +fn ghash_update(y: &mut [u8; 16], h: &[u8; 16], data: &[u8]) { + let mut offset = 0; + while offset + 16 <= data.len() { + let block: [u8; 16] = data[offset..offset + 16].try_into().unwrap(); + xor_block(y, &block); + *y = gf128_mul(y, h); + offset += 16; + } + if offset < data.len() { + let mut block = [0u8; 16]; + block[..data.len() - offset].copy_from_slice(&data[offset..]); + xor_block(y, &block); + *y = gf128_mul(y, h); + } +} + +/// GHASH(H, AAD, C) = process AAD ∥ C ∥ lengths through the universal hash. +fn ghash(h: &[u8; 16], aad: &[u8], ciphertext: &[u8]) -> [u8; 16] { + let mut y = [0u8; 16]; + + ghash_update(&mut y, h, aad); + ghash_update(&mut y, h, ciphertext); + + // Length block: [len(A) in bits]_64 ∥ [len(C) in bits]_64 + let mut len_block = [0u8; 16]; + len_block[0..8].copy_from_slice(&((aad.len() as u64) * 8).to_be_bytes()); + len_block[8..16].copy_from_slice(&((ciphertext.len() as u64) * 8).to_be_bytes()); + xor_block(&mut y, &len_block); + y = gf128_mul(&y, h); + + y +} + +// --------------------------------------------------------------------------- +// GCM counter +// --------------------------------------------------------------------------- + +/// Increment the rightmost 32 bits of a 128-bit counter block. +fn inc32(counter: &mut [u8; 16]) { + let c = u32::from_be_bytes(counter[12..16].try_into().unwrap()); + counter[12..16].copy_from_slice(&c.wrapping_add(1).to_be_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 +} + +// --------------------------------------------------------------------------- +// GCM core encrypt / decrypt +// --------------------------------------------------------------------------- + +fn gcm_encrypt( + round_keys: &[[u8; 16]], + nonce: &[u8; 12], + plaintext: &[u8], + aad: &[u8], +) -> (Vec, [u8; 16]) { + // H = E_K(0^128) + let h = aes_encrypt_block(&[0u8; 16], round_keys); + + // J_0 = IV ∥ 0^31 ∥ 1 + let mut j0 = [0u8; 16]; + j0[0..12].copy_from_slice(nonce); + j0[15] = 1; + + // GCTR: encrypt plaintext with counter starting at inc32(J_0) + let mut counter = j0; + inc32(&mut counter); + + let mut ciphertext = Vec::with_capacity(plaintext.len()); + let mut offset = 0; + while offset < plaintext.len() { + let keystream = aes_encrypt_block(&counter, round_keys); + let remaining = (plaintext.len() - offset).min(16); + for i in 0..remaining { + ciphertext.push(plaintext[offset + i] ^ keystream[i]); + } + offset += remaining; + inc32(&mut counter); + } + + // Tag = GHASH(H, AAD, C) ⊕ E_K(J_0) + let s = ghash(&h, aad, &ciphertext); + let ek_j0 = aes_encrypt_block(&j0, round_keys); + let mut tag = [0u8; 16]; + for i in 0..16 { + tag[i] = s[i] ^ ek_j0[i]; + } + + (ciphertext, tag) +} + +fn gcm_decrypt( + round_keys: &[[u8; 16]], + nonce: &[u8; 12], + ciphertext: &[u8], + aad: &[u8], + tag: &[u8; 16], +) -> Option> { + // H = E_K(0^128) + let h = aes_encrypt_block(&[0u8; 16], round_keys); + + // J_0 = IV ∥ 0^31 ∥ 1 + let mut j0 = [0u8; 16]; + j0[0..12].copy_from_slice(nonce); + j0[15] = 1; + + // Verify tag before decrypting + let s = ghash(&h, aad, ciphertext); + let ek_j0 = aes_encrypt_block(&j0, round_keys); + let mut expected_tag = [0u8; 16]; + for i in 0..16 { + expected_tag[i] = s[i] ^ ek_j0[i]; + } + + if !ct_eq_16(&expected_tag, tag) { + return None; + } + + // GCTR: decrypt ciphertext + let mut counter = j0; + inc32(&mut counter); + + let mut plaintext = Vec::with_capacity(ciphertext.len()); + let mut offset = 0; + while offset < ciphertext.len() { + let keystream = aes_encrypt_block(&counter, round_keys); + let remaining = (ciphertext.len() - offset).min(16); + for i in 0..remaining { + plaintext.push(ciphertext[offset + i] ^ keystream[i]); + } + offset += remaining; + inc32(&mut counter); + } + + Some(plaintext) +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/// AES-128-GCM encrypt. Returns `(ciphertext, 128-bit tag)`. +pub fn aes128_gcm_encrypt( + key: &[u8; 16], + nonce: &[u8; 12], + plaintext: &[u8], + aad: &[u8], +) -> (Vec, [u8; 16]) { + let rk = aes128_expand_key(key); + gcm_encrypt(&rk, nonce, plaintext, aad) +} + +/// AES-128-GCM decrypt. Returns `None` if the authentication tag is invalid. +pub fn aes128_gcm_decrypt( + key: &[u8; 16], + nonce: &[u8; 12], + ciphertext: &[u8], + aad: &[u8], + tag: &[u8; 16], +) -> Option> { + let rk = aes128_expand_key(key); + gcm_decrypt(&rk, nonce, ciphertext, aad, tag) +} + +/// AES-256-GCM encrypt. Returns `(ciphertext, 128-bit tag)`. +pub fn aes256_gcm_encrypt( + key: &[u8; 32], + nonce: &[u8; 12], + plaintext: &[u8], + aad: &[u8], +) -> (Vec, [u8; 16]) { + let rk = aes256_expand_key(key); + gcm_encrypt(&rk, nonce, plaintext, aad) +} + +/// AES-256-GCM decrypt. Returns `None` if the authentication tag is invalid. +pub fn aes256_gcm_decrypt( + key: &[u8; 32], + nonce: &[u8; 12], + ciphertext: &[u8], + aad: &[u8], + tag: &[u8; 16], +) -> Option> { + let rk = aes256_expand_key(key); + gcm_decrypt(&rk, nonce, ciphertext, aad, tag) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() + } + + 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 hex16(s: &str) -> [u8; 16] { + from_hex(s).try_into().unwrap() + } + + // ----------------------------------------------------------------------- + // AES block cipher (ECB) — NIST test vectors + // ----------------------------------------------------------------------- + + #[test] + fn aes128_ecb_zero_key() { + let key = [0u8; 16]; + let pt = [0u8; 16]; + let rk = aes128_expand_key(&key); + assert_eq!( + hex(&aes_encrypt_block(&pt, &rk)), + "66e94bd4ef8a2c3b884cfa59ca342b2e" + ); + } + + #[test] + fn aes128_ecb_nist() { + let key = hex16("2b7e151628aed2a6abf7158809cf4f3c"); + let pt = hex16("6bc1bee22e409f96e93d7e117393172a"); + let rk = aes128_expand_key(&key); + assert_eq!( + hex(&aes_encrypt_block(&pt, &rk)), + "3ad77bb40d7a3660a89ecaf32466ef97" + ); + } + + #[test] + fn aes256_ecb_zero_key() { + let key = [0u8; 32]; + let pt = [0u8; 16]; + let rk = aes256_expand_key(&key); + assert_eq!( + hex(&aes_encrypt_block(&pt, &rk)), + "dc95c078a2408989ad48a21492842087" + ); + } + + #[test] + fn aes256_ecb_nist() { + let key: [u8; 32] = + from_hex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4") + .try_into() + .unwrap(); + let pt = hex16("6bc1bee22e409f96e93d7e117393172a"); + let rk = aes256_expand_key(&key); + assert_eq!( + hex(&aes_encrypt_block(&pt, &rk)), + "f3eed1bdb5d2a03c064b5a7e3db181f8" + ); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 1: AES-128, empty plaintext, empty AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_128_case1_encrypt() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let (ct, tag) = aes128_gcm_encrypt(&key, &nonce, &[], &[]); + assert!(ct.is_empty()); + assert_eq!(hex(&tag), "58e2fccefa7e3061367f1d57a4e7455a"); + } + + #[test] + fn gcm_128_case1_decrypt() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let tag = hex16("58e2fccefa7e3061367f1d57a4e7455a"); + let pt = aes128_gcm_decrypt(&key, &nonce, &[], &[], &tag).unwrap(); + assert!(pt.is_empty()); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 2: AES-128, 16-byte plaintext, empty AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_128_case2_encrypt() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let pt = [0u8; 16]; + let (ct, tag) = aes128_gcm_encrypt(&key, &nonce, &pt, &[]); + assert_eq!(hex(&ct), "0388dace60b6a392f328c2b971b2fe78"); + assert_eq!(hex(&tag), "ab6e47d42cec13bdf53a67b21257bddf"); + } + + #[test] + fn gcm_128_case2_decrypt() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let ct = from_hex("0388dace60b6a392f328c2b971b2fe78"); + let tag = hex16("ab6e47d42cec13bdf53a67b21257bddf"); + let pt = aes128_gcm_decrypt(&key, &nonce, &ct, &[], &tag).unwrap(); + assert_eq!(pt, vec![0u8; 16]); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 3: AES-128, 64-byte plaintext, empty AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_128_case3_encrypt() { + let key = hex16("feffe9928665731c6d6a8f9467308308"); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let pt = from_hex( + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b391aafd255", + ); + let (ct, tag) = aes128_gcm_encrypt(&key, &nonce, &pt, &[]); + assert_eq!( + hex(&ct), + "42831ec2217774244b7221b784d0d49c\ + e3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa05\ + 1ba30b396a0aac973d58e091473f5985" + ); + assert_eq!(hex(&tag), "4d5c2af327cd64a62cf35abd2ba6fab4"); + } + + #[test] + fn gcm_128_case3_decrypt() { + let key = hex16("feffe9928665731c6d6a8f9467308308"); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let ct = from_hex( + "42831ec2217774244b7221b784d0d49c\ + e3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa05\ + 1ba30b396a0aac973d58e091473f5985", + ); + let tag = hex16("4d5c2af327cd64a62cf35abd2ba6fab4"); + let pt = aes128_gcm_decrypt(&key, &nonce, &ct, &[], &tag).unwrap(); + assert_eq!( + hex(&pt), + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b391aafd255" + ); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 4: AES-128, 60-byte plaintext, 20-byte AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_128_case4_encrypt() { + let key = hex16("feffe9928665731c6d6a8f9467308308"); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let pt = from_hex( + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b39", + ); + let aad = from_hex("feedfacedeadbeeffeedfacedeadbeefabaddad2"); + let (ct, tag) = aes128_gcm_encrypt(&key, &nonce, &pt, &aad); + assert_eq!( + hex(&ct), + "42831ec2217774244b7221b784d0d49c\ + e3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa05\ + 1ba30b396a0aac973d58e091", + ); + assert_eq!(hex(&tag), "5bc94fbc3221a5db94fae95ae7121a47"); + } + + #[test] + fn gcm_128_case4_decrypt() { + let key = hex16("feffe9928665731c6d6a8f9467308308"); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let ct = from_hex( + "42831ec2217774244b7221b784d0d49c\ + e3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa05\ + 1ba30b396a0aac973d58e091", + ); + let aad = from_hex("feedfacedeadbeeffeedfacedeadbeefabaddad2"); + let tag = hex16("5bc94fbc3221a5db94fae95ae7121a47"); + let pt = aes128_gcm_decrypt(&key, &nonce, &ct, &aad, &tag).unwrap(); + assert_eq!( + hex(&pt), + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b39" + ); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 13: AES-256, empty plaintext, empty AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_256_case13_encrypt() { + let key = [0u8; 32]; + let nonce = [0u8; 12]; + let (ct, tag) = aes256_gcm_encrypt(&key, &nonce, &[], &[]); + assert!(ct.is_empty()); + assert_eq!(hex(&tag), "530f8afbc74536b9a963b4f1c4cb738b"); + } + + #[test] + fn gcm_256_case13_decrypt() { + let key = [0u8; 32]; + let nonce = [0u8; 12]; + let tag = hex16("530f8afbc74536b9a963b4f1c4cb738b"); + let pt = aes256_gcm_decrypt(&key, &nonce, &[], &[], &tag).unwrap(); + assert!(pt.is_empty()); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 14: AES-256, 16-byte plaintext, empty AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_256_case14_encrypt() { + let key = [0u8; 32]; + let nonce = [0u8; 12]; + let pt = [0u8; 16]; + let (ct, tag) = aes256_gcm_encrypt(&key, &nonce, &pt, &[]); + assert_eq!(hex(&ct), "cea7403d4d606b6e074ec5d3baf39d18"); + assert_eq!(hex(&tag), "d0d1c8a799996bf0265b98b5d48ab919"); + } + + #[test] + fn gcm_256_case14_decrypt() { + let key = [0u8; 32]; + let nonce = [0u8; 12]; + let ct = from_hex("cea7403d4d606b6e074ec5d3baf39d18"); + let tag = hex16("d0d1c8a799996bf0265b98b5d48ab919"); + let pt = aes256_gcm_decrypt(&key, &nonce, &ct, &[], &tag).unwrap(); + assert_eq!(pt, vec![0u8; 16]); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 15: AES-256, 64-byte plaintext, empty AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_256_case15_encrypt() { + let key: [u8; 32] = + from_hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308") + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let pt = from_hex( + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b391aafd255", + ); + let (ct, tag) = aes256_gcm_encrypt(&key, &nonce, &pt, &[]); + assert_eq!( + hex(&ct), + "522dc1f099567d07f47f37a32a84427d\ + 643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838\ + c5f61e6393ba7a0abcc9f662898015ad" + ); + assert_eq!(hex(&tag), "b094dac5d93471bdec1a502270e3cc6c"); + } + + #[test] + fn gcm_256_case15_decrypt() { + let key: [u8; 32] = + from_hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308") + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let ct = from_hex( + "522dc1f099567d07f47f37a32a84427d\ + 643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838\ + c5f61e6393ba7a0abcc9f662898015ad", + ); + let tag = hex16("b094dac5d93471bdec1a502270e3cc6c"); + let pt = aes256_gcm_decrypt(&key, &nonce, &ct, &[], &tag).unwrap(); + assert_eq!( + hex(&pt), + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b391aafd255" + ); + } + + // ----------------------------------------------------------------------- + // GCM Test Case 16: AES-256, 60-byte plaintext, 20-byte AAD + // ----------------------------------------------------------------------- + + #[test] + fn gcm_256_case16_encrypt() { + let key: [u8; 32] = + from_hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308") + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let pt = from_hex( + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b39", + ); + let aad = from_hex("feedfacedeadbeeffeedfacedeadbeefabaddad2"); + let (ct, tag) = aes256_gcm_encrypt(&key, &nonce, &pt, &aad); + assert_eq!( + hex(&ct), + "522dc1f099567d07f47f37a32a84427d\ + 643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838\ + c5f61e6393ba7a0abcc9f662", + ); + assert_eq!(hex(&tag), "76fc6ece0f4e1768cddf8853bb2d551b"); + } + + #[test] + fn gcm_256_case16_decrypt() { + let key: [u8; 32] = + from_hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308") + .try_into() + .unwrap(); + let nonce: [u8; 12] = from_hex("cafebabefacedbaddecaf888").try_into().unwrap(); + let ct = from_hex( + "522dc1f099567d07f47f37a32a84427d\ + 643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838\ + c5f61e6393ba7a0abcc9f662", + ); + let aad = from_hex("feedfacedeadbeeffeedfacedeadbeefabaddad2"); + let tag = hex16("76fc6ece0f4e1768cddf8853bb2d551b"); + let pt = aes256_gcm_decrypt(&key, &nonce, &ct, &aad, &tag).unwrap(); + assert_eq!( + hex(&pt), + "d9313225f88406e5a55909c5aff5269a\ + 86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525\ + b16aedf5aa0de657ba637b39" + ); + } + + // ----------------------------------------------------------------------- + // Tag tamper detection + // ----------------------------------------------------------------------- + + #[test] + fn decrypt_rejects_tampered_tag() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let pt = b"hello world"; + let (ct, mut tag) = aes128_gcm_encrypt(&key, &nonce, pt, &[]); + tag[0] ^= 1; + assert!(aes128_gcm_decrypt(&key, &nonce, &ct, &[], &tag).is_none()); + } + + #[test] + fn decrypt_rejects_tampered_ciphertext() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let pt = b"hello world"; + let (mut ct, tag) = aes128_gcm_encrypt(&key, &nonce, pt, &[]); + ct[0] ^= 1; + assert!(aes128_gcm_decrypt(&key, &nonce, &ct, &[], &tag).is_none()); + } + + #[test] + fn decrypt_rejects_tampered_aad() { + let key = [0u8; 16]; + let nonce = [0u8; 12]; + let pt = b"hello world"; + let aad = b"associated data"; + let (ct, tag) = aes128_gcm_encrypt(&key, &nonce, pt, aad); + let bad_aad = b"Associated data"; + assert!(aes128_gcm_decrypt(&key, &nonce, &ct, bad_aad, &tag).is_none()); + } + + // ----------------------------------------------------------------------- + // Round-trip + // ----------------------------------------------------------------------- + + #[test] + fn aes128_gcm_roundtrip() { + let key = hex16("deadbeefdeadbeefdeadbeefdeadbeef"); + 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) = aes128_gcm_encrypt(&key, &nonce, pt, aad); + let recovered = aes128_gcm_decrypt(&key, &nonce, &ct, aad, &tag).unwrap(); + assert_eq!(recovered, pt); + } + + #[test] + fn aes256_gcm_roundtrip() { + let key: [u8; 32] = + from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .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) = aes256_gcm_encrypt(&key, &nonce, pt, aad); + let recovered = aes256_gcm_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 e71cf2c..dc1b7a0 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -1,5 +1,6 @@ //! Pure Rust cryptography — AES-GCM, ChaCha20-Poly1305, SHA-2, X25519, RSA, X.509, ASN.1. +pub mod aes_gcm; pub mod hkdf; pub mod hmac; pub mod sha2; -- 2.51.2