#[inline(always)]
pub(crate) 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 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);
}
let mut out = [0u8; 64];
for i in 0..16 {
let word = working[i].wrapping_add(state[i]);
out[4 * i..4 * i + 4].copy_from_slice(&word.to_le_bytes());
}
out
}
#[derive(Clone)]
pub struct ChaCha20 {
state: [u32; 16],
buffer: [u8; 64],
buf_pos: usize,
}
impl ChaCha20 {
pub fn new(key: &[u8; 32], nonce: &[u8; 12], counter: u32) -> Self {
let mut state = [0u32; 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().unwrap());
}
state[12] = counter;
for i in 0..3 {
state[13 + i] = u32::from_le_bytes(nonce[4 * i..4 * i + 4].try_into().unwrap());
}
Self {
state,
buffer: [0u8; 64],
buf_pos: 64,
} }
pub fn apply_keystream(&mut self, data: &mut [u8]) {
let mut pos = 0;
while pos < data.len() {
if self.buf_pos == 64 {
self.buffer = block(&self.state);
self.state[12] = self.state[12].wrapping_add(1);
self.buf_pos = 0;
}
let take = (64 - self.buf_pos).min(data.len() - pos);
for i in 0..take {
data[pos + i] ^= self.buffer[self.buf_pos + i];
}
self.buf_pos += take;
pos += take;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(s: &str) -> Vec<u8> {
assert!(s.len() % 2 == 0);
(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_1_1_quarter_round() {
let mut s = [0u32; 16];
s[0] = 0x11111111;
s[1] = 0x01020304;
s[2] = 0x9b8d6f43;
s[3] = 0x01234567;
quarter_round(&mut s, 0, 1, 2, 3);
assert_eq!(s[0], 0xea2a92f4);
assert_eq!(s[1], 0xcb1cf8ce);
assert_eq!(s[2], 0x4581472e);
assert_eq!(s[3], 0x5881c4bb);
}
#[test]
fn rfc8439_2_3_2_block_vector() {
let key: [u8; 32] = hex_arr("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
let nonce: [u8; 12] = hex_arr("000000090000004a00000000");
let cipher = ChaCha20::new(&key, &nonce, 1);
let out = block(&cipher.state);
let expected = hex("10f1e7e4d13b5915500fdd1fa32071c4\
c7d1f4c733c068030422aa9ac3d46c4e\
d2826446079faa0914c2d705d98b02a2\
b5129cd1de164eb9cbd083e8a2503c4e");
assert_eq!(out.to_vec(), expected);
}
#[test]
fn rfc8439_2_4_2_encryption_vector() {
let key: [u8; 32] = hex_arr("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
let nonce: [u8; 12] = hex_arr("000000000000004a00000000");
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 mut buf = plaintext.to_vec();
let mut cipher = ChaCha20::new(&key, &nonce, 1);
cipher.apply_keystream(&mut buf);
let expected = hex("6e2e359a2568f98041ba0728dd0d6981\
e97e7aec1d4360c20a27afccfd9fae0b\
f91b65c5524733ab8f593dabcd62b357\
1639d624e65152ab8f530c359f0861d8\
07ca0dbf500d6a6156a38e088a22b65e\
52bc514d16ccf806818ce91ab7793736\
5af90bbf74a35be6b40b8eedf2785e42\
874d");
assert_eq!(buf, expected);
}
#[test]
fn chacha20_encrypt_decrypt_roundtrip() {
let key = [0x42u8; 32];
let nonce = [0xa5u8; 12];
let plaintext = b"the quick brown fox jumps over the lazy dog";
let mut buf = plaintext.to_vec();
let mut enc = ChaCha20::new(&key, &nonce, 1);
enc.apply_keystream(&mut buf);
assert_ne!(buf.as_slice(), plaintext);
let mut dec = ChaCha20::new(&key, &nonce, 1);
dec.apply_keystream(&mut buf);
assert_eq!(buf.as_slice(), plaintext);
}
#[test]
fn chacha20_chunked_apply_matches_single() {
let key = [0x77u8; 32];
let nonce = [0x11u8; 12];
let plaintext: Vec<u8> = (0..200).map(|i| i as u8).collect();
let mut single = plaintext.clone();
ChaCha20::new(&key, &nonce, 1).apply_keystream(&mut single);
let mut chunked = plaintext.clone();
let mut c = ChaCha20::new(&key, &nonce, 1);
c.apply_keystream(&mut chunked[..30]);
c.apply_keystream(&mut chunked[30..70]); c.apply_keystream(&mut chunked[70..130]); c.apply_keystream(&mut chunked[130..]);
assert_eq!(chunked, single);
}
#[test]
fn chacha20_zero_key_zero_nonce_block_zero() {
let key = [0u8; 32];
let nonce = [0u8; 12];
let cipher = ChaCha20::new(&key, &nonce, 0);
let out = block(&cipher.state);
let expected_prefix = hex("76b8e0ada0f13d90405d6ae55386bd28\
bdd219b8a08ded1aa836efcc8b770dc7");
assert_eq!(&out[..32], expected_prefix.as_slice());
}
}