use std::{
fmt,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
pub const MAX_RANDOM_BYTES: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RandomError {
InvalidLimits,
TooLarge,
AtCapacity,
RuntimeUnavailable,
Unavailable,
TimedOut,
Cancelled,
WorkerFailed,
}
impl fmt::Display for RandomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidLimits => "invalid secure entropy limits",
Self::TooLarge => "secure entropy request exceeds byte limit",
Self::AtCapacity => "secure entropy capacity exhausted",
Self::RuntimeUnavailable => "secure entropy requires an active async runtime",
Self::Unavailable => "operating-system secure entropy unavailable",
Self::TimedOut => "secure entropy deadline expired",
Self::Cancelled => "secure entropy request cancelled",
Self::WorkerFailed => "secure entropy worker failed",
})
}
}
impl std::error::Error for RandomError {}
#[derive(Clone, Debug)]
pub struct SecureRandom {
permits: Arc<tokio::sync::Semaphore>,
timeout: Duration,
}
impl SecureRandom {
pub fn new(max_operations: usize, timeout: Duration) -> Result<Self, RandomError> {
if !(1..=64).contains(&max_operations)
|| timeout.is_zero()
|| timeout > Duration::from_secs(86400)
{
return Err(RandomError::InvalidLimits);
}
Ok(Self {
permits: Arc::new(tokio::sync::Semaphore::new(max_operations)),
timeout,
})
}
pub async fn bytes(&self, length: usize) -> Result<Vec<u8>, RandomError> {
self.generate_with(length, |bytes| getrandom::fill(bytes).map_err(|_| ()))
.await
}
async fn generate_with<F>(&self, length: usize, generate: F) -> Result<Vec<u8>, RandomError>
where
F: FnOnce(&mut [u8]) -> Result<(), ()> + Send + 'static,
{
if length > MAX_RANDOM_BYTES {
return Err(RandomError::TooLarge);
}
if length == 0 {
return Ok(Vec::new());
}
crate::async_engine::RuntimeHandle::current()
.map_err(|_| RandomError::RuntimeUnavailable)?;
let permit = self
.permits
.clone()
.try_acquire_owned()
.map_err(|_| RandomError::AtCapacity)?;
let stopped = Arc::new(AtomicBool::new(false));
let _cancel = StopOnDrop(stopped.clone());
let worker = tokio::task::spawn_blocking(move || {
let _permit = permit;
if stopped.load(Ordering::Acquire) {
return Err(RandomError::Cancelled);
}
let mut bytes = vec![0; length];
generate(&mut bytes).map_err(|()| RandomError::Unavailable)?;
if stopped.load(Ordering::Acquire) {
return Err(RandomError::Cancelled);
}
Ok(bytes)
});
tokio::time::timeout(self.timeout, worker)
.await
.map_err(|_| RandomError::TimedOut)?
.map_err(|_| RandomError::WorkerFailed)?
}
}
struct StopOnDrop(Arc<AtomicBool>);
impl Drop for StopOnDrop {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn success_and_partial_failure_never_confuse_output() {
let random = SecureRandom::new(1, Duration::from_secs(2)).unwrap();
assert_eq!(
random
.generate_with(4, |bytes| {
bytes.copy_from_slice(&[1, 2, 3, 4]);
Ok(())
})
.await
.unwrap(),
[1, 2, 3, 4]
);
assert_eq!(
random
.generate_with(4, |bytes| {
bytes[0] = 42;
Err(())
})
.await,
Err(RandomError::Unavailable)
);
assert_eq!(
random.bytes(MAX_RANDOM_BYTES + 1).await,
Err(RandomError::TooLarge)
);
assert!(random
.generate_with(0, |_| panic!("empty request reached backend"))
.await
.unwrap()
.is_empty());
assert_eq!(
random
.generate_with(MAX_RANDOM_BYTES + 1, |_| panic!(
"oversize request reached backend"
))
.await,
Err(RandomError::TooLarge)
);
}
#[tokio::test]
async fn timed_out_native_work_keeps_capacity_until_completion() {
let random = SecureRandom::new(1, Duration::from_millis(100)).unwrap();
let (release, waiting) = std::sync::mpsc::channel();
let (started, began) = tokio::sync::oneshot::channel();
let clone = random.clone();
let task = tokio::spawn(async move {
clone
.generate_with(1, move |_| {
let _ = started.send(());
let _ = waiting.recv();
Ok(())
})
.await
});
began.await.unwrap();
assert_eq!(task.await.unwrap(), Err(RandomError::TimedOut));
assert_eq!(random.bytes(1).await, Err(RandomError::AtCapacity));
release.send(()).unwrap();
tokio::time::timeout(Duration::from_secs(2), async {
while random.permits.available_permits() == 0 {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
assert_eq!(random.generate_with(1, |_| Ok(())).await.unwrap().len(), 1);
}
#[tokio::test]
async fn dropped_request_keeps_capacity_until_completion() {
let random = SecureRandom::new(1, Duration::from_secs(2)).unwrap();
let (release, waiting) = std::sync::mpsc::channel();
let (started, began) = tokio::sync::oneshot::channel();
let clone = random.clone();
let task = tokio::spawn(async move {
clone
.generate_with(1, move |_| {
let _ = started.send(());
let _ = waiting.recv();
Ok(())
})
.await
});
began.await.unwrap();
task.abort();
assert!(task.await.unwrap_err().is_cancelled());
assert_eq!(random.bytes(1).await, Err(RandomError::AtCapacity));
release.send(()).unwrap();
tokio::time::timeout(Duration::from_secs(2), async {
while random.permits.available_permits() == 0 {
tokio::task::yield_now().await;
}
})
.await
.unwrap();
}
}