Skip to main content

oxicrypto_rand/
oxirng.rs

1//! ChaCha-based CSPRNG implementations: `OxiRng` (ChaCha20), `OxiRng8`
2//! (ChaCha8), and `OxiRng12` (ChaCha12).
3//!
4//! All three variants are fork-safe on Unix via PID tracking.
5
6use oxicrypto_core::{CryptoError, Rng};
7use rand_chacha::ChaCha20Rng;
8use rand_core::{SeedableRng, TryRng};
9
10// ── OxiRng (ChaCha20) ────────────────────────────────────────────────────────
11
12/// A ChaCha20 CSPRNG seeded from the OS random source.
13///
14/// Use [`OxiRng::new`] to create an instance.  The seed is obtained from
15/// `getrandom::fill` which calls `/dev/urandom`, `RtlGenRandom`, or
16/// `arc4random` depending on the platform — no C library required.
17///
18/// On Unix platforms, [`OxiRng`] automatically reseeds itself if the process
19/// PID changes (i.e. after a `fork()`), preventing parent/child state sharing.
20pub struct OxiRng {
21    pub(crate) inner: ChaCha20Rng,
22    #[cfg(unix)]
23    pub(crate) last_pid: u32,
24}
25
26impl OxiRng {
27    /// Create a new [`OxiRng`] seeded from the OS.
28    ///
29    /// Returns [`CryptoError::Internal`] if `getrandom` fails.
30    #[must_use = "the RNG must be stored and used; discarding it serves no purpose"]
31    pub fn new() -> Result<Self, CryptoError> {
32        let mut seed = [0u8; 32];
33        getrandom::fill(&mut seed).map_err(|_| CryptoError::Internal("getrandom failed"))?;
34        Ok(Self {
35            inner: ChaCha20Rng::from_seed(seed),
36            #[cfg(unix)]
37            last_pid: std::process::id(),
38        })
39    }
40
41    /// Reseed this RNG from OS entropy.
42    ///
43    /// Replaces the internal ChaCha20 state with a fresh 32-byte seed and
44    /// updates the stored PID to the current process.  The free function
45    /// [`crate::reseed`] is kept for backward compatibility.
46    ///
47    /// Returns [`CryptoError::Rng`] if `getrandom` fails.
48    pub fn reseed(&mut self) -> Result<(), CryptoError> {
49        crate::helpers::reseed(self)
50    }
51
52    /// Fill a fixed-size array with cryptographically random bytes.
53    pub fn fill_exact<const N: usize>(&mut self, dst: &mut [u8; N]) -> Result<(), CryptoError> {
54        self.fill(dst.as_mut_slice())
55    }
56
57    /// Detect fork: if PID changed since construction, reseed from OS entropy.
58    ///
59    /// This prevents child processes from sharing CSPRNG state with the parent.
60    #[cfg(unix)]
61    pub(crate) fn check_fork(&mut self) -> Result<(), CryptoError> {
62        let current_pid = std::process::id();
63        if current_pid != self.last_pid {
64            // PID changed: we're in a child process — reseed to avoid state sharing.
65            let mut seed = [0u8; 32];
66            getrandom::fill(&mut seed).map_err(|_| CryptoError::Rng)?;
67            self.inner = ChaCha20Rng::from_seed(seed);
68            self.last_pid = current_pid;
69        }
70        Ok(())
71    }
72}
73
74impl Rng for OxiRng {
75    fn fill(&mut self, dst: &mut [u8]) -> Result<(), CryptoError> {
76        #[cfg(unix)]
77        self.check_fork()?;
78        self.inner
79            .try_fill_bytes(dst)
80            .map_err(|_| CryptoError::Rng)?;
81        Ok(())
82    }
83}
84
85impl core::fmt::Debug for OxiRng {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        f.write_str("OxiRng { [state redacted] }")
88    }
89}
90
91impl core::fmt::Display for OxiRng {
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        f.write_str("OxiRng(ChaCha20)")
94    }
95}
96
97// Implement `rand_core::TryRng` so that `OxiRng` can be used with crates
98// requiring `TryCryptoRng` bounds (e.g. `ml-kem`, `ml-dsa`, `p256`, etc.).
99//
100// Error type is `CryptoError` because `try_fill_bytes` calls `check_fork`,
101// which may fail if `getrandom` is unavailable after a fork.
102
103impl rand_core::TryRng for OxiRng {
104    type Error = CryptoError;
105
106    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
107        self.inner.try_next_u32().map_err(|_| CryptoError::Rng)
108    }
109
110    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
111        self.inner.try_next_u64().map_err(|_| CryptoError::Rng)
112    }
113
114    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
115        #[cfg(unix)]
116        self.check_fork()?;
117        self.inner
118            .try_fill_bytes(dest)
119            .map_err(|_| CryptoError::Rng)
120    }
121}
122
123impl rand_core::TryCryptoRng for OxiRng {}
124
125// ── OxiRng8 (ChaCha8) ────────────────────────────────────────────────────────
126
127/// A ChaCha8 CSPRNG seeded from the OS random source.
128///
129/// ChaCha8 uses 8 rounds instead of 20, offering higher throughput at the cost
130/// of a smaller security margin.  Suitable for performance-critical paths where
131/// full ChaCha20 is not needed.
132///
133/// Fork-safe: on Unix, detects `fork()` via PID tracking and reseeds.
134pub struct OxiRng8 {
135    inner: rand_chacha::ChaCha8Rng,
136    #[cfg(unix)]
137    last_pid: u32,
138}
139
140impl OxiRng8 {
141    /// Create a new [`OxiRng8`] seeded from the OS.
142    pub fn new() -> Result<Self, CryptoError> {
143        let mut seed = [0u8; 32];
144        getrandom::fill(&mut seed).map_err(|_| CryptoError::Internal("getrandom failed"))?;
145        Ok(Self {
146            inner: rand_chacha::ChaCha8Rng::from_seed(seed),
147            #[cfg(unix)]
148            last_pid: std::process::id(),
149        })
150    }
151
152    /// Reseed from OS entropy.
153    pub fn reseed(&mut self) -> Result<(), CryptoError> {
154        let mut seed = [0u8; 32];
155        getrandom::fill(&mut seed).map_err(|_| CryptoError::Rng)?;
156        self.inner = rand_chacha::ChaCha8Rng::from_seed(seed);
157        #[cfg(unix)]
158        {
159            self.last_pid = std::process::id();
160        }
161        Ok(())
162    }
163
164    #[cfg(unix)]
165    fn check_fork(&mut self) -> Result<(), CryptoError> {
166        let current_pid = std::process::id();
167        if current_pid != self.last_pid {
168            let mut seed = [0u8; 32];
169            getrandom::fill(&mut seed).map_err(|_| CryptoError::Rng)?;
170            self.inner = rand_chacha::ChaCha8Rng::from_seed(seed);
171            self.last_pid = current_pid;
172        }
173        Ok(())
174    }
175}
176
177impl Rng for OxiRng8 {
178    fn fill(&mut self, dst: &mut [u8]) -> Result<(), CryptoError> {
179        #[cfg(unix)]
180        self.check_fork()?;
181        self.inner.try_fill_bytes(dst).map_err(|_| CryptoError::Rng)
182    }
183}
184
185impl rand_core::TryRng for OxiRng8 {
186    type Error = CryptoError;
187
188    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
189        self.inner.try_next_u32().map_err(|_| CryptoError::Rng)
190    }
191
192    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
193        self.inner.try_next_u64().map_err(|_| CryptoError::Rng)
194    }
195
196    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
197        #[cfg(unix)]
198        self.check_fork()?;
199        self.inner
200            .try_fill_bytes(dest)
201            .map_err(|_| CryptoError::Rng)
202    }
203}
204
205impl rand_core::TryCryptoRng for OxiRng8 {}
206
207impl core::fmt::Debug for OxiRng8 {
208    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
209        f.debug_struct("OxiRng8").finish_non_exhaustive()
210    }
211}
212
213// ── OxiRng12 (ChaCha12) ──────────────────────────────────────────────────────
214
215/// A ChaCha12 CSPRNG seeded from the OS random source.
216///
217/// ChaCha12 uses 12 rounds — a middle ground between ChaCha8 (8 rounds) and
218/// ChaCha20 (20 rounds), offering good performance with a higher security
219/// margin than ChaCha8.
220///
221/// Fork-safe: on Unix, detects `fork()` via PID tracking and reseeds.
222pub struct OxiRng12 {
223    inner: rand_chacha::ChaCha12Rng,
224    #[cfg(unix)]
225    last_pid: u32,
226}
227
228impl OxiRng12 {
229    /// Create a new [`OxiRng12`] seeded from the OS.
230    pub fn new() -> Result<Self, CryptoError> {
231        let mut seed = [0u8; 32];
232        getrandom::fill(&mut seed).map_err(|_| CryptoError::Internal("getrandom failed"))?;
233        Ok(Self {
234            inner: rand_chacha::ChaCha12Rng::from_seed(seed),
235            #[cfg(unix)]
236            last_pid: std::process::id(),
237        })
238    }
239
240    /// Reseed from OS entropy.
241    pub fn reseed(&mut self) -> Result<(), CryptoError> {
242        let mut seed = [0u8; 32];
243        getrandom::fill(&mut seed).map_err(|_| CryptoError::Rng)?;
244        self.inner = rand_chacha::ChaCha12Rng::from_seed(seed);
245        #[cfg(unix)]
246        {
247            self.last_pid = std::process::id();
248        }
249        Ok(())
250    }
251
252    #[cfg(unix)]
253    fn check_fork(&mut self) -> Result<(), CryptoError> {
254        let current_pid = std::process::id();
255        if current_pid != self.last_pid {
256            let mut seed = [0u8; 32];
257            getrandom::fill(&mut seed).map_err(|_| CryptoError::Rng)?;
258            self.inner = rand_chacha::ChaCha12Rng::from_seed(seed);
259            self.last_pid = current_pid;
260        }
261        Ok(())
262    }
263}
264
265impl Rng for OxiRng12 {
266    fn fill(&mut self, dst: &mut [u8]) -> Result<(), CryptoError> {
267        #[cfg(unix)]
268        self.check_fork()?;
269        self.inner.try_fill_bytes(dst).map_err(|_| CryptoError::Rng)
270    }
271}
272
273impl rand_core::TryRng for OxiRng12 {
274    type Error = CryptoError;
275
276    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
277        self.inner.try_next_u32().map_err(|_| CryptoError::Rng)
278    }
279
280    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
281        self.inner.try_next_u64().map_err(|_| CryptoError::Rng)
282    }
283
284    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
285        #[cfg(unix)]
286        self.check_fork()?;
287        self.inner
288            .try_fill_bytes(dest)
289            .map_err(|_| CryptoError::Rng)
290    }
291}
292
293impl rand_core::TryCryptoRng for OxiRng12 {}
294
295impl core::fmt::Debug for OxiRng12 {
296    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
297        f.debug_struct("OxiRng12").finish_non_exhaustive()
298    }
299}
300
301// ── Unit tests that require private field access ──────────────────────────────
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn oxi_rng_creates_ok() {
309        let rng = OxiRng::new();
310        assert!(rng.is_ok(), "OxiRng::new() should succeed on this platform");
311    }
312
313    #[test]
314    fn oxi_rng_fills_buffer() {
315        let mut rng = OxiRng::new().expect("OxiRng::new failed");
316        let mut buf = [0u8; 64];
317        rng.fill(&mut buf).expect("fill should succeed");
318        assert_ne!(buf, [0u8; 64], "Random bytes should not all be zero");
319    }
320
321    #[test]
322    fn oxi_rng_two_outputs_differ() {
323        let mut rng = OxiRng::new().expect("OxiRng::new failed");
324        let mut buf1 = [0u8; 32];
325        let mut buf2 = [0u8; 32];
326        rng.fill(&mut buf1).expect("fill 1 failed");
327        rng.fill(&mut buf2).expect("fill 2 failed");
328        assert_ne!(buf1, buf2, "Consecutive RNG outputs should differ");
329    }
330
331    #[test]
332    fn oxi_rng_reseed_method_changes_output() {
333        let mut rng = OxiRng::new().expect("new failed");
334        let mut buf1 = [0u8; 32];
335        rng.fill(&mut buf1).expect("fill 1 failed");
336        rng.reseed().expect("OxiRng::reseed() failed");
337        let mut buf2 = [0u8; 32];
338        rng.fill(&mut buf2).expect("fill 2 failed");
339        assert_ne!(buf1, buf2, "Output after reseed() should differ");
340    }
341
342    #[cfg(unix)]
343    #[test]
344    fn fork_safe_pid_simulation() {
345        let mut rng = OxiRng::new().expect("OxiRng::new failed");
346        let mut before = [0u8; 32];
347        rng.fill(&mut before).expect("fill before failed");
348        // Simulate a fork by setting last_pid to a fake value.
349        rng.last_pid = 0; // PID 0 is never a real user process PID.
350        let mut after = [0u8; 32];
351        rng.fill(&mut after)
352            .expect("fill after fork-simulation failed");
353        assert_ne!(
354            before, after,
355            "After fork simulation, RNG should have reseeded"
356        );
357        assert_eq!(rng.last_pid, std::process::id());
358    }
359
360    #[test]
361    fn oxi_rng_implements_try_crypto_rng() {
362        fn requires_try_crypto_rng<R: rand_core::TryCryptoRng>(_rng: &mut R) {}
363        let mut rng = OxiRng::new().expect("new failed");
364        requires_try_crypto_rng(&mut rng);
365    }
366
367    #[test]
368    fn oxi_rng_debug_does_not_leak_state() {
369        let rng = OxiRng::new().expect("OxiRng::new failed");
370        let dbg = std::format!("{rng:?}");
371        assert!(dbg.contains("OxiRng"), "Debug must include type name");
372        assert!(
373            dbg.contains("redacted"),
374            "Debug must not expose internal state"
375        );
376    }
377
378    #[test]
379    fn oxi_rng_display_shows_algorithm() {
380        let rng = OxiRng::new().expect("OxiRng::new failed");
381        let display = std::format!("{rng}");
382        assert_eq!(
383            display, "OxiRng(ChaCha20)",
384            "Display must identify the algorithm"
385        );
386    }
387
388    #[test]
389    fn fill_exact_works() {
390        let mut rng = OxiRng::new().expect("OxiRng::new failed");
391        let mut arr = [0u8; 16];
392        rng.fill_exact(&mut arr).expect("fill_exact failed");
393        assert_ne!(
394            arr, [0u8; 16],
395            "fill_exact must not produce all-zero output"
396        );
397    }
398
399    #[test]
400    fn oxi_rng8_fills_buffer() {
401        let mut rng = OxiRng8::new().expect("OxiRng8::new failed");
402        let mut buf = [0u8; 64];
403        rng.fill(&mut buf).expect("OxiRng8::fill failed");
404        assert_ne!(buf, [0u8; 64], "OxiRng8 output should not be all zeros");
405    }
406
407    #[test]
408    fn oxi_rng12_fills_buffer() {
409        let mut rng = OxiRng12::new().expect("OxiRng12::new failed");
410        let mut buf = [0u8; 64];
411        rng.fill(&mut buf).expect("OxiRng12::fill failed");
412        assert_ne!(buf, [0u8; 64], "OxiRng12 output should not be all zeros");
413    }
414
415    #[test]
416    fn oxi_rng8_two_instances_differ() {
417        let mut rng1 = OxiRng8::new().expect("OxiRng8::new 1 failed");
418        let mut rng2 = OxiRng8::new().expect("OxiRng8::new 2 failed");
419        let mut buf1 = [0u8; 32];
420        let mut buf2 = [0u8; 32];
421        rng1.fill(&mut buf1).expect("fill 1 failed");
422        rng2.fill(&mut buf2).expect("fill 2 failed");
423        assert_ne!(
424            buf1, buf2,
425            "Two independently seeded OxiRng8 instances should differ"
426        );
427    }
428
429    #[test]
430    fn oxi_rng12_two_instances_differ() {
431        let mut rng1 = OxiRng12::new().expect("OxiRng12::new 1 failed");
432        let mut rng2 = OxiRng12::new().expect("OxiRng12::new 2 failed");
433        let mut buf1 = [0u8; 32];
434        let mut buf2 = [0u8; 32];
435        rng1.fill(&mut buf1).expect("fill 1 failed");
436        rng2.fill(&mut buf2).expect("fill 2 failed");
437        assert_ne!(
438            buf1, buf2,
439            "Two independently seeded OxiRng12 instances should differ"
440        );
441    }
442
443    #[test]
444    fn oxi_rng8_reseed_changes_output() {
445        let mut rng = OxiRng8::new().expect("OxiRng8::new failed");
446        let mut buf1 = [0u8; 32];
447        rng.fill(&mut buf1).expect("fill 1 failed");
448        rng.reseed().expect("OxiRng8::reseed failed");
449        let mut buf2 = [0u8; 32];
450        rng.fill(&mut buf2).expect("fill 2 failed");
451        assert_ne!(buf1, buf2, "Output after OxiRng8::reseed should differ");
452    }
453
454    #[test]
455    fn oxi_rng12_reseed_changes_output() {
456        let mut rng = OxiRng12::new().expect("OxiRng12::new failed");
457        let mut buf1 = [0u8; 32];
458        rng.fill(&mut buf1).expect("fill 1 failed");
459        rng.reseed().expect("OxiRng12::reseed failed");
460        let mut buf2 = [0u8; 32];
461        rng.fill(&mut buf2).expect("fill 2 failed");
462        assert_ne!(buf1, buf2, "Output after OxiRng12::reseed should differ");
463    }
464
465    #[test]
466    fn oxi_rng8_implements_try_crypto_rng() {
467        fn requires_try_crypto_rng<R: rand_core::TryCryptoRng>(_rng: &mut R) {}
468        let mut rng = OxiRng8::new().expect("OxiRng8::new failed");
469        requires_try_crypto_rng(&mut rng);
470    }
471
472    #[test]
473    fn oxi_rng12_implements_try_crypto_rng() {
474        fn requires_try_crypto_rng<R: rand_core::TryCryptoRng>(_rng: &mut R) {}
475        let mut rng = OxiRng12::new().expect("OxiRng12::new failed");
476        requires_try_crypto_rng(&mut rng);
477    }
478
479    #[test]
480    fn fill_various_sizes() {
481        let mut rng = OxiRng::new().expect("OxiRng::new failed");
482        for size in [0usize, 1, 31, 32, 33, 1024] {
483            let mut buf = std::vec![0u8; size];
484            rng.fill(&mut buf)
485                .expect("fill should succeed for all sizes");
486            assert_eq!(buf.len(), size, "fill must not change buffer length");
487        }
488    }
489
490    #[test]
491    fn fill_one_byte_not_stuck_at_zero() {
492        let mut rng = OxiRng::new().expect("OxiRng::new failed");
493        let mut saw_nonzero = false;
494        for _ in 0..256 {
495            let mut buf = [0u8; 1];
496            rng.fill(&mut buf).expect("fill 1 byte");
497            if buf[0] != 0 {
498                saw_nonzero = true;
499                break;
500            }
501        }
502        assert!(
503            saw_nonzero,
504            "At least one single-byte fill should be non-zero"
505        );
506    }
507}