#![allow(dead_code)]
use sha2::{Digest, Sha256};
const CHACHA20_CONSTANT: [u32; 4] = [0x6170_7865, 0x3320_646e, 0x7962_2d32, 0x6b20_6574];
#[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 chacha20_initial_state(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u32; 16] {
let mut state = [0u32; 16];
state[0] = CHACHA20_CONSTANT[0];
state[1] = CHACHA20_CONSTANT[1];
state[2] = CHACHA20_CONSTANT[2];
state[3] = CHACHA20_CONSTANT[3];
for i in 0..8 {
state[4 + i] =
u32::from_le_bytes([key[i * 4], key[i * 4 + 1], key[i * 4 + 2], key[i * 4 + 3]]);
}
state[12] = counter;
state[13] = u32::from_le_bytes([nonce[0], nonce[1], nonce[2], nonce[3]]);
state[14] = u32::from_le_bytes([nonce[4], nonce[5], nonce[6], nonce[7]]);
state[15] = u32::from_le_bytes([nonce[8], nonce[9], nonce[10], nonce[11]]);
state
}
fn chacha20_block(key: &[u8; 32], counter: u32, nonce: &[u8; 12]) -> [u8; 64] {
let initial = chacha20_initial_state(key, counter, nonce);
let mut working = initial;
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);
}
let mut output = [0u8; 64];
for i in 0..16 {
let word = working[i].wrapping_add(initial[i]);
output[i * 4..i * 4 + 4].copy_from_slice(&word.to_le_bytes());
}
output
}
fn chacha20_xor_stream(
key: &[u8; 32],
nonce: &[u8; 12],
start_counter: u32,
data: &[u8],
) -> Vec<u8> {
let mut output = Vec::with_capacity(data.len());
let mut offset = 0usize;
let mut counter: u32 = start_counter;
while offset < data.len() {
let block = chacha20_block(key, counter, nonce);
let remaining = data.len() - offset;
let chunk_len = remaining.min(64);
for i in 0..chunk_len {
output.push(data[offset + i] ^ block[i]);
}
offset += chunk_len;
counter = counter.wrapping_add(1);
}
output
}
fn poly1305_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(key: &[u8; 32], msg: &[u8]) -> [u8; 16] {
let mut r_bytes = [0u8; 16];
r_bytes.copy_from_slice(&key[0..16]);
poly1305_clamp_r(&mut r_bytes);
let s_bytes = &key[16..32];
let r_lo = u64::from_le_bytes([
r_bytes[0], r_bytes[1], r_bytes[2], r_bytes[3], r_bytes[4], r_bytes[5], r_bytes[6],
r_bytes[7],
]);
let r_hi = u64::from_le_bytes([
r_bytes[8],
r_bytes[9],
r_bytes[10],
r_bytes[11],
r_bytes[12],
r_bytes[13],
r_bytes[14],
r_bytes[15],
]);
let r0 = r_lo & 0x3ff_ffff;
let r1 = (r_lo >> 26) & 0x3ff_ffff;
let r2 = ((r_lo >> 52) | (r_hi << 12)) & 0x3ff_ffff;
let r3 = (r_hi >> 14) & 0x3ff_ffff;
let r4 = (r_hi >> 40) & 0x3ff_ffff;
let r1_5 = r1 * 5;
let r2_5 = r2 * 5;
let r3_5 = r3 * 5;
let r4_5 = r4 * 5;
let mut h0: u64 = 0;
let mut h1: u64 = 0;
let mut h2: u64 = 0;
let mut h3: u64 = 0;
let mut h4: u64 = 0;
let mut offset = 0usize;
while offset < msg.len() {
let remaining = msg.len() - offset;
let block_len = remaining.min(16);
let mut block = [0u8; 17];
block[..block_len].copy_from_slice(&msg[offset..offset + block_len]);
block[block_len] = 1;
let b_lo = u64::from_le_bytes([
block[0], block[1], block[2], block[3], block[4], block[5], block[6], block[7],
]);
let b_hi = u64::from_le_bytes([
block[8], block[9], block[10], block[11], block[12], block[13], block[14], block[15],
]);
let n0 = b_lo & 0x3ff_ffff;
let n1 = (b_lo >> 26) & 0x3ff_ffff;
let n2 = ((b_lo >> 52) | (b_hi << 12)) & 0x3ff_ffff;
let n3 = (b_hi >> 14) & 0x3ff_ffff;
let n4 = (b_hi >> 40) | ((block[16] as u64) << 24);
h0 = h0.wrapping_add(n0);
h1 = h1.wrapping_add(n1);
h2 = h2.wrapping_add(n2);
h3 = h3.wrapping_add(n3);
h4 = h4.wrapping_add(n4);
let d0: u64 = h0 * r0 + h1 * r4_5 + h2 * r3_5 + h3 * r2_5 + h4 * r1_5;
let d1: u64 = h0 * r1 + h1 * r0 + h2 * r4_5 + h3 * r3_5 + h4 * r2_5;
let d2: u64 = h0 * r2 + h1 * r1 + h2 * r0 + h3 * r4_5 + h4 * r3_5;
let d3: u64 = h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * r4_5;
let d4: u64 = h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
let c0 = d0 >> 26;
h0 = d0 & 0x3ff_ffff;
let c1 = (d1 + c0) >> 26;
h1 = (d1 + c0) & 0x3ff_ffff;
let c2 = (d2 + c1) >> 26;
h2 = (d2 + c1) & 0x3ff_ffff;
let c3 = (d3 + c2) >> 26;
h3 = (d3 + c2) & 0x3ff_ffff;
let c4 = (d4 + c3) >> 26;
h4 = (d4 + c3) & 0x3ff_ffff;
h0 += c4 * 5;
let c5 = h0 >> 26;
h0 &= 0x3ff_ffff;
h1 += c5;
offset += block_len;
}
let c1 = h1 >> 26;
h1 &= 0x3ff_ffff;
let c2 = (h2 + c1) >> 26;
h2 = (h2 + c1) & 0x3ff_ffff;
let c3 = (h3 + c2) >> 26;
h3 = (h3 + c2) & 0x3ff_ffff;
let c4 = (h4 + c3) >> 26;
h4 = (h4 + c3) & 0x3ff_ffff;
h0 += c4 * 5;
let c5 = h0 >> 26;
h0 &= 0x3ff_ffff;
h1 += c5;
let g0 = h0.wrapping_add(5);
let g_carry0 = g0 >> 26;
let g0 = g0 & 0x3ff_ffff;
let g1 = h1.wrapping_add(g_carry0);
let g_carry1 = g1 >> 26;
let g1 = g1 & 0x3ff_ffff;
let g2 = h2.wrapping_add(g_carry1);
let g_carry2 = g2 >> 26;
let g2 = g2 & 0x3ff_ffff;
let g3 = h3.wrapping_add(g_carry2);
let g_carry3 = g3 >> 26;
let g3 = g3 & 0x3ff_ffff;
let g4 = h4.wrapping_add(g_carry3).wrapping_sub(1u64 << 26);
let mask = (g4 >> 63).wrapping_sub(1); h0 = (h0 & !mask) | (g0 & mask);
h1 = (h1 & !mask) | (g1 & mask);
h2 = (h2 & !mask) | (g2 & mask);
h3 = (h3 & !mask) | (g3 & mask);
h4 = (h4 & !mask) | (g4 & mask);
let f0 = h0 | (h1 << 26);
let f1 = (h1 >> 6) | (h2 << 20);
let f2 = (h2 >> 12) | (h3 << 14);
let f3 = (h3 >> 18) | (h4 << 8);
let s0 = u32::from_le_bytes([s_bytes[0], s_bytes[1], s_bytes[2], s_bytes[3]]) as u64;
let s1 = u32::from_le_bytes([s_bytes[4], s_bytes[5], s_bytes[6], s_bytes[7]]) as u64;
let s2 = u32::from_le_bytes([s_bytes[8], s_bytes[9], s_bytes[10], s_bytes[11]]) as u64;
let s3 = u32::from_le_bytes([s_bytes[12], s_bytes[13], s_bytes[14], s_bytes[15]]) as u64;
let t0 = (f0 & 0xffff_ffff) + s0;
let t1 = (f1 & 0xffff_ffff) + s1 + (t0 >> 32);
let t2 = (f2 & 0xffff_ffff) + s2 + (t1 >> 32);
let t3 = (f3 & 0xffff_ffff) + s3 + (t2 >> 32);
let mut tag = [0u8; 16];
tag[0..4].copy_from_slice(&(t0 as u32).to_le_bytes());
tag[4..8].copy_from_slice(&(t1 as u32).to_le_bytes());
tag[8..12].copy_from_slice(&(t2 as u32).to_le_bytes());
tag[12..16].copy_from_slice(&(t3 as u32).to_le_bytes());
tag
}
#[inline]
fn pad16(buf: &mut Vec<u8>, current_len: usize) {
let remainder = current_len % 16;
if remainder != 0 {
let padding = 16 - remainder;
buf.extend_from_slice(&vec![0u8; padding]);
}
}
fn chacha20_poly1305_seal(
key: &[u8; 32],
nonce: &[u8; 12],
plaintext: &[u8],
aad: &[u8],
) -> Vec<u8> {
let otk_block = chacha20_block(key, 0, nonce);
let mut otk = [0u8; 32];
otk.copy_from_slice(&otk_block[0..32]);
let ciphertext = chacha20_xor_stream(key, nonce, 1, plaintext);
let mut mac_data: Vec<u8> = Vec::new();
mac_data.extend_from_slice(aad);
pad16(&mut mac_data, aad.len());
mac_data.extend_from_slice(&ciphertext);
pad16(&mut mac_data, ciphertext.len());
mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
let tag = poly1305_mac(&otk, &mac_data);
let mut output = ciphertext;
output.extend_from_slice(&tag);
output
}
fn chacha20_poly1305_open(
key: &[u8; 32],
nonce: &[u8; 12],
ciphertext_with_tag: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, String> {
if ciphertext_with_tag.len() < 16 {
return Err(
"chacha20-poly1305: ciphertext_with_tag too short (need ≥ 16 bytes)".to_string(),
);
}
let ct_len = ciphertext_with_tag.len() - 16;
let ciphertext = &ciphertext_with_tag[..ct_len];
let received_tag = &ciphertext_with_tag[ct_len..];
let otk_block = chacha20_block(key, 0, nonce);
let mut otk = [0u8; 32];
otk.copy_from_slice(&otk_block[0..32]);
let mut mac_data: Vec<u8> = Vec::new();
mac_data.extend_from_slice(aad);
pad16(&mut mac_data, aad.len());
mac_data.extend_from_slice(ciphertext);
pad16(&mut mac_data, ciphertext.len());
mac_data.extend_from_slice(&(aad.len() as u64).to_le_bytes());
mac_data.extend_from_slice(&(ciphertext.len() as u64).to_le_bytes());
let expected_tag = poly1305_mac(&otk, &mac_data);
let mut diff = 0u8;
for i in 0..16 {
diff |= received_tag[i] ^ expected_tag[i];
}
if diff != 0 {
return Err("chacha20-poly1305: authentication tag mismatch".to_string());
}
Ok(chacha20_xor_stream(key, nonce, 1, ciphertext))
}
#[derive(Debug, Clone)]
pub struct ChaChaConfig {
pub rounds: u8,
pub tag_len: usize,
}
impl Default for ChaChaConfig {
fn default() -> Self {
Self {
rounds: 20,
tag_len: 16,
}
}
}
#[derive(Debug, Clone)]
pub struct ChaChaCipher {
pub config: ChaChaConfig,
key: [u8; 32],
}
impl ChaChaCipher {
pub fn new(key: [u8; 32], config: ChaChaConfig) -> Self {
Self { config, key }
}
pub fn default_cipher(key: [u8; 32]) -> Self {
Self::new(key, ChaChaConfig::default())
}
}
pub fn chacha_encrypt(key: &[u8], nonce: &[u8; 12], plaintext: &[u8]) -> Result<Vec<u8>, String> {
let key32 = key_to_32(key)?;
let ct_and_tag = chacha20_poly1305_seal(&key32, nonce, plaintext, &[]);
let mut out = Vec::with_capacity(12 + ct_and_tag.len());
out.extend_from_slice(nonce);
out.extend_from_slice(&ct_and_tag);
Ok(out)
}
pub fn chacha_decrypt(key: &[u8], packet: &[u8]) -> Result<Vec<u8>, String> {
if packet.len() < 28 {
return Err("chacha_decrypt: packet too short (need nonce + tag = 28 bytes)".to_string());
}
let key32 = key_to_32(key)?;
let nonce: [u8; 12] = [
packet[0], packet[1], packet[2], packet[3], packet[4], packet[5], packet[6], packet[7],
packet[8], packet[9], packet[10], packet[11],
];
let ct_with_tag = &packet[12..];
chacha20_poly1305_open(&key32, &nonce, ct_with_tag, &[])
}
pub fn chacha_encrypt_aad(
key: &[u8],
nonce: &[u8; 12],
plaintext: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, String> {
let key32 = key_to_32(key)?;
Ok(chacha20_poly1305_seal(&key32, nonce, plaintext, aad))
}
pub fn chacha_decrypt_aad(
key: &[u8],
nonce: &[u8; 12],
ciphertext_with_tag: &[u8],
aad: &[u8],
) -> Result<Vec<u8>, String> {
let key32 = key_to_32(key)?;
chacha20_poly1305_open(&key32, nonce, ciphertext_with_tag, aad)
}
pub fn chacha_key_from_seed(seed: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(seed);
hasher.finalize().into()
}
pub fn chacha_nonce_len() -> usize {
12
}
pub fn chacha_roundtrip_ok(data: &[u8]) -> bool {
let key = [0x42u8; 32];
let nonce = [0u8; 12];
match chacha_encrypt(&key, &nonce, data) {
Ok(enc) => chacha_decrypt(&key, &enc)
.map(|d| d == data)
.unwrap_or(false),
Err(_) => false,
}
}
fn key_to_32(key: &[u8]) -> Result<[u8; 32], String> {
if key.len() != 32 {
return Err(format!(
"chacha20: key must be exactly 32 bytes, got {}",
key.len()
));
}
let mut out = [0u8; 32];
out.copy_from_slice(key);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kat_chacha20_poly1305() {
let key = hex::decode("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
.expect("key hex");
let nonce_bytes = hex::decode("070000004041424344454647").expect("nonce hex");
let aad = hex::decode("50515253c0c1c2c3c4c5c6c7").expect("aad hex");
let plaintext = hex::decode(
"4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f6620273939",
)
.expect("pt hex");
let expected_ct = hex::decode(
"d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca96712",
)
.expect("ct hex");
let expected_tag = hex::decode("f180d4e9016c65a7dde15e3106075ebd").expect("tag hex");
let nonce: [u8; 12] = nonce_bytes.try_into().expect("nonce len");
let result =
chacha_encrypt_aad(&key, &nonce, &plaintext, &aad).expect("encrypt_aad failed");
let ct = &result[..result.len() - 16];
let tag = &result[result.len() - 16..];
assert_eq!(ct, expected_ct.as_slice(), "ciphertext mismatch");
assert_eq!(tag, expected_tag.as_slice(), "tag mismatch");
let decrypted =
chacha_decrypt_aad(&key, &nonce, &result, &aad).expect("decrypt_aad failed");
assert_eq!(decrypted, plaintext, "decryption round-trip failed");
}
#[test]
fn kat_chacha20_block() {
let key = hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
.expect("key hex");
let nonce = hex::decode("000000090000004a00000000").expect("nonce hex");
let counter: u32 = 1;
let expected = hex::decode(concat!(
"10f1e7e4d13b5915500fdd1fa32071c4",
"c7d1f4c733c068030422aa9ac3d46c4e",
"d2826446079faa0914c2d705d98b02a2",
"b5129cd1de164eb9cbd083e8a2503c4e",
))
.expect("expected hex");
let key32: [u8; 32] = key.try_into().expect("key len");
let nonce12: [u8; 12] = nonce.try_into().expect("nonce len");
let block = chacha20_block(&key32, counter, &nonce12);
assert_eq!(&block[..], expected.as_slice(), "block mismatch");
}
#[test]
fn test_default_rounds() {
assert_eq!(ChaChaConfig::default().rounds, 20);
}
#[test]
fn test_nonce_len() {
assert_eq!(chacha_nonce_len(), 12);
}
#[test]
fn test_encrypt_output_len() {
let key = [0u8; 32];
let nonce = [0u8; 12];
let enc = chacha_encrypt(&key, &nonce, b"hello").expect("encrypt");
assert_eq!(enc.len(), 33);
}
#[test]
fn test_roundtrip_hello() {
assert!(chacha_roundtrip_ok(b"Hello ChaCha20!"));
}
#[test]
fn test_roundtrip_binary() {
let data: Vec<u8> = (0u8..=255).collect();
assert!(chacha_roundtrip_ok(&data));
}
#[test]
fn test_roundtrip_empty() {
assert!(chacha_roundtrip_ok(&[]));
}
#[test]
fn test_decrypt_short() {
let key = [0u8; 32];
assert!(chacha_decrypt(&key, &[0u8; 10]).is_err());
}
#[test]
fn test_bad_key_len() {
let key = [0u8; 16]; let nonce = [0u8; 12];
assert!(chacha_encrypt(&key, &nonce, b"data").is_err());
}
#[test]
fn test_key_from_seed_len() {
let k = chacha_key_from_seed(b"seed_bytes");
assert_eq!(k.len(), 32);
}
#[test]
fn test_cipher_new() {
let key = [0u8; 32];
let c = ChaChaCipher::default_cipher(key);
assert_eq!(c.config.rounds, 20);
}
#[test]
fn test_different_keys_give_different_ciphertext() {
let nonce = [0u8; 12];
let k1 = [0u8; 32];
let k2 = [1u8; 32];
let e1 = chacha_encrypt(&k1, &nonce, b"data").expect("e1");
let e2 = chacha_encrypt(&k2, &nonce, b"data").expect("e2");
assert_ne!(e1[28..], e2[28..]);
}
#[test]
fn test_wrong_key_decrypt_fails() {
let k_enc = [0u8; 32];
let k_dec = [1u8; 32];
let nonce = [0u8; 12];
let enc = chacha_encrypt(&k_enc, &nonce, b"secret").expect("enc");
assert!(chacha_decrypt(&k_dec, &enc).is_err());
}
#[test]
fn test_aad_roundtrip() {
let key = [0xABu8; 32];
let nonce = [0x01u8; 12];
let plaintext = b"authenticate me";
let aad = b"header";
let enc = chacha_encrypt_aad(&key, &nonce, plaintext, aad).expect("enc");
let dec = chacha_decrypt_aad(&key, &nonce, &enc, aad).expect("dec");
assert_eq!(dec, plaintext);
}
#[test]
fn test_aad_tamper_fails() {
let key = [0xABu8; 32];
let nonce = [0x01u8; 12];
let plaintext = b"authenticate me";
let aad = b"header";
let enc = chacha_encrypt_aad(&key, &nonce, plaintext, aad).expect("enc");
assert!(chacha_decrypt_aad(&key, &nonce, &enc, b"WRONG").is_err());
}
#[test]
fn test_ciphertext_tamper_fails() {
let key = [0xCDu8; 32];
let nonce = [0x07u8; 12];
let mut enc = chacha_encrypt(&key, &nonce, b"tamper test").expect("enc");
let last = enc.len() - 1;
enc[last] ^= 0xff;
assert!(chacha_decrypt(&key, &enc).is_err());
}
}