Skip to main content

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}