1use oxicrypto_core::CryptoError;
5use rand_chacha::ChaCha20Rng;
6use rand_core::SeedableRng;
7
8use crate::OxiRng;
9
10#[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#[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#[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
56pub 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 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
82pub(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
103pub fn random_bool(probability: f64) -> Result<bool, CryptoError> {
111 let mut rng = OxiRng::new()?;
112 random_bool_with_rng(&mut rng, probability)
113}
114
115pub 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
137pub fn weighted_choice(weights: &[u64]) -> Result<usize, CryptoError> {
148 let mut rng = OxiRng::new()?;
149 weighted_choice_with_rng(&mut rng, weights)
150}
151
152pub 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 Err(CryptoError::Internal(
176 "weighted_choice: internal invariant violated",
177 ))
178}
179
180#[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
194pub 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
211pub 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
224pub 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
235pub 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
246pub 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 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
264pub 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#[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}