use crate::error::{Error, Result};
use crate::types::nonce::ChaCha20Compatible;
use crate::types::Nonce;
use dcrypt_common::security::{EphemeralSecret, SecretBuffer};
use dcrypt_internal::zeroing::{Zeroize, ZeroizeOnDrop, Zeroizing};
pub const CHACHA20_KEY_SIZE: usize = 32;
pub const CHACHA20_NONCE_SIZE: usize = 12;
pub const CHACHA20_BLOCK_SIZE: usize = 64;
#[derive(Clone)]
pub struct ChaCha20 {
state: [u32; 16],
buffer: [u8; CHACHA20_BLOCK_SIZE],
position: usize,
counter: u32,
exhausted: bool,
}
impl Zeroize for ChaCha20 {
fn zeroize(&mut self) {
self.state.zeroize();
self.buffer.zeroize();
self.position.zeroize();
self.counter.zeroize();
self.exhausted.zeroize();
}
}
impl Drop for ChaCha20 {
fn drop(&mut self) {
self.zeroize();
}
}
impl ZeroizeOnDrop for ChaCha20 {}
impl ChaCha20 {
pub fn new<const N: usize>(key: &[u8; CHACHA20_KEY_SIZE], nonce: &Nonce<N>) -> Self
where
Nonce<N>: ChaCha20Compatible,
{
let key_buf = SecretBuffer::new(*key);
Self::with_counter_secure(&key_buf, nonce, 0)
}
pub fn with_counter<const N: usize>(
key: &[u8; CHACHA20_KEY_SIZE],
nonce: &Nonce<N>,
counter: u32,
) -> Self
where
Nonce<N>: ChaCha20Compatible,
{
let key_buf = SecretBuffer::new(*key);
Self::with_counter_secure(&key_buf, nonce, counter)
}
fn with_counter_secure<const N: usize>(
key: &SecretBuffer<CHACHA20_KEY_SIZE>,
nonce: &Nonce<N>,
counter: u32,
) -> Self
where
Nonce<N>: ChaCha20Compatible,
{
let mut state = Zeroizing::new([0u32; 16]);
state[0] = 0x61707865;
state[1] = 0x3320646e;
state[2] = 0x79622d32;
state[3] = 0x6b206574;
let key_bytes = key.as_ref();
for i in 0..8 {
state[4 + i] =
u32::from_le_bytes(key_bytes[i * 4..i * 4 + 4].try_into().expect("four bytes"));
}
state[12] = counter;
let nonce_bytes = nonce.as_ref();
state[13] = u32::from_le_bytes(nonce_bytes[0..4].try_into().expect("four bytes"));
state[14] = u32::from_le_bytes(nonce_bytes[4..8].try_into().expect("four bytes"));
state[15] = u32::from_le_bytes(nonce_bytes[8..12].try_into().expect("four bytes"));
let instance = Self {
state: state.into_inner(),
buffer: [0; CHACHA20_BLOCK_SIZE],
position: CHACHA20_BLOCK_SIZE, counter,
exhausted: false,
};
instance
}
#[inline]
fn quarter_round(state: &mut [u32], 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 generate_keystream(&mut self) -> Result<()> {
if self.exhausted {
return Err(Error::Processing {
operation: "ChaCha20",
details: "block counter exhausted",
});
}
let mut working_state = self.state;
working_state[12] = self.counter;
for _ in 0..10 {
Self::quarter_round(&mut working_state, 0, 4, 8, 12);
Self::quarter_round(&mut working_state, 1, 5, 9, 13);
Self::quarter_round(&mut working_state, 2, 6, 10, 14);
Self::quarter_round(&mut working_state, 3, 7, 11, 15);
Self::quarter_round(&mut working_state, 0, 5, 10, 15);
Self::quarter_round(&mut working_state, 1, 6, 11, 12);
Self::quarter_round(&mut working_state, 2, 7, 8, 13);
Self::quarter_round(&mut working_state, 3, 4, 9, 14);
}
let mut output_state = EphemeralSecret::new([0u32; 16]);
for i in 0..16 {
let original_val = if i == 12 { self.counter } else { self.state[i] };
output_state[i] = working_state[i].wrapping_add(original_val);
}
for i in 0..16 {
self.buffer[i * 4..i * 4 + 4].copy_from_slice(&output_state[i].to_le_bytes());
}
working_state.zeroize();
self.position = 0;
if self.counter == u32::MAX {
self.exhausted = true;
} else {
self.counter += 1;
}
Ok(())
}
fn ensure_capacity(&self, data_len: usize) -> Result<()> {
let buffered = if self.position < CHACHA20_BLOCK_SIZE {
CHACHA20_BLOCK_SIZE - self.position
} else {
0
};
let bytes_requiring_blocks = data_len.saturating_sub(buffered);
let blocks_required = bytes_requiring_blocks.div_ceil(CHACHA20_BLOCK_SIZE) as u64;
let blocks_available = if self.exhausted {
0
} else {
u64::from(u32::MAX - self.counter) + 1
};
if blocks_required > blocks_available {
return Err(Error::Processing {
operation: "ChaCha20",
details: "message would wrap the block counter",
});
}
Ok(())
}
pub fn process(&mut self, data: &mut [u8]) -> Result<()> {
self.ensure_capacity(data.len())?;
for byte in data.iter_mut() {
if self.position >= CHACHA20_BLOCK_SIZE {
self.generate_keystream()?;
}
*byte ^= self.buffer[self.position];
self.position += 1;
}
Ok(())
}
pub fn encrypt(&mut self, data: &mut [u8]) -> Result<()> {
self.process(data)
}
pub fn decrypt(&mut self, data: &mut [u8]) -> Result<()> {
self.process(data)
}
pub fn keystream(&mut self, output: &mut [u8]) -> Result<()> {
for byte in output.iter_mut() {
*byte = 0;
}
self.position = CHACHA20_BLOCK_SIZE;
self.process(output)
}
pub fn seek(&mut self, block_offset: u32) -> Result<()> {
self.counter = block_offset.checked_add(1).ok_or(Error::Processing {
operation: "ChaCha20 seek",
details: "block offset would wrap the counter",
})?;
self.exhausted = false;
self.position = CHACHA20_BLOCK_SIZE;
self.buffer.zeroize();
Ok(())
}
pub fn reset(&mut self) {
self.counter = self.state[12]; self.exhausted = false;
self.position = CHACHA20_BLOCK_SIZE; self.buffer.zeroize(); }
}
pub(crate) fn hchacha20(
key: &[u8; CHACHA20_KEY_SIZE],
nonce: &[u8; 16],
) -> Zeroizing<[u8; CHACHA20_KEY_SIZE]> {
let mut state = Zeroizing::new([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[i * 4..i * 4 + 4].try_into().expect("four bytes"));
}
for i in 0..4 {
state[12 + i] = u32::from_le_bytes(nonce[i * 4..i * 4 + 4].try_into().expect("four bytes"));
}
for _ in 0..10 {
ChaCha20::quarter_round(&mut state[..], 0, 4, 8, 12);
ChaCha20::quarter_round(&mut state[..], 1, 5, 9, 13);
ChaCha20::quarter_round(&mut state[..], 2, 6, 10, 14);
ChaCha20::quarter_round(&mut state[..], 3, 7, 11, 15);
ChaCha20::quarter_round(&mut state[..], 0, 5, 10, 15);
ChaCha20::quarter_round(&mut state[..], 1, 6, 11, 12);
ChaCha20::quarter_round(&mut state[..], 2, 7, 8, 13);
ChaCha20::quarter_round(&mut state[..], 3, 4, 9, 14);
}
let words = Zeroizing::new([
state[0], state[1], state[2], state[3], state[12], state[13], state[14], state[15],
]);
let mut out = Zeroizing::new([0u8; CHACHA20_KEY_SIZE]);
for (chunk, word) in out.chunks_exact_mut(4).zip(words.iter().copied()) {
chunk[0] = word as u8;
chunk[1] = (word >> 8) as u8;
chunk[2] = (word >> 16) as u8;
chunk[3] = (word >> 24) as u8;
}
out
}
#[cfg(test)]
mod tests;