#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
use std::sync::{Arc, Mutex};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use uuid::Uuid;
pub trait Entropy: std::fmt::Debug + Send + Sync + 'static {
fn next_u64(&self) -> u64;
fn fill_bytes(&self, dest: &mut [u8]);
#[must_use]
fn uuid_v4(&self) -> Uuid {
let mut bytes = [0u8; 16];
self.fill_bytes(&mut bytes);
uuid_v4_from_bytes(bytes)
}
#[must_use]
fn uuid_v7(&self, unix_millis: u64) -> Uuid {
let mut rand_bytes = [0u8; 10];
self.fill_bytes(&mut rand_bytes);
uuid_v7_from_parts(unix_millis, rand_bytes)
}
}
#[must_use]
pub(crate) const fn uuid_v4_from_bytes(mut bytes: [u8; 16]) -> Uuid {
bytes[6] = (bytes[6] & 0x0F) | 0x40; bytes[8] = (bytes[8] & 0x3F) | 0x80; Uuid::from_bytes(bytes)
}
#[must_use]
pub(crate) fn uuid_v7_from_parts(unix_millis: u64, rand_bytes: [u8; 10]) -> Uuid {
let mut bytes = [0u8; 16];
let ts = unix_millis.to_be_bytes();
bytes[0..6].copy_from_slice(&ts[2..8]);
bytes[6..16].copy_from_slice(&rand_bytes);
bytes[6] = (bytes[6] & 0x0F) | 0x70; bytes[8] = (bytes[8] & 0x3F) | 0x80; Uuid::from_bytes(bytes)
}
#[must_use]
pub(crate) fn derive_uuid_from(seed: u64, purpose_tag: &[u8]) -> Uuid {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = FNV_OFFSET;
for byte in seed.to_le_bytes().iter().chain(purpose_tag) {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
let mut rng = ChaCha8Rng::seed_from_u64(hash);
let mut bytes = [0u8; 16];
rng.fill_bytes(&mut bytes);
uuid_v4_from_bytes(bytes)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OsEntropy;
impl Entropy for OsEntropy {
fn next_u64(&self) -> u64 {
let mut buf = [0u8; 8];
getrandom::getrandom(&mut buf).expect("OS RNG must not fail on supported platforms");
u64::from_le_bytes(buf)
}
fn fill_bytes(&self, dest: &mut [u8]) {
getrandom::getrandom(dest).expect("OS RNG must not fail on supported platforms");
}
}
#[derive(Debug)]
pub struct SeededEntropy {
seed: u64,
inner: Mutex<ChaCha8Rng>,
}
impl SeededEntropy {
#[must_use]
pub fn new(seed: u64) -> Self {
Self {
seed,
inner: Mutex::new(ChaCha8Rng::seed_from_u64(seed)),
}
}
#[must_use]
pub fn derive_uuid(&self, purpose_tag: impl AsRef<[u8]>) -> Uuid {
derive_uuid_from(self.seed, purpose_tag.as_ref())
}
#[must_use]
pub fn shared(seed: u64) -> Arc<dyn Entropy> {
Arc::new(Self::new(seed))
}
fn with_inner<R>(&self, f: impl FnOnce(&mut ChaCha8Rng) -> R) -> R {
let mut guard = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
f(&mut guard)
}
}
impl Entropy for SeededEntropy {
fn next_u64(&self) -> u64 {
self.with_inner(RngCore::next_u64)
}
fn fill_bytes(&self, dest: &mut [u8]) {
self.with_inner(|rng| rng.fill_bytes(dest));
}
}
#[derive(Clone)]
pub struct Rng(Arc<dyn Entropy>);
impl Rng {
#[must_use]
pub fn from_source(source: Arc<dyn Entropy>) -> Self {
Self(source)
}
#[must_use]
pub fn source(&self) -> &dyn Entropy {
self.0.as_ref()
}
#[must_use]
pub fn next_u64(&self) -> u64 {
self.0.next_u64()
}
pub fn fill_bytes(&self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
#[must_use]
pub fn uuid_v4(&self) -> Uuid {
self.0.uuid_v4()
}
#[must_use]
pub fn uuid_v7(&self, unix_millis: u64) -> Uuid {
self.0.uuid_v7(unix_millis)
}
}
impl std::fmt::Debug for Rng {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Rng").finish_non_exhaustive()
}
}
impl axum::extract::FromRequestParts<crate::state::AppState> for Rng {
type Rejection = std::convert::Infallible;
async fn from_request_parts(
_parts: &mut axum::http::request::Parts,
state: &crate::state::AppState,
) -> Result<Self, Self::Rejection> {
Ok(Self(state.entropy_arc()))
}
}
#[cfg(test)]
mod tests {
use super::{Entropy, OsEntropy, SeededEntropy, uuid_v4_from_bytes, uuid_v7_from_parts};
#[test]
fn uuid_v4_sets_version_and_variant_bits() {
let id = uuid_v4_from_bytes([0xFF; 16]);
assert_eq!(id.get_version_num(), 4);
assert_eq!(id.as_bytes()[8] & 0xC0, 0x80);
}
#[test]
fn uuid_v7_embeds_timestamp_and_sets_bits() {
let id = uuid_v7_from_parts(0x0000_0102_0304_0506, [0xAA; 10]);
assert_eq!(id.get_version_num(), 7);
assert_eq!(id.as_bytes()[8] & 0xC0, 0x80);
assert_eq!(&id.as_bytes()[0..6], &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
}
#[test]
fn seeded_entropy_is_reproducible() {
let a = SeededEntropy::new(42);
let b = SeededEntropy::new(42);
assert_eq!(a.next_u64(), b.next_u64());
assert_eq!(a.uuid_v4(), b.uuid_v4());
assert_eq!(a.uuid_v7(1_700_000_000_000), b.uuid_v7(1_700_000_000_000));
}
#[test]
fn seeded_entropy_diverges_by_seed() {
let a = SeededEntropy::new(1);
let b = SeededEntropy::new(2);
assert_ne!(a.uuid_v4(), b.uuid_v4());
}
#[test]
fn derive_uuid_is_stable_tag_sensitive_and_stream_independent() {
let e = SeededEntropy::new(7);
assert_eq!(e.derive_uuid("tenant:acme"), e.derive_uuid("tenant:acme"));
assert_ne!(e.derive_uuid("tenant:acme"), e.derive_uuid("tenant:beta"));
let before = e.derive_uuid("tenant:acme");
let _ = e.uuid_v4();
let _ = e.next_u64();
assert_eq!(before, e.derive_uuid("tenant:acme"));
assert_eq!(before, SeededEntropy::new(7).derive_uuid("tenant:acme"));
assert_ne!(before, SeededEntropy::new(8).derive_uuid("tenant:acme"));
assert_eq!(before.get_version_num(), 4);
}
#[test]
fn os_entropy_draws_distinct_values() {
let os = OsEntropy;
assert_ne!(os.uuid_v4(), os.uuid_v4());
}
}