Skip to main content

oxicrypto_rand/
reseeding.rs

1//! `ReseedingRng` — a wrapper around [`OxiRng`] that automatically reseeds
2//! from OS entropy after generating a configurable number of bytes.
3
4use oxicrypto_core::{CryptoError, Rng};
5
6use crate::OxiRng;
7
8/// Default threshold for automatic reseeding: 1 MiB of output.
9const DEFAULT_RESEED_THRESHOLD: u64 = 1 << 20;
10
11/// A [`OxiRng`] wrapper that automatically reseeds from OS entropy after
12/// generating a configurable number of bytes (default: 1 MiB).
13///
14/// This implements a forward-secrecy interval consistent with NIST SP 800-90A
15/// §9.2 recommendations.  After each reseed the internal byte counter resets.
16///
17/// # Observability
18///
19/// Use [`ReseedingRng::bytes_generated`] to inspect how many bytes have been
20/// produced since the last reseed.
21pub struct ReseedingRng {
22    inner: OxiRng,
23    bytes_generated: u64,
24    reseed_threshold: u64,
25}
26
27impl ReseedingRng {
28    /// Create a new [`ReseedingRng`] with the default 1 MiB reseed threshold.
29    pub fn new() -> Result<Self, CryptoError> {
30        Self::with_threshold(DEFAULT_RESEED_THRESHOLD)
31    }
32
33    /// Create a new [`ReseedingRng`] with a custom reseed threshold (bytes).
34    ///
35    /// A threshold of `0` would reseed on every call — technically valid but
36    /// very slow.  Reasonable values are 64 KiB to 64 MiB.
37    pub fn with_threshold(threshold: u64) -> Result<Self, CryptoError> {
38        Ok(Self {
39            inner: OxiRng::new()?,
40            bytes_generated: 0,
41            reseed_threshold: threshold,
42        })
43    }
44
45    /// Number of bytes generated since the last reseed.
46    pub fn bytes_generated(&self) -> u64 {
47        self.bytes_generated
48    }
49
50    /// Configured reseed threshold in bytes.
51    pub fn reseed_threshold(&self) -> u64 {
52        self.reseed_threshold
53    }
54
55    /// Reseed immediately from OS entropy, regardless of the threshold.
56    pub fn reseed(&mut self) -> Result<(), CryptoError> {
57        self.inner.reseed()?;
58        self.bytes_generated = 0;
59        Ok(())
60    }
61
62    /// Check whether the threshold has been crossed and reseed if so.
63    fn maybe_reseed(&mut self) -> Result<(), CryptoError> {
64        if self.bytes_generated >= self.reseed_threshold {
65            self.inner.reseed()?;
66            self.bytes_generated = 0;
67        }
68        Ok(())
69    }
70}
71
72impl Rng for ReseedingRng {
73    fn fill(&mut self, dst: &mut [u8]) -> Result<(), CryptoError> {
74        self.maybe_reseed()?;
75        self.inner.fill(dst)?;
76        self.bytes_generated = self.bytes_generated.saturating_add(dst.len() as u64);
77        Ok(())
78    }
79}
80
81impl rand_core::TryRng for ReseedingRng {
82    type Error = CryptoError;
83
84    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
85        self.maybe_reseed()?;
86        let v = self.inner.try_next_u32()?;
87        self.bytes_generated = self.bytes_generated.saturating_add(4);
88        Ok(v)
89    }
90
91    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
92        self.maybe_reseed()?;
93        let v = self.inner.try_next_u64()?;
94        self.bytes_generated = self.bytes_generated.saturating_add(8);
95        Ok(v)
96    }
97
98    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
99        self.maybe_reseed()?;
100        self.inner.try_fill_bytes(dest)?;
101        self.bytes_generated = self.bytes_generated.saturating_add(dest.len() as u64);
102        Ok(())
103    }
104}
105
106impl rand_core::TryCryptoRng for ReseedingRng {}
107
108impl core::fmt::Debug for ReseedingRng {
109    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
110        f.debug_struct("ReseedingRng")
111            .field("bytes_generated", &self.bytes_generated)
112            .field("reseed_threshold", &self.reseed_threshold)
113            .finish()
114    }
115}
116
117// ── Unit tests ────────────────────────────────────────────────────────────────
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn reseeding_rng_new_works() {
125        let mut rng = ReseedingRng::new().expect("ReseedingRng::new failed");
126        let mut buf = [0u8; 32];
127        rng.fill(&mut buf).expect("ReseedingRng::fill failed");
128        assert_ne!(buf, [0u8; 32], "Output should not be all zeros");
129    }
130
131    #[test]
132    fn reseeding_rng_threshold_triggers_reseed() {
133        let mut rng =
134            ReseedingRng::with_threshold(16).expect("ReseedingRng::with_threshold failed");
135        assert_eq!(rng.bytes_generated(), 0);
136        let mut buf = [0u8; 20];
137        rng.fill(&mut buf).expect("first fill failed");
138        assert_eq!(rng.bytes_generated(), 20, "20 bytes should be tracked");
139        let mut buf2 = [0u8; 20];
140        rng.fill(&mut buf2).expect("second fill failed");
141        assert_eq!(
142            rng.bytes_generated(),
143            20,
144            "counter should reset after reseed"
145        );
146    }
147
148    #[test]
149    fn reseeding_rng_debug_does_not_leak_state() {
150        let rng = ReseedingRng::new().expect("ReseedingRng::new failed");
151        let dbg = format!("{rng:?}");
152        assert!(
153            dbg.contains("ReseedingRng"),
154            "Debug format should include type name"
155        );
156    }
157}