Skip to main content

vrd/
chacha.rs

1// Copyright © 2023-2026 vrd. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! ChaCha20-based CSPRNG backend (feature `crypto`).
5//!
6//! Thin wrapper around `rand_chacha::ChaCha20Rng` - the
7//! `rand`-ecosystem reference implementation, audited via its
8//! upstream maintainers. vrd does not roll its own crypto.
9//!
10//! ChaCha20 is a stream cipher with a 256-bit key and an
11//! `2⁶⁴`-block period (sufficient for any single application).
12//! It's the standard CSPRNG for security-adjacent random output:
13//! session tokens, salts, UUIDs that need unpredictability.
14//!
15//! # CSPRNG contract
16//!
17//! Given an unpredictable seed (e.g. [`Random::new_secure`]'s OS
18//! entropy), the output stream is computationally indistinguishable
19//! from true random bytes to any polynomial-time observer that does
20//! not see the seed.
21//!
22//! [`Random::new_secure`]: crate::Random::new_secure
23
24use core::convert::Infallible;
25use rand::rand_core::{Rng, SeedableRng, TryRng};
26use rand_chacha::ChaCha20Rng;
27
28/// ChaCha20-based CSPRNG state. Wraps `rand_chacha::ChaCha20Rng`
29/// and exposes the same `TryRng` / `SeedableRng` surface as the
30/// other vrd backends.
31///
32/// # Examples
33///
34/// ```
35/// use vrd::chacha::ChaChaRng;
36///
37/// let mut rng = ChaChaRng::from_seed([0u8; 32]);
38/// let _ = rng.next_u32();
39/// ```
40#[derive(Clone, Debug, PartialEq, Eq)]
41#[cfg_attr(
42    feature = "serde",
43    derive(serde::Serialize, serde::Deserialize)
44)]
45pub struct ChaChaRng {
46    /// Wrapped `rand_chacha::ChaCha20Rng` - vrd doesn't roll its
47    /// own crypto, this is the audited reference implementation.
48    inner: ChaCha20Rng,
49}
50
51impl ChaChaRng {
52    /// Builds a CSPRNG from a deterministic 32-byte seed. Use this
53    /// for reproducible tests; use [`Self::from_os_rng`] for real
54    /// crypto-quality randomness.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use vrd::chacha::ChaChaRng;
60    ///
61    /// let mut rng = ChaChaRng::from_seed([1u8; 32]);
62    /// assert_ne!(rng.next_u64(), 0);
63    /// ```
64    pub fn from_seed(seed: [u8; 32]) -> Self {
65        Self {
66            inner: ChaCha20Rng::from_seed(seed),
67        }
68    }
69
70    /// Seeds the CSPRNG from the operating system entropy source.
71    /// Requires `std`.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use vrd::chacha::ChaChaRng;
77    ///
78    /// # #[cfg(feature = "std")]
79    /// # {
80    /// let mut rng = ChaChaRng::from_os_rng();
81    /// let _ = rng.next_u32();
82    /// # }
83    /// ```
84    #[cfg(feature = "std")]
85    pub fn from_os_rng() -> Self {
86        let mut seed = [0u8; 32];
87        for byte in &mut seed {
88            *byte = rand::random::<u8>();
89        }
90        Self::from_seed(seed)
91    }
92
93    /// Generates the next random `u32`.
94    #[inline]
95    pub fn next_u32(&mut self) -> u32 {
96        self.inner.next_u32()
97    }
98
99    /// Generates the next random `u64`.
100    #[inline]
101    pub fn next_u64(&mut self) -> u64 {
102        self.inner.next_u64()
103    }
104
105    /// Fills `dest` with CSPRNG-grade random bytes.
106    pub fn fill_bytes(&mut self, dest: &mut [u8]) {
107        self.inner.fill_bytes(dest);
108    }
109}
110
111impl TryRng for ChaChaRng {
112    type Error = Infallible;
113
114    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
115        Ok(self.next_u32())
116    }
117
118    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
119        Ok(self.next_u64())
120    }
121
122    fn try_fill_bytes(
123        &mut self,
124        dest: &mut [u8],
125    ) -> Result<(), Self::Error> {
126        self.fill_bytes(dest);
127        Ok(())
128    }
129}
130
131impl SeedableRng for ChaChaRng {
132    type Seed = [u8; 32];
133
134    fn from_seed(seed: [u8; 32]) -> Self {
135        ChaChaRng::from_seed(seed)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn deterministic_from_seed() {
145        let mut a = ChaChaRng::from_seed([42u8; 32]);
146        let mut b = ChaChaRng::from_seed([42u8; 32]);
147        for _ in 0..16 {
148            assert_eq!(a.next_u64(), b.next_u64());
149        }
150    }
151
152    #[test]
153    fn fill_bytes_unaligned() {
154        let mut rng = ChaChaRng::from_seed([7u8; 32]);
155        let mut buf = [0u8; 65];
156        rng.fill_bytes(&mut buf);
157        assert!(buf.iter().any(|&b| b != 0));
158    }
159
160    /// Bit-exact compatibility with the reference
161    /// `rand_chacha::ChaCha20Rng::from_seed` output. If this test
162    /// fails the wrapper has somehow diverged from the upstream
163    /// implementation.
164    #[test]
165    fn matches_reference_rand_chacha() {
166        let seed = [9u8; 32];
167        let mut ours = ChaChaRng::from_seed(seed);
168        let mut reference = ChaCha20Rng::from_seed(seed);
169        for _ in 0..32 {
170            assert_eq!(ours.next_u64(), reference.next_u64());
171        }
172    }
173
174    /// Exercises the `TryRng` and `SeedableRng` impls (covers the
175    /// trait dispatch lines that the inherent-method tests miss).
176    #[test]
177    fn try_rng_and_seedable_impls() {
178        let mut rng = <ChaChaRng as SeedableRng>::from_seed([1u8; 32]);
179        assert!(TryRng::try_next_u32(&mut rng).is_ok());
180        assert!(TryRng::try_next_u64(&mut rng).is_ok());
181        let mut buf = [0u8; 16];
182        assert!(TryRng::try_fill_bytes(&mut rng, &mut buf).is_ok());
183        assert!(buf.iter().any(|&b| b != 0));
184    }
185}