use crate::antihacker::ct_eq;
use crate::api::{decode, encode, Options};
use crate::dictionaries;
use crate::kdf::KdfParams;
use std::time::{Duration, Instant};
pub fn median_time(samples: usize, mut op: impl FnMut()) -> Duration {
let n = samples.max(1);
let mut times = Vec::with_capacity(n);
for _ in 0..n {
let t = Instant::now();
op();
times.push(t.elapsed());
}
times.sort_unstable();
times[times.len() / 2]
}
pub const DUDECT_T_THRESHOLD: f64 = 10.0;
fn mean_var(x: &[f64]) -> (f64, f64) {
let n = x.len() as f64;
let mean = x.iter().sum::<f64>() / n;
let var = x.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
(mean, var)
}
pub fn welch_t(a: &[f64], b: &[f64]) -> f64 {
if a.len() < 2 || b.len() < 2 {
return 0.0;
}
let (ma, va) = mean_var(a);
let (mb, vb) = mean_var(b);
let denom = (va / a.len() as f64 + vb / b.len() as f64).sqrt();
let diff = ma - mb;
if denom == 0.0 {
return if diff == 0.0 { 0.0 } else { f64::INFINITY * diff.signum() };
}
diff / denom
}
pub struct TimingReport {
pub name: &'static str,
pub a: Duration,
pub b: Duration,
}
impl TimingReport {
pub fn ratio(&self) -> f64 {
let a = self.a.as_secs_f64().max(1e-12);
self.b.as_secs_f64() / a
}
pub fn within(&self, lo: f64, hi: f64) -> bool {
let r = self.ratio();
r >= lo && r <= hi
}
}
pub fn ct_eq_timing(samples: usize) -> TimingReport {
let base = [0x5Au8; 64];
let mut diff_first = base;
diff_first[0] ^= 0xFF;
let mut diff_last = base;
diff_last[63] ^= 0xFF;
let a = median_time(samples, || {
std::hint::black_box(ct_eq(&base, std::hint::black_box(&diff_first)));
});
let b = median_time(samples, || {
std::hint::black_box(ct_eq(&base, std::hint::black_box(&diff_last)));
});
TimingReport {
name: "ct_eq/first-vs-last-diff",
a,
b,
}
}
pub fn decode_timing(samples: usize) -> TimingReport {
let dict = dictionaries::ascii94();
let opts = Options {
pepper: b"",
kdf_params: KdfParams {
mem_kib: 8 * 1024,
iterations: 2,
parallelism: 1,
},
codebook_id: 0,
};
let secret = b"contenido protegido para el banco de timing";
let sym = encode(secret, "passphrase-correcta", &dict, &opts);
let a = median_time(samples, || {
std::hint::black_box(decode(&sym, "passphrase-correcta", &dict, b"").is_ok());
});
let b = median_time(samples, || {
std::hint::black_box(decode(&sym, "passphrase-incorrecta", &dict, b"").is_ok());
});
TimingReport {
name: "decode/correct-vs-wrong-pass",
a,
b,
}
}
pub struct DudectReport {
pub name: &'static str,
pub t: f64,
pub n: usize,
}
impl DudectReport {
pub fn from_classes(name: &'static str, a: &[f64], b: &[f64]) -> Self {
DudectReport {
name,
t: welch_t(a, b),
n: a.len().min(b.len()),
}
}
pub fn is_constant_time(&self, threshold: f64) -> bool {
self.t.abs() <= threshold
}
}
fn sample_two_classes_interleaved(
samples: usize,
mut op_a: impl FnMut(),
mut op_b: impl FnMut(),
) -> (Vec<f64>, Vec<f64>) {
let n = samples.max(2);
let mut a = Vec::with_capacity(n);
let mut b = Vec::with_capacity(n);
for _ in 0..n {
let ta = Instant::now();
op_a();
a.push(ta.elapsed().as_nanos() as f64);
let tb = Instant::now();
op_b();
b.push(tb.elapsed().as_nanos() as f64);
}
(a, b)
}
pub fn dudect_ct_eq(samples: usize) -> DudectReport {
let base = [0x5Au8; 64];
let mut diff_first = base;
diff_first[0] ^= 0xFF;
let mut diff_last = base;
diff_last[63] ^= 0xFF;
let (a, b) = sample_two_classes_interleaved(
samples,
|| {
std::hint::black_box(ct_eq(
std::hint::black_box(&base),
std::hint::black_box(&diff_first),
));
},
|| {
std::hint::black_box(ct_eq(
std::hint::black_box(&base),
std::hint::black_box(&diff_last),
));
},
);
DudectReport::from_classes("dudect/ct_eq", &a, &b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn median_time_measures_something() {
let d = median_time(16, || {
std::hint::black_box((0..100).sum::<u64>());
});
assert!(d >= Duration::ZERO);
}
#[test]
fn ratio_and_within_work() {
let r = TimingReport {
name: "t",
a: Duration::from_micros(100),
b: Duration::from_micros(110),
};
assert!(r.within(0.5, 2.0));
assert!((r.ratio() - 1.1).abs() < 0.01);
}
#[test]
fn ct_eq_shows_no_gross_timing_leak() {
let report = ct_eq_timing(2000);
assert!(
report.within(0.5, 2.0),
"ct_eq no debería depender de dónde difieren los bytes: ratio={}",
report.ratio()
);
}
#[test]
fn decode_time_independent_of_passphrase_correctness() {
let report = decode_timing(24);
assert!(
report.within(0.5, 2.0),
"decode con pass correcta vs incorrecta debe costar ~lo mismo (Argon2 domina): ratio={}",
report.ratio()
);
}
#[test]
fn welch_t_is_zero_for_identical_samples() {
assert_eq!(welch_t(&[1.0, 2.0, 3.0, 4.0], &[1.0, 2.0, 3.0, 4.0]), 0.0);
}
#[test]
fn welch_t_is_antisymmetric() {
let a = [2.0, 4.0, 6.0];
let b = [1.0, 2.0, 3.0];
assert!((welch_t(&a, &b) + welch_t(&b, &a)).abs() < 1e-12);
}
#[test]
fn welch_t_known_value() {
let a = [2.0, 4.0, 6.0, 8.0, 10.0];
let b = [1.0, 2.0, 3.0, 4.0, 5.0];
assert!((welch_t(&a, &b) - 1.897366).abs() < 1e-4);
}
#[test]
fn welch_t_handles_too_small_samples() {
assert_eq!(welch_t(&[1.0], &[1.0, 2.0]), 0.0);
}
#[test]
fn welch_t_infinite_for_zero_variance_different_means() {
assert_eq!(welch_t(&[5.0, 5.0], &[3.0, 3.0]), f64::INFINITY);
assert_eq!(welch_t(&[3.0, 3.0], &[5.0, 5.0]), f64::NEG_INFINITY);
assert_eq!(welch_t(&[4.0, 4.0], &[4.0, 4.0]), 0.0);
}
#[test]
fn dudect_verdict_constant_time_for_similar_classes() {
let a: Vec<f64> = (0..100).map(|i| if i % 2 == 0 { 9.0 } else { 11.0 }).collect();
let b = a.clone();
let r = DudectReport::from_classes("t", &a, &b);
assert!(r.is_constant_time(DUDECT_T_THRESHOLD), "t={}", r.t);
}
#[test]
fn dudect_verdict_flags_leaky_classes() {
let a: Vec<f64> = (0..100).map(|i| if i % 2 == 0 { 9.0 } else { 11.0 }).collect();
let b: Vec<f64> = (0..100).map(|i| if i % 2 == 0 { 29.0 } else { 31.0 }).collect();
let r = DudectReport::from_classes("t", &a, &b);
assert!(!r.is_constant_time(DUDECT_T_THRESHOLD), "t={}", r.t);
}
#[test]
fn interleaved_sampling_runs_both_classes_equally() {
let mut count_a = 0usize;
let mut count_b = 0usize;
let (a, b) = sample_two_classes_interleaved(
32,
|| count_a += 1,
|| count_b += 1,
);
assert_eq!(a.len(), 32);
assert_eq!(b.len(), 32);
assert_eq!(count_a, 32);
assert_eq!(count_b, 32);
}
}