use alloc::vec::Vec;
use core::convert::TryInto;
pub type Key = [u8; 32];
pub type XNonce = [u8; 24];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AeadError;
pub struct Payload<'a> {
pub msg: &'a [u8],
pub aad: &'a [u8],
}
pub struct XChaCha20Poly1305 {
key: Key,
}
impl XChaCha20Poly1305 {
pub fn new(key: &Key) -> Self {
Self { key: *key }
}
pub fn encrypt(&self, nonce: &XNonce, payload: Payload<'_>) -> Result<Vec<u8>, AeadError> {
let ciphertext = xchacha20_encrypt(&self.key, nonce, payload.msg);
let tag = poly1305_tag(&self.key, nonce, &ciphertext, payload.aad);
let mut output = Vec::with_capacity(ciphertext.len() + TAG_LEN);
output.extend_from_slice(&ciphertext);
output.extend_from_slice(&tag);
Ok(output)
}
pub fn decrypt(&self, nonce: &XNonce, payload: Payload<'_>) -> Result<Vec<u8>, AeadError> {
if payload.msg.len() < TAG_LEN {
return Err(AeadError);
}
let split = payload.msg.len() - TAG_LEN;
let (ciphertext, tag) = payload.msg.split_at(split);
let expected = poly1305_tag(&self.key, nonce, ciphertext, payload.aad);
if !constant_time_eq(&expected, tag) {
return Err(AeadError);
}
Ok(xchacha20_decrypt(&self.key, nonce, ciphertext))
}
}
const TAG_LEN: usize = 16;
fn chacha20_block(state: &[u32; 16]) -> [u8; 64] {
let mut working = *state;
for _ in 0..10 {
quarter_round(&mut working, 0, 4, 8, 12);
quarter_round(&mut working, 1, 5, 9, 13);
quarter_round(&mut working, 2, 6, 10, 14);
quarter_round(&mut working, 3, 7, 11, 15);
quarter_round(&mut working, 0, 5, 10, 15);
quarter_round(&mut working, 1, 6, 11, 12);
quarter_round(&mut working, 2, 7, 8, 13);
quarter_round(&mut working, 3, 4, 9, 14);
}
for i in 0..16 {
working[i] = working[i].wrapping_add(state[i]);
}
let mut output = [0_u8; 64];
for (i, word) in working.iter().enumerate() {
output[4 * i..4 * i + 4].copy_from_slice(&word.to_le_bytes());
}
output
}
#[inline(always)]
fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
state[a] = state[a].wrapping_add(state[b]);
state[d] ^= state[a];
state[d] = state[d].rotate_left(16);
state[c] = state[c].wrapping_add(state[d]);
state[b] ^= state[c];
state[b] = state[b].rotate_left(12);
state[a] = state[a].wrapping_add(state[b]);
state[d] ^= state[a];
state[d] = state[d].rotate_left(8);
state[c] = state[c].wrapping_add(state[d]);
state[b] ^= state[c];
state[b] = state[b].rotate_left(7);
}
fn init_state(key: &Key, counter: u32, nonce: &[u8]) -> [u32; 16] {
let mut state = [0_u32; 16];
state[0] = 0x6170_7865; state[1] = 0x3320_646e; state[2] = 0x7962_2d32; state[3] = 0x6b20_6574; for i in 0..8 {
state[4 + i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().expect("key word"));
}
state[12] = counter;
for i in 0..3 {
state[13 + i] = u32::from_le_bytes(nonce[4 * i..4 * i + 4].try_into().expect("nonce word"));
}
state
}
fn chacha20_keystream(key: &Key, nonce: &[u8; 12], counter: u32, len: usize) -> Vec<u8> {
let mut output = Vec::with_capacity(len);
let mut block_counter = counter;
while output.len() < len {
let state = init_state(key, block_counter, nonce);
let block = chacha20_block(&state);
let take = core::cmp::min(len - output.len(), 64);
output.extend_from_slice(&block[..take]);
block_counter = block_counter.wrapping_add(1);
}
output
}
fn hchacha20(key: &Key, nonce16: &[u8; 16]) -> [u8; 32] {
let mut state = [0_u32; 16];
state[0] = 0x6170_7865;
state[1] = 0x3320_646e;
state[2] = 0x7962_2d32;
state[3] = 0x6b20_6574;
for i in 0..8 {
state[4 + i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().expect("key word"));
}
for i in 0..4 {
state[12 + i] =
u32::from_le_bytes(nonce16[4 * i..4 * i + 4].try_into().expect("nonce word"));
}
for _ in 0..10 {
quarter_round(&mut state, 0, 4, 8, 12);
quarter_round(&mut state, 1, 5, 9, 13);
quarter_round(&mut state, 2, 6, 10, 14);
quarter_round(&mut state, 3, 7, 11, 15);
quarter_round(&mut state, 0, 5, 10, 15);
quarter_round(&mut state, 1, 6, 11, 12);
quarter_round(&mut state, 2, 7, 8, 13);
quarter_round(&mut state, 3, 4, 9, 14);
}
let mut output = [0_u8; 32];
let words = [
state[0], state[1], state[2], state[3], state[12], state[13], state[14], state[15],
];
for (i, word) in words.iter().enumerate() {
output[4 * i..4 * i + 4].copy_from_slice(&word.to_le_bytes());
}
output
}
fn xchacha20_keystream(key: &Key, nonce: &XNonce, counter: u32, len: usize) -> Vec<u8> {
let subkey = hchacha20(key, nonce[..16].try_into().expect("16-byte nonce"));
let mut chacha_nonce = [0_u8; 12];
chacha_nonce[4..12].copy_from_slice(&nonce[16..24]);
chacha20_keystream(&subkey, &chacha_nonce, counter, len)
}
fn xchacha20_encrypt(key: &Key, nonce: &XNonce, plaintext: &[u8]) -> Vec<u8> {
let keystream = xchacha20_keystream(key, nonce, 1, plaintext.len());
plaintext
.iter()
.zip(keystream.iter())
.map(|(p, k)| p ^ k)
.collect()
}
fn xchacha20_decrypt(key: &Key, nonce: &XNonce, ciphertext: &[u8]) -> Vec<u8> {
xchacha20_encrypt(key, nonce, ciphertext)
}
fn poly1305(key: &Key, nonce: &XNonce, msg: &[u8]) -> [u8; 16] {
let keystream = xchacha20_keystream(key, nonce, 0, 64);
let mut r = [0_u8; 16];
r.copy_from_slice(&keystream[..16]);
clamp_r(&mut r);
let mut s = [0_u8; 16];
s.copy_from_slice(&keystream[16..32]);
poly1305_mac(&r, &s, msg)
}
fn clamp_r(r: &mut [u8; 16]) {
r[3] &= 0x0f;
r[7] &= 0x0f;
r[11] &= 0x0f;
r[15] &= 0x0f;
r[4] &= 0xfc;
r[8] &= 0xfc;
r[12] &= 0xfc;
}
fn poly1305_mac(r: &[u8; 16], s: &[u8; 16], msg: &[u8]) -> [u8; 16] {
let mut r0 = u32::from_le_bytes([r[0], r[1], r[2], r[3]]) & 0x3ff_ffff;
let mut r1 = (u32::from_le_bytes([r[3], r[4], r[5], r[6]]) >> 2) & 0x3ff_ffff;
let mut r2 = (u32::from_le_bytes([r[6], r[7], r[8], r[9]]) >> 4) & 0x3ff_ffff;
let mut r3 = (u32::from_le_bytes([r[9], r[10], r[11], r[12]]) >> 6) & 0x3ff_ffff;
let mut r4 = (u32::from_le_bytes([r[12], r[13], r[14], r[15]]) >> 8) & 0x3ff_ffff;
r0 &= 0x3ff_ffff;
r1 &= 0x3ff_ffff;
r2 &= 0x3ff_ffff;
r3 &= 0x3ff_ffff;
r4 &= 0x3ff_ffff;
let s0 = u32::from_le_bytes([s[0], s[1], s[2], s[3]]);
let s1 = u32::from_le_bytes([s[4], s[5], s[6], s[7]]);
let s2 = u32::from_le_bytes([s[8], s[9], s[10], s[11]]);
let s3 = u32::from_le_bytes([s[12], s[13], s[14], s[15]]);
let mut h = [0_u32; 5];
let mut chunks = msg.chunks_exact(16);
for chunk in &mut chunks {
let mut block = [0_u8; 17];
block[..16].copy_from_slice(chunk);
block[16] = 1;
poly1305_accumulate(&mut h, &block, r0, r1, r2, r3, r4);
}
let remainder = chunks.remainder();
if !remainder.is_empty() {
let mut block = [0_u8; 17];
block[..remainder.len()].copy_from_slice(remainder);
block[remainder.len()] = 1;
poly1305_accumulate(&mut h, &block, r0, r1, r2, r3, r4);
}
let h_low = (h[0] as u128)
| ((h[1] as u128) << 26)
| ((h[2] as u128) << 52)
| ((h[3] as u128) << 78)
| (((h[4] & 0x00ff_ffff) as u128) << 104);
let s128 = (s0 as u128) | ((s1 as u128) << 32) | ((s2 as u128) << 64) | ((s3 as u128) << 96);
let sum = h_low.wrapping_add(s128);
sum.to_le_bytes()
}
fn poly1305_accumulate(
h: &mut [u32; 5],
block: &[u8; 17],
r0: u32,
r1: u32,
r2: u32,
r3: u32,
r4: u32,
) {
let b0 = u32::from_le_bytes([block[0], block[1], block[2], block[3]]) & 0x3ff_ffff;
let b1 = (u32::from_le_bytes([block[3], block[4], block[5], block[6]]) >> 2) & 0x3ff_ffff;
let b2 = (u32::from_le_bytes([block[6], block[7], block[8], block[9]]) >> 4) & 0x3ff_ffff;
let b3 = (u32::from_le_bytes([block[9], block[10], block[11], block[12]]) >> 6) & 0x3ff_ffff;
let b4 = (u32::from_le_bytes([block[12], block[13], block[14], block[15]]) >> 8) & 0x3ff_ffff;
let b4 = b4 | (block[16] as u32) << 24;
let mut h0 = h[0] as u64 + b0 as u64;
let mut h1 = h[1] as u64 + b1 as u64;
let mut h2 = h[2] as u64 + b2 as u64;
let mut h3 = h[3] as u64 + b3 as u64;
let mut h4 = h[4] as u64 + b4 as u64;
let d0 = h0 * r0 as u64
+ h1 * (5 * r4 as u64)
+ h2 * (5 * r3 as u64)
+ h3 * (5 * r2 as u64)
+ h4 * (5 * r1 as u64);
let d1 = h0 * r1 as u64
+ h1 * r0 as u64
+ h2 * (5 * r4 as u64)
+ h3 * (5 * r3 as u64)
+ h4 * (5 * r2 as u64);
let d2 = h0 * r2 as u64
+ h1 * r1 as u64
+ h2 * r0 as u64
+ h3 * (5 * r4 as u64)
+ h4 * (5 * r3 as u64);
let d3 =
h0 * r3 as u64 + h1 * r2 as u64 + h2 * r1 as u64 + h3 * r0 as u64 + h4 * (5 * r4 as u64);
let d4 = h0 * r4 as u64 + h1 * r3 as u64 + h2 * r2 as u64 + h3 * r1 as u64 + h4 * r0 as u64;
let mut carry = d0 >> 26;
h0 = d0 & 0x3ff_ffff;
carry += d1;
h1 = carry & 0x3ff_ffff;
carry >>= 26;
carry += d2;
h2 = carry & 0x3ff_ffff;
carry >>= 26;
carry += d3;
h3 = carry & 0x3ff_ffff;
carry >>= 26;
carry += d4;
h4 = carry & 0x3ff_ffff;
carry >>= 26;
h0 += carry * 5;
carry = h0 >> 26;
h0 &= 0x3ff_ffff;
h1 += carry;
carry = h1 >> 26;
h1 &= 0x3ff_ffff;
h2 += carry;
h[0] = h0 as u32;
h[1] = h1 as u32;
h[2] = h2 as u32;
h[3] = h3 as u32;
h[4] = h4 as u32;
}
fn poly1305_tag(key: &Key, nonce: &XNonce, ciphertext: &[u8], aad: &[u8]) -> [u8; 16] {
let aad_padded = pad_to_16(aad.len());
let ct_padded = pad_to_16(ciphertext.len());
let total = aad.len() + aad_padded + ciphertext.len() + ct_padded + 16;
let mut message = Vec::with_capacity(total);
message.extend_from_slice(aad);
message.extend_from_slice(&[0_u8; 16][..aad_padded]);
message.extend_from_slice(ciphertext);
message.extend_from_slice(&[0_u8; 16][..ct_padded]);
message.extend_from_slice(&(aad.len() as u64).to_le_bytes());
message.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
poly1305(key, nonce, &message)
}
fn pad_to_16(len: usize) -> usize {
(16 - (len % 16)) % 16
}
fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let mut diff = 0_u8;
for (l, r) in left.iter().zip(right.iter()) {
diff |= l ^ r;
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(encoded: &str) -> Vec<u8> {
(0..encoded.len())
.step_by(2)
.map(|i| u8::from_str_radix(&encoded[i..i + 2], 16).expect("valid hex"))
.collect()
}
#[test]
fn rfc8439_chacha20_block_vector() {
let key: Key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
.try_into()
.unwrap();
let nonce: [u8; 12] = hex("000000090000004a00000000").try_into().unwrap();
let keystream = chacha20_keystream(&key, &nonce, 1, 64);
assert_eq!(
keystream,
hex("10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4ed2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e")
);
}
#[test]
fn rfc8439_chacha20_encryption_vector() {
let key: Key = hex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
.try_into()
.unwrap();
let nonce: [u8; 12] = hex("000000000000004a00000000").try_into().unwrap();
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 encrypted = chacha20_keystream(&key, &nonce, 1, plaintext.len());
let ciphertext: Vec<u8> = plaintext
.iter()
.zip(encrypted.iter())
.map(|(p, k)| p ^ k)
.collect();
assert_eq!(
ciphertext,
hex("6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0bf91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d807ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab77937365af90bbf74a35be6b40b8eedf2785e42874d")
);
}
#[test]
fn rfc8439_poly1305_vector() {
let key = hex("85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b");
let msg = b"Cryptographic Forum Research Group";
let tag = poly1305_oneshot(&key, msg);
assert_eq!(tag[..], hex("a8061dc1305136c6c22b8baf0c0127a9")[..]);
}
fn poly1305_oneshot(key: &[u8], msg: &[u8]) -> [u8; 16] {
let mut r = [0_u8; 16];
r.copy_from_slice(&key[..16]);
clamp_r(&mut r);
let mut s = [0_u8; 16];
s.copy_from_slice(&key[16..]);
poly1305_mac(&r, &s, msg)
}
#[test]
fn rfc8439_aead_vector() {
let key: Key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
.try_into()
.unwrap();
let nonce: [u8; 12] = hex("070000004041424344454647").try_into().unwrap();
let aad = hex("50515253c0c1c2c3c4c5c6c7");
let plaintext = hex(
"4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e",
);
let ciphertext = hex(
"d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116",
);
let expected_tag = hex("1ae10b594f09e26a7e902ecbd0600691");
let one_time_block = chacha20_keystream(&key, &nonce, 0, 64);
let keystream = chacha20_keystream(&key, &nonce, 1, plaintext.len());
let computed_ciphertext: Vec<u8> = plaintext
.iter()
.zip(keystream.iter())
.map(|(p, k)| p ^ k)
.collect();
assert_eq!(computed_ciphertext, ciphertext);
let mut message = Vec::new();
message.extend_from_slice(&aad);
message.extend_from_slice(&[0_u8; 16][..pad_to_16(aad.len())]);
message.extend_from_slice(&ciphertext);
message.extend_from_slice(&[0_u8; 16][..pad_to_16(ciphertext.len())]);
message.extend_from_slice(&(aad.len() as u64).to_le_bytes());
message.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
let mut r = [0_u8; 16];
r.copy_from_slice(&one_time_block[..16]);
clamp_r(&mut r);
let mut s = [0_u8; 16];
s.copy_from_slice(&one_time_block[16..32]);
let tag = poly1305_mac(&r, &s, &message);
assert_eq!(tag[..], expected_tag[..]);
}
#[test]
fn xchacha20_roundtrip() {
let cipher = XChaCha20Poly1305::new(&[0x42; 32]);
let nonce = [0x99; 24];
let plaintext = b"attack at dawn";
let aad = b"header";
let sealed = cipher
.encrypt(
&nonce,
Payload {
msg: plaintext,
aad,
},
)
.unwrap();
assert_eq!(sealed.len(), plaintext.len() + 16);
let opened = cipher
.decrypt(&nonce, Payload { msg: &sealed, aad })
.unwrap();
assert_eq!(opened, plaintext);
}
#[test]
fn xchacha20_rejects_tampering() {
let cipher = XChaCha20Poly1305::new(&[0x5a; 32]);
let nonce = [0x11; 24];
let sealed = cipher
.encrypt(
&nonce,
Payload {
msg: b"payload",
aad: b"aad",
},
)
.unwrap();
for i in 0..sealed.len() {
let mut tampered = sealed.clone();
tampered[i] ^= 0x01;
assert!(
cipher
.decrypt(
&nonce,
Payload {
msg: &tampered,
aad: b"aad"
}
)
.is_err(),
"tamper at byte {i} was not detected"
);
}
assert!(cipher
.decrypt(
&nonce,
Payload {
msg: &sealed,
aad: b"other"
}
)
.is_err());
let mut wrong_nonce = nonce;
wrong_nonce[0] ^= 1;
assert!(cipher
.decrypt(
&wrong_nonce,
Payload {
msg: &sealed,
aad: b"aad"
}
)
.is_err());
}
#[test]
fn xchacha20_nonces_are_independent() {
let cipher = XChaCha20Poly1305::new(&[0x7c; 32]);
let a = cipher
.encrypt(
&[1; 24],
Payload {
msg: b"same",
aad: b"",
},
)
.unwrap();
let b = cipher
.encrypt(
&[2; 24],
Payload {
msg: b"same",
aad: b"",
},
)
.unwrap();
assert_ne!(a, b);
}
}