use secure_data::envelope::{decrypt_for_use, encrypt_for_storage};
use secure_data::kms::StaticDevKeyProvider;
use std::time::Instant;
const SAMPLE_COUNT: usize = 100;
fn welchs_t(a: &[f64], b: &[f64]) -> f64 {
let mean_a = a.iter().sum::<f64>() / a.len() as f64;
let mean_b = b.iter().sum::<f64>() / b.len() as f64;
let var_a = a.iter().map(|x| (x - mean_a).powi(2)).sum::<f64>() / (a.len() - 1) as f64;
let var_b = b.iter().map(|x| (x - mean_b).powi(2)).sum::<f64>() / (b.len() - 1) as f64;
let se = (var_a / a.len() as f64 + var_b / b.len() as f64).sqrt();
if se == 0.0 {
return 0.0;
}
((mean_a - mean_b) / se).abs()
}
#[test]
#[ignore = "timing test — run locally on a stable machine: cargo test -p secure_data -- timing_ --ignored"]
fn timing_aead_tag_verification_constant_time() {
let rt = tokio::runtime::Runtime::new().unwrap();
let provider = StaticDevKeyProvider::new();
let plaintext = vec![0xABu8; 64];
let envelope = rt
.block_on(encrypt_for_storage(&plaintext, "default", &provider))
.expect("encryption must succeed");
let mut tampered_start = envelope.clone();
if !tampered_start.ciphertext.is_empty() {
tampered_start.ciphertext[0] ^= 0xFF;
}
let mut tampered_end = envelope.clone();
if !tampered_end.ciphertext.is_empty() {
let last = tampered_end.ciphertext.len() - 1;
tampered_end.ciphertext[last] ^= 0xFF;
}
for _ in 0..10 {
let _ = rt.block_on(decrypt_for_use(&tampered_start, &provider));
let _ = rt.block_on(decrypt_for_use(&tampered_end, &provider));
}
let mut times_start = Vec::with_capacity(SAMPLE_COUNT);
let mut times_end = Vec::with_capacity(SAMPLE_COUNT);
for _ in 0..SAMPLE_COUNT {
let start = Instant::now();
let _ = rt.block_on(decrypt_for_use(&tampered_start, &provider));
times_start.push(start.elapsed().as_nanos() as f64);
let start = Instant::now();
let _ = rt.block_on(decrypt_for_use(&tampered_end, &provider));
times_end.push(start.elapsed().as_nanos() as f64);
}
let t = welchs_t(×_start, ×_end);
assert!(
t < 4.5,
"Suspicious timing difference (Welch's t={t:.2}) between start-tampered vs \
end-tampered ciphertext rejection. This may indicate non-constant-time AEAD tag verification."
);
}