use std::fs::File;
use std::io::{self, Read};
#[cfg(feature = "zeroize")]
use zeroize::Zeroizing;
pub const RCT_CUTOFF: usize = 5;
pub const APT_CUTOFF: usize = 20;
pub const AUTO_LOWER_BOUND: usize = 1856;
pub const AUTO_UPPER_BOUND: usize = 2240;
pub const SYMBOL_MIN_UNIQUE: usize = 180;
pub const JITTER_RCT_CUTOFF: usize = 16;
pub const JITTER_APT_CUTOFF: usize = 250;
pub const JITTER_AUTO_LOWER_BOUND: usize = 200;
pub const JITTER_AUTO_UPPER_BOUND: usize = 3800;
pub const JITTER_SYMBOL_MIN_UNIQUE: usize = 32;
#[derive(Debug)]
pub struct HealthTests {
rct_previous_sample: Option<u8>,
rct_count: usize,
rct_cutoff: usize,
apt_window: [u8; 512],
apt_index: usize,
apt_base_sample: Option<u8>,
apt_count: usize,
apt_cutoff: usize,
#[cfg(feature = "advance")]
auto_flips: usize,
#[cfg(feature = "advance")]
auto_lower_bound: usize,
#[cfg(feature = "advance")]
auto_upper_bound: usize,
#[cfg(feature = "advance")]
symbol_seen: [u64; 4],
#[cfg(feature = "advance")]
symbol_min_unique: usize,
}
impl HealthTests {
#[cfg(feature = "advance")]
pub fn new(
rct_cutoff: usize,
apt_cutoff: usize,
auto_lower_bound: usize,
auto_upper_bound: usize,
symbol_min_unique: usize,
) -> Self {
Self {
rct_previous_sample: None,
rct_count: 0,
rct_cutoff,
apt_window: [0; 512],
apt_index: 0,
apt_base_sample: None,
apt_count: 0,
apt_cutoff,
auto_flips: 0,
auto_lower_bound,
auto_upper_bound,
symbol_seen: [0u64; 4],
symbol_min_unique,
}
}
#[cfg(not(feature = "advance"))]
pub fn new(rct_cutoff: usize, apt_cutoff: usize) -> Self {
Self {
rct_previous_sample: None,
rct_count: 0,
rct_cutoff,
apt_window: [0; 512],
apt_index: 0,
apt_base_sample: None,
apt_count: 0,
apt_cutoff,
}
}
pub fn process_sample(&mut self, sample: u8) {
self.advanced_tests(sample);
self.repetition_count_test(sample);
self.adaptive_proportion_test(sample);
}
fn repetition_count_test(&mut self, sample: u8) {
match self.rct_previous_sample {
Some(prev) if sample == prev => {
self.rct_count += 1;
if self.rct_count >= self.rct_cutoff {
panic!(
"NIST SP 800-90B Repetition Count Test failed! \
Entropy source is stuck."
);
}
}
_ => {
self.rct_previous_sample = Some(sample);
self.rct_count = 1;
}
}
}
#[cfg(feature = "advance")]
fn advanced_tests(&mut self, sample: u8) {
let word_idx = sample as usize / 64;
let bit_idx = sample as usize % 64;
self.symbol_seen[word_idx] |= 1u64 << bit_idx;
if let Some(prev) = self.rct_previous_sample {
self.auto_flips += (sample ^ prev).count_ones() as usize;
}
}
#[cfg(not(feature = "advance"))]
#[inline(always)]
fn advanced_tests(&mut self, _sample: u8) {}
fn adaptive_proportion_test(&mut self, sample: u8) {
self.apt_window[self.apt_index] = sample;
match self.apt_base_sample {
None => {
self.apt_base_sample = Some(sample);
self.apt_count = 1;
}
Some(base) if sample == base => {
self.apt_count += 1;
if self.apt_count >= self.apt_cutoff {
panic!(
"NIST SP 800-90B Adaptive Proportion Test failed! \
Entropy source lacks variation."
);
}
}
_ => {}
}
self.apt_index += 1;
if self.apt_index == 512 {
#[cfg(feature = "advance")]
self.evaluate_advanced_tests();
self.apt_base_sample = None;
self.apt_count = 0;
self.apt_index = 0;
}
}
#[cfg(feature = "advance")]
fn evaluate_advanced_tests(&mut self) {
if self.auto_lower_bound > 0
&& self.auto_upper_bound > 0
&& (self.auto_flips < self.auto_lower_bound
|| self.auto_flips > self.auto_upper_bound)
{
panic!(
"Advanced Autocorrelation Test failed! \
Source shows dangerous periodicity."
);
}
let unique_symbols: usize = self
.symbol_seen
.iter()
.map(|&word| word.count_ones() as usize)
.sum();
if unique_symbols < self.symbol_min_unique {
panic!(
"Advanced Symbol Diversity Test failed! \
Source is heavily biased."
);
}
self.auto_flips = 0;
self.symbol_seen = [0u64; 4];
}
}
#[derive(Debug)]
pub struct OsRng {
health_tests: HealthTests,
}
impl OsRng {
pub fn new() -> io::Result<Self> {
File::open("/dev/urandom")?;
#[cfg(feature = "advance")]
let health_tests = HealthTests::new(
RCT_CUTOFF,
APT_CUTOFF,
AUTO_LOWER_BOUND,
AUTO_UPPER_BOUND,
SYMBOL_MIN_UNIQUE,
);
#[cfg(not(feature = "advance"))]
let health_tests = HealthTests::new(RCT_CUTOFF, APT_CUTOFF);
Ok(Self { health_tests })
}
pub fn fill_bytes(&mut self, dest: &mut [u8]) -> io::Result<()> {
#[cfg(feature = "zeroize")]
let mut tmp = Zeroizing::new(vec![0u8; dest.len()]);
#[cfg(not(feature = "zeroize"))]
let mut tmp = vec![0u8; dest.len()];
let mut file = File::open("/dev/urandom")?;
file.read_exact(&mut tmp)?;
for (i, &byte) in tmp.iter().enumerate() {
self.health_tests.process_sample(byte);
dest[i] = byte;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn os_rng_health() -> HealthTests {
#[cfg(feature = "advance")]
return HealthTests::new(
RCT_CUTOFF,
APT_CUTOFF,
AUTO_LOWER_BOUND,
AUTO_UPPER_BOUND,
SYMBOL_MIN_UNIQUE,
);
#[cfg(not(feature = "advance"))]
return HealthTests::new(RCT_CUTOFF, APT_CUTOFF);
}
#[test]
#[should_panic(
expected = "NIST SP 800-90B Repetition Count Test failed! Entropy source is stuck."
)]
fn rct_failure_on_stuck_source() {
let mut h = os_rng_health();
for _ in 0..RCT_CUTOFF {
h.process_sample(0x42);
}
}
#[test]
#[should_panic(
expected = "NIST SP 800-90B Adaptive Proportion Test failed! Entropy source lacks variation."
)]
fn apt_failure_on_skewed_distribution() {
let mut h = os_rng_health();
let base = 0x77u8;
for i in 0..512usize {
h.process_sample(if i % 2 == 0 { base } else { (i % 250) as u8 });
}
}
#[test]
#[should_panic(
expected = "NIST SP 800-90B Repetition Count Test failed! Entropy source is stuck."
)]
fn rct_catches_constant_time_jitter_attack() {
eprintln!("\n--- SIMULATING CONSTANT TIME (ZERO JITTER) ATTACK ---");
#[cfg(feature = "advance")]
let mut h = HealthTests::new(
JITTER_RCT_CUTOFF,
JITTER_APT_CUTOFF,
JITTER_AUTO_LOWER_BOUND,
JITTER_AUTO_UPPER_BOUND,
JITTER_SYMBOL_MIN_UNIQUE,
);
#[cfg(not(feature = "advance"))]
let mut h = HealthTests::new(JITTER_RCT_CUTOFF, JITTER_APT_CUTOFF);
for _ in 0..100 {
h.process_sample(123);
}
}
#[test]
#[cfg(feature = "advance")]
#[should_panic(expected = "Advanced Symbol Diversity Test failed! Source is heavily biased.")]
fn symbol_diversity_catches_low_complexity_pattern() {
eprintln!("\n--- SIMULATING MARKOV PERIODICITY ATTACK ---");
let mut h = HealthTests::new(
JITTER_RCT_CUTOFF,
JITTER_APT_CUTOFF,
JITTER_AUTO_LOWER_BOUND,
JITTER_AUTO_UPPER_BOUND,
JITTER_SYMBOL_MIN_UNIQUE,
);
let pattern = [0u8, 1, 2, 3];
for i in 0..512 {
h.process_sample(pattern[i % 4]);
}
}
#[test]
fn rct_passes_on_healthy_drbg_output() {
let mut h = os_rng_health();
let mut drbg = crate::drbg::HashDrbg::instantiate(
b"test_seed_for_rct_success_with_sufficient_length",
);
let mut buf = [0u8; 1024];
drbg.generate(&mut buf);
for byte in buf {
h.process_sample(byte);
}
}
#[test]
fn apt_passes_on_healthy_drbg_output() {
let mut h = os_rng_health();
let mut drbg = crate::drbg::HashDrbg::instantiate(
b"test_seed_for_apt_success_with_sufficient_length",
);
let mut buf = [0u8; 1024];
drbg.generate(&mut buf);
for byte in buf {
h.process_sample(byte);
}
}
#[test]
fn os_rng_reads_and_passes_health_tests() {
let mut rng = OsRng::new().expect("OsRng init failed");
let mut buf = [0u8; 1024];
assert!(rng.fill_bytes(&mut buf).is_ok());
assert!(buf.iter().any(|&b| b != 0), "should not be all-zero");
}
}