Skip to main content

oxicrypto_rand/
helpers.rs

1//! Convenience free functions: `random_bytes`, `random_nonce`, `random_range`,
2//! `reseed`, `shuffle`, and related helpers.
3
4use oxicrypto_core::CryptoError;
5use rand_chacha::ChaCha20Rng;
6use rand_core::SeedableRng;
7
8use crate::OxiRng;
9
10// ── random_bytes ──────────────────────────────────────────────────────────────
11
12/// Allocate and fill a `Vec<u8>` with `len` cryptographically secure random
13/// bytes.
14///
15/// Returns [`CryptoError::Rng`] if the OS random source is unavailable.
16#[must_use = "random bytes should be used; discarding them is likely a bug"]
17pub fn random_bytes(len: usize) -> Result<Vec<u8>, CryptoError> {
18    let mut rng = OxiRng::new()?;
19    let mut buf = vec![0u8; len];
20    use oxicrypto_core::Rng;
21    rng.fill(&mut buf)?;
22    Ok(buf)
23}
24
25// ── random_range ──────────────────────────────────────────────────────────────
26
27/// Generate a random integer in `[min, max)` using rejection sampling to
28/// eliminate modulo bias.
29///
30/// Returns [`CryptoError::BadInput`] if `min >= max`.
31///
32/// # Note
33///
34/// The old single-argument form `random_range(max)` has been renamed to
35/// [`random_range_to`].  Any existing callers should update to the two-argument
36/// form or use [`random_range_to`] explicitly.
37#[must_use = "random range value should be used; discarding it is likely a bug"]
38pub fn random_range(min: u64, max: u64) -> Result<u64, CryptoError> {
39    let mut rng = OxiRng::new()?;
40    random_range_unbiased(&mut rng, min, max)
41}
42
43/// Generate a random integer in `[0, max)` using rejection sampling.
44///
45/// This is the renamed form of the old single-argument `random_range(max)`.
46/// Returns [`CryptoError::BadInput`] if `max == 0`.
47#[must_use = "random range value should be used; discarding it is likely a bug"]
48pub fn random_range_to(max: u64) -> Result<u64, CryptoError> {
49    if max == 0 {
50        return Err(CryptoError::BadInput);
51    }
52    let mut rng = OxiRng::new()?;
53    random_range_unbiased(&mut rng, 0, max)
54}
55
56/// Generate a random integer in `[min, max)` using an existing RNG, with
57/// rejection sampling to eliminate modulo bias.
58///
59/// Returns [`CryptoError::BadInput`] if `min >= max`.
60pub fn random_range_unbiased(rng: &mut OxiRng, min: u64, max: u64) -> Result<u64, CryptoError> {
61    if min >= max {
62        return Err(CryptoError::BadInput);
63    }
64    let range = max - min;
65    if range == 1 {
66        return Ok(min);
67    }
68    // Rejection threshold: largest value such that the number of valid values
69    // is an exact multiple of `range`, eliminating modulo bias.
70    let threshold = u64::MAX - (u64::MAX % range);
71    loop {
72        let mut buf = [0u8; 8];
73        use oxicrypto_core::Rng;
74        rng.fill(&mut buf)?;
75        let val = u64::from_le_bytes(buf);
76        if val < threshold {
77            return Ok(min + (val % range));
78        }
79    }
80}
81
82/// Internal helper: generate random in `[0, max)` using the provided rng.
83/// Kept for use by [`shuffle`].
84pub(crate) fn random_range_with_rng(max: u64, rng: &mut OxiRng) -> Result<u64, CryptoError> {
85    if max == 0 {
86        return Err(CryptoError::BadInput);
87    }
88    if max == 1 {
89        return Ok(0);
90    }
91    let threshold = u64::MAX - (u64::MAX % max);
92    loop {
93        let mut buf = [0u8; 8];
94        use oxicrypto_core::Rng;
95        rng.fill(&mut buf)?;
96        let val = u64::from_le_bytes(buf);
97        if val < threshold {
98            return Ok(val % max);
99        }
100    }
101}
102
103// ── random_bool ───────────────────────────────────────────────────────────────
104
105/// Generate a random `bool` with the given probability of being `true`.
106///
107/// - `probability == 0.0` always returns `false`.
108/// - `probability == 1.0` always returns `true`.
109/// - Returns [`CryptoError::BadInput`] if `probability` is outside `[0.0, 1.0]`.
110pub fn random_bool(probability: f64) -> Result<bool, CryptoError> {
111    let mut rng = OxiRng::new()?;
112    random_bool_with_rng(&mut rng, probability)
113}
114
115/// Generate a random `bool` using an existing RNG, with the given probability
116/// of being `true`.
117///
118/// Returns [`CryptoError::BadInput`] if `probability` is outside `[0.0, 1.0]`.
119pub fn random_bool_with_rng(rng: &mut OxiRng, probability: f64) -> Result<bool, CryptoError> {
120    if !(0.0..=1.0).contains(&probability) {
121        return Err(CryptoError::BadInput);
122    }
123    if probability == 0.0 {
124        return Ok(false);
125    }
126    if probability == 1.0 {
127        return Ok(true);
128    }
129    let threshold = (probability * (u64::MAX as f64)) as u64;
130    let mut buf = [0u8; 8];
131    use oxicrypto_core::Rng;
132    rng.fill(&mut buf)?;
133    let val = u64::from_le_bytes(buf);
134    Ok(val < threshold)
135}
136
137// ── weighted_choice ───────────────────────────────────────────────────────────
138
139/// Sample an index from a weighted distribution.
140///
141/// Given a slice of non-negative integer weights, returns a random index `i`
142/// such that the probability of each index is proportional to `weights[i]`.
143///
144/// Returns [`CryptoError::BadInput`] if:
145/// - `weights` is empty, or
146/// - all weights are zero.
147pub fn weighted_choice(weights: &[u64]) -> Result<usize, CryptoError> {
148    let mut rng = OxiRng::new()?;
149    weighted_choice_with_rng(&mut rng, weights)
150}
151
152/// Sample an index from a weighted distribution using an existing RNG.
153///
154/// See [`weighted_choice`] for details.
155pub fn weighted_choice_with_rng(rng: &mut OxiRng, weights: &[u64]) -> Result<usize, CryptoError> {
156    if weights.is_empty() {
157        return Err(CryptoError::BadInput);
158    }
159    let total: u64 = weights
160        .iter()
161        .try_fold(0u64, |acc, &w| acc.checked_add(w))
162        .ok_or(CryptoError::BadInput)?;
163    if total == 0 {
164        return Err(CryptoError::BadInput);
165    }
166    let pick = random_range_unbiased(rng, 0, total)?;
167    let mut cumulative: u64 = 0;
168    for (i, &w) in weights.iter().enumerate() {
169        cumulative = cumulative.saturating_add(w);
170        if pick < cumulative {
171            return Ok(i);
172        }
173    }
174    // Should never be reached if `total` and cumulative sums are consistent.
175    Err(CryptoError::Internal(
176        "weighted_choice: internal invariant violated",
177    ))
178}
179
180// ── random_nonce ──────────────────────────────────────────────────────────────
181
182/// Generate a random nonce of `N` bytes for use with AEAD algorithms.
183///
184/// Returns [`CryptoError::Rng`] if the OS random source is unavailable.
185#[must_use = "random nonce should be used; discarding it is likely a bug"]
186pub fn random_nonce<const N: usize>() -> Result<[u8; N], CryptoError> {
187    let mut rng = OxiRng::new()?;
188    let mut nonce = [0u8; N];
189    use oxicrypto_core::Rng;
190    rng.fill(&mut nonce)?;
191    Ok(nonce)
192}
193
194// ── reseed ────────────────────────────────────────────────────────────────────
195
196/// Perform a manual reseed of the given `OxiRng` from OS entropy.
197///
198/// This replaces the internal ChaCha20 state with a fresh 32-byte seed and
199/// updates the stored PID to the current process.
200pub fn reseed(rng: &mut OxiRng) -> Result<(), CryptoError> {
201    let mut seed = [0u8; 32];
202    getrandom::fill(&mut seed).map_err(|_| CryptoError::Rng)?;
203    rng.inner = ChaCha20Rng::from_seed(seed);
204    #[cfg(unix)]
205    {
206        rng.last_pid = std::process::id();
207    }
208    Ok(())
209}
210
211// ── random_u32 / random_u64 / random_u128 ────────────────────────────────────
212
213/// Generate a cryptographically secure random `u32`.
214///
215/// Returns [`CryptoError::Rng`] if the OS random source is unavailable.
216pub fn random_u32() -> Result<u32, CryptoError> {
217    let mut rng = OxiRng::new()?;
218    let mut buf = [0u8; 4];
219    use oxicrypto_core::Rng;
220    rng.fill(&mut buf)?;
221    Ok(u32::from_le_bytes(buf))
222}
223
224/// Generate a cryptographically secure random `u64`.
225///
226/// Returns [`CryptoError::Rng`] if the OS random source is unavailable.
227pub fn random_u64() -> Result<u64, CryptoError> {
228    let mut rng = OxiRng::new()?;
229    let mut buf = [0u8; 8];
230    use oxicrypto_core::Rng;
231    rng.fill(&mut buf)?;
232    Ok(u64::from_le_bytes(buf))
233}
234
235/// Generate a cryptographically secure random `u128`.
236///
237/// Returns [`CryptoError::Rng`] if the OS random source is unavailable.
238pub fn random_u128() -> Result<u128, CryptoError> {
239    let mut rng = OxiRng::new()?;
240    let mut buf = [0u8; 16];
241    use oxicrypto_core::Rng;
242    rng.fill(&mut buf)?;
243    Ok(u128::from_le_bytes(buf))
244}
245
246// ── shuffle ───────────────────────────────────────────────────────────────────
247
248/// Cryptographically secure in-place Fisher-Yates shuffle.
249///
250/// Returns `Ok(())` on success, `Err(CryptoError::Rng)` if the RNG fails.
251pub fn shuffle<T>(slice: &mut [T], rng: &mut OxiRng) -> Result<(), CryptoError> {
252    let n = slice.len();
253    if n <= 1 {
254        return Ok(());
255    }
256    // Fisher-Yates: for i from n-1 down to 1, swap slice[i] with slice[rand(0..=i)]
257    for i in (1..n).rev() {
258        let j = random_range_with_rng(i as u64 + 1, rng)? as usize;
259        slice.swap(i, j);
260    }
261    Ok(())
262}
263
264// ── check_entropy ──────────────────────────────────────────────────────────────
265
266/// Perform a basic OS-entropy smoke test.
267///
268/// Draws two 32-byte samples from `getrandom` and verifies:
269/// 1. Neither buffer is all-zero (a sign of catastrophic RNG failure).
270/// 2. Both buffers differ from each other (two identical draws would also
271///    indicate a catastrophic failure).
272///
273/// # Note
274///
275/// This is a smoke test, **not** a cryptographic NIST SP 800-90B health test.
276/// It catches the most obvious hardware/OS RNG failures only.
277///
278/// Returns [`CryptoError::Rng`] if either check fails.
279pub fn check_entropy() -> Result<(), CryptoError> {
280    let mut a = [0u8; 32];
281    let mut b = [0u8; 32];
282    getrandom::fill(&mut a).map_err(|_| CryptoError::Rng)?;
283    getrandom::fill(&mut b).map_err(|_| CryptoError::Rng)?;
284    if a == [0u8; 32] || b == [0u8; 32] {
285        return Err(CryptoError::Rng);
286    }
287    if a == b {
288        return Err(CryptoError::Rng);
289    }
290    Ok(())
291}
292
293// ── Unit tests ────────────────────────────────────────────────────────────────
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn random_bytes_returns_correct_length() {
301        let bytes = random_bytes(64).expect("random_bytes failed");
302        assert_eq!(bytes.len(), 64);
303        assert_ne!(bytes, vec![0u8; 64]);
304    }
305
306    #[test]
307    fn random_bytes_zero_length() {
308        let bytes = random_bytes(0).expect("random_bytes(0) failed");
309        assert!(bytes.is_empty());
310    }
311
312    #[test]
313    fn random_range_to_zero_errors() {
314        let result = random_range_to(0);
315        assert_eq!(result, Err(CryptoError::BadInput));
316    }
317
318    #[test]
319    fn random_range_to_one_returns_zero() {
320        let val = random_range_to(1).expect("random_range_to(1) failed");
321        assert_eq!(val, 0);
322    }
323
324    #[test]
325    fn random_range_to_bounded() {
326        for _ in 0..100 {
327            let val = random_range_to(10).expect("random_range_to(10) failed");
328            assert!(val < 10, "random_range_to(10) returned {val} >= 10");
329        }
330    }
331
332    #[test]
333    fn random_range_two_arg_in_bounds() {
334        for _ in 0..200 {
335            let val = random_range(5, 10).expect("random_range(5, 10) failed");
336            assert!((5..10).contains(&val), "random_range(5, 10) returned {val}");
337        }
338    }
339
340    #[test]
341    fn random_range_two_arg_min_ge_max_errors() {
342        assert_eq!(random_range(10, 5), Err(CryptoError::BadInput));
343        assert_eq!(random_range(5, 5), Err(CryptoError::BadInput));
344    }
345
346    #[test]
347    fn random_range_two_arg_zero_one_always_zero() {
348        for _ in 0..50 {
349            let val = random_range(0, 1).expect("random_range(0, 1) failed");
350            assert_eq!(val, 0, "random_range(0, 1) must always be 0");
351        }
352    }
353
354    #[test]
355    fn random_bool_zero_always_false() {
356        for _ in 0..50 {
357            let b = random_bool(0.0).expect("random_bool(0.0) failed");
358            assert!(!b, "random_bool(0.0) must always be false");
359        }
360    }
361
362    #[test]
363    fn random_bool_one_always_true() {
364        for _ in 0..50 {
365            let b = random_bool(1.0).expect("random_bool(1.0) failed");
366            assert!(b, "random_bool(1.0) must always be true");
367        }
368    }
369
370    #[test]
371    fn random_bool_invalid_probability() {
372        assert_eq!(random_bool(-0.1), Err(CryptoError::BadInput));
373        assert_eq!(random_bool(1.1), Err(CryptoError::BadInput));
374        assert_eq!(random_bool(f64::NAN), Err(CryptoError::BadInput));
375    }
376
377    #[test]
378    fn random_bool_half_has_both_outcomes() {
379        let mut trues = 0u32;
380        let mut falses = 0u32;
381        for _ in 0..1000 {
382            if random_bool(0.5).expect("random_bool(0.5) failed") {
383                trues += 1;
384            } else {
385                falses += 1;
386            }
387        }
388        assert!(
389            trues > 300 && falses > 300,
390            "Expected roughly equal trues/falses, got {trues}/{falses}"
391        );
392    }
393
394    #[test]
395    fn weighted_choice_single_nonzero_always_returns_it() {
396        for _ in 0..50 {
397            let idx = weighted_choice(&[0, 1, 0]).expect("weighted_choice failed");
398            assert_eq!(idx, 1, "Only index 1 has non-zero weight");
399        }
400    }
401
402    #[test]
403    fn weighted_choice_empty_errors() {
404        assert_eq!(weighted_choice(&[]), Err(CryptoError::BadInput));
405    }
406
407    #[test]
408    fn weighted_choice_all_zero_errors() {
409        assert_eq!(weighted_choice(&[0, 0]), Err(CryptoError::BadInput));
410    }
411
412    #[test]
413    fn weighted_choice_proportional() {
414        let mut count0 = 0u32;
415        let mut count1 = 0u32;
416        for _ in 0..1000 {
417            match weighted_choice(&[3, 1]).expect("weighted_choice failed") {
418                0 => count0 += 1,
419                1 => count1 += 1,
420                _ => panic!("unexpected index"),
421            }
422        }
423        assert!(
424            count0 > count1,
425            "Index 0 (weight 3) should win more than index 1 (weight 1); got {count0} vs {count1}"
426        );
427    }
428
429    #[test]
430    fn random_nonce_12_works() {
431        let nonce: [u8; 12] = random_nonce().expect("random_nonce failed");
432        assert_ne!(nonce, [0u8; 12]);
433    }
434
435    #[test]
436    fn random_nonce_24_works() {
437        let nonce: [u8; 24] = random_nonce().expect("random_nonce failed");
438        assert_ne!(nonce, [0u8; 24]);
439    }
440
441    #[test]
442    fn reseed_free_fn_changes_output() {
443        let mut rng = OxiRng::new().expect("new failed");
444        let mut buf1 = [0u8; 32];
445        use oxicrypto_core::Rng;
446        rng.fill(&mut buf1).expect("fill 1 failed");
447        reseed(&mut rng).expect("reseed failed");
448        let mut buf2 = [0u8; 32];
449        rng.fill(&mut buf2).expect("fill 2 failed");
450        assert_ne!(buf1, buf2, "Output after reseed should differ");
451    }
452
453    #[test]
454    fn random_u32_nonzero_variance() {
455        let vals: Vec<u32> = (0..1000)
456            .map(|_| random_u32().expect("random_u32 failed"))
457            .collect();
458        let first = vals[0];
459        assert!(
460            vals.iter().any(|&v| v != first),
461            "1000 consecutive random_u32() values must not all be equal"
462        );
463    }
464
465    #[test]
466    fn random_u64_type_check() {
467        let v: u64 = random_u64().expect("random_u64 failed");
468        let _ = v;
469    }
470
471    #[test]
472    fn random_u128_type_check() {
473        let v: u128 = random_u128().expect("random_u128 failed");
474        let _ = v;
475    }
476
477    #[test]
478    fn shuffle_preserves_elements() {
479        let mut rng = OxiRng::new().expect("OxiRng::new failed");
480        let original: Vec<i32> = (0..100).collect();
481        let mut shuffled = original.clone();
482        shuffle(&mut shuffled, &mut rng).expect("shuffle failed");
483        let mut sorted_original = original.clone();
484        let mut sorted_shuffled = shuffled.clone();
485        sorted_original.sort_unstable();
486        sorted_shuffled.sort_unstable();
487        assert_eq!(
488            sorted_original, sorted_shuffled,
489            "Shuffle must preserve all elements"
490        );
491    }
492
493    #[test]
494    fn shuffle_empty() {
495        let mut rng = OxiRng::new().expect("OxiRng::new failed");
496        let mut empty: Vec<u8> = Vec::new();
497        let result = shuffle(&mut empty, &mut rng);
498        assert!(result.is_ok(), "Shuffling an empty slice should be Ok");
499    }
500
501    #[test]
502    fn check_entropy_passes_on_healthy_system() {
503        check_entropy().expect("check_entropy() should pass on a healthy system");
504    }
505
506    #[test]
507    fn test_random_range_bounds() {
508        for _ in 0..100 {
509            let v = random_range(5, 10).expect("random_range(5, 10)");
510            assert!((5..10).contains(&v), "value {v} out of [5, 10)");
511        }
512    }
513
514    #[test]
515    fn test_random_range_min_equals_max_errors() {
516        assert_eq!(random_range(5, 5), Err(CryptoError::BadInput));
517    }
518
519    #[test]
520    fn test_random_range_min_greater_than_max_errors() {
521        assert_eq!(random_range(10, 5), Err(CryptoError::BadInput));
522    }
523
524    #[test]
525    fn test_random_range_wide_range() {
526        for _ in 0..50 {
527            let v = random_range(0, u64::MAX).expect("random_range(0, u64::MAX)");
528            let _ = v;
529        }
530    }
531}