use core::fmt;
use core::marker::PhantomData;
use p256::elliptic_curve::rand_core::{TryCryptoRng, TryRng};
use zeroize::Zeroize;
use crate::{Error, Result};
pub(crate) fn fill_random(dest: &mut [u8]) -> Result<()> {
let outcome = getrandom::fill(dest);
fail_closed(dest, outcome)
}
fn fail_closed(dest: &mut [u8], outcome: core::result::Result<(), getrandom::Error>) -> Result<()> {
match outcome {
Ok(()) => Ok(()),
Err(_) => {
dest.zeroize();
Err(Error::RandomnessUnavailable)
}
}
}
pub(crate) struct CryptoRngAdapter<'a>(Source<'a>);
enum Source<'a> {
Os(PhantomData<&'a mut ()>),
#[cfg(any(test, feature = "insecure-test-vectors"))]
Fixed(&'a mut insecure_test_vectors::FixedRng),
#[cfg(test)]
Failing(getrandom::Error),
}
#[cfg(not(any(test, feature = "insecure-test-vectors")))]
const _: () = assert!(core::mem::size_of::<CryptoRngAdapter<'static>>() == 0);
impl CryptoRngAdapter<'_> {
pub(crate) fn os() -> Self {
CryptoRngAdapter(Source::Os(PhantomData))
}
fn fill(&mut self, dest: &mut [u8]) -> Result<()> {
match &mut self.0 {
Source::Os(_) => fill_random(dest),
#[cfg(any(test, feature = "insecure-test-vectors"))]
Source::Fixed(rng) => rng.fill(dest),
#[cfg(test)]
Source::Failing(injected) => fail_closed(dest, Err(*injected)),
}
}
}
#[cfg(any(test, feature = "insecure-test-vectors"))]
impl<'a> CryptoRngAdapter<'a> {
pub(crate) fn fixed(rng: &'a mut insecure_test_vectors::FixedRng) -> Self {
CryptoRngAdapter(Source::Fixed(rng))
}
}
#[cfg(test)]
impl CryptoRngAdapter<'_> {
pub(crate) fn failing(injected: getrandom::Error) -> Self {
CryptoRngAdapter(Source::Failing(injected))
}
}
impl fmt::Debug for CryptoRngAdapter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let source = match self.0 {
Source::Os(_) => "Os",
#[cfg(any(test, feature = "insecure-test-vectors"))]
Source::Fixed(_) => "Fixed",
#[cfg(test)]
Source::Failing(_) => "Failing",
};
f.debug_tuple("CryptoRngAdapter").field(&source).finish()
}
}
impl TryRng for CryptoRngAdapter<'_> {
type Error = Error;
fn try_next_u32(&mut self) -> Result<u32> {
let mut bytes = [0u8; 4];
self.fill(&mut bytes)?;
Ok(u32::from_le_bytes(bytes))
}
fn try_next_u64(&mut self) -> Result<u64> {
let mut bytes = [0u8; 8];
self.fill(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<()> {
self.fill(dst)
}
}
impl TryCryptoRng for CryptoRngAdapter<'_> {}
#[cfg(any(test, feature = "insecure-test-vectors"))]
pub mod insecure_test_vectors {
use zeroize::{Zeroize, Zeroizing};
use crate::{Error, Result};
pub struct FixedRng {
bytes: Zeroizing<Vec<u8>>,
pos: usize,
}
impl FixedRng {
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
Self {
bytes: Zeroizing::new(bytes.into()),
pos: 0,
}
}
pub fn fill(&mut self, dest: &mut [u8]) -> Result<()> {
let next = self
.pos
.checked_add(dest.len())
.and_then(|end| self.bytes.get(self.pos..end).map(|src| (src, end)));
match next {
Some((src, end)) => {
dest.copy_from_slice(src);
self.pos = end;
Ok(())
}
None => {
dest.zeroize();
Err(Error::RandomnessUnavailable)
}
}
}
pub fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.pos)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replays_bytes_in_order_across_calls() {
let mut rng = FixedRng::new([1u8, 2, 3, 4, 5]);
let (mut a, mut b) = ([0u8; 2], [0u8; 3]);
assert_eq!(rng.fill(&mut a), Ok(()));
assert_eq!(rng.fill(&mut b), Ok(()));
assert_eq!((a, b), ([1, 2], [3, 4, 5]));
assert_eq!(rng.remaining(), 0);
}
#[test]
fn empty_request_consumes_nothing() {
let mut rng = FixedRng::new([7u8]);
assert_eq!(rng.fill(&mut []), Ok(()));
assert_eq!(rng.remaining(), 1);
}
#[test]
fn exhaustion_fails_closed_without_wrapping() {
let mut rng = FixedRng::new([1u8, 2, 3]);
let mut buf = [0xA5u8; 4];
assert_eq!(rng.fill(&mut buf), Err(Error::RandomnessUnavailable));
assert_eq!(buf, [0u8; 4]);
assert_eq!(rng.remaining(), 3);
let mut rest = [0u8; 3];
assert_eq!(rng.fill(&mut rest), Ok(()));
assert_eq!(rest, [1, 2, 3]);
assert_eq!(rng.fill(&mut [0u8; 1]), Err(Error::RandomnessUnavailable));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fills_with_os_randomness() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
assert_eq!(fill_random(&mut a), Ok(()));
assert_eq!(fill_random(&mut b), Ok(()));
assert_ne!(a, [0u8; 32]);
assert_ne!(a, b);
}
#[test]
fn fill_random_never_consults_a_fixed_rng() {
let rng = insecure_test_vectors::FixedRng::new([0u8; 32]);
let mut buf = [0u8; 32];
assert_eq!(fill_random(&mut buf), Ok(()));
assert_ne!(buf, [0u8; 32]);
assert_eq!(rng.remaining(), 32);
}
#[test]
fn fill_random_has_one_unconditional_source() {
let src = include_str!("rng.rs");
let start = src.find("pub(crate) fn fill_random(");
assert!(start.is_some(), "fill_random not found");
let body = &src[start.unwrap_or_default()..];
let end = body.find("\n}\n");
assert!(end.is_some(), "end of fill_random not found");
let body = &body[..end.unwrap_or_default()];
assert!(body.contains("getrandom::fill(dest)"), "{body}");
for banned in ["cfg", "FixedRng", "insecure", "static", "thread"] {
assert!(!body.contains(banned), "fill_random mentions {banned}");
}
}
#[test]
fn no_global_or_thread_local_state_can_switch_the_rng() {
let banned = [
["thread", "_local!"].concat(),
["Once", "Lock"].concat(),
["Once", "Cell"].concat(),
["Lazy", "Lock"].concat(),
["Lazy", "Cell"].concat(),
["Atomic", "Ptr"].concat(),
];
let is_static_item = |line: &str| {
let item = ["pub(crate) ", "pub(super) ", "pub "]
.iter()
.find_map(|vis| line.strip_prefix(vis))
.unwrap_or(line);
item.starts_with(&["static", " "].concat())
};
let src = std::path::PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/src"));
let exempt = src.join(["test", "_counters.rs"].concat());
let exempt_mod = ["test", "_counters"].concat();
let declares_exempt = |line: &str| {
let words: Vec<&str> = line
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.filter(|w| !w.is_empty())
.collect();
words
.windows(2)
.any(|w| w[0] == "mod" && w[1] == exempt_mod)
};
let mut declarations = Vec::new();
let mut exempt_text = None;
let mut dirs = vec![src.clone()];
let mut scanned = 0;
while let Some(dir) = dirs.pop() {
let entries = std::fs::read_dir(&dir);
assert!(entries.is_ok(), "cannot read {}", dir.display());
for entry in entries.into_iter().flatten() {
assert!(entry.is_ok(), "cannot list {}", dir.display());
let Ok(path) = entry.map(|e| e.path()) else {
continue;
};
if path.is_dir() {
dirs.push(path);
continue;
}
if path.extension().is_none_or(|ext| ext != "rs") {
continue;
}
let text = std::fs::read_to_string(&path);
assert!(text.is_ok(), "cannot read {}", path.display());
let text = text.unwrap_or_default();
scanned += 1;
let mut previous = "";
for (n, line) in text.lines().enumerate() {
let line = line.trim();
if !line.starts_with("//") && declares_exempt(line) {
declarations.push((path.clone(), line.to_owned(), previous.to_owned()));
}
previous = line;
if path == exempt || line.starts_with("//") {
continue;
}
let hit = banned.iter().any(|b| line.contains(b.as_str()));
assert!(
!hit && !is_static_item(line),
"{}:{}: global or thread-local state: {line}",
path.display(),
n + 1
);
}
if path == exempt {
exempt_text = Some(text);
}
}
}
assert!(scanned >= 10, "scanned only {scanned} source files");
if let Some(text) = exempt_text {
let first_code = text
.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with("//"));
assert_eq!(
first_code,
Some("#![cfg(test)]"),
"{} must open with #![cfg(test)]",
exempt.display()
);
let expected = [(
src.join("lib.rs"),
["mod ", &exempt_mod, ";"].concat(),
"#[cfg(test)]".to_owned(),
)];
assert_eq!(
declarations, expected,
"{exempt_mod} must be declared exactly once, as `mod {exempt_mod};` \
in lib.rs directly under #[cfg(test)]"
);
}
}
#[test]
fn crypto_rng_adapter_is_the_only_rand_core_rng() {
let try_rng = ["Try", "Rng"].concat();
let try_crypto_rng = ["Try", "Crypto", "Rng"].concat();
let implements = |line: &str, name: &str| {
let words: Vec<&str> = line
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.filter(|w| !w.is_empty())
.collect();
words.contains(&"impl") && words.windows(2).any(|w| w[0] == name && w[1] == "for")
};
let src = std::path::PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/src"));
let mut rng_impls = Vec::new();
let mut crypto_rng_impls = Vec::new();
let mut dirs = vec![src.clone()];
let mut scanned = 0;
while let Some(dir) = dirs.pop() {
let entries = std::fs::read_dir(&dir);
assert!(entries.is_ok(), "cannot read {}", dir.display());
for entry in entries.into_iter().flatten() {
assert!(entry.is_ok(), "cannot list {}", dir.display());
let Ok(path) = entry.map(|e| e.path()) else {
continue;
};
if path.is_dir() {
dirs.push(path);
continue;
}
if path.extension().is_none_or(|ext| ext != "rs") {
continue;
}
let text = std::fs::read_to_string(&path);
assert!(text.is_ok(), "cannot read {}", path.display());
let text = text.unwrap_or_default();
scanned += 1;
for line in text.lines().map(str::trim) {
if line.starts_with("//") {
continue;
}
if implements(line, &try_rng) {
rng_impls.push((path.clone(), line.to_owned()));
}
if implements(line, &try_crypto_rng) {
crypto_rng_impls.push((path.clone(), line.to_owned()));
}
}
}
}
assert!(scanned >= 10, "scanned only {scanned} source files");
let rng_rs = src.join("rng.rs");
let header =
|name: &str, body: &str| ["impl ", name, " for CryptoRngAdapter<'_> ", body].concat();
assert_eq!(
rng_impls,
[(rng_rs.clone(), header(&try_rng, "{"))],
"exactly one {try_rng} impl, for CryptoRngAdapter in rng.rs"
);
assert_eq!(
crypto_rng_impls,
[(rng_rs, header(&try_crypto_rng, "{}"))],
"exactly one {try_crypto_rng} impl, for CryptoRngAdapter in rng.rs"
);
}
#[test]
fn empty_buffer_is_ok() {
assert_eq!(fill_random(&mut []), Ok(()));
}
#[test]
fn fills_every_chunk_of_a_large_buffer() {
let mut buf = vec![0u8; 65_536 + 4_096];
assert_eq!(fill_random(&mut buf), Ok(()));
assert!(buf.chunks(32).all(|c| c.iter().any(|&b| b != 0)));
}
fn injected_errors() -> [getrandom::Error; 5] {
[
getrandom::Error::UNSUPPORTED,
getrandom::Error::ERRNO_NOT_POSITIVE,
getrandom::Error::UNEXPECTED,
getrandom::Error::new_custom(0),
getrandom::Error::new_custom(u16::MAX),
]
}
#[test]
fn success_passes_through_untouched() {
let mut buf = [0xA5u8; 16];
assert_eq!(fail_closed(&mut buf, Ok(())), Ok(()));
assert_eq!(buf, [0xA5u8; 16]);
}
#[test]
fn failure_leaves_no_partial_output() {
for injected in injected_errors() {
let mut buf = [0xA5u8; 16];
assert!(fail_closed(&mut buf, Err(injected)).is_err());
assert_eq!(buf, [0u8; 16], "{injected:?}");
}
}
#[test]
fn every_getrandom_error_maps_to_randomness_unavailable() {
for injected in injected_errors() {
let e = fail_closed(&mut [0u8; 4], Err(injected)).err();
assert_eq!(e, Some(Error::RandomnessUnavailable), "{injected:?}");
}
}
#[test]
fn mapped_error_carries_no_backend_detail() {
for injected in injected_errors() {
let Err(e) = fail_closed(&mut [0u8; 4], Err(injected)) else {
unreachable!("an injected error must fail");
};
assert_eq!(e.code(), "RandomnessUnavailable");
assert_eq!(format!("{e:?}"), "RandomnessUnavailable");
assert_eq!(e.to_string(), "randomness unavailable");
assert!(!e.to_string().contains(&injected.to_string()));
assert!(std::error::Error::source(&e).is_none());
}
}
fn require_try_crypto_rng<R: TryCryptoRng<Error = Error>>(_: &R) {}
#[test]
fn every_adapter_source_is_a_try_crypto_rng() {
let mut fixed = insecure_test_vectors::FixedRng::new([0u8; 1]);
require_try_crypto_rng(&CryptoRngAdapter::os());
require_try_crypto_rng(&CryptoRngAdapter::fixed(&mut fixed));
require_try_crypto_rng(&CryptoRngAdapter::failing(getrandom::Error::UNSUPPORTED));
}
#[test]
fn os_adapter_fills_bytes_from_the_os() {
let mut rng = CryptoRngAdapter::os();
let (mut a, mut b) = ([0u8; 32], [0u8; 32]);
assert_eq!(rng.try_fill_bytes(&mut a), Ok(()));
assert_eq!(rng.try_fill_bytes(&mut b), Ok(()));
assert_ne!(a, [0u8; 32]);
assert_ne!(a, b);
assert_eq!(rng.try_fill_bytes(&mut []), Ok(()));
}
#[test]
fn os_adapter_draws_words_from_the_os() {
let mut rng = CryptoRngAdapter::os();
let (a, b) = (rng.try_next_u64(), rng.try_next_u64());
assert!(a.is_ok() && b.is_ok());
assert_ne!(a, b);
let words: Vec<_> = (0..4).map(|_| rng.try_next_u32()).collect();
assert!(words.iter().all(Result::is_ok));
assert!(words.windows(2).any(|w| w[0] != w[1]), "{words:?}");
}
#[test]
fn os_adapters_are_independent_of_a_fixed_rng_in_scope() {
let mut fixed = insecure_test_vectors::FixedRng::new([0u8; 32]);
let _fixed_adapter = CryptoRngAdapter::fixed(&mut fixed);
let mut buf = [0u8; 32];
assert_eq!(CryptoRngAdapter::os().try_fill_bytes(&mut buf), Ok(()));
assert_ne!(buf, [0u8; 32]);
}
#[test]
fn fixed_adapter_replays_the_bytes_little_endian() {
let bytes: Vec<u8> = (1..=16).collect();
let mut fixed = insecure_test_vectors::FixedRng::new(bytes);
{
let mut rng = CryptoRngAdapter::fixed(&mut fixed);
assert_eq!(rng.try_next_u32(), Ok(0x0403_0201));
assert_eq!(rng.try_next_u64(), Ok(0x0c0b_0a09_0807_0605));
let mut rest = [0u8; 4];
assert_eq!(rng.try_fill_bytes(&mut rest), Ok(()));
assert_eq!(rest, [13, 14, 15, 16]);
}
assert_eq!(fixed.remaining(), 0);
}
#[test]
fn fixed_adapter_fails_closed_when_exhausted() {
let mut fixed = insecure_test_vectors::FixedRng::new([1u8, 2, 3]);
let mut rng = CryptoRngAdapter::fixed(&mut fixed);
assert_eq!(rng.try_next_u32(), Err(Error::RandomnessUnavailable));
let mut buf = [0xA5u8; 4];
assert_eq!(
rng.try_fill_bytes(&mut buf),
Err(Error::RandomnessUnavailable)
);
assert_eq!(buf, [0u8; 4]);
}
#[test]
fn adapter_debug_names_the_source_only() {
let mut fixed = insecure_test_vectors::FixedRng::new([0x5Au8; 8]);
assert_eq!(
format!("{:?}", CryptoRngAdapter::os()),
r#"CryptoRngAdapter("Os")"#
);
let debug = format!("{:?}", CryptoRngAdapter::fixed(&mut fixed));
assert_eq!(debug, r#"CryptoRngAdapter("Fixed")"#);
}
#[test]
fn injected_getrandom_failure_surfaces_from_the_adapter() {
for injected in injected_errors() {
let mut rng = CryptoRngAdapter::failing(injected);
let mut buf = [0xA5u8; 32];
assert_eq!(
rng.try_fill_bytes(&mut buf),
Err(Error::RandomnessUnavailable),
"{injected:?}"
);
assert_eq!(buf, [0u8; 32]);
assert_eq!(rng.try_next_u32(), Err(Error::RandomnessUnavailable));
assert_eq!(rng.try_next_u64(), Err(Error::RandomnessUnavailable));
}
}
}