From 30c906667c116c631dedab372ed129ac3f7c1378 Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Wed, 23 Sep 2026 11:12:51 +0200 Subject: [PATCH] Keccak AEAD --- src/keccak_aead.rs | 445 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 2 files changed, 446 insertions(+) create mode 100644 src/keccak_aead.rs diff --git a/src/keccak_aead.rs b/src/keccak_aead.rs new file mode 100644 index 0000000..70e6b41 --- /dev/null +++ b/src/keccak_aead.rs @@ -0,0 +1,445 @@ +use crate::keccak::shake256; + +const RATE_SIZE: usize = 16; +const CAPACITY_SIZE: usize = 24; +const STATE_SIZE: usize = RATE_SIZE + CAPACITY_SIZE; +const CHUNK_SIZE: usize = 16; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncryptionResult { + pub cipher: Vec, + pub tag: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecryptionResult { + pub plaintext: Vec, + pub tag: Vec, +} + +#[derive(Debug, Clone)] +pub struct KeccakAead { + state: [u8; STATE_SIZE], +} + +impl KeccakAead { + /// Initialize the AEAD state. + pub fn new(iv: &[u8], key: &[u8], nonce: &[u8]) -> Self { + assert!( + key.len() <= CAPACITY_SIZE, + "key must be at most {} bytes", + CAPACITY_SIZE + ); + + let mut input = Vec::with_capacity( + iv.len() + key.len() + nonce.len(), + ); + + input.extend_from_slice(iv); + input.extend_from_slice(key); + input.extend_from_slice(nonce); + + let squeezed = shake256(&input, STATE_SIZE); + + let mut state = [0u8; STATE_SIZE]; + state.copy_from_slice(&squeezed); + + for i in 0..key.len() { + state[RATE_SIZE + i] ^= key[i]; + } + + Self { state } + } + + /// Process associated data. + /// Data is processed in 16-byte chunks. + pub fn associated_data_processing( + &mut self, + associated_data: &[u8], + ) { + for chunk in associated_data.chunks(CHUNK_SIZE) { + let mut input = [0u8; STATE_SIZE]; + + // r = chunk XOR state[0..16] + for i in 0..chunk.len() { + input[i] = + chunk[i] ^ self.state[i]; + } + + // c = state[16..40] + input[RATE_SIZE..STATE_SIZE] + .copy_from_slice(&self.state[RATE_SIZE..STATE_SIZE]); + + self.state = shake256_state(&input); + } + + self.state[STATE_SIZE - 1] ^= 1; + } + + /// Encrypt plaintext. + /// Returns the ciphertext split logically into 16-byte chunks, then concatenated + pub fn plaintext_processing( + &mut self, + plaintext: &[u8], + ) -> Vec { + let mut ciphertext = + Vec::with_capacity(plaintext.len()); + + for chunk in plaintext.chunks(CHUNK_SIZE) { + let mut input = [0u8; STATE_SIZE]; + + // r = chunk XOR state[0..16] + for i in 0..chunk.len() { + ciphertext.push(chunk[i] ^ self.state[i]); + } + + // input = r || c + // + // The rate portion must be the generated ciphertext. + let start = ciphertext.len() - chunk.len(); + + input[..chunk.len()] + .copy_from_slice(&ciphertext[start..]); + + input[RATE_SIZE..STATE_SIZE] + .copy_from_slice(&self.state[RATE_SIZE..STATE_SIZE]); + + self.state = shake256_state(&input); + } + + ciphertext + } + + /// Decrypt ciphertext. + pub fn ciphertext_processing( + &mut self, + ciphertext: &[u8], + ) -> Vec { + let mut plaintext = + Vec::with_capacity(ciphertext.len()); + + for chunk in ciphertext.chunks(CHUNK_SIZE) { + let mut input = [0u8; STATE_SIZE]; + + // plaintext = ciphertext XOR state[0..16] + for i in 0..chunk.len() { + plaintext.push(chunk[i] ^ self.state[i]); + } + + // input = ciphertext || c + input[..chunk.len()] + .copy_from_slice(chunk); + + input[RATE_SIZE..STATE_SIZE] + .copy_from_slice(&self.state[RATE_SIZE..STATE_SIZE]); + + self.state = shake256_state(&input); + } + + plaintext + } + + /// Finalize and generate the authentication tag. + /// The tag has the same length as the key. + pub fn finalize(&mut self, key: &[u8]) -> Vec { + assert!( + key.len() <= CAPACITY_SIZE, + "key must be at most {} bytes", + CAPACITY_SIZE + ); + + // Construct padded key: + let mut padded_key = [0u8; CAPACITY_SIZE]; + + let padding_len = + CAPACITY_SIZE - key.len(); + + padded_key[padding_len..] + .copy_from_slice(key); + + // XOR padded key into c. + for i in 0..CAPACITY_SIZE { + self.state[RATE_SIZE + i] ^= padded_key[i]; + } + + // SHAKE256(state, 40) + let output = shake256( + &self.state, + STATE_SIZE, + ); + + let tag_start = + STATE_SIZE - key.len(); + + let mut tag = + Vec::with_capacity(key.len()); + + for i in 0..key.len() { + tag.push( + key[i] + ^ output[tag_start + i], + ); + } + + tag + } + + /// Encrypt: + /// + /// sponge = KeccakAead(iv, key, nonce) + /// sponge.associated_data_processing(ad) + /// ciphertext = sponge.plaintext_processing(plaintext) + /// tag = sponge.finalize(key) + pub fn encrypt( + key: &[u8], + plaintext: &[u8], + iv: &[u8], + associated_data: &[u8], + nonce: &[u8], + ) -> EncryptionResult { + let mut sponge = + Self::new(iv, key, nonce); + + sponge.associated_data_processing( + associated_data, + ); + + let cipher = + sponge.plaintext_processing(plaintext); + + let tag = + sponge.finalize(key); + + EncryptionResult { + cipher, + tag, + } + } + + /// Decrypt. + pub fn decrypt( + key: &[u8], + ciphertext: &[u8], + iv: &[u8], + associated_data: &[u8], + nonce: &[u8], + ) -> DecryptionResult { + let mut sponge = + Self::new(iv, key, nonce); + + sponge.associated_data_processing( + associated_data, + ); + + let plaintext = + sponge.ciphertext_processing(ciphertext); + + let tag = + sponge.finalize(key); + + DecryptionResult { + plaintext, + tag, + } + } +} + +/// SHAKE256 wrapper returning exactly a 40-byte AEAD state. +#[inline] +fn shake256_state( + state: &[u8; STATE_SIZE], +) -> [u8; STATE_SIZE] { + let output = + shake256(state, STATE_SIZE); + + let mut result = + [0u8; STATE_SIZE]; + + result.copy_from_slice(&output); + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt_roundtrip() { + let key = [0x00u8; 16]; + let iv = [0x11u8; 16]; + let nonce = [0x22u8; 16]; + + let associated_data = + b"associated data"; + + let plaintext = + b"Hello, Keccak AEAD!"; + + let encrypted = KeccakAead::encrypt( + &key, + plaintext, + &iv, + associated_data, + &nonce, + ); + + let decrypted = KeccakAead::decrypt( + &key, + &encrypted.cipher, + &iv, + associated_data, + &nonce, + ); + + assert_eq!( + decrypted.plaintext, + plaintext + ); + + assert_eq!( + decrypted.tag, + encrypted.tag + ); + } + + #[test] + fn handles_empty_plaintext() { + let key = [0x42u8; 16]; + let iv = [0x11u8; 16]; + let nonce = [0x22u8; 16]; + + let result = KeccakAead::encrypt( + &key, + &[], + &iv, + b"test", + &nonce, + ); + + assert_eq!(result.cipher.len(), 0); + assert_eq!(result.tag.len(), key.len()); + } + + #[test] + fn handles_empty_associated_data() { + let key = [0x42u8; 16]; + let iv = [0x11u8; 16]; + let nonce = [0x22u8; 16]; + + let plaintext = + b"test plaintext"; + + let result = KeccakAead::encrypt( + &key, + plaintext, + &iv, + &[], + &nonce, + ); + + let decrypted = + KeccakAead::decrypt( + &key, + &result.cipher, + &iv, + &[], + &nonce, + ); + + assert_eq!( + decrypted.plaintext, + plaintext + ); + + assert_eq!( + decrypted.tag, + result.tag + ); + } + + #[test] + fn handles_multiple_chunks() { + let key = [0x42u8; 16]; + let iv = [0x11u8; 16]; + let nonce = [0x22u8; 16]; + + // > 16 bytes for multiple chunks. + let plaintext = + b"0123456789abcdef0123456789abcdef0123"; + + let associated_data = + b"abcdefghijklmnopqrstuvwxyz"; + + let encrypted = + KeccakAead::encrypt( + &key, + plaintext, + &iv, + associated_data, + &nonce, + ); + + assert_eq!( + encrypted.cipher.len(), + plaintext.len() + ); + + let decrypted = + KeccakAead::decrypt( + &key, + &encrypted.cipher, + iv.as_slice(), + associated_data, + nonce.as_slice(), + ); + + assert_eq!( + decrypted.plaintext, + plaintext + ); + + assert_eq!( + decrypted.tag, + encrypted.tag + ); + } + + #[test] + fn tag_changes_when_ciphertext_changes() { + let key = [0x42u8; 16]; + let iv = [0x11u8; 16]; + let nonce = [0x22u8; 16]; + + let plaintext = + b"test plaintext"; + + let encrypted = + KeccakAead::encrypt( + &key, + plaintext, + &iv, + b"ad", + &nonce, + ); + + let mut modified = + encrypted.cipher.clone(); + + modified[0] ^= 1; + + let decrypted = + KeccakAead::decrypt( + &key, + &modified, + &iv, + b"ad", + &nonce, + ); + + assert_ne!( + decrypted.tag, + encrypted.tag + ); + } +} diff --git a/src/main.rs b/src/main.rs index c35e42b..efcd85a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod keccak; +mod keccak_aead; fn main() { println!("Hello, world!");