#![cfg(feature = "alloc")]
use lib_q_aead::{
Aead,
AeadKey,
AeadWithMetadata,
Nonce,
};
struct Rng(u64);
impl Rng {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn bytes(&mut self, n: usize) -> Vec<u8> {
let mut out = vec![0u8; n];
for chunk in out.chunks_mut(8) {
let v = self.next_u64().to_le_bytes();
let k = chunk.len();
chunk.copy_from_slice(&v[..k]);
}
out
}
}
fn oracle_discriminates(name: &str, aead: &dyn Aead, key_len: usize, nonce_len: usize) {
let good_bytes = Rng(0xA11C_E000_0000_0001).bytes(key_len);
let good = AeadKey::new(good_bytes.clone());
let mut bad_bytes = good_bytes;
bad_bytes[0] ^= 0x01;
let bad = AeadKey::new(bad_bytes);
let nonce = Nonce::new(Rng(0xBEEF_0000_0000_0001).bytes(nonce_len));
let ad: &[u8] = b"control";
let ct = aead
.encrypt(&good, &nonce, b"oracle control", Some(ad))
.unwrap_or_else(|e| panic!("{name}: encrypt failed: {e:?}"));
assert!(
aead.decrypt(&good, &nonce, &ct, Some(ad)).is_ok(),
"{name}: oracle refused the correct key — a null search result would be meaningless"
);
assert!(
aead.decrypt(&bad, &nonce, &ct, Some(ad)).is_err(),
"{name}: oracle accepted a key differing in one bit — this gate cannot fail"
);
}
fn cmt1_search(
name: &str,
aead: &dyn Aead,
key_len: usize,
nonce_len: usize,
trials: u32,
plant_at: Option<u32>,
) -> u32 {
let mut rng = Rng(0xC0FF_EE00_0000_0001);
let k1_bytes = Rng(0xA11C_E000_0000_0001).bytes(key_len);
let n1_bytes = Rng(0xBEEF_0000_0000_0001).bytes(nonce_len);
let k1 = AeadKey::new(k1_bytes.clone());
let n1 = Nonce::new(n1_bytes.clone());
let ad1: &[u8] = b"side-1-associated-data";
let ct = aead
.encrypt(&k1, &n1, b"the quick brown fox", Some(ad1))
.unwrap_or_else(|e| panic!("{name}: encrypt failed: {e:?}"));
let mut hits = 0u32;
for t in 0..trials {
let (k2, n2, ad2) = if plant_at == Some(t) {
(
AeadKey::new(k1_bytes.clone()),
Nonce::new(n1_bytes.clone()),
ad1.to_vec(),
)
} else {
(
AeadKey::new(rng.bytes(key_len)),
Nonce::new(rng.bytes(nonce_len)),
rng.bytes(8),
)
};
if aead.decrypt(&k2, &n2, &ct, Some(&ad2)).is_ok() {
hits += 1;
}
}
hits
}
macro_rules! cmt1_case {
($feature:literal, $modname:ident, $ty:path, $label:literal, $trials:expr) => {
#[cfg(feature = $feature)]
mod $modname {
use super::*;
fn build() -> ($ty, usize, usize) {
let a = <$ty>::new();
let kl = a.key_size();
let nl = a.nonce_size();
(a, kl, nl)
}
#[test]
fn oracle_discriminates_control() {
let (a, kl, nl) = build();
oracle_discriminates($label, &a, kl, nl);
}
#[test]
fn search_reports_a_planted_hit() {
let (a, kl, nl) = build();
let trials: u32 = 16;
let plant_at = 9u32;
let hits = cmt1_search($label, &a, kl, nl, trials, Some(plant_at));
println!(
"{}: planted-hit control: {} trials with an accepting triple at index {} \
-> {} hits counted (expect 1)",
$label, trials, plant_at, hits
);
assert_eq!(
hits, 1,
"{}: the search loop failed to count a triple that decrypts — every null \
result it produces is worthless",
$label
);
}
#[test]
fn bounded_cmt1_search() {
let (a, kl, nl) = build();
let trials: u32 = $trials;
let hits = cmt1_search($label, &a, kl, nl, trials, None);
println!(
"{}: key={}B nonce={}B tag={}B | {} random (key, nonce, ad) triples tried \
against a fixed ciphertext, {} accepted. NOT a commitment result: the \
expected count is ~0 for a committing AND for a non-committing mode at \
this tag size; it only rules out a break cheaper than ~2^{} tries.",
$label,
kl,
nl,
a.tag_size(),
trials,
hits,
(u32::BITS - trials.leading_zeros())
);
assert_eq!(
hits, 0,
"{}: a random second key was accepted — that would be a practical CMT-1 break",
$label
);
}
}
};
}
cmt1_case!(
"shake256",
shake256,
lib_q_aead::Shake256Aead,
"Shake256Aead",
20_000
);
cmt1_case!(
"saturnin",
saturnin,
lib_q_aead::SaturninAead,
"SaturninAead (CTR-Cascade)",
20_000
);
cmt1_case!(
"duplex-sponge-aead",
duplex,
lib_q_aead::DuplexSpongeAead,
"DuplexSpongeAead",
20_000
);
cmt1_case!(
"tweak-aead",
tweak,
lib_q_aead::TweakAead,
"TweakAead",
20_000
);
cmt1_case!(
"romulus-n",
romulus_n,
lib_q_aead::RomulusNAead,
"RomulusNAead",
20_000
);
cmt1_case!(
"romulus-m",
romulus_m,
lib_q_aead::RomulusMAead,
"RomulusMAead",
20_000
);
cmt1_case!(
"rocca-s",
rocca_s,
lib_q_aead::RoccaSAead,
"RoccaSAead",
20_000
);