Skip to main content

dcrypt_algorithms/stream/chacha/chacha20/
mod.rs

1//! ChaCha20 stream cipher implementation
2//!
3//! This module implements the ChaCha20 stream cipher as defined in RFC 8439.
4
5use crate::error::{Error, Result};
6use crate::types::nonce::ChaCha20Compatible;
7use crate::types::Nonce;
8use byteorder::{ByteOrder, LittleEndian};
9use dcrypt_common::security::{EphemeralSecret, SecretBuffer};
10use zeroize::{Zeroize, ZeroizeOnDrop};
11
12/// Size of ChaCha20 key in bytes
13pub const CHACHA20_KEY_SIZE: usize = 32;
14/// Size of ChaCha20 nonce in bytes
15pub const CHACHA20_NONCE_SIZE: usize = 12;
16/// Size of ChaCha20 block in bytes
17pub const CHACHA20_BLOCK_SIZE: usize = 64;
18
19/// ChaCha20 stream cipher
20#[derive(Clone, Zeroize, ZeroizeOnDrop)]
21pub struct ChaCha20 {
22    /// The key schedule
23    state: [u32; 16],
24    /// Keystream buffer
25    buffer: [u8; CHACHA20_BLOCK_SIZE],
26    /// Current position in the buffer
27    position: usize,
28    /// Current block counter
29    counter: u32,
30    /// Set after counter `u32::MAX` has been consumed.
31    exhausted: bool,
32}
33
34impl ChaCha20 {
35    /// Creates a new ChaCha20 instance with the specified key and nonce
36    pub fn new<const N: usize>(key: &[u8; CHACHA20_KEY_SIZE], nonce: &Nonce<N>) -> Self
37    where
38        Nonce<N>: ChaCha20Compatible,
39    {
40        // Wrap key in SecretBuffer for secure handling
41        let key_buf = SecretBuffer::new(*key);
42        Self::with_counter_secure(&key_buf, nonce, 0)
43    }
44
45    /// Creates a new ChaCha20 instance with the specified key, nonce, and counter
46    pub fn with_counter<const N: usize>(
47        key: &[u8; CHACHA20_KEY_SIZE],
48        nonce: &Nonce<N>,
49        counter: u32,
50    ) -> Self
51    where
52        Nonce<N>: ChaCha20Compatible,
53    {
54        // Wrap key in SecretBuffer for secure handling
55        let key_buf = SecretBuffer::new(*key);
56        Self::with_counter_secure(&key_buf, nonce, counter)
57    }
58
59    /// Internal method that works with SecretBuffer for secure key handling
60    fn with_counter_secure<const N: usize>(
61        key: &SecretBuffer<CHACHA20_KEY_SIZE>,
62        nonce: &Nonce<N>,
63        counter: u32,
64    ) -> Self
65    where
66        Nonce<N>: ChaCha20Compatible,
67    {
68        // Initialize state with constants and key
69        let mut state = [0u32; 16];
70
71        // "expand 32-byte k" in little-endian
72        state[0] = 0x61707865;
73        state[1] = 0x3320646e;
74        state[2] = 0x79622d32;
75        state[3] = 0x6b206574;
76
77        // Key (8 words) - use secure key access
78        let key_bytes = key.as_ref();
79        for i in 0..8 {
80            state[4 + i] = LittleEndian::read_u32(&key_bytes[i * 4..]);
81        }
82
83        // Counter (1 word)
84        state[12] = counter;
85
86        // Nonce (3 words)
87        let nonce_bytes = nonce.as_ref();
88        state[13] = LittleEndian::read_u32(&nonce_bytes[0..4]);
89        state[14] = LittleEndian::read_u32(&nonce_bytes[4..8]);
90        state[15] = LittleEndian::read_u32(&nonce_bytes[8..12]);
91
92        let instance = Self {
93            state,
94            buffer: [0; CHACHA20_BLOCK_SIZE],
95            position: CHACHA20_BLOCK_SIZE, // Force initial keystream generation
96            counter,
97            exhausted: false,
98        };
99        state.zeroize();
100        instance
101    }
102
103    /// The ChaCha20 quarter round function
104    #[inline]
105    fn quarter_round(state: &mut [u32], a: usize, b: usize, c: usize, d: usize) {
106        state[a] = state[a].wrapping_add(state[b]);
107        state[d] ^= state[a];
108        state[d] = state[d].rotate_left(16);
109
110        state[c] = state[c].wrapping_add(state[d]);
111        state[b] ^= state[c];
112        state[b] = state[b].rotate_left(12);
113
114        state[a] = state[a].wrapping_add(state[b]);
115        state[d] ^= state[a];
116        state[d] = state[d].rotate_left(8);
117
118        state[c] = state[c].wrapping_add(state[d]);
119        state[b] ^= state[c];
120        state[b] = state[b].rotate_left(7);
121    }
122
123    /// Generate a block of keystream
124    fn generate_keystream(&mut self) -> Result<()> {
125        if self.exhausted {
126            return Err(Error::Processing {
127                operation: "ChaCha20",
128                details: "block counter exhausted",
129            });
130        }
131
132        // Create a working copy of the state
133        let mut working_state = self.state;
134
135        // Ensure the current counter is set in the working state
136        working_state[12] = self.counter;
137
138        // 20 rounds of ChaCha20: 10 column rounds, 10 diagonal rounds
139        for _ in 0..10 {
140            // Column rounds
141            Self::quarter_round(&mut working_state, 0, 4, 8, 12);
142            Self::quarter_round(&mut working_state, 1, 5, 9, 13);
143            Self::quarter_round(&mut working_state, 2, 6, 10, 14);
144            Self::quarter_round(&mut working_state, 3, 7, 11, 15);
145
146            // Diagonal rounds
147            Self::quarter_round(&mut working_state, 0, 5, 10, 15);
148            Self::quarter_round(&mut working_state, 1, 6, 11, 12);
149            Self::quarter_round(&mut working_state, 2, 7, 8, 13);
150            Self::quarter_round(&mut working_state, 3, 4, 9, 14);
151        }
152
153        // Create output by adding the working state to the original state
154        // Use EphemeralSecret to ensure intermediate values are zeroized
155        let mut output_state = EphemeralSecret::new([0u32; 16]);
156        for i in 0..16 {
157            let original_val = if i == 12 { self.counter } else { self.state[i] };
158            output_state[i] = working_state[i].wrapping_add(original_val);
159        }
160
161        // Convert to bytes (little-endian)
162        for i in 0..16 {
163            LittleEndian::write_u32(&mut self.buffer[i * 4..], output_state[i]);
164        }
165        working_state.zeroize();
166
167        // Reset position and increment counter for next block
168        self.position = 0;
169        if self.counter == u32::MAX {
170            self.exhausted = true;
171        } else {
172            self.counter += 1;
173        }
174        Ok(())
175    }
176
177    fn ensure_capacity(&self, data_len: usize) -> Result<()> {
178        let buffered = if self.position < CHACHA20_BLOCK_SIZE {
179            CHACHA20_BLOCK_SIZE - self.position
180        } else {
181            0
182        };
183        let bytes_requiring_blocks = data_len.saturating_sub(buffered);
184        let blocks_required = bytes_requiring_blocks.div_ceil(CHACHA20_BLOCK_SIZE) as u64;
185        let blocks_available = if self.exhausted {
186            0
187        } else {
188            u64::from(u32::MAX - self.counter) + 1
189        };
190
191        if blocks_required > blocks_available {
192            return Err(Error::Processing {
193                operation: "ChaCha20",
194                details: "message would wrap the block counter",
195            });
196        }
197        Ok(())
198    }
199
200    /// Encrypt or decrypt data in place using the ChaCha20 stream cipher
201    pub fn process(&mut self, data: &mut [u8]) -> Result<()> {
202        self.ensure_capacity(data.len())?;
203        for byte in data.iter_mut() {
204            // Generate new keystream block if needed
205            if self.position >= CHACHA20_BLOCK_SIZE {
206                self.generate_keystream()?;
207            }
208
209            // XOR data with keystream
210            *byte ^= self.buffer[self.position];
211            self.position += 1;
212        }
213        Ok(())
214    }
215
216    /// Encrypt data in place
217    pub fn encrypt(&mut self, data: &mut [u8]) -> Result<()> {
218        self.process(data)
219    }
220
221    /// Decrypt data in place
222    pub fn decrypt(&mut self, data: &mut [u8]) -> Result<()> {
223        self.process(data)
224    }
225
226    /// Generate keystream directly into an output buffer
227    pub fn keystream(&mut self, output: &mut [u8]) -> Result<()> {
228        // Zero the output buffer
229        for byte in output.iter_mut() {
230            *byte = 0;
231        }
232
233        // Force generation from a block boundary (ignore any leftover position)
234        self.position = CHACHA20_BLOCK_SIZE;
235
236        // Then run the encryption pass to copy the keystream
237        self.process(output)
238    }
239
240    /// Seek to a specific block position
241    ///
242    /// `block_offset` is the number of full blocks that have been consumed;
243    /// after seeking, the next generated block will be at `block_offset + 1`.
244    pub fn seek(&mut self, block_offset: u32) -> Result<()> {
245        // Set counter so that generate_keystream() yields the next block
246        self.counter = block_offset.checked_add(1).ok_or(Error::Processing {
247            operation: "ChaCha20 seek",
248            details: "block offset would wrap the counter",
249        })?;
250        self.exhausted = false;
251
252        // Force regeneration on next use
253        self.position = CHACHA20_BLOCK_SIZE;
254
255        // Clear any old keystream
256        self.buffer.zeroize();
257        Ok(())
258    }
259
260    /// Reset to initial state with the same key
261    pub fn reset(&mut self) {
262        self.counter = self.state[12]; // Restore original counter
263        self.exhausted = false;
264        self.position = CHACHA20_BLOCK_SIZE; // Force keystream regeneration
265        self.buffer.zeroize(); // Clear keystream buffer
266    }
267}
268
269/// HChaCha20 core used to validate XChaCha20 test vectors.
270///
271/// Unlike a ChaCha20 block, HChaCha20 does not add the initial state back to
272/// the post-round state. It returns words 0..=3 and 12..=15.
273#[cfg(test)]
274pub(crate) fn hchacha20(
275    key: &[u8; CHACHA20_KEY_SIZE],
276    nonce: &[u8; 16],
277) -> [u8; CHACHA20_KEY_SIZE] {
278    let mut state = [0u32; 16];
279    state[0] = 0x6170_7865;
280    state[1] = 0x3320_646e;
281    state[2] = 0x7962_2d32;
282    state[3] = 0x6b20_6574;
283    for i in 0..8 {
284        state[4 + i] = LittleEndian::read_u32(&key[i * 4..i * 4 + 4]);
285    }
286    for i in 0..4 {
287        state[12 + i] = LittleEndian::read_u32(&nonce[i * 4..i * 4 + 4]);
288    }
289
290    for _ in 0..10 {
291        ChaCha20::quarter_round(&mut state, 0, 4, 8, 12);
292        ChaCha20::quarter_round(&mut state, 1, 5, 9, 13);
293        ChaCha20::quarter_round(&mut state, 2, 6, 10, 14);
294        ChaCha20::quarter_round(&mut state, 3, 7, 11, 15);
295        ChaCha20::quarter_round(&mut state, 0, 5, 10, 15);
296        ChaCha20::quarter_round(&mut state, 1, 6, 11, 12);
297        ChaCha20::quarter_round(&mut state, 2, 7, 8, 13);
298        ChaCha20::quarter_round(&mut state, 3, 4, 9, 14);
299    }
300
301    let words = [
302        state[0], state[1], state[2], state[3], state[12], state[13], state[14], state[15],
303    ];
304    let mut out = [0u8; CHACHA20_KEY_SIZE];
305    for (chunk, word) in out.chunks_exact_mut(4).zip(words) {
306        LittleEndian::write_u32(chunk, word);
307    }
308    state.zeroize();
309    out
310}
311
312#[cfg(test)]
313mod tests;