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}