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}