Skip to main content

vitaminc_random/
safe_rand.rs

1//! A secure random number generator that is safe to use for cryptographic purposes.
2//! It is intentionally opinionated so that developers don't have to think about what Rng they should use
3//! for cryptographic purposes.
4//!
5//! Internally it uses `ChaCha20Rng` from the RustCrypto `chacha20` crate (via `rand`), built with
6//! its `zeroize` feature so the generator's key schedule and buffered keystream are wiped on drop.
7use std::convert::Infallible;
8
9use rand::{rngs::SysRng, Rng, SeedableRng, TryCryptoRng, TryRng};
10use vitaminc_protected::Controlled;
11use zeroize::{ZeroizeOnDrop, Zeroizing};
12
13/// A secure random number generator that is safe to use for cryptographic purposes.
14///
15/// Wipes its key schedule and buffered keystream on drop.
16pub struct SafeRand(rand::rngs::ChaCha20Rng);
17
18// `SafeRand` has no `Drop` of its own; the wipe is the field's drop glue,
19// which is `ChaCha20Rng`'s `ZeroizeOnDrop`. That impl exists only when
20// `chacha20` is built with its `zeroize` feature (see this crate's
21// `Cargo.toml`). The bound below fails to compile if the feature ever lapses,
22// so the marker impl can never silently become a lie.
23impl ZeroizeOnDrop for SafeRand {}
24const _: fn() = assert_zeroize_on_drop::<rand::rngs::ChaCha20Rng>;
25fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
26
27impl SafeRand {
28    /// A value in `0..n`: at least `0`, strictly below `n`, uniform to
29    /// within the bias bound stated below. This is the bound every
30    /// index-shaped use wants (`n` items, pick one) and the one `std` and
31    /// `rand` ranges use.
32    ///
33    /// Exactly one 64-bit draw per call, reduced with Lemire's multiply-high
34    /// method: no rejection loop and no branch on the value drawn, so the
35    /// number of draws does not depend on the values drawn. That matters when
36    /// the generator is seeded from secret material, as in permutation-key
37    /// derivation, where a rejection loop's retry count would leak through
38    /// timing.
39    ///
40    /// The reduction's statistical distance from uniform is at most
41    /// `n / 2⁶⁴`: below 2⁻⁵⁵ for `n ≤ 256` and still at most 2⁻³² at
42    /// `n = u32::MAX`. A protocol that needs exact uniformity must account
43    /// for that term.
44    ///
45    /// The bound may be a plain `u32`, a [`Protected<u32>`], or a
46    /// [`Protected<NonZeroU32>`]; the last is the form for a secret bound
47    /// that might be zero, since it moves the zero check to construction and
48    /// makes the draw itself total. All three are the
49    /// [`BoundedRng::next_below`] trait method, reachable here without
50    /// importing the trait.
51    ///
52    /// # Panics
53    ///
54    /// Panics if `n == 0`: the range `0..0` is empty and has no value to
55    /// return. Callers that compute `n` should check it first. When `n` is
56    /// a [`Protected<u32>`] the panic is observable on a secret, so a caller
57    /// whose secret bound may be zero should pass a
58    /// [`Protected<NonZeroU32>`] instead, which never panics.
59    ///
60    /// [`Protected<u32>`]: vitaminc_protected::Protected
61    /// [`Protected<NonZeroU32>`]: vitaminc_protected::Protected
62    /// [`BoundedRng::next_below`]: crate::BoundedRng::next_below
63    pub fn next_below<T>(&mut self, n: T) -> <Self as crate::BoundedRng<T>>::Output
64    where
65        Self: crate::BoundedRng<T>,
66    {
67        <Self as crate::BoundedRng<T>>::next_below(self, n)
68    }
69
70    /// A value in `0..=max`, for every `max` up to and including `u32::MAX`,
71    /// with the same fixed-count draw and the same `(max + 1) / 2⁶⁴` bias
72    /// bound as [`next_below`](Self::next_below). This is the
73    /// [`BoundedRngInclusive::next_bounded`] trait method at `u32`.
74    ///
75    /// Deprecated: earlier versions honoured the inclusive bound only when
76    /// `max` was not a power of two and were exclusive otherwise, so callers
77    /// written against either meaning were wrong for some inputs
78    /// (cipherstash/vitaminc#198). The equivalent call is
79    /// `next_below(max + 1)` (for `max == u32::MAX` that is the whole word:
80    /// use [`Rng::next_u32`](rand::Rng::next_u32)), or `next_below(n)` when
81    /// the caller has a length `n` rather than a maximum.
82    ///
83    /// Besides the power-of-two case, both the value drawn for a given seed
84    /// and the number of words taken from the stream changed; see
85    /// [`BoundedRngInclusive`](crate::BoundedRngInclusive) for what that
86    /// means for existing callers.
87    ///
88    /// [`BoundedRngInclusive::next_bounded`]: crate::BoundedRngInclusive::next_bounded
89    #[deprecated(
90        note = "inclusive `0..=max`; use `next_below(max + 1)`, or `next_below(n)` when you have a length `n`"
91    )]
92    pub fn next_bounded_u32(&mut self, max: u32) -> u32 {
93        crate::bounded::upto_u32(self, max)
94    }
95
96    /// Creates a new `SafeRand` seeded from the OS random number generator.
97    pub fn from_entropy() -> Result<Self, crate::RandomError> {
98        Ok(Self::try_from_rng(&mut SysRng)?)
99    }
100
101    /// A safer alternative to `from_seed`: the seed is wiped once the
102    /// generator is built, on every exit from this function.
103    ///
104    /// The unwrapped bytes live in a [`Zeroizing`] wrapper from the moment
105    /// they leave `seed`'s custody, so the wipe is done by drop glue rather
106    /// than by a trailing statement. An unwind between unwrapping and
107    /// returning (e.g. a panic in the generator's constructor) still wipes
108    /// them.
109    pub fn from_controlled_seed<C>(seed: C) -> Self
110    where
111        C: Controlled<Inner = [u8; 32]>,
112    {
113        let seed = Zeroizing::new(seed.risky_unwrap());
114        Self(rand::rngs::ChaCha20Rng::from_seed(*seed))
115    }
116}
117
118impl TryCryptoRng for SafeRand {}
119
120impl TryRng for SafeRand {
121    type Error = Infallible;
122
123    #[inline]
124    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
125        Ok(self.0.next_u32())
126    }
127
128    #[inline]
129    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
130        Ok(self.0.next_u64())
131    }
132
133    #[inline]
134    fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
135        self.0.fill_bytes(dst);
136        Ok(())
137    }
138}
139
140impl SeedableRng for SafeRand {
141    // TODO: This should be a ProtectedSeed! Maybe a GAT?
142    type Seed = [u8; 32];
143
144    fn from_seed(seed: Self::Seed) -> Self {
145        Self(rand::rngs::ChaCha20Rng::from_seed(seed))
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::SafeRand;
152    use rand::{rngs::ChaCha20Rng, Rng, SeedableRng, TryRng};
153
154    const SEED: [u8; 32] = [7u8; 32];
155
156    /// `SafeRand` is a transparent wrapper: for the same seed it must yield
157    /// exactly the ChaCha20 stream, word for word and byte for byte. Pins the
158    /// `TryRng` impl to real output rather than to "some value", which is
159    /// what a mutant returning a constant would otherwise pass as.
160    #[test]
161    fn try_rng_yields_the_chacha20_stream_for_the_seed() {
162        let mut safe = SafeRand::from_seed(SEED);
163        let mut reference = ChaCha20Rng::from_seed(SEED);
164
165        for _ in 0..4 {
166            assert_eq!(safe.try_next_u32().unwrap(), reference.next_u32());
167        }
168        for _ in 0..4 {
169            assert_eq!(safe.try_next_u64().unwrap(), reference.next_u64());
170        }
171        let mut got = [0u8; 40];
172        let mut want = [0u8; 40];
173        safe.try_fill_bytes(&mut got).unwrap();
174        reference.fill_bytes(&mut want);
175        assert_eq!(got, want);
176        assert_ne!(got, [0u8; 40], "fill_bytes must write the buffer");
177    }
178
179    /// What `next_below` promises, spelled out over the same seed: one
180    /// 64-bit draw, multiplied by `n`, high word kept. Comparing exact values
181    /// (not just `< n`) pins the draw width, the reduction, and that exactly
182    /// one draw is consumed per call, at powers of two, their neighbours, and
183    /// both ends of the `u32` range.
184    fn reference_below(rng: &mut ChaCha20Rng, n: u64) -> u32 {
185        ((u128::from(rng.next_u64()) * u128::from(n)) >> 64) as u32
186    }
187
188    const BOUNDS: [u32; 14] = [
189        1,
190        2,
191        3,
192        4,
193        5,
194        6,
195        7,
196        16,
197        100,
198        1000,
199        (1 << 31) - 1,
200        1 << 31,
201        (1 << 31) + 1,
202        u32::MAX,
203    ];
204
205    #[test]
206    fn next_below_matches_the_lemire_reference() {
207        for n in BOUNDS {
208            let mut safe = SafeRand::from_seed(SEED);
209            let mut reference = ChaCha20Rng::from_seed(SEED);
210            for _ in 0..256 {
211                let got = safe.next_below(n);
212                assert_eq!(
213                    got,
214                    reference_below(&mut reference, u64::from(n)),
215                    "n = {n}"
216                );
217                assert!(got < n, "n = {n}");
218            }
219        }
220    }
221
222    /// The deprecated inclusive form is `next_below(max + 1)` for every
223    /// `max`, including `u32::MAX`, where `max + 1` does not fit a `u32`.
224    /// `BOUNDS` shifted down by one tops out at `u32::MAX - 1`, so
225    /// `u32::MAX` itself is appended: that is the input where the inclusive
226    /// path must widen to `u64` before adding one.
227    #[test]
228    #[allow(deprecated)]
229    fn next_bounded_u32_is_the_inclusive_form_of_next_below() {
230        for max in BOUNDS.map(|n| n - 1).into_iter().chain([u32::MAX]) {
231            let mut safe = SafeRand::from_seed(SEED);
232            let mut reference = ChaCha20Rng::from_seed(SEED);
233            for _ in 0..256 {
234                let got = safe.next_bounded_u32(max);
235                assert_eq!(
236                    got,
237                    reference_below(&mut reference, u64::from(max) + 1),
238                    "max = {max}"
239                );
240                assert!(got <= max, "max = {max}");
241            }
242        }
243    }
244
245    /// Every value in `0..n` is reachable and `n` itself never is; the
246    /// power-of-two case is the one the old code got wrong.
247    #[test]
248    fn next_below_covers_exactly_the_half_open_range() {
249        for n in [5usize, 8] {
250            let mut rng = SafeRand::from_seed(SEED);
251            let mut seen = vec![false; n + 1];
252            for _ in 0..512 {
253                seen[rng.next_below(n as u32) as usize] = true;
254            }
255            assert!(seen[..n].iter().all(|&s| s), "n = {n}");
256            assert!(!seen[n], "n = {n}");
257        }
258    }
259
260    #[test]
261    #[should_panic(expected = "range must be non-zero")]
262    fn next_below_zero_panics() {
263        SafeRand::from_seed(SEED).next_below(0);
264    }
265
266    #[test]
267    fn different_seeds_diverge_and_the_same_seed_repeats() {
268        let mut a = SafeRand::from_seed(SEED);
269        let mut b = SafeRand::from_seed(SEED);
270        let mut c = SafeRand::from_seed([8u8; 32]);
271        let (x, y, z) = (
272            a.try_next_u64().unwrap(),
273            b.try_next_u64().unwrap(),
274            c.try_next_u64().unwrap(),
275        );
276        assert_eq!(x, y);
277        assert_ne!(x, z);
278    }
279
280    #[test]
281    fn next_below_from_entropy_is_half_open() -> Result<(), crate::RandomError> {
282        let mut rng = SafeRand::from_entropy()?;
283        for n in [1, 2, 4, 5, 52, 62, 64, 94, u32::MAX] {
284            for _ in 0..1000 {
285                assert!(rng.next_below(n) < n);
286            }
287        }
288        Ok(())
289    }
290
291    #[test]
292    fn next_below_is_uniform() {
293        // Chi-squared test over a non-power-of-two range with a fixed seed.
294        // Would catch a modulo-style bias or a broken reduction.
295        const RANGE: u32 = 5;
296        const SAMPLES: u32 = 100_000;
297        let mut rng = SafeRand::from_seed([3u8; 32]);
298        let mut counts = [0u32; RANGE as usize];
299        for _ in 0..SAMPLES {
300            counts[rng.next_below(RANGE) as usize] += 1;
301        }
302        let expected = f64::from(SAMPLES) / f64::from(RANGE);
303        let chi2: f64 = counts
304            .iter()
305            .map(|&c| {
306                let d = f64::from(c) - expected;
307                d * d / expected
308            })
309            .sum();
310        // 4 degrees of freedom; p = 0.001 critical value is 18.47. The seed is
311        // fixed, so this is deterministic — no flakiness.
312        assert!(chi2 < 18.47, "chi-squared too high: {chi2}");
313    }
314}