use num_bigint::BigUint;
use num_traits::One;
use serde_json::Value;
use crate::engine::{cipher_block_with_key, ff1_traced, TraceRecord};
use crate::test_fixtures;
use crate::FF1;
fn hex(s: &str) -> Vec<u8> {
let digits: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
assert!(digits.len().is_multiple_of(2), "odd-length hex {s:?}");
digits
.chunks(2)
.map(|pair| {
let pair = std::str::from_utf8(pair).expect("ASCII hex");
u8::from_str_radix(pair, 16).expect("hex digit")
})
.collect()
}
fn str_field<'a>(v: &'a Value, key: &str) -> &'a str {
v[key]
.as_str()
.unwrap_or_else(|| panic!("{key} is not a string"))
}
fn u64_field(v: &Value, key: &str) -> u64 {
v[key]
.as_u64()
.unwrap_or_else(|| panic!("{key} is not an unsigned integer"))
}
fn bytes_field(v: &Value, key: &str) -> Vec<u8> {
v[key]
.as_array()
.unwrap_or_else(|| panic!("{key} is not a list"))
.iter()
.map(|b| u8::try_from(b.as_u64().expect("byte")).expect("byte range"))
.collect()
}
fn numerals_field(v: &Value, key: &str) -> Vec<u16> {
v[key]
.as_array()
.unwrap_or_else(|| panic!("{key} is not a list"))
.iter()
.map(|n| u16::try_from(n.as_u64().expect("numeral")).expect("u16 range"))
.collect()
}
fn nist_alphabet(radix: u64) -> String {
"0123456789abcdefghijklmnopqrstuvwxyz"[..usize::try_from(radix).expect("small")].to_string()
}
fn to_numerals(s: &str, alphabet: &str) -> Vec<u16> {
let chars: Vec<char> = alphabet.chars().collect();
s.chars()
.map(|c| {
let i = chars.iter().position(|&a| a == c).expect("in alphabet");
u16::try_from(i).expect("u16")
})
.collect()
}
#[test]
fn nist_samples_encrypt_and_decrypt_through_the_public_api() {
let Some(vectors) = test_fixtures::load("nist_ff1_samples.json") else {
return;
};
let vectors = vectors["vectors"].as_array().expect("vectors");
assert_eq!(vectors.len(), 9, "all nine published samples");
for v in vectors {
let key = hex(str_field(v, "key"));
let tweak = hex(str_field(v, "tweak"));
let ff1 = FF1::builder(&key, u32::try_from(u64_field(v, "radix")).expect("radix"))
.alphabet(str_field(v, "alphabet"))
.tweak(&tweak)
.build()
.expect("NIST configuration is valid");
let name = str_field(v, "name");
assert_eq!(
ff1.encrypt(str_field(v, "plaintext"), None).expect("valid"),
str_field(v, "ciphertext"),
"encrypt {name}"
);
assert_eq!(
ff1.decrypt(str_field(v, "ciphertext"), None)
.expect("valid"),
str_field(v, "plaintext"),
"decrypt {name}"
);
}
}
fn assert_round(rec: &TraceRecord, want: &Value, name: &str) {
let round = u64_field(want, "i");
let ctx = format!("{name} round {round}");
assert_eq!(u64::from(rec.i), round, "{ctx}: i");
assert_eq!(rec.p, bytes_field(want, "P"), "{ctx}: P");
assert_eq!(rec.q, bytes_field(want, "Q"), "{ctx}: Q");
assert_eq!(rec.r, bytes_field(want, "R"), "{ctx}: R");
assert_eq!(rec.s, bytes_field(want, "S"), "{ctx}: S");
assert_eq!(
BigUint::from_bytes_be(&rec.y).to_string(),
want["y"].to_string(),
"{ctx}: y"
);
assert_eq!(rec.m as u64, u64_field(want, "m"), "{ctx}: m");
assert_eq!(
BigUint::from_bytes_be(&rec.c).to_string(),
want["c"].to_string(),
"{ctx}: c"
);
assert_eq!(rec.c_block, numerals_field(want, "C"), "{ctx}: C");
}
#[test]
fn nist_per_round_intermediates() {
let Some(vectors) = test_fixtures::load("nist_ff1_intermediates.json") else {
return;
};
let vectors = vectors["vectors"].as_array().expect("vectors");
assert_eq!(vectors.len(), 9);
for v in vectors {
let name = str_field(v, "name");
let radix = u64_field(v, "radix");
let alphabet = nist_alphabet(radix);
let x = to_numerals(str_field(v, "plaintext"), &alphabet);
let (out, trace) = ff1_traced(
&hex(str_field(v, "key")),
u32::try_from(radix).expect("radix"),
&x,
&hex(str_field(v, "tweak")),
)
.expect("valid NIST input");
assert_eq!(
out,
to_numerals(str_field(v, "ciphertext"), &alphabet),
"{name}: output"
);
let rounds = v["rounds"].as_array().expect("rounds");
assert_eq!(trace.len(), 10, "{name}: exactly ten rounds");
assert_eq!(rounds.len(), 10);
for (rec, want) in trace.iter().zip(rounds) {
assert_eq!(rec.u as u64, u64_field(v, "u"), "{name}: u");
assert_eq!(rec.v as u64, u64_field(v, "v"), "{name}: v");
assert_eq!(rec.b as u64, u64_field(v, "b"), "{name}: b");
assert_eq!(rec.d as u64, u64_field(v, "d"), "{name}: d");
assert_round(rec, want, name);
}
}
}
#[test]
fn nist_trace_halves_chain_across_rounds() {
let Some(vectors) = test_fixtures::load("nist_ff1_intermediates.json") else {
return;
};
for v in vectors["vectors"].as_array().expect("vectors") {
let name = str_field(v, "name");
let radix = u64_field(v, "radix");
let x = to_numerals(str_field(v, "plaintext"), &nist_alphabet(radix));
let (out, trace) = ff1_traced(
&hex(str_field(v, "key")),
u32::try_from(radix).expect("radix"),
&x,
&hex(str_field(v, "tweak")),
)
.expect("valid NIST input");
let u = x.len() / 2;
assert_eq!(trace[0].a_before, x[..u], "{name}: round 0 A");
assert_eq!(trace[0].b_before, x[u..], "{name}: round 0 B");
for pair in trace.windows(2) {
let (previous, current) = (&pair[0], &pair[1]);
let round = current.i;
assert_eq!(
current.a_before, previous.b_before,
"{name}: round {round} A"
);
assert_eq!(
current.b_before, previous.c_block,
"{name}: round {round} B"
);
}
let last = trace.last().expect("ten rounds");
let mut joined = last.b_before.clone();
joined.extend_from_slice(&last.c_block);
assert_eq!(out, joined, "{name}: output is the final A || B");
}
}
#[test]
fn fips197_aes_known_answer_vectors() {
let Some(vectors) = test_fixtures::load("aes_kat_fips197.json") else {
return;
};
let vectors = vectors["vectors"].as_array().expect("vectors");
assert_eq!(vectors.len(), 3, "AES-128, -192 and -256");
for v in vectors {
let block: [u8; 16] = hex(str_field(v, "plaintext")).try_into().expect("16 bytes");
let out = cipher_block_with_key(&hex(str_field(v, "key")), &block).expect("valid key");
assert_eq!(
out.to_vec(),
hex(str_field(v, "ciphertext")),
"{}",
str_field(v, "name")
);
}
}
#[test]
fn frozen_oracle_vectors_through_the_public_api() {
let Some(vectors) = test_fixtures::load("oracle_kat_frozen.json") else {
return;
};
let vectors = vectors["vectors"].as_array().expect("vectors");
assert!(
vectors.len() >= 40,
"the frozen set has {} vectors",
vectors.len()
);
let mut max_d = 0usize;
for v in vectors {
let radix = u32::try_from(u64_field(v, "radix")).expect("radix");
let key = hex(str_field(v, "key_hex"));
let tweak = hex(str_field(v, "tweak_hex"));
let alphabet = str_field(v, "alphabet");
let ff1 = FF1::builder(&key, radix)
.alphabet(alphabet)
.tweak(&tweak)
.build()
.expect("frozen configuration is valid");
let (pt, ct) = (str_field(v, "plaintext"), str_field(v, "ciphertext"));
assert_eq!(
ff1.encrypt(pt, None).expect("valid"),
ct,
"encrypt radix {radix}"
);
assert_eq!(
ff1.decrypt(ct, None).expect("valid"),
pt,
"decrypt radix {radix}"
);
let x = to_numerals(pt, alphabet);
let (_, trace) = ff1_traced(&key, radix, &x, &tweak).expect("valid");
max_d = max_d.max(trace[0].d);
}
assert!(
max_d > 16,
"no frozen vector reaches d > 16 (max d = {max_d})"
);
}
fn expected_b(radix: u32, v: usize) -> usize {
let bits = (BigUint::from(radix).pow(u32::try_from(v).expect("small")) - BigUint::one()).bits();
usize::try_from(bits.div_ceil(8)).expect("small")
}
#[test]
fn power_of_two_radices_round_trip_with_exact_b() {
for (radix, n) in [
(2u32, 20usize),
(4, 10),
(8, 7),
(16, 5),
(32, 4),
(64, 4),
(256, 3),
] {
let key = [0x11u8; 16];
let x: Vec<u16> = (0..n)
.map(|i| u16::try_from(i as u32 % radix).expect("u16"))
.collect();
let ff1 = FF1::new(&key, radix).expect("valid");
let ct = ff1.encrypt_numerals(&x, None).expect("valid");
assert_eq!(ct.len(), n, "radix {radix}: length preserved");
assert!(
ct.iter().all(|&c| u32::from(c) < radix),
"radix {radix}: range preserved"
);
assert_eq!(
ff1.decrypt_numerals(&ct, None).expect("valid"),
x,
"radix {radix}"
);
let (_, trace) = ff1_traced(&key, radix, &x, b"").expect("valid");
assert_eq!(
trace[0].b,
expected_b(radix, trace[0].v),
"radix {radix}: b"
);
}
}
#[test]
fn b_is_derived_from_v_not_u_and_q_is_block_aligned() {
let tweak = [0x42u8; 12];
let (_, trace) = ff1_traced(&[0u8; 16], 256, &[1, 2, 3, 4, 5], &tweak).expect("valid");
let first = &trace[0];
assert_eq!((first.u, first.v, first.b), (2, 3, 3), "b must come from v");
let pad = (16 - (tweak.len() + first.b + 1) % 16) % 16;
assert_eq!(first.q.len(), tweak.len() + pad + 1 + first.b);
assert_eq!(&first.q[..tweak.len()], &tweak, "the tweak leads Q");
assert!(
first.q[tweak.len()..tweak.len() + pad]
.iter()
.all(|&z| z == 0),
"zero padding"
);
assert_eq!(
(first.p.len() + first.q.len()) % 16,
0,
"PRF input is block aligned"
);
}
fn code_only(source: &str) -> String {
let mut out = String::new();
for line in source.lines() {
let mut in_str = false;
let mut prev = '\0';
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if !in_str && c == '/' && chars.peek() == Some(&'/') {
break;
}
if c == '"' && prev != '\\' {
in_str = !in_str;
} else if !in_str {
out.push(c);
}
prev = c;
}
out.push('\n');
}
out
}
#[test]
fn no_floating_point_in_any_ff1_path() {
for (name, source) in [
("engine.rs", include_str!("engine.rs")),
("validate.rs", include_str!("validate.rs")),
("ff1.rs", include_str!("ff1.rs")),
("error.rs", include_str!("error.rs")),
("lib.rs", include_str!("lib.rs")),
] {
assert_no_float(name, &code_only(source));
}
}
fn assert_no_float(name: &str, code: &str) {
for token in [
"f32", "f64", "libm", "log2(", "log10(", ".ln(", "powf(", "powi(", "sqrt(", ".ceil(",
".floor(", ".round(",
] {
assert!(
!code.contains(token),
"{name}: forbidden float token {token:?}"
);
}
let bytes = code.as_bytes();
for w in bytes.windows(3) {
if w[0].is_ascii_digit() && w[1] == b'.' && w[2].is_ascii_digit() {
panic!(
"{name}: float literal near {:?}",
String::from_utf8_lossy(w)
);
}
}
}