Skip to main content

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}