Keccak AEAD
This commit is contained in:
@@ -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,4 +1,5 @@
|
||||
mod keccak;
|
||||
mod keccak_aead;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
|
||||
Reference in New Issue
Block a user