use super::error::HandshakeError;
use core::mem::ManuallyDrop;
use cryptoxide::aes_gcm::{AesGcm256, DecryptionResult, Tag};
#[cfg(test)]
mod wycheproof;
pub trait Cipher {
const NAME: &'static str;
const TAG_SIZE: usize;
type Key: Send + Sync + 'static;
fn key(k: &[u8; 32]) -> Self::Key;
fn encrypt(
key: &Self::Key,
nonce: u64,
ad: &[u8],
plaintext: &[u8],
output: &mut [u8],
) -> Result<usize, HandshakeError>;
fn decrypt(
key: &Self::Key,
nonce: u64,
ad: &[u8],
ciphertext: &[u8],
output: &mut [u8],
) -> Result<usize, HandshakeError>;
}
fn nonce_le(n: u64) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[4..].copy_from_slice(&n.to_le_bytes());
nonce
}
fn nonce_be(n: u64) -> [u8; 12] {
let mut nonce = [0u8; 12];
nonce[4..].copy_from_slice(&n.to_be_bytes());
nonce
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ChaChaPoly;
pub struct ChaChaPolyKey([u8; 32]);
impl Drop for ChaChaPolyKey {
fn drop(&mut self) {
crate::zeroize::zeroize_array(&mut self.0);
}
}
impl Cipher for ChaChaPoly {
const NAME: &'static str = "ChaChaPoly";
const TAG_SIZE: usize = 16;
type Key = ChaChaPolyKey;
fn key(k: &[u8; 32]) -> ChaChaPolyKey {
ChaChaPolyKey(*k)
}
fn encrypt(
key: &ChaChaPolyKey,
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_le(nonce);
let (ct, tag_out) = output[..total].split_at_mut(ct_len);
let mut cipher = cryptoxide::chacha20poly1305::ChaCha20Poly1305::new(&key.0, &nonce, ad);
cipher.encrypt(plaintext, ct, tag_out);
Ok(total)
}
fn decrypt(
key: &ChaChaPolyKey,
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_le(nonce);
let (ct, tag) = ciphertext.split_at(pt_len);
let mut cipher = cryptoxide::chacha20poly1305::ChaCha20Poly1305::new(&key.0, &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)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AesGcm;
pub struct AesGcmKey(ManuallyDrop<AesGcm256>);
impl Drop for AesGcmKey {
fn drop(&mut self) {
crate::zeroize::zeroize_storage(&mut self.0);
}
}
impl Cipher for AesGcm {
const NAME: &'static str = "AESGCM";
const TAG_SIZE: usize = 16;
type Key = AesGcmKey;
fn key(k: &[u8; 32]) -> AesGcmKey {
AesGcmKey(ManuallyDrop::new(AesGcm256::new(k)))
}
fn encrypt(
key: &AesGcmKey,
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_be(nonce);
let (ct, tag_out) = output[..total].split_at_mut(ct_len);
let mut tag = Tag([0u8; Self::TAG_SIZE]);
key.0.encrypt(&nonce, ad, plaintext, ct, &mut tag);
tag_out.copy_from_slice(&tag.0);
Ok(total)
}
fn decrypt(
key: &AesGcmKey,
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_be(nonce);
let (ct, tag) = ciphertext.split_at(pt_len);
let tag = Tag(tag.try_into().expect("split at len - TAG_SIZE"));
match key.0.decrypt(&nonce, ad, ct, &mut output[..pt_len], &tag) {
DecryptionResult::Match => Ok(pt_len),
DecryptionResult::MisMatch => {
crate::zeroize::zeroize_bytes(&mut output[..pt_len]);
Err(HandshakeError::DecryptionFailed)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::mem::MaybeUninit;
unsafe fn count_nonzero(p: *mut u8, len: usize) -> usize {
(0..len)
.filter(|&i| unsafe { p.add(i).read_volatile() } != 0)
.count()
}
unsafe fn scrub_probe<K>(build: impl FnOnce() -> K) -> (usize, usize) {
let mut slot = MaybeUninit::new(build());
let size = size_of::<K>();
let before = unsafe { count_nonzero(slot.as_mut_ptr().cast::<u8>(), size) };
unsafe { slot.assume_init_drop() };
let after = unsafe { count_nonzero(slot.as_mut_ptr().cast::<u8>(), size) };
(before, after)
}
#[test]
fn chachapoly_key_scrubs_on_drop() {
let (before, after) = unsafe { scrub_probe(|| ChaChaPoly::key(&[0xA5u8; 32])) };
assert_eq!(before, 32, "the probe must see the live key first");
assert_eq!(after, 0, "ChaChaPolyKey must leave zeros behind on drop");
}
#[test]
fn nonces_differ_in_byte_order_from_counter_one() {
assert_eq!(nonce_le(0), [0u8; 12], "counter 0 is all zeros either way");
assert_eq!(nonce_be(0), [0u8; 12], "counter 0 is all zeros either way");
let mut le = [0u8; 12];
le[4] = 1;
assert_eq!(
nonce_le(1),
le,
"Noise §12.3 ChaChaPoly nonce is LITTLE-endian"
);
let mut be = [0u8; 12];
be[11] = 1;
assert_eq!(nonce_be(1), be, "Noise §12.4 AESGCM nonce is BIG-endian");
assert_ne!(
nonce_be(1),
nonce_le(1),
"the two encodings diverge at n = 1"
);
assert_eq!(
nonce_be(0x0102_0304_0506_0708)[4..],
[1, 2, 3, 4, 5, 6, 7, 8],
"most-significant byte first"
);
assert_eq!(
nonce_le(0x0102_0304_0506_0708)[4..],
[8, 7, 6, 5, 4, 3, 2, 1],
"least-significant byte first"
);
}
#[test]
fn chachapoly_decrypt_failure_zeroes_output() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n = ChaChaPoly::encrypt(&ChaChaPoly::key(&key), 0, &[], b"secret payload", &mut ct)
.unwrap();
ct[n - 1] ^= 0xFF; let mut pt = [0xAAu8; 64];
let err =
ChaChaPoly::decrypt(&ChaChaPoly::key(&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"
);
}
mod aes_gcm {
use super::*;
#[test]
fn raw_cryptoxide_verifies_before_writing() {
let key = [0x42u8; 32];
let nonce = [7u8; 12];
let cipher = AesGcm256::new(&key);
let mut ct = [0u8; 14];
let mut tag = Tag([0u8; 16]);
cipher.encrypt(&nonce, b"ad", b"secret payload", &mut ct, &mut tag);
let mut bad = Tag(tag.0);
bad.0[0] ^= 0xFF;
let mut out = [0xAAu8; 14];
assert_eq!(
cipher.decrypt(&nonce, b"ad", &ct, &mut out, &bad),
DecryptionResult::MisMatch
);
assert!(
out.iter().all(|&b| b == 0xAA),
"cryptoxide AES-GCM verifies BEFORE writing: the buffer is untouched \
on mismatch (this is *why* the impl zeroes explicitly)"
);
}
#[test]
fn decrypt_failure_zeroes_output() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n =
AesGcm::encrypt(&AesGcm::key(&key), 0, &[], b"secret payload", &mut ct).unwrap();
let pt_len = n - AesGcm::TAG_SIZE;
let mut corrupted = ct;
corrupted[n - 1] ^= 0xFF;
let mut pt = [0xAAu8; 64];
let err =
AesGcm::decrypt(&AesGcm::key(&key), 0, &[], &corrupted[..n], &mut pt).unwrap_err();
assert!(matches!(err, HandshakeError::DecryptionFailed));
assert!(
pt[..pt_len].iter().all(|&b| b == 0),
"plaintext region must be zeroed on auth failure"
);
let mut pt = [0u8; 64];
AesGcm::decrypt(&AesGcm::key(&key), 0, &[], &ct[..n], &mut pt).unwrap();
assert_eq!(&pt[..pt_len], b"secret payload", "prior message decrypted");
let err =
AesGcm::decrypt(&AesGcm::key(&key), 0, &[], &corrupted[..n], &mut pt).unwrap_err();
assert!(matches!(err, HandshakeError::DecryptionFailed));
assert!(
pt[..pt_len].iter().all(|&b| b == 0),
"stale plaintext from the PREVIOUS message must not survive a failed decrypt"
);
}
#[test]
fn truncated_tags_rejected() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n =
AesGcm::encrypt(&AesGcm::key(&key), 3, b"ad", b"secret payload", &mut ct).unwrap();
let mut out = [0u8; 64];
for short in 0..AesGcm::TAG_SIZE {
assert!(
matches!(
AesGcm::decrypt(&AesGcm::key(&key), 3, b"ad", &ct[..short], &mut out),
Err(HandshakeError::DecryptionFailed)
),
"{short}-byte input is shorter than the tag and must be rejected"
);
}
assert!(
AesGcm::decrypt(&AesGcm::key(&key), 3, b"ad", &ct[..n - 1], &mut out).is_err(),
"a one-byte truncation must not authenticate"
);
}
#[test]
fn every_flipped_tag_bit_rejected() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n =
AesGcm::encrypt(&AesGcm::key(&key), 3, b"ad", b"secret payload", &mut ct).unwrap();
let pt_len = n - AesGcm::TAG_SIZE;
let mut out = [0u8; 64];
for bit in 0..(AesGcm::TAG_SIZE * 8) {
let mut bad = ct;
bad[pt_len + bit / 8] ^= 1 << (bit % 8);
assert!(
AesGcm::decrypt(&AesGcm::key(&key), 3, b"ad", &bad[..n], &mut out).is_err(),
"tag bit {bit} flipped must be rejected"
);
}
assert_eq!(
AesGcm::decrypt(&AesGcm::key(&key), 3, b"ad", &ct[..n], &mut out).unwrap(),
pt_len
);
}
#[test]
fn flipped_ciphertext_rejected() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n =
AesGcm::encrypt(&AesGcm::key(&key), 3, b"ad", b"secret payload", &mut ct).unwrap();
let pt_len = n - AesGcm::TAG_SIZE;
let mut out = [0u8; 64];
for byte in 0..pt_len {
let mut bad = ct;
bad[byte] ^= 0x01;
assert!(
AesGcm::decrypt(&AesGcm::key(&key), 3, b"ad", &bad[..n], &mut out).is_err(),
"ciphertext byte {byte} flipped must be rejected"
);
}
}
#[test]
fn wrong_associated_data_rejected() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let n =
AesGcm::encrypt(&AesGcm::key(&key), 1, b"ad", b"secret payload", &mut ct).unwrap();
let mut out = [0u8; 64];
assert!(AesGcm::decrypt(&AesGcm::key(&key), 1, b"AD", &ct[..n], &mut out).is_err());
assert!(AesGcm::decrypt(&AesGcm::key(&key), 1, &[], &ct[..n], &mut out).is_err());
assert!(AesGcm::decrypt(&AesGcm::key(&key), 2, b"ad", &ct[..n], &mut out).is_err());
assert!(AesGcm::decrypt(&AesGcm::key(&key), 1, b"ad", &ct[..n], &mut out).is_ok());
}
#[test]
fn short_output_buffer_is_its_own_error() {
let key = [0x42u8; 32];
let mut ct = [0u8; 64];
let mut tiny = [0u8; 8];
assert!(matches!(
AesGcm::encrypt(&AesGcm::key(&key), 0, &[], b"secret payload", &mut tiny),
Err(HandshakeError::OutputBufferTooSmall {
needed: 30,
actual: 8
})
));
let n =
AesGcm::encrypt(&AesGcm::key(&key), 0, &[], b"secret payload", &mut ct).unwrap();
assert_eq!(n, 14 + AesGcm::TAG_SIZE, "ciphertext‖tag, tag appended");
let mut tiny = [0u8; 8];
assert!(matches!(
AesGcm::decrypt(&AesGcm::key(&key), 0, &[], &ct[..n], &mut tiny),
Err(HandshakeError::OutputBufferTooSmall {
needed: 14,
actual: 8
})
));
}
#[test]
fn empty_plaintext_round_trips() {
let key = [0x42u8; 32];
let mut ct = [0u8; 32];
let n = AesGcm::encrypt(&AesGcm::key(&key), 0, b"ad", &[], &mut ct).unwrap();
assert_eq!(n, AesGcm::TAG_SIZE, "tag only");
let mut out = [0u8; 32];
assert_eq!(
AesGcm::decrypt(&AesGcm::key(&key), 0, b"ad", &ct[..n], &mut out).unwrap(),
0
);
let mut bad = ct;
bad[0] ^= 0x01;
assert!(AesGcm::decrypt(&AesGcm::key(&key), 0, b"ad", &bad[..n], &mut out).is_err());
}
#[test]
fn key_scrubs_on_drop() {
let (before, after) = unsafe { super::scrub_probe(|| AesGcm::key(&[0xA5u8; 32])) };
assert!(
before > 32,
"the probe must see a live, expanded schedule first (saw {before} non-zero bytes)"
);
assert_eq!(after, 0, "AesGcmKey must leave zeros behind on drop");
}
#[test]
fn noise_constants() {
assert_eq!(AesGcm::NAME, "AESGCM");
assert_eq!(AesGcm::TAG_SIZE, 16, "Noise §12.4: 128-bit tag");
}
}
}