Skip to main content

vitaminc_random/
bounded.rs

1use std::num::NonZeroU32;
2
3use crate::SafeRand;
4use rand::CryptoRng;
5use vitaminc_protected::{Controlled, Protected};
6
7/// Bounded draws over one fixed-count Lemire reduction.
8///
9/// [`next_below(n)`](BoundedRng::next_below) is the primitive: a value in
10/// `0..n`, the half-open bound every index-shaped use wants (`n` items, pick
11/// one) and the one `std` and `rand` ranges use. It is implemented for a
12/// `u32` bound, for a [`Protected<u32>`] bound, and for a
13/// [`Protected<NonZeroU32>`] bound, and is also reachable as
14/// [`SafeRand::next_below`] without importing this trait.
15///
16/// The [`Protected<NonZeroU32>`] form is the one to use when the bound is a
17/// secret. `0..n` is empty when `n == 0`, so a bound of zero has to be
18/// rejected somewhere; a `Protected<u32>` bound can only be rejected by
19/// checking the secret at the call, which is a branch on a secret. A
20/// [`NonZeroU32`] moves that check to construction, where the caller
21/// decides how to handle it, so every bound the type admits takes the same
22/// path through the draw: no branch on the secret, and nothing left to
23/// panic on.
24///
25/// The older **inclusive** form, `0..=max`, lives on the deprecated
26/// [`BoundedRngInclusive`] trait so that it can be removed later without a
27/// second breaking change here.
28///
29/// Every draw makes exactly one 64-bit call on the generator, reduced with
30/// Lemire's multiply-high method: no rejection loop and no branch on the
31/// value drawn, so the number of draws does not depend on the values drawn.
32/// The reduction's statistical distance from uniform is at most `n / 2⁶⁴`:
33/// below 2⁻⁵⁵ for `n ≤ 256` and still at most 2⁻³² at `n = u32::MAX`. That
34/// is a ceiling, and a loose one for most bounds. The distance for a given
35/// `n` is exactly `r(n − r) / (n · 2⁶⁴)`, where `r = 2⁶⁴ mod n` is the
36/// number of values that get one extra word of the draw space; it is zero
37/// when `n` divides `2⁶⁴`, and largest when `r` is near `n / 2`. A protocol
38/// that needs exact uniformity must account for that term.
39pub trait BoundedRng<T> {
40    /// The type of the value drawn. This is the bound's own type for `u32`
41    /// and [`Protected<u32>`], and [`Protected<u32>`] for a
42    /// [`Protected<NonZeroU32>`] bound, since `0` is a valid draw.
43    ///
44    /// The result no longer has the bound's own type, so a call that leaves
45    /// the drawn value's type to inference may now need an annotation where
46    /// it did not before.
47    type Output;
48
49    /// A value in `0..n`: at least `0`, strictly below `n`, uniform to
50    /// within the `n / 2⁶⁴` bias bound documented on [`BoundedRng`].
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; a caller
57    /// whose secret bound may be zero should construct a
58    /// [`Protected<NonZeroU32>`] instead, for which this method never
59    /// panics.
60    fn next_below(&mut self, n: T) -> Self::Output;
61}
62
63/// The older **inclusive** bounded draw, `0..=max`.
64///
65/// Deprecated: before cipherstash/vitaminc#198 the inclusive bound was
66/// honoured only when `max` was not a power of two, so callers written
67/// against either meaning were wrong for some inputs. It now does what its
68/// doc always said, for every `max` up to and including `u32::MAX`, with the
69/// same fixed-count draw and the same `(max + 1) / 2⁶⁴` bias bound as
70/// [`BoundedRng::next_below`].
71///
72/// Two things changed for existing callers besides the power-of-two case.
73/// Every call now consumes exactly one 64-bit word of the generator's
74/// stream, where it previously consumed one or more 32-bit words, so any
75/// sequence that interleaves bounded and raw draws from a fixed seed yields
76/// different values than before. And the value drawn for a given seed is
77/// different, because the reduction is different.
78///
79/// This is a separate trait rather than a deprecated method on
80/// [`BoundedRng`] so that implementors of `BoundedRng` are not forced to
81/// write a method they are told not to call, and so that removing it later
82/// is a trait deletion rather than another breaking change to `BoundedRng`.
83#[deprecated(
84    note = "inclusive `0..=max`; use `BoundedRng::next_below(max + 1)`, or `next_below(n)` when you have a length `n`"
85)]
86pub trait BoundedRngInclusive<T> {
87    /// A value in `0..=max`, uniform to within the `(max + 1) / 2⁶⁴` bias
88    /// bound documented on [`BoundedRng`].
89    ///
90    /// The equivalent call is `BoundedRng::next_below(max + 1)` (for
91    /// `max == u32::MAX` that is the whole word: use
92    /// [`Rng::next_u32`](rand::Rng::next_u32)), or `next_below(n)` when the
93    /// caller has a length `n` rather than a maximum.
94    fn next_bounded(&mut self, max: T) -> T;
95}
96
97impl BoundedRng<u32> for SafeRand {
98    type Output = u32;
99
100    fn next_below(&mut self, n: u32) -> u32 {
101        below_u32(self, n)
102    }
103}
104
105impl BoundedRng<Protected<u32>> for SafeRand {
106    type Output = Protected<u32>;
107
108    /// See [`BoundedRng::next_below`].
109    ///
110    /// # Panics
111    ///
112    /// Panics if the wrapped bound is zero. That panic is observable on a
113    /// secret; a caller whose secret bound may be zero should construct a
114    /// [`Protected<NonZeroU32>`] and use that impl, which never panics.
115    fn next_below(&mut self, n: Protected<u32>) -> Protected<u32> {
116        // Check the bound before `map` unwraps it: `Controlled::map` hands
117        // the raw inner value to the closure, so a zero check inside the
118        // closure would panic with the secret already out of its wrapper and
119        // unwinding without being wiped. Checked here, an unwind drops `n`
120        // still wrapped, and `Protected`'s drop glue zeroizes it.
121        assert!(*n.risky_ref() != 0, "range must be non-zero");
122        n.map(|n| below_u32(self, n))
123    }
124}
125
126impl BoundedRng<Protected<NonZeroU32>> for SafeRand {
127    type Output = Protected<u32>;
128
129    /// See [`BoundedRng::next_below`]. The bound carries its own non-zero
130    /// proof, so every input takes the same path here: nothing checks the
131    /// secret, and the non-zero assertion in the shared reduction cannot
132    /// fire for a bound of this type.
133    fn next_below(&mut self, n: Protected<NonZeroU32>) -> Protected<u32> {
134        n.map(|n| below_u32(self, n.get()))
135    }
136}
137
138#[allow(deprecated)]
139impl BoundedRngInclusive<u32> for SafeRand {
140    fn next_bounded(&mut self, max: u32) -> u32 {
141        upto_u32(self, max)
142    }
143}
144
145#[allow(deprecated)]
146impl BoundedRngInclusive<Protected<u32>> for SafeRand {
147    fn next_bounded(&mut self, max: Protected<u32>) -> Protected<u32> {
148        // `0..=max` is never empty, so no pre-check is needed here.
149        max.map(|max| upto_u32(self, max))
150    }
151}
152
153/// A value in `0..range`, for `range >= 1`, uniform to within `range / 2⁶⁴`.
154///
155/// One 64-bit draw reduced with Lemire's multiply-high method. See
156/// [`BoundedRng`] for the contract, including the `range / 2⁶⁴` bias bound;
157/// that bound is why this helper is not exposed for ranges near `2⁶⁴`, where
158/// a single 64-bit draw is no longer close to uniform.
159///
160/// # Panics
161///
162/// Panics if `range` is zero.
163pub(crate) fn below_u64<R: CryptoRng>(rng: &mut R, range: u64) -> u64 {
164    assert!(range > 0, "range must be non-zero");
165    ((u128::from(rng.next_u64()) * u128::from(range)) >> 64) as u64
166}
167
168/// [`below_u64`] at `u32` width: a value in `0..range`.
169pub(crate) fn below_u32<R: CryptoRng>(rng: &mut R, range: u32) -> u32 {
170    below_u64(rng, u64::from(range)) as u32
171}
172
173/// The inclusive form, a value in `0..=max`, for every `max` including
174/// `u32::MAX`: `max + 1` always fits a `u64`, so the sampler sees the full
175/// inclusive range without overflow. Every inclusive entry point routes
176/// through here so the edge is handled exactly once.
177pub(crate) fn upto_u32<R: CryptoRng>(rng: &mut R, max: u32) -> u32 {
178    below_u64(rng, u64::from(max) + 1) as u32
179}
180
181#[cfg(test)]
182mod test {
183    use std::convert::Infallible;
184    use std::num::NonZeroU32;
185
186    use rand::TryCryptoRng;
187
188    use super::{below_u32, upto_u32};
189
190    /// Yields one fixed word, so the reduction can be pinned to exact
191    /// outputs at the ends and the midpoint of the draw space.
192    struct FixedDraw(u64);
193    impl rand::TryRng for FixedDraw {
194        type Error = Infallible;
195
196        fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
197            Ok(self.0 as u32)
198        }
199
200        fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
201            Ok(self.0)
202        }
203
204        fn try_fill_bytes(&mut self, _dest: &mut [u8]) -> Result<(), Self::Error> {
205            unimplemented!()
206        }
207    }
208    impl TryCryptoRng for FixedDraw {}
209
210    // These tests drive the same `below_u32` / `upto_u32` helpers that every
211    // `BoundedRng` impl and `next_bounded_u32` call, so a regression in the
212    // reduction cannot hide behind a test-only copy of it.
213
214    #[test]
215    fn min_draw_maps_to_zero() {
216        assert_eq!(0, upto_u32(&mut FixedDraw(0), 9));
217        assert_eq!(0, below_u32(&mut FixedDraw(0), 10));
218    }
219
220    #[test]
221    fn max_draw_maps_to_the_top_of_the_range() {
222        // Inclusive: the bound itself is the top value. `31` is the case the
223        // old power-of-two branch could never return.
224        assert_eq!(9, upto_u32(&mut FixedDraw(u64::MAX), 9));
225        assert_eq!(31, upto_u32(&mut FixedDraw(u64::MAX), 31));
226        assert_eq!(32, upto_u32(&mut FixedDraw(u64::MAX), 32));
227        // Half-open: the bound is excluded.
228        assert_eq!(9, below_u32(&mut FixedDraw(u64::MAX), 10));
229        assert_eq!(31, below_u32(&mut FixedDraw(u64::MAX), 32));
230    }
231
232    #[test]
233    fn midpoint_draw_maps_to_half_range() {
234        assert_eq!(5, upto_u32(&mut FixedDraw(1 << 63), 9));
235        assert_eq!(5, below_u32(&mut FixedDraw(1 << 63), 10));
236    }
237
238    #[test]
239    fn a_bound_of_zero_is_always_zero_inclusively() {
240        // `0..=0` has one value; `0..0` has none.
241        assert_eq!(0, upto_u32(&mut FixedDraw(u64::MAX), 0));
242        assert_eq!(0, below_u32(&mut FixedDraw(u64::MAX), 1));
243    }
244
245    #[test]
246    #[should_panic(expected = "range must be non-zero")]
247    fn an_empty_half_open_range_panics() {
248        below_u32(&mut FixedDraw(0), 0);
249    }
250
251    #[test]
252    fn max_of_u32_max_covers_the_whole_word() {
253        assert_eq!(u32::MAX, upto_u32(&mut FixedDraw(u64::MAX), u32::MAX));
254        assert_eq!(0, upto_u32(&mut FixedDraw(0), u32::MAX));
255    }
256
257    #[test]
258    fn a_zero_protected_bound_panics_before_it_is_unwrapped() {
259        use crate::SafeRand;
260        use rand::SeedableRng;
261        use vitaminc_protected::Protected;
262
263        // Pins the behaviour documented on the `Protected<u32>` impl: a
264        // zero secret bound panics, and it panics before `map` unwraps the
265        // value, so the generator is never called.
266        let mut rng = SafeRand::from_seed([5u8; 32]);
267        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
268            let _: Protected<u32> = rng.next_below(Protected::new(0));
269        }));
270        let msg = caught.expect_err("a zero bound must panic");
271        let msg = msg
272            .downcast_ref::<String>()
273            .map(String::as_str)
274            .or_else(|| msg.downcast_ref::<&str>().copied())
275            .unwrap_or_default();
276        assert!(msg.contains("range must be non-zero"), "{msg}");
277        // No draw was spent on the failed call.
278        let mut untouched = SafeRand::from_seed([5u8; 32]);
279        assert_eq!(untouched.next_below(1000), rng.next_below(1000));
280    }
281
282    #[test]
283    fn trait_impls_route_through_the_shared_helpers() {
284        use super::BoundedRng;
285        #[allow(deprecated)]
286        use super::BoundedRngInclusive;
287        use crate::SafeRand;
288        use rand::SeedableRng;
289        use vitaminc_protected::{Controlled, Protected};
290
291        // The plain, `Protected<u32>` and `Protected<NonZeroU32>` impls,
292        // and the inherent `SafeRand::next_below`, must all be the same draw
293        // and reduction as the helpers over the same seed: not a constant,
294        // not a differently reduced value, and one draw per call.
295        let mut helper = SafeRand::from_seed([5u8; 32]);
296        let mut plain = SafeRand::from_seed([5u8; 32]);
297        let mut protected = SafeRand::from_seed([5u8; 32]);
298        let mut nonzero = SafeRand::from_seed([5u8; 32]);
299        let mut inherent = SafeRand::from_seed([5u8; 32]);
300        for _ in 0..100 {
301            let want = below_u32(&mut helper, 1000);
302            assert_eq!(want, BoundedRng::next_below(&mut plain, 1000u32));
303            let p: Protected<u32> = protected.next_below(Protected::new(1000));
304            assert_eq!(want, p.risky_unwrap());
305            let bound = Protected::new(NonZeroU32::new(1000).unwrap());
306            let p: Protected<u32> = nonzero.next_below(bound);
307            assert_eq!(want, p.risky_unwrap());
308            assert_eq!(want, inherent.next_below(1000));
309        }
310
311        #[allow(deprecated)]
312        for _ in 0..100 {
313            let want = upto_u32(&mut helper, 999);
314            assert_eq!(want, plain.next_bounded(999u32));
315            let p: Protected<u32> = protected.next_bounded(Protected::new(999));
316            assert_eq!(want, p.risky_unwrap());
317            assert_eq!(want, inherent.next_bounded_u32(999));
318        }
319    }
320
321    #[test]
322    fn a_nonzero_protected_bound_of_one_draws_without_rejecting() {
323        use crate::SafeRand;
324        use rand::SeedableRng;
325        use vitaminc_protected::{Controlled, Protected};
326
327        // The smallest bound the type admits: `0..1` has exactly one value,
328        // and the impl has no check that could reject it.
329        let mut rng = SafeRand::from_seed([5u8; 32]);
330        for _ in 0..16 {
331            let p: Protected<u32> = rng.next_below(Protected::new(NonZeroU32::MIN));
332            assert_eq!(0, p.risky_unwrap(), "`0..1` has only one value to draw");
333        }
334    }
335}