autumn_web/entropy.rs
1//! Deterministic, injectable randomness (entropy).
2//!
3//! Autumn exposes an [`Rng`] extractor so handlers — and framework internals
4//! that mint identifiers — draw random bytes and UUIDs through the framework's
5//! injected [`Entropy`] source instead of calling [`uuid::Uuid::new_v4`] or the
6//! OS RNG directly. This is the exact mirror of the [`crate::time::Clock`]
7//! extractor / [`crate::time::ClockSource`] seam for wall-clock time:
8//!
9//! - **Production** uses [`OsEntropy`] (the silent default), which reads the
10//! operating-system CSPRNG on every draw — indistinguishable from calling
11//! `getrandom` directly.
12//! - **Simulation / tests** inject a [`SeededEntropy`] (a `ChaCha8Rng` seeded
13//! from a single `u64`) via [`crate::state::AppState::with_entropy`], so the
14//! same seed replays the same byte-for-byte identifier stream on every
15//! machine and every run. The seeded simulation handle is
16//! [`crate::sim::SimRng`], reachable from a `#[sim_test]` through
17//! [`crate::sim::Sim`].
18//!
19//! # Quick example
20//!
21//! ```rust,no_run
22//! use autumn_web::prelude::*;
23//! use autumn_web::entropy::Rng;
24//!
25//! #[get("/token")]
26//! async fn token(rng: Rng) -> String {
27//! // A fresh v4 UUID drawn through the injected entropy source.
28//! rng.uuid_v4().to_string()
29//! }
30//! ```
31
32// autumn-determinism-gate: production code in this module must read time and
33// mint identifiers through the framework's injected seams (ClockSource /
34// Entropy), never `Instant::now()` / `Utc::now()` / `SystemTime::now()` /
35// `Uuid::new_v4()` directly. See CONTRIBUTING.md "Determinism seam gate"
36// (issue #1797). Justify exceptions with
37// #[allow(clippy::disallowed_methods, reason = "…")] at the narrowest scope.
38#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
39
40use std::sync::{Arc, Mutex};
41
42use rand::{Rng as _, SeedableRng};
43use rand_chacha::ChaCha8Rng;
44use uuid::Uuid;
45
46// ── Entropy source trait ──────────────────────────────────────────────────────
47
48/// Source of random bytes used internally by the framework and by the [`Rng`]
49/// extractor.
50///
51/// Production apps see [`OsEntropy`] (the silent default). Simulation tests swap
52/// it out for a [`SeededEntropy`] via
53/// [`crate::state::AppState::with_entropy`], making every framework-minted
54/// identifier reproducible under a fixed seed.
55///
56/// Implementations must be cheap to call through a shared reference: the
57/// framework holds the source as `Arc<dyn Entropy>` and draws from it
58/// concurrently, so stateful generators (like [`SeededEntropy`]) use interior
59/// mutability. This mirrors [`crate::time::ClockSource`], whose `now(&self)` is
60/// likewise a shared-reference read.
61///
62/// The [`uuid_v4`](Entropy::uuid_v4) / [`uuid_v7`](Entropy::uuid_v7) helpers are
63/// provided methods built on [`fill_bytes`](Entropy::fill_bytes), so every
64/// source generates RFC 4122 UUIDs identically — a custom source only implements
65/// the two byte-drawing methods.
66pub trait Entropy: std::fmt::Debug + Send + Sync + 'static {
67 /// Draw the next random `u64`.
68 fn next_u64(&self) -> u64;
69
70 /// Fill `dest` with random bytes.
71 fn fill_bytes(&self, dest: &mut [u8]);
72
73 /// Draw a random version-4 (fully random) [`Uuid`] from this source.
74 ///
75 /// Deterministic under a [`SeededEntropy`]: the same seed and the same
76 /// number of prior draws always yield the same UUID.
77 #[must_use]
78 fn uuid_v4(&self) -> Uuid {
79 let mut bytes = [0u8; 16];
80 self.fill_bytes(&mut bytes);
81 uuid_v4_from_bytes(bytes)
82 }
83
84 /// Draw a version-7 (time-ordered) [`Uuid`] whose 48-bit timestamp is
85 /// `unix_millis` and whose remaining 74 bits are drawn from this source.
86 ///
87 /// The random portion is deterministic under a [`SeededEntropy`]; overall
88 /// determinism additionally requires a deterministic `unix_millis` (e.g.
89 /// sourced from the simulation clock). Callers with only wall-clock time
90 /// available get a monotonic-ish but non-reproducible timestamp prefix.
91 #[must_use]
92 fn uuid_v7(&self, unix_millis: u64) -> Uuid {
93 let mut rand_bytes = [0u8; 10];
94 self.fill_bytes(&mut rand_bytes);
95 uuid_v7_from_parts(unix_millis, rand_bytes)
96 }
97
98 /// The source under a capsule recording wrapper, or `None` when this is
99 /// not one.
100 ///
101 /// The framework uses it for draws that a capsule replay does not make
102 /// again (HTTP retry jitter, an automatic `Idempotency-Key`). Those draws
103 /// then stay off the capsule's random tape (issue #3054).
104 #[doc(hidden)]
105 fn unrecorded(&self) -> Option<Arc<dyn Entropy>> {
106 None
107 }
108}
109
110/// Build a version-4 [`Uuid`] from 16 raw bytes by stamping the RFC 4122
111/// version (4) and variant bits. Shared by [`Entropy::uuid_v4`] and
112/// [`crate::sim::SimRng::uuid_v4`] so both paths produce identical UUIDs.
113#[must_use]
114pub(crate) const fn uuid_v4_from_bytes(mut bytes: [u8; 16]) -> Uuid {
115 bytes[6] = (bytes[6] & 0x0F) | 0x40; // version 4
116 bytes[8] = (bytes[8] & 0x3F) | 0x80; // RFC 4122 variant
117 Uuid::from_bytes(bytes)
118}
119
120/// Build a version-7 [`Uuid`] from a 48-bit millisecond timestamp and 10 random
121/// bytes, stamping the RFC 4122 version (7) and variant bits. Shared by
122/// [`Entropy::uuid_v7`] and [`crate::sim::SimRng::uuid_v7`].
123#[must_use]
124pub(crate) fn uuid_v7_from_parts(unix_millis: u64, rand_bytes: [u8; 10]) -> Uuid {
125 let mut bytes = [0u8; 16];
126 let ts = unix_millis.to_be_bytes();
127 // 48-bit big-endian timestamp occupies bytes 0..6.
128 bytes[0..6].copy_from_slice(&ts[2..8]);
129 bytes[6..16].copy_from_slice(&rand_bytes);
130 bytes[6] = (bytes[6] & 0x0F) | 0x70; // version 7
131 bytes[8] = (bytes[8] & 0x3F) | 0x80; // RFC 4122 variant
132 Uuid::from_bytes(bytes)
133}
134
135/// Derive a stable [`Uuid`] from a `seed` and a `purpose_tag` namespace,
136/// **independently of any draw stream** (issue #1797, seed-derived ids).
137///
138/// The same `(seed, purpose_tag)` pair always yields the same UUID, regardless
139/// of how many other values have been drawn from any RNG — it is a pure
140/// function of its inputs, so it never perturbs (and is never perturbed by) the
141/// main deterministic draw sequence. This makes it ideal for byte-reproducible
142/// multi-tenant fixtures: `derive(seed, "tenant:acme")` is a stable id for
143/// "acme" under that seed.
144///
145/// Mechanism: a stable, platform-independent FNV-1a 64-bit hash of the seed
146/// bytes followed by the tag bytes yields a sub-seed; a `ChaCha8Rng` seeded from
147/// that sub-seed fills 16 bytes, which are stamped as a version-4 (RFC 4122)
148/// UUID. Version 4 is used so a derived id is indistinguishable in shape from a
149/// normally-drawn one; its reproducibility comes purely from the derivation, not
150/// from any embedded structure.
151#[must_use]
152pub(crate) fn derive_uuid_from(seed: u64, purpose_tag: &[u8]) -> Uuid {
153 // FNV-1a 64-bit over seed bytes then tag bytes. Chosen for a fully stable,
154 // dependency-free, cross-platform hash (unlike std's DefaultHasher, whose
155 // output is not guaranteed stable across Rust versions).
156 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
157 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
158 let mut hash = FNV_OFFSET;
159 for byte in seed.to_le_bytes().iter().chain(purpose_tag) {
160 hash ^= u64::from(*byte);
161 hash = hash.wrapping_mul(FNV_PRIME);
162 }
163 let mut rng = ChaCha8Rng::seed_from_u64(hash);
164 let mut bytes = [0u8; 16];
165 rng.fill_bytes(&mut bytes);
166 uuid_v4_from_bytes(bytes)
167}
168
169/// A shared entropy handle is itself an entropy source.
170///
171/// The [`ClockSource`](crate::time::ClockSource) sibling of this impl exists
172/// for the same reason: a capsule replay hands its already-`Arc`ed source
173/// straight to `TestApp::with_entropy`, with no forwarding newtype in between.
174impl Entropy for std::sync::Arc<dyn Entropy> {
175 fn next_u64(&self) -> u64 {
176 (**self).next_u64()
177 }
178
179 fn fill_bytes(&self, dest: &mut [u8]) {
180 (**self).fill_bytes(dest);
181 }
182
183 // The *provided* methods are forwarded too, not left to the default. A
184 // source that overrides `uuid_v4`/`uuid_v7` would otherwise have its
185 // override silently dropped the moment it was handed through an `Arc`,
186 // which is precisely how this impl is used.
187 fn uuid_v4(&self) -> Uuid {
188 (**self).uuid_v4()
189 }
190
191 fn uuid_v7(&self, unix_millis: u64) -> Uuid {
192 (**self).uuid_v7(unix_millis)
193 }
194
195 fn unrecorded(&self) -> Option<Arc<dyn Entropy>> {
196 (**self).unrecorded()
197 }
198}
199
200// ── OS (real) entropy ─────────────────────────────────────────────────────────
201
202/// Real operating-system entropy implementation of [`Entropy`].
203///
204/// This is the default when no seeded source is configured. It reads the OS
205/// CSPRNG via `getrandom` on every draw and carries zero reproducibility — the
206/// silent production behavior, matching a direct [`uuid::Uuid::new_v4`] call.
207#[derive(Debug, Clone, Copy, Default)]
208pub struct OsEntropy;
209
210impl Entropy for OsEntropy {
211 fn next_u64(&self) -> u64 {
212 let mut buf = [0u8; 8];
213 getrandom::fill(&mut buf).expect("OS RNG must not fail on supported platforms");
214 u64::from_le_bytes(buf)
215 }
216
217 fn fill_bytes(&self, dest: &mut [u8]) {
218 getrandom::fill(dest).expect("OS RNG must not fail on supported platforms");
219 }
220}
221
222// ── Seeded (deterministic) entropy ─────────────────────────────────────────────
223
224/// A deterministic [`Entropy`] source seeded from a single `u64`.
225///
226/// Wraps a `ChaCha8Rng` behind a [`Mutex`] so it satisfies the
227/// shared-reference [`Entropy`] contract while remaining fully reproducible: the
228/// same seed and the same sequence of draws always produce the same bytes. This
229/// is the source a simulation injects via
230/// [`crate::state::AppState::with_entropy`]; it shares its seeding with
231/// [`crate::sim::SimRng`] so a `Sim` handle and the `AppState` it mounts agree.
232#[derive(Debug)]
233pub struct SeededEntropy {
234 seed: u64,
235 inner: Mutex<ChaCha8Rng>,
236}
237
238impl SeededEntropy {
239 /// Seed a fresh deterministic entropy source from `seed`.
240 #[must_use]
241 pub fn new(seed: u64) -> Self {
242 Self {
243 seed,
244 inner: Mutex::new(ChaCha8Rng::seed_from_u64(seed)),
245 }
246 }
247
248 /// Derive a stable [`Uuid`] from this source's seed and a `purpose_tag`
249 /// namespace, **independently of the draw stream** (seed-derived ids).
250 ///
251 /// The same seed and `purpose_tag` always yield the same UUID no matter how
252 /// many bytes have been drawn, so it is safe for byte-reproducible
253 /// multi-tenant fixtures without disturbing the deterministic id stream. See
254 /// `derive_uuid_from` for the mechanism and the version bits it sets.
255 #[must_use]
256 pub fn derive_uuid(&self, purpose_tag: impl AsRef<[u8]>) -> Uuid {
257 derive_uuid_from(self.seed, purpose_tag.as_ref())
258 }
259
260 /// Seed a fresh deterministic entropy source from `seed`, boxed as a shared
261 /// [`Entropy`] handle ready to hand to
262 /// [`crate::state::AppState::with_entropy`].
263 #[must_use]
264 pub fn shared(seed: u64) -> Arc<dyn Entropy> {
265 Arc::new(Self::new(seed))
266 }
267
268 fn with_inner<R>(&self, f: impl FnOnce(&mut ChaCha8Rng) -> R) -> R {
269 let mut guard = self
270 .inner
271 .lock()
272 .unwrap_or_else(std::sync::PoisonError::into_inner);
273 f(&mut guard)
274 }
275}
276
277impl Entropy for SeededEntropy {
278 fn next_u64(&self) -> u64 {
279 self.with_inner(rand::Rng::next_u64)
280 }
281
282 fn fill_bytes(&self, dest: &mut [u8]) {
283 self.with_inner(|rng| rng.fill_bytes(dest));
284 }
285}
286
287// ── Extractor ─────────────────────────────────────────────────────────────────
288
289/// Axum extractor that resolves the framework's injected [`Entropy`] source.
290///
291/// Use as a handler argument to draw random bytes or UUIDs through the injected
292/// source instead of calling [`uuid::Uuid::new_v4`] or the OS RNG directly. This
293/// lets simulation tests make handler-minted identifiers reproducible via
294/// [`crate::state::AppState::with_entropy`], exactly as the
295/// [`crate::time::Clock`] extractor lets tests control time.
296///
297/// Unlike [`Clock`](crate::time::Clock), which snapshots the current instant at
298/// extraction time, `Rng` holds a live handle to the source — an RNG is stateful
299/// and each draw advances it.
300///
301/// ```rust,ignore
302/// use autumn_web::entropy::Rng;
303///
304/// async fn handler(rng: Rng) -> String {
305/// rng.uuid_v4().to_string()
306/// }
307/// ```
308#[derive(Clone)]
309pub struct Rng(Arc<dyn Entropy>);
310
311impl Rng {
312 /// Wrap an [`Entropy`] handle as an [`Rng`] (framework / test helper).
313 #[must_use]
314 pub fn from_source(source: Arc<dyn Entropy>) -> Self {
315 Self(source)
316 }
317
318 /// Borrow the underlying [`Entropy`] source.
319 #[must_use]
320 pub fn source(&self) -> &dyn Entropy {
321 self.0.as_ref()
322 }
323
324 /// Draw the next random `u64` from the injected source.
325 #[must_use]
326 pub fn next_u64(&self) -> u64 {
327 self.0.next_u64()
328 }
329
330 /// Fill `dest` with random bytes from the injected source.
331 pub fn fill_bytes(&self, dest: &mut [u8]) {
332 self.0.fill_bytes(dest);
333 }
334
335 /// Draw a random version-4 [`Uuid`] from the injected source.
336 #[must_use]
337 pub fn uuid_v4(&self) -> Uuid {
338 self.0.uuid_v4()
339 }
340
341 /// Draw a version-7 [`Uuid`] with the given millisecond timestamp from the
342 /// injected source.
343 #[must_use]
344 pub fn uuid_v7(&self, unix_millis: u64) -> Uuid {
345 self.0.uuid_v7(unix_millis)
346 }
347}
348
349impl std::fmt::Debug for Rng {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 f.debug_struct("Rng").finish_non_exhaustive()
352 }
353}
354
355impl axum::extract::FromRequestParts<crate::state::AppState> for Rng {
356 type Rejection = std::convert::Infallible;
357
358 async fn from_request_parts(
359 _parts: &mut axum::http::request::Parts,
360 state: &crate::state::AppState,
361 ) -> Result<Self, Self::Rejection> {
362 Ok(Self(state.entropy_arc()))
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::{Entropy, OsEntropy, SeededEntropy, uuid_v4_from_bytes, uuid_v7_from_parts};
369
370 #[test]
371 fn uuid_v4_sets_version_and_variant_bits() {
372 let id = uuid_v4_from_bytes([0xFF; 16]);
373 assert_eq!(id.get_version_num(), 4);
374 // RFC 4122 variant: high two bits of byte 8 are 0b10.
375 assert_eq!(id.as_bytes()[8] & 0xC0, 0x80);
376 }
377
378 #[test]
379 fn uuid_v7_embeds_timestamp_and_sets_bits() {
380 let id = uuid_v7_from_parts(0x0000_0102_0304_0506, [0xAA; 10]);
381 assert_eq!(id.get_version_num(), 7);
382 assert_eq!(id.as_bytes()[8] & 0xC0, 0x80);
383 // 48-bit big-endian timestamp in the first six bytes.
384 assert_eq!(&id.as_bytes()[0..6], &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
385 }
386
387 #[test]
388 fn seeded_entropy_is_reproducible() {
389 let a = SeededEntropy::new(42);
390 let b = SeededEntropy::new(42);
391 assert_eq!(a.next_u64(), b.next_u64());
392 assert_eq!(a.uuid_v4(), b.uuid_v4());
393 assert_eq!(a.uuid_v7(1_700_000_000_000), b.uuid_v7(1_700_000_000_000));
394 }
395
396 #[test]
397 fn seeded_entropy_diverges_by_seed() {
398 let a = SeededEntropy::new(1);
399 let b = SeededEntropy::new(2);
400 assert_ne!(a.uuid_v4(), b.uuid_v4());
401 }
402
403 #[test]
404 fn derive_uuid_is_stable_tag_sensitive_and_stream_independent() {
405 let e = SeededEntropy::new(7);
406 // Same tag ⇒ same id.
407 assert_eq!(e.derive_uuid("tenant:acme"), e.derive_uuid("tenant:acme"));
408 // Different tags ⇒ different ids.
409 assert_ne!(e.derive_uuid("tenant:acme"), e.derive_uuid("tenant:beta"));
410 // Stable regardless of interleaved main-stream draws (order-independent).
411 let before = e.derive_uuid("tenant:acme");
412 let _ = e.uuid_v4();
413 let _ = e.next_u64();
414 assert_eq!(before, e.derive_uuid("tenant:acme"));
415 // Reproducible from a fresh source with the same seed.
416 assert_eq!(before, SeededEntropy::new(7).derive_uuid("tenant:acme"));
417 // Diverges by seed.
418 assert_ne!(before, SeededEntropy::new(8).derive_uuid("tenant:acme"));
419 // Shaped as a v4 UUID.
420 assert_eq!(before.get_version_num(), 4);
421 }
422
423 #[test]
424 fn os_entropy_draws_distinct_values() {
425 let os = OsEntropy;
426 // Vanishingly unlikely to collide; guards against a stuck source.
427 assert_ne!(os.uuid_v4(), os.uuid_v4());
428 }
429}