use super::error::HandshakeError;
pub trait Cipher {
const NAME: &'static str;
const TAG_SIZE: usize;
fn encrypt(
key: &[u8; 32],
nonce: u64,
ad: &[u8],
plaintext: &[u8],
output: &mut [u8],
) -> Result<usize, HandshakeError>;
fn decrypt(
key: &[u8; 32],
nonce: u64,
ad: &[u8],
ciphertext: &[u8],
output: &mut [u8],
) -> Result<usize, HandshakeError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ChaChaPoly;
fn nonce_bytes(n: u64) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[4..].copy_from_slice(&n.to_le_bytes());
nonce
}
impl Cipher for ChaChaPoly {
const NAME: &'static str = "ChaChaPoly";
const TAG_SIZE: usize = 16;
fn encrypt(
key: &[u8; 32],
nonce: u64,
ad: &[u8],
plaintext: &[u8],
output: &mut [u8],
) -> Result<usize, HandshakeError> {
let ct_len = plaintext.len();
let total = ct_len + Self::TAG_SIZE;
if output.len() < total {
return Err(HandshakeError::OutputBufferTooSmall {
needed: total,
actual: output.len(),
});
}
let nonce = nonce_bytes(nonce);
let (ct, tag_out) = output[..total].split_at_mut(ct_len);
let mut cipher = cryptoxide::chacha20poly1305::ChaCha20Poly1305::new(key, &nonce, ad);
cipher.encrypt(plaintext, ct, tag_out);
Ok(total)
}
fn decrypt(
key: &[u8; 32],
nonce: u64,
ad: &[u8],
ciphertext: &[u8],
output: &mut [u8],
) -> Result<usize, HandshakeError> {
if ciphertext.len() < Self::TAG_SIZE {
return Err(HandshakeError::DecryptionFailed);
}
let pt_len = ciphertext.len() - Self::TAG_SIZE;
if output.len() < pt_len {
return Err(HandshakeError::OutputBufferTooSmall {
needed: pt_len,
actual: output.len(),
});
}
let nonce = nonce_bytes(nonce);
let (ct, tag) = ciphertext.split_at(pt_len);
let mut cipher = cryptoxide::chacha20poly1305::ChaCha20Poly1305::new(key, &nonce, ad);
if !cipher.decrypt(ct, &mut output[..pt_len], tag) {
crate::zeroize::zeroize_bytes(&mut output[..pt_len]);
return Err(HandshakeError::DecryptionFailed);
}
Ok(pt_len)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decrypt_failure_zeroes_output() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n = ChaChaPoly::encrypt(&key, 0, &[], b"secret payload", &mut ct).unwrap();
ct[n - 1] ^= 0xFF; let mut pt = [0xAAu8; 64];
let err = ChaChaPoly::decrypt(&key, 0, &[], &ct[..n], &mut pt).unwrap_err();
assert!(matches!(err, HandshakeError::DecryptionFailed));
let pt_len = n - ChaChaPoly::TAG_SIZE;
assert!(
pt[..pt_len].iter().all(|&b| b == 0),
"plaintext region must be zeroed on auth failure"
);
}
}