#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::capsule::capture::current_scope;
use crate::entropy::Entropy;
#[derive(Debug)]
pub struct RecordingEntropy {
inner: Arc<dyn Entropy>,
}
impl RecordingEntropy {
#[must_use]
pub const fn new(inner: Arc<dyn Entropy>) -> Self {
Self { inner }
}
}
impl Entropy for RecordingEntropy {
fn next_u64(&self) -> u64 {
let drawn = self.inner.next_u64();
if let Some(scope) = current_scope() {
scope.record_random(drawn.to_le_bytes().to_vec());
}
drawn
}
fn fill_bytes(&self, dest: &mut [u8]) {
self.inner.fill_bytes(dest);
if let Some(scope) = current_scope() {
scope.record_random(dest.to_vec());
}
}
fn unrecorded(&self) -> Option<Arc<dyn Entropy>> {
Some(Arc::clone(&self.inner))
}
}
#[derive(Debug)]
pub struct ReplayEntropy {
draws: Mutex<VecDeque<Vec<u8>>>,
over_draws: AtomicUsize,
consumed: AtomicUsize,
width_mismatches: Mutex<Vec<(usize, usize, usize)>>,
}
impl ReplayEntropy {
#[must_use]
pub fn new(draws: Vec<Vec<u8>>) -> Self {
Self {
draws: Mutex::new(draws.into()),
over_draws: AtomicUsize::new(0),
consumed: AtomicUsize::new(0),
width_mismatches: Mutex::new(Vec::new()),
}
}
#[must_use]
pub fn width_mismatches(&self) -> Vec<(usize, usize, usize)> {
self.width_mismatches
.lock()
.map(|mismatches| mismatches.clone())
.unwrap_or_default()
}
#[must_use]
pub fn over_draws(&self) -> usize {
self.over_draws.load(Ordering::SeqCst)
}
#[must_use]
pub fn unconsumed(&self) -> usize {
self.draws.lock().map_or(0, |draws| draws.len())
}
fn next_draw(&self, width: usize) -> Option<Vec<u8>> {
if !crate::capsule::clock::in_replay_request() {
return None;
}
let mut draws = self.draws.lock().ok()?;
let Some(bytes) = draws.pop_front() else {
drop(draws);
self.over_draws.fetch_add(1, Ordering::SeqCst);
return None;
};
drop(draws);
let position = self.consumed.fetch_add(1, Ordering::SeqCst);
if bytes.len() != width
&& let Ok(mut mismatches) = self.width_mismatches.lock()
{
mismatches.push((position, bytes.len(), width));
}
Some(bytes)
}
}
impl Entropy for ReplayEntropy {
fn next_u64(&self) -> u64 {
let Some(bytes) = self.next_draw(8) else {
return 0;
};
let mut buf = [0u8; 8];
let take = bytes.len().min(8);
if let (Some(head), Some(source)) = (buf.get_mut(..take), bytes.get(..take)) {
head.copy_from_slice(source);
}
u64::from_le_bytes(buf)
}
fn fill_bytes(&self, dest: &mut [u8]) {
dest.fill(0);
let Some(bytes) = self.next_draw(dest.len()) else {
return;
};
let take = dest.len().min(bytes.len());
if let (Some(head), Some(source)) = (dest.get_mut(..take), bytes.get(..take)) {
head.copy_from_slice(source);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capsule::capture::{CaptureScope, CaptureSettings};
use crate::capsule::clock::with_replay_request_scope;
fn scope() -> Arc<CaptureScope> {
Arc::new(CaptureScope::new(
"entropy-test".to_owned(),
Arc::new(CaptureSettings::default()),
Arc::new(crate::log::filter::ParameterFilter::default()),
))
}
#[tokio::test]
async fn a_recorded_uuid_replays_byte_for_byte() {
let scope = scope();
let recording = RecordingEntropy::new(Arc::new(crate::entropy::OsEntropy));
let minted = crate::capsule::capture::with_capture_scope(Arc::clone(&scope), async {
recording.uuid_v4()
})
.await;
let draws: Vec<Vec<u8>> = scope
.effects_snapshot()
.random
.into_iter()
.map(|draw| draw.bytes)
.collect();
assert_eq!(draws.len(), 1, "a v4 UUID is exactly one 16-byte draw");
let replay = ReplayEntropy::new(draws);
let replayed = with_replay_request_scope(async { replay.uuid_v4() }).await;
assert_eq!(
replayed, minted,
"the identifier the failing request minted must reappear on replay"
);
assert_eq!(replay.over_draws(), 0);
assert_eq!(replay.unconsumed(), 0);
}
#[tokio::test]
async fn unrecorded_draws_stay_off_the_tape() {
let scope = scope();
let recording = RecordingEntropy::new(Arc::new(crate::entropy::OsEntropy));
let unrecorded = recording
.unrecorded()
.expect("a recording has an inner source");
crate::capsule::capture::with_capture_scope(Arc::clone(&scope), async {
let _ = unrecorded.next_u64();
let _ = unrecorded.uuid_v4();
})
.await;
assert!(scope.effects_snapshot().random.is_empty());
assert!(crate::entropy::OsEntropy.unrecorded().is_none());
}
#[tokio::test]
async fn drawing_past_the_recording_is_counted_not_faked() {
let replay = ReplayEntropy::new(Vec::new());
let value = with_replay_request_scope(async { replay.next_u64() }).await;
assert_eq!(
value, 0,
"an exhausted tape must not reach for real entropy"
);
assert_eq!(replay.over_draws(), 1);
}
#[tokio::test]
async fn draws_outside_the_replayed_request_do_not_consume_the_tape() {
let replay = ReplayEntropy::new(vec![vec![7u8; 16]]);
let _ = replay.uuid_v4();
assert_eq!(replay.unconsumed(), 1);
assert_eq!(
replay.over_draws(),
0,
"an out-of-scope draw is not an over-draw either"
);
}
}