use std::time::{
Duration,
Instant,
};
use digest::Digest;
use lib_q_keccak_digest::Keccak256;
use lib_q_sha3::{
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
};
#[cfg(not(tarpaulin))]
const TIMING_ITERATIONS: usize = 10_000;
#[cfg(tarpaulin)]
const TIMING_ITERATIONS: usize = 500;
#[cfg(not(tarpaulin))]
const TIMING_RUNS: usize = 9;
#[cfg(tarpaulin)]
const TIMING_RUNS: usize = 5;
#[cfg(not(tarpaulin))]
const TIMING_WARMUP: usize = 1_000;
#[cfg(tarpaulin)]
const TIMING_WARMUP: usize = 100;
#[cfg(not(tarpaulin))]
const TIMING_ATTEMPTS: usize = 3;
#[cfg(tarpaulin)]
const TIMING_ATTEMPTS: usize = 1;
const MAX_SPREAD_PERCENT: u128 = 80;
fn build_fixed_length_inputs(len: usize) -> Vec<Vec<u8>> {
vec![
vec![0u8; len],
vec![0xFFu8; len],
vec![0xA5u8; len],
(0..len).map(|i| (i % 251) as u8).collect::<Vec<_>>(),
(0..len)
.map(|i| ((i.wrapping_mul(73) + 19) % 256) as u8)
.collect::<Vec<_>>(),
]
}
fn trimmed_mean_duration<F>(mut op: F) -> Duration
where
F: FnMut(),
{
for _ in 0..TIMING_WARMUP {
op();
}
let mut run_times = Vec::with_capacity(TIMING_RUNS);
for _ in 0..TIMING_RUNS {
let start = Instant::now();
for _ in 0..TIMING_ITERATIONS {
op();
}
run_times.push(start.elapsed());
}
run_times.sort_unstable();
let trimmed = &run_times[1..run_times.len() - 1];
trimmed.iter().copied().sum::<Duration>() / trimmed.len() as u32
}
fn timing_spread_is_within_limit(timings: &[Duration]) -> bool {
let min_timing = timings.iter().copied().min().expect("at least one timing");
let max_timing = timings.iter().copied().max().expect("at least one timing");
let min_ns = min_timing.as_nanos();
let max_ns = max_timing.as_nanos();
let rhs = min_ns * (100 + MAX_SPREAD_PERCENT);
let lhs = max_ns * 100;
lhs <= rhs
}
fn assert_timing_spread_with_retries<F>(label: &str, mut collect_timings: F)
where
F: FnMut() -> Vec<Duration>,
{
let mut last_failure: Option<(u128, u128, Vec<u128>)> = None;
for attempt in 1..=TIMING_ATTEMPTS {
let timings = collect_timings();
let min_ns = timings
.iter()
.copied()
.min()
.expect("at least one timing")
.as_nanos();
let max_ns = timings
.iter()
.copied()
.max()
.expect("at least one timing")
.as_nanos();
let timing_ns = timings
.iter()
.map(|duration| duration.as_nanos())
.collect::<Vec<_>>();
let ratio = max_ns as f64 / min_ns as f64;
eprintln!(
"{} timing attempt {}/{}: timings={:?} min={}ns max={}ns ratio={:.3}",
label, attempt, TIMING_ATTEMPTS, timing_ns, min_ns, max_ns, ratio
);
if timing_spread_is_within_limit(&timings) {
return;
}
last_failure = Some((min_ns, max_ns, timing_ns));
}
let (min_ns, max_ns, timing_ns) = last_failure.expect("at least one attempt");
panic!(
"{} timing spread too high after {} attempts: timings={:?} min={}ns max={}ns allowed_ratio<=1.{}",
label, TIMING_ATTEMPTS, timing_ns, min_ns, max_ns, MAX_SPREAD_PERCENT
);
}
#[test]
fn test_sha3_224_constant_time() {
let test_inputs = build_fixed_length_inputs(128);
assert_timing_spread_with_retries("SHA3-224", || {
let mut timings = Vec::new();
for input in &test_inputs {
let timing = trimmed_mean_duration(|| {
let mut hasher = Sha3_224::new();
hasher.update(input);
let _result = hasher.finalize();
std::hint::black_box(_result);
});
timings.push(timing);
}
timings
});
}
#[test]
fn test_sha3_256_constant_time() {
let test_inputs = build_fixed_length_inputs(128);
assert_timing_spread_with_retries("SHA3-256", || {
let mut timings = Vec::new();
for input in &test_inputs {
let timing = trimmed_mean_duration(|| {
let mut hasher = Sha3_256::new();
hasher.update(input);
let _result = hasher.finalize();
std::hint::black_box(_result);
});
timings.push(timing);
}
timings
});
}
#[test]
fn test_keccak_256_constant_time() {
let test_inputs = build_fixed_length_inputs(128);
assert_timing_spread_with_retries("Keccak256", || {
let mut timings = Vec::new();
for input in &test_inputs {
let timing = trimmed_mean_duration(|| {
let mut hasher = Keccak256::new();
hasher.update(input);
let _result = hasher.finalize();
std::hint::black_box(_result);
});
timings.push(timing);
}
timings
});
}
#[test]
fn test_hash_algorithm_timing_relationships() {
let test_input = b"test input for timing analysis";
const ITERATIONS: usize = 1000;
let start = Instant::now();
for _ in 0..ITERATIONS {
let mut hasher = Sha3_224::new();
hasher.update(test_input);
let _result = hasher.finalize();
let _ = std::hint::black_box(_result);
}
let sha3_224_time = start.elapsed();
let start = Instant::now();
for _ in 0..ITERATIONS {
let mut hasher = Sha3_256::new();
hasher.update(test_input);
let _result = hasher.finalize();
let _ = std::hint::black_box(_result);
}
let sha3_256_time = start.elapsed();
let start = Instant::now();
for _ in 0..ITERATIONS {
let mut hasher = Sha3_384::new();
hasher.update(test_input);
let _result = hasher.finalize();
let _ = std::hint::black_box(_result);
}
let sha3_384_time = start.elapsed();
let start = Instant::now();
for _ in 0..ITERATIONS {
let mut hasher = Sha3_512::new();
hasher.update(test_input);
let _result = hasher.finalize();
let _ = std::hint::black_box(_result);
}
let sha3_512_time = start.elapsed();
const MAX_RATIO_512_TO_256: f64 = 5.0;
let ratio_512_to_256 = sha3_512_time.as_nanos() as f64 / sha3_256_time.as_nanos() as f64;
assert!(
ratio_512_to_256 > 0.5 && ratio_512_to_256 < MAX_RATIO_512_TO_256,
"SHA3-512 vs SHA3-256 time ratio out of range (expected < {}), got: {}",
MAX_RATIO_512_TO_256,
ratio_512_to_256
);
const MAX_RATIO_384_TO_256: f64 = 5.0;
let ratio_384_to_256 = sha3_384_time.as_nanos() as f64 / sha3_256_time.as_nanos() as f64;
assert!(
ratio_384_to_256 > 0.5 && ratio_384_to_256 < MAX_RATIO_384_TO_256,
"SHA3-384 vs SHA3-256 time ratio out of range (expected < {}), got: {}",
MAX_RATIO_384_TO_256,
ratio_384_to_256
);
let ratio_224_to_256 = sha3_224_time.as_nanos() as f64 / sha3_256_time.as_nanos() as f64;
assert!(
ratio_224_to_256 > 0.5 && ratio_224_to_256 < 3.0,
"SHA3-224 should have similar timing to SHA3-256, got ratio: {}",
ratio_224_to_256
);
}