use super::chacha20::ChaCha20;
use super::poly1305::Poly1305;
pub struct ChaCha20Poly1305;
impl ChaCha20Poly1305 {
pub fn encrypt(key: &[u8; 32], nonce: &[u8; 12], aad: &[u8], plaintext: &[u8]) -> (Vec<u8>, [u8; 16]) {
let poly_key = poly_key_gen(key, nonce);
let mut ct = plaintext.to_vec();
let mut cipher = ChaCha20::new(key, nonce, 1);
cipher.apply_keystream(&mut ct);
let tag = compute_tag(&poly_key, aad, &ct);
(ct, tag)
}
pub fn decrypt(key: &[u8; 32], nonce: &[u8; 12], aad: &[u8], ciphertext: &[u8], tag: &[u8; 16]) -> Option<Vec<u8>> {
let poly_key = poly_key_gen(key, nonce);
let expected_tag = compute_tag(&poly_key, aad, ciphertext);
let mut diff = 0u8;
for i in 0..16 {
diff |= expected_tag[i] ^ tag[i];
}
if diff != 0 {
return None;
}
let mut pt = ciphertext.to_vec();
let mut cipher = ChaCha20::new(key, nonce, 1);
cipher.apply_keystream(&mut pt);
Some(pt)
}
}
fn poly_key_gen(key: &[u8; 32], nonce: &[u8; 12]) -> [u8; 32] {
let mut buf = [0u8; 32];
let mut cipher = ChaCha20::new(key, nonce, 0);
cipher.apply_keystream(&mut buf);
buf
}
fn compute_tag(poly_key: &[u8; 32], aad: &[u8], ct: &[u8]) -> [u8; 16] {
let mut poly = Poly1305::new(poly_key);
poly.update(aad);
poly.update(&zero_pad_to_16(aad.len()));
poly.update(ct);
poly.update(&zero_pad_to_16(ct.len()));
let mut len_bytes = [0u8; 16];
len_bytes[0..8].copy_from_slice(&(aad.len() as u64).to_le_bytes());
len_bytes[8..16].copy_from_slice(&(ct.len() as u64).to_le_bytes());
poly.update(&len_bytes);
poly.finalize()
}
fn zero_pad_to_16(n: usize) -> Vec<u8> {
let pad_len = (16 - (n % 16)) % 16;
vec![0u8; pad_len]
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(s: &str) -> Vec<u8> {
let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();
assert!(s.len() % 2 == 0, "odd hex length: {}", s.len());
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
fn hex_arr<const N: usize>(s: &str) -> [u8; N] {
let v = hex(s);
assert_eq!(v.len(), N);
let mut out = [0u8; N];
out.copy_from_slice(&v);
out
}
#[test]
fn rfc8439_2_6_2_poly_key_gen() {
let key: [u8; 32] = hex_arr("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");
let nonce: [u8; 12] = hex_arr("000000000001020304050607");
let pk = poly_key_gen(&key, &nonce);
let expected = hex("8ad5a08b905f81cc815040274ab29471\
a833b637e3fd0da508dbb8e2fdd1a646");
assert_eq!(pk.to_vec(), expected);
}
#[test]
fn rfc8439_2_8_2_aead_vector() {
let key: [u8; 32] = hex_arr("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");
let nonce: [u8; 12] = hex_arr("070000004041424344454647");
let aad = hex("50515253c0c1c2c3c4c5c6c7");
let plaintext = b"Ladies and Gentlemen of the class of '99: \
If I could offer you only one tip for the future, sunscreen \
would be it.";
let (ct, tag) = ChaCha20Poly1305::encrypt(&key, &nonce, &aad, plaintext);
let expected_ct = hex("d31a8d34648e60db7b86afbc53ef7ec2
a4aded51296e08fea9e2b5a736ee62d6
3dbea45e8ca9671282fafb69da92728b
1a71de0a9e060b2905d6a5b67ecd3b36
92ddbd7f2d778b8c9803aee328091b58
fab324e4fad675945585808b4831d7bc
3ff4def08e4b7a9de576d26586cec64b
6116");
let expected_tag = hex("1ae10b594f09e26a7e902ecbd0600691");
assert_eq!(ct, expected_ct);
assert_eq!(tag.to_vec(), expected_tag);
let mut tag_arr = [0u8; 16];
tag_arr.copy_from_slice(&expected_tag);
let pt = ChaCha20Poly1305::decrypt(&key, &nonce, &aad, &expected_ct, &tag_arr).expect("authentic");
assert_eq!(pt, plaintext);
}
#[test]
fn aead_roundtrip_random_inputs() {
let key = [0x42u8; 32];
let nonce = [0xa5u8; 12];
let aad = b"some context";
let pt = b"hello world; this is a test of moderate length to span more than one ChaCha20 block.";
let (ct, tag) = ChaCha20Poly1305::encrypt(&key, &nonce, aad, pt);
assert_ne!(ct.as_slice(), pt.as_slice());
let back = ChaCha20Poly1305::decrypt(&key, &nonce, aad, &ct, &tag).expect("authentic");
assert_eq!(back.as_slice(), pt.as_slice());
}
#[test]
fn aead_rejects_tampered_ciphertext() {
let key = [0x01u8; 32];
let nonce = [0x02u8; 12];
let pt = b"do not modify";
let (mut ct, tag) = ChaCha20Poly1305::encrypt(&key, &nonce, b"", pt);
ct[0] ^= 0x01;
assert!(ChaCha20Poly1305::decrypt(&key, &nonce, b"", &ct, &tag).is_none());
}
#[test]
fn aead_rejects_tampered_tag() {
let key = [0x01u8; 32];
let nonce = [0x02u8; 12];
let pt = b"do not modify";
let (ct, mut tag) = ChaCha20Poly1305::encrypt(&key, &nonce, b"", pt);
tag[0] ^= 0x01;
assert!(ChaCha20Poly1305::decrypt(&key, &nonce, b"", &ct, &tag).is_none());
}
#[test]
fn aead_rejects_modified_aad() {
let key = [0xffu8; 32];
let nonce = [0x10u8; 12];
let aad = b"context-A";
let pt = b"shared payload";
let (ct, tag) = ChaCha20Poly1305::encrypt(&key, &nonce, aad, pt);
assert!(ChaCha20Poly1305::decrypt(&key, &nonce, b"context-B", &ct, &tag).is_none());
}
#[test]
fn aead_rejects_wrong_key() {
let key1 = [0x33u8; 32];
let mut key2 = key1;
key2[0] ^= 0x01;
let nonce = [0x44u8; 12];
let pt = b"sensitive";
let (ct, tag) = ChaCha20Poly1305::encrypt(&key1, &nonce, b"", pt);
assert!(ChaCha20Poly1305::decrypt(&key2, &nonce, b"", &ct, &tag).is_none());
}
#[test]
fn aead_empty_plaintext() {
let key = [0x55u8; 32];
let nonce = [0x66u8; 12];
let aad = b"only-context";
let (ct, tag) = ChaCha20Poly1305::encrypt(&key, &nonce, aad, b"");
assert!(ct.is_empty());
let back = ChaCha20Poly1305::decrypt(&key, &nonce, aad, &ct, &tag).unwrap();
assert!(back.is_empty());
assert!(ChaCha20Poly1305::decrypt(&key, &nonce, b"other", &ct, &tag).is_none());
}
}