Keccak AEAD

This commit is contained in:
2026-09-23 11:12:51 +02:00
parent 1fce281b9d
commit 30c906667c
2 changed files with 446 additions and 0 deletions
+445
View File
@@ -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<u8>,
pub tag: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecryptionResult {
pub plaintext: Vec<u8>,
pub tag: Vec<u8>,
}
#[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<u8> {
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<u8> {
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<u8> {
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
);
}
}
+1
View File
@@ -1,4 +1,5 @@
mod keccak;
mod keccak_aead;
fn main() {
println!("Hello, world!");