Skip to main content

simoom_tls/crypto/
rng.rs

1//! Cryptographically secure random numbers: a ChaCha20 DRBG with fast key erasure, seeded and
2//! periodically reseeded from the operating system (`simoom_sys::arch::os_random`).
3//!
4//! Each thread keeps its own generator, so there is no lock on the handshake path. After every
5//! request the next 32 bytes of keystream replace the key, so a later compromise of the state
6//! does not reveal earlier output. A failing OS source panics: weak randomness is never used.
7
8use super::chacha::chacha20_block;
9use std::cell::RefCell;
10
11const RESEED_BYTES: u64 = 1 << 20;
12
13struct Drbg {
14    key: [u8; 32],
15    counter: u64,
16    generated: u64,
17}
18
19fn os_seed(out: &mut [u8; 32]) {
20    assert!(simoom_sys::arch::os_random(out), "simoom-tls: the operating system random source failed");
21}
22
23impl Drbg {
24    fn seeded() -> Drbg {
25        let mut key = [0u8; 32];
26        os_seed(&mut key);
27        Drbg { key, counter: 0, generated: 0 }
28    }
29
30    fn block(&mut self) -> [u8; 64] {
31        let mut nonce = [0u8; 12];
32        nonce[4..].copy_from_slice(&self.counter.to_le_bytes());
33        self.counter = self.counter.wrapping_add(1);
34        chacha20_block(&self.key, 0, &nonce)
35    }
36
37    fn fill(&mut self, out: &mut [u8]) {
38        if self.generated > RESEED_BYTES {
39            let mut fresh = [0u8; 32];
40            os_seed(&mut fresh);
41            for (k, f) in self.key.iter_mut().zip(fresh) {
42                *k ^= f;
43            }
44            self.generated = 0;
45        }
46        for chunk in out.chunks_mut(64) {
47            let b = self.block();
48            chunk.copy_from_slice(&b[..chunk.len()]);
49        }
50        let b = self.block();
51        self.key.copy_from_slice(&b[..32]);
52        self.generated += out.len() as u64 + 64;
53    }
54}
55
56thread_local! {
57    static RNG: RefCell<Option<Drbg>> = const { RefCell::new(None) };
58}
59
60/// Fills `out` with random bytes.
61pub fn fill(out: &mut [u8]) {
62    RNG.with(|r| r.borrow_mut().get_or_insert_with(Drbg::seeded).fill(out));
63}
64
65/// `N` random bytes.
66pub fn array<const N: usize>() -> [u8; N] {
67    let mut a = [0u8; N];
68    fill(&mut a);
69    a
70}
71
72/// `n` random bytes.
73pub fn bytes(n: usize) -> Vec<u8> {
74    let mut v = vec![0u8; n];
75    fill(&mut v);
76    v
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn outputs_differ_and_are_balanced() {
85        assert_ne!(array::<32>(), array::<32>());
86        let big = bytes(1 << 16);
87        let ones: u32 = big.iter().map(|x| x.count_ones()).sum();
88        let ratio = ones as f64 / (big.len() * 8) as f64;
89        assert!((ratio - 0.5).abs() < 0.01, "bit ratio {ratio}");
90    }
91
92    #[test]
93    fn deterministic_given_key_and_erases_it() {
94        let mut d1 = Drbg { key: [7; 32], counter: 0, generated: 0 };
95        let mut d2 = Drbg { key: [7; 32], counter: 0, generated: 0 };
96        let (mut a, mut b) = ([0u8; 100], [0u8; 100]);
97        d1.fill(&mut a);
98        d2.fill(&mut b);
99        assert_eq!(a, b);
100        assert_ne!(d1.key, [7; 32]);
101        d1.fill(&mut a);
102        assert_ne!(a, b);
103    }
104}