Skip to main content

dcrypt_internal/
random.rs

1//! Caller-supplied randomness traits.
2//!
3//! dcrypt intentionally provides no operating-system RNG. Applications choose
4//! and own their entropy source and pass it into every randomized operation.
5
6use core::fmt;
7
8use crate::zeroing::{Zeroize, ZeroizeOnDrop};
9
10/// An error returned by a fallible caller-provided randomness source.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct Error;
13
14impl fmt::Display for Error {
15    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
16        formatter.write_str("caller-provided randomness source failed")
17    }
18}
19
20#[cfg(feature = "std")]
21impl std::error::Error for Error {}
22
23/// The byte-oriented randomness interface used by dcrypt.
24pub trait RngCore {
25    fn next_u32(&mut self) -> u32 {
26        let mut bytes = crate::zeroing::Zeroizing::new([0u8; 4]);
27        self.fill_bytes(&mut bytes[..]);
28        u32::from(bytes[0])
29            | (u32::from(bytes[1]) << 8)
30            | (u32::from(bytes[2]) << 16)
31            | (u32::from(bytes[3]) << 24)
32    }
33
34    fn next_u64(&mut self) -> u64 {
35        let mut bytes = crate::zeroing::Zeroizing::new([0u8; 8]);
36        self.fill_bytes(&mut bytes[..]);
37        let mut value = crate::zeroing::Zeroizing::new(0u64);
38        for (index, byte) in bytes.iter().enumerate() {
39            *value |= u64::from(*byte) << (index * 8);
40        }
41        *value
42    }
43
44    fn fill_bytes(&mut self, destination: &mut [u8]) {
45        try_fill_bytes_zeroing_on_error(self, destination)
46            .expect("caller-provided randomness source failed")
47    }
48
49    /// Fill `destination`, propagating failures from the caller's entropy
50    /// source rather than silently substituting weak randomness.
51    fn try_fill_bytes(&mut self, destination: &mut [u8]) -> Result<(), Error>;
52}
53
54impl<R: RngCore + ?Sized> RngCore for &mut R {
55    fn next_u32(&mut self) -> u32 {
56        (**self).next_u32()
57    }
58
59    fn next_u64(&mut self) -> u64 {
60        (**self).next_u64()
61    }
62
63    fn fill_bytes(&mut self, destination: &mut [u8]) {
64        try_fill_bytes_zeroing_on_error(&mut **self, destination)
65            .expect("caller-provided randomness source failed")
66    }
67
68    fn try_fill_bytes(&mut self, destination: &mut [u8]) -> Result<(), Error> {
69        (**self).try_fill_bytes(destination)
70    }
71}
72
73/// Fill a destination and clear it completely if the RNG reports failure.
74///
75/// `RngCore` implementations are caller supplied and may write only part of a
76/// destination before returning an error. Secret constructors use this helper
77/// so those partial bytes never remain live on an error path.
78pub fn try_fill_bytes_zeroing_on_error<R: RngCore + ?Sized>(
79    rng: &mut R,
80    destination: &mut [u8],
81) -> Result<(), Error> {
82    let result = rng.try_fill_bytes(destination);
83    if result.is_err() {
84        destination.zeroize();
85    }
86    result
87}
88
89/// Marker for generators suitable for cryptographic use.
90pub trait CryptoRng: RngCore {}
91
92impl<R: CryptoRng + ?Sized> CryptoRng for &mut R {}
93
94/// A deterministic ChaCha20 generator seeded entirely by its caller.
95///
96/// This type never obtains operating-system entropy. A caller that uses it for
97/// cryptography must provide a fresh, unpredictable 32-byte seed and must not
98/// reuse that seed across independent generator instances.
99///
100/// The generator intentionally does not implement `Clone`: duplicating a
101/// stream state can repeat nonces or key material.
102///
103/// ```compile_fail
104/// use dcrypt_internal::random::ChaCha20Rng;
105/// let generator = ChaCha20Rng::from_seed([7u8; 32]);
106/// let duplicate = generator.clone();
107/// ```
108pub struct ChaCha20Rng {
109    key: [u32; 8],
110    counter: u32,
111    buffer: [u8; 64],
112    offset: usize,
113    exhausted: bool,
114}
115
116impl ChaCha20Rng {
117    pub fn from_seed(seed: [u8; 32]) -> Self {
118        let seed = crate::zeroing::Zeroizing::new(seed);
119        let mut generator = Self {
120            key: [0u32; 8],
121            counter: 0,
122            buffer: [0u8; 64],
123            offset: 64,
124            exhausted: false,
125        };
126        for index in 0..8 {
127            let offset = index * 4;
128            generator.key[index] = u32::from(seed[offset])
129                | (u32::from(seed[offset + 1]) << 8)
130                | (u32::from(seed[offset + 2]) << 16)
131                | (u32::from(seed[offset + 3]) << 24);
132        }
133        generator
134    }
135
136    fn refill(&mut self) -> Result<(), Error> {
137        if self.exhausted {
138            return Err(Error);
139        }
140        chacha20_block(&self.key, self.counter, &mut self.buffer);
141        self.offset = 0;
142        if self.counter == u32::MAX {
143            self.exhausted = true;
144        } else {
145            self.counter += 1;
146        }
147        Ok(())
148    }
149}
150
151impl RngCore for ChaCha20Rng {
152    fn try_fill_bytes(&mut self, destination: &mut [u8]) -> Result<(), Error> {
153        let mut written = 0;
154        while written < destination.len() {
155            if self.offset == self.buffer.len() && self.refill().is_err() {
156                destination.zeroize();
157                return Err(Error);
158            }
159            let available = self.buffer.len() - self.offset;
160            let take = core::cmp::min(available, destination.len() - written);
161            destination[written..written + take]
162                .copy_from_slice(&self.buffer[self.offset..self.offset + take]);
163            self.offset += take;
164            written += take;
165        }
166        Ok(())
167    }
168}
169
170impl CryptoRng for ChaCha20Rng {}
171
172impl Zeroize for ChaCha20Rng {
173    fn zeroize(&mut self) {
174        self.key.zeroize();
175        self.counter.zeroize();
176        self.buffer.zeroize();
177        self.offset.zeroize();
178        self.exhausted.zeroize();
179    }
180}
181
182impl Drop for ChaCha20Rng {
183    fn drop(&mut self) {
184        self.zeroize();
185    }
186}
187
188impl ZeroizeOnDrop for ChaCha20Rng {}
189
190#[inline(always)]
191fn quarter_round(state: &mut [u32; 16], a: usize, b: usize, c: usize, d: usize) {
192    state[a] = state[a].wrapping_add(state[b]);
193    state[d] ^= state[a];
194    state[d] = state[d].rotate_left(16);
195    state[c] = state[c].wrapping_add(state[d]);
196    state[b] ^= state[c];
197    state[b] = state[b].rotate_left(12);
198    state[a] = state[a].wrapping_add(state[b]);
199    state[d] ^= state[a];
200    state[d] = state[d].rotate_left(8);
201    state[c] = state[c].wrapping_add(state[d]);
202    state[b] ^= state[c];
203    state[b] = state[b].rotate_left(7);
204}
205
206fn chacha20_block(key: &[u32; 8], counter: u32, output: &mut [u8; 64]) {
207    let initial = crate::zeroing::Zeroizing::new([
208        0x6170_7865,
209        0x3320_646e,
210        0x7962_2d32,
211        0x6b20_6574,
212        key[0],
213        key[1],
214        key[2],
215        key[3],
216        key[4],
217        key[5],
218        key[6],
219        key[7],
220        counter,
221        0,
222        0,
223        0,
224    ]);
225    let mut state = crate::zeroing::Zeroizing::new(*initial);
226    for _ in 0..10 {
227        quarter_round(&mut state, 0, 4, 8, 12);
228        quarter_round(&mut state, 1, 5, 9, 13);
229        quarter_round(&mut state, 2, 6, 10, 14);
230        quarter_round(&mut state, 3, 7, 11, 15);
231        quarter_round(&mut state, 0, 5, 10, 15);
232        quarter_round(&mut state, 1, 6, 11, 12);
233        quarter_round(&mut state, 2, 7, 8, 13);
234        quarter_round(&mut state, 3, 4, 9, 14);
235    }
236    for index in 0..16 {
237        let word = crate::zeroing::Zeroizing::new(state[index].wrapping_add(initial[index]));
238        for byte in 0..4 {
239            output[index * 4 + byte] = (*word >> (byte * 8)) as u8;
240        }
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::{chacha20_block, try_fill_bytes_zeroing_on_error, ChaCha20Rng, Error, RngCore};
247    use crate::zeroing::Zeroize;
248
249    struct PartiallyFailingRng;
250
251    impl RngCore for PartiallyFailingRng {
252        fn try_fill_bytes(&mut self, destination: &mut [u8]) -> Result<(), Error> {
253            let written = core::cmp::min(3, destination.len());
254            destination[..written].fill(0xA5);
255            Err(Error)
256        }
257    }
258
259    #[test]
260    fn defensive_fill_erases_partial_rng_output_on_error() {
261        let mut rng = PartiallyFailingRng;
262        let mut destination = crate::zeroing::Zeroizing::new([0xCC; 8]);
263        assert!(try_fill_bytes_zeroing_on_error(&mut rng, &mut destination[..]).is_err());
264        assert_eq!(*destination, [0; 8]);
265    }
266
267    #[test]
268    fn chacha_rng_erases_partial_output_when_counter_exhausts() {
269        let mut rng = ChaCha20Rng::from_seed([9; 32]);
270        rng.counter = u32::MAX;
271        rng.offset = rng.buffer.len();
272
273        let mut destination = crate::zeroing::Zeroizing::new([0xCC; 65]);
274        assert!(rng.try_fill_bytes(&mut destination[..]).is_err());
275        assert_eq!(*destination, [0; 65]);
276    }
277
278    #[test]
279    fn zero_key_zero_nonce_block_matches_rfc_8439_primitive() {
280        let expected = [
281            0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, 0x40, 0x5d, 0x6a, 0xe5, 0x53, 0x86,
282            0xbd, 0x28, 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, 0xa8, 0x36, 0xef, 0xcc,
283            0x8b, 0x77, 0x0d, 0xc7, 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, 0x8d, 0x77, 0x24,
284            0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, 0x6a, 0x43, 0xb8, 0xf4, 0x15, 0x18, 0xa1, 0x1c,
285            0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86,
286        ];
287        let mut actual = crate::zeroing::Zeroizing::new([0u8; 64]);
288        chacha20_block(&[0u32; 8], 0, &mut actual);
289        assert_eq!(*actual, expected);
290    }
291
292    #[test]
293    fn chunking_does_not_change_the_stream() {
294        let seed = crate::zeroing::Zeroizing::new([0x42; 32]);
295        let mut whole = ChaCha20Rng::from_seed(*seed);
296        let mut chunked = ChaCha20Rng::from_seed(*seed);
297        let mut left = crate::zeroing::Zeroizing::new([0u8; 137]);
298        let mut right = crate::zeroing::Zeroizing::new([0u8; 137]);
299        whole.fill_bytes(&mut left[..]);
300        chunked.fill_bytes(&mut right[..3]);
301        chunked.fill_bytes(&mut right[3..91]);
302        chunked.fill_bytes(&mut right[91..]);
303        assert_eq!(*left, *right);
304    }
305
306    #[test]
307    fn explicit_zeroize_clears_generator_state() {
308        let mut generator = ChaCha20Rng::from_seed([0x42; 32]);
309        let mut output = crate::zeroing::Zeroizing::new([0u8; 8]);
310        generator.fill_bytes(&mut output[..]);
311        generator.zeroize();
312        assert_eq!(generator.key, [0; 8]);
313        assert_eq!(generator.counter, 0);
314        assert_eq!(generator.buffer, [0; 64]);
315        assert_eq!(generator.offset, 0);
316        assert!(!generator.exhausted);
317    }
318}