codlet_core/rng.rs
1//! Randomness abstraction (RFC-020).
2//!
3//! All secret generation goes through [`RandomSource`] so it can be made
4//! deterministic in tests and so that production failure propagates instead of
5//! silently degrading. The cardinal rule: **RNG failure is fatal to the
6//! operation; no fallback value is ever produced** (INV-3).
7
8use crate::error::RandomError;
9
10/// A source of cryptographically secure random bytes.
11///
12/// Implementations must fill the entire buffer with unpredictable bytes or
13/// return [`RandomError`]. Returning `Ok(())` after a partial or zeroed fill is
14/// a security defect.
15pub trait RandomSource {
16 /// Fill `dest` entirely with secure random bytes, or fail.
17 ///
18 /// # Errors
19 /// Returns [`RandomError`] if secure randomness cannot be obtained. Callers
20 /// must propagate the error and must not substitute any default value.
21 fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), RandomError>;
22}
23
24/// Production randomness backed by the platform CSPRNG via `getrandom`.
25///
26/// On WASM/Workers this delegates to `crypto.getRandomValues` (matching the
27/// source service's RNG path).
28#[derive(Debug, Default, Clone, Copy)]
29pub struct SystemRandom;
30
31impl SystemRandom {
32 /// Construct the system randomness source.
33 #[must_use]
34 pub fn new() -> Self {
35 Self
36 }
37}
38
39impl RandomSource for SystemRandom {
40 fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), RandomError> {
41 // Propagate any failure as RandomError. Never `unwrap`/`expect` here:
42 // the source service's `random_token` used `.expect("getrandom failed")`
43 // which would panic rather than fail closed gracefully; codlet returns
44 // a typed error so callers can map it to a generic public failure.
45 getrandom::getrandom(dest).map_err(|_| RandomError)
46 }
47}
48
49/// Deterministic test randomness. Available under the `test-utils` feature and
50/// in this crate's own tests. **Never** use in production: output is
51/// predictable by construction.
52#[cfg(any(test, feature = "test-utils"))]
53#[derive(Debug, Clone)]
54pub struct FixedBytesRandom {
55 bytes: Vec<u8>,
56 pos: usize,
57}
58
59#[cfg(any(test, feature = "test-utils"))]
60impl FixedBytesRandom {
61 /// Create a source that yields the given bytes in order, cycling when
62 /// exhausted. Useful for steering rejection sampling in tests.
63 #[must_use]
64 pub fn new(bytes: Vec<u8>) -> Self {
65 assert!(
66 !bytes.is_empty(),
67 "FixedBytesRandom needs at least one byte"
68 );
69 Self { bytes, pos: 0 }
70 }
71}
72
73#[cfg(any(test, feature = "test-utils"))]
74impl RandomSource for FixedBytesRandom {
75 fn fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), RandomError> {
76 for slot in dest.iter_mut() {
77 *slot = self.bytes[self.pos % self.bytes.len()];
78 self.pos += 1;
79 }
80 Ok(())
81 }
82}
83
84/// A randomness source that always fails. Used to prove fail-closed behavior
85/// (RFC-003 ยง11.5 acceptance: "RNG failure test uses a fake RNG that always
86/// errors").
87#[cfg(any(test, feature = "test-utils"))]
88#[derive(Debug, Default, Clone, Copy)]
89pub struct AlwaysFailRandom;
90
91#[cfg(any(test, feature = "test-utils"))]
92impl RandomSource for AlwaysFailRandom {
93 fn fill_bytes(&mut self, _dest: &mut [u8]) -> Result<(), RandomError> {
94 Err(RandomError)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn system_random_fills_distinct_buffers() {
104 let mut r = SystemRandom::new();
105 let mut a = [0u8; 32];
106 let mut b = [0u8; 32];
107 r.fill_bytes(&mut a).unwrap();
108 r.fill_bytes(&mut b).unwrap();
109 // Astronomically unlikely to be equal if real entropy is used.
110 assert_ne!(a, b);
111 assert!(a.iter().any(|&x| x != 0));
112 }
113
114 #[test]
115 fn always_fail_random_errors() {
116 let mut r = AlwaysFailRandom;
117 let mut buf = [0u8; 4];
118 assert_eq!(r.fill_bytes(&mut buf), Err(RandomError));
119 }
120
121 #[test]
122 fn fixed_bytes_random_is_deterministic() {
123 let mut r = FixedBytesRandom::new(vec![1, 2, 3]);
124 let mut buf = [0u8; 5];
125 r.fill_bytes(&mut buf).unwrap();
126 assert_eq!(buf, [1, 2, 3, 1, 2]);
127 }
128}