fpr-ff1 2.1.1

NIST SP 800-38G FF1 format-preserving encryption: exact integer arithmetic, conformance-tested against the published vectors.
Documentation
//! Unit tests for the Algorithm 7 port (plan 00003 STEP-08/09).
//!
//! STEP-08 verified the structural pieces (conversion, bit-length
//! derivation, padding, parity) against hand-computed spec values. STEP-09
//! wires the AES seams, which makes the full core callable for the first
//! time: the round-trip test below exercises all ten rounds, both
//! conversion directions, and the previously-unreachable `d > 16`
//! S-expansion branch (radix 10 n=60 and radix 65535 n=13 both reach it).
//!
//! Round-trips are SELF-CONSISTENCY only — a wrong-but-symmetric AES or S
//! expansion still round-trips. Cryptographic correctness is established
//! independently by the NIST FIPS 197 KAT and the PRF equality with the
//! Python path (`tests/test_rust_aes_validation.py`), and FF1-level
//! conformance by the NIST vectors and per-round intermediates once the
//! STEP-11 bridge lands.

use num_bigint::BigUint;
use num_traits::One;

use crate::engine::{
    encode_len_u32, ff1, num_radix, num_radix_reference, prf_with_key, str_radix,
    str_radix_reference, D_C_THRESHOLD,
};

#[test]
fn num_radix_decodes_big_endian() {
    // NUM_radix([1,2,3]) base 10 = 123.
    assert_eq!(num_radix(10, &[1, 2, 3]), BigUint::from(123u32));
    // Leading zeros contribute nothing.
    assert_eq!(num_radix(10, &[0, 0, 1]), BigUint::from(1u32));
    // All-max digits at a wide radix.
    assert_eq!(num_radix(256, &[255, 255]), BigUint::from(65_535u32));
}

#[test]
fn str_radix_encodes_big_endian_and_truncates() {
    assert_eq!(str_radix(&BigUint::from(123u32), 10, 3), vec![1, 2, 3]);
    // Zero padding to length.
    assert_eq!(str_radix(&BigUint::from(1u32), 10, 3), vec![0, 0, 1]);
    // Truncation contract: values >= radix**length drop high digits.
    assert_eq!(str_radix(&BigUint::from(1_234u32), 10, 3), vec![2, 3, 4]);
}

#[test]
fn round_trip_conversion() {
    for &radix in &[2u32, 10, 36, 256, 65_535] {
        for len in [1usize, 2, 7, 64, 130] {
            let numerals: Vec<u16> = (0..len).map(|i| (i % radix as usize) as u16).collect();
            let value = num_radix(radix, &numerals);
            assert_eq!(
                str_radix(&value, radix, len),
                numerals,
                "radix {radix} len {len}"
            );
        }
    }
}

#[test]
fn b_derivation_uses_v_not_u() {
    // radix 256, n=5: u=2, v=3. b from v = ceil(bits(256**3 - 1)/8) = 3;
    // b from u would be 2. Pin the correct derivation (AGENTS.md gotcha).
    let v = 3usize;
    // Kept as `(bits + 7) / 8` rather than `bits.div_ceil(8)` to match
    // SP 800-38G Algorithm 7 step 3 / `_ff1.py` line for line (decision D5).
    #[allow(clippy::manual_div_ceil)]
    let b = ((BigUint::from(256u32).pow(v as u32) - BigUint::one()).bits() + 7) / 8;
    assert_eq!(b, 3);
    let u = 2usize;
    #[allow(clippy::manual_div_ceil)]
    let b_wrong = ((BigUint::from(256u32).pow(u as u32) - BigUint::one()).bits() + 7) / 8;
    assert_eq!(b_wrong, 2, "sanity: the two derivations must disagree here");
}

#[test]
fn padding_formula_matches_python() {
    // Python: pad = (-t - b - 1) % 16, always non-negative. The Rust port
    // must negate the sum BEFORE reducing, not after.
    for t in [0usize, 1, 3, 11, 12, 13, 16, 27, 32] {
        for b in [4usize, 9, 20] {
            let py_pad = (-(t as isize) - b as isize - 1).rem_euclid(16) as usize;
            // (t + pad + 1 + b) must be a multiple of 16.
            assert_eq!((t + py_pad + 1 + b) % 16, 0, "t={t} b={b}");
        }
    }
}

#[test]
fn radix_bounds_representable() {
    // The supported radix range is 2 <= radix < 2**16 (AGENTS.md subset).
    assert_eq!(num_radix(2, &[1, 0, 1, 1]), BigUint::from(11u32));
    // radix 65535: numerals up to 65534, exercising the u16 width.
    // NUM_radix([65534, 1]) = 65534 * 65535 + 1.
    assert_eq!(
        num_radix(65_535, &[65_534, 1]),
        BigUint::from(65534u64 * 65535 + 1)
    );
}

#[test]
fn prf_structural_properties() {
    // Output shape and basic behaviour; the cryptographic content is
    // validated Python-side (KAT + equality with the reference path).
    let key = [7u8; 32];
    let data = [1u8; 48]; // three aligned blocks
    let tag1 = prf_with_key(&key, &data).expect("valid key");
    let tag2 = prf_with_key(&key, &data).expect("valid key");
    assert_eq!(tag1.len(), 16, "CBC-MAC tag is one block");
    assert_eq!(tag1, tag2, "PRF is deterministic for fixed inputs");
    // Key sensitivity: a different key must move the tag.
    let other_key = [8u8; 32];
    let tag3 = prf_with_key(&other_key, &data).expect("valid key");
    assert_ne!(tag1, tag3);
    // Data sensitivity: flipping one input bit must move the tag.
    let mut flipped = data;
    flipped[0] ^= 1;
    let tag4 = prf_with_key(&key, &flipped).expect("valid key");
    assert_ne!(tag1, tag4);
}

#[test]
fn ff1_round_trip_self_consistency() {
    // All three key sizes; odd n (u != v); the d > 16 S-expansion cases
    // (radix 10 n=60 and radix 65535 n=13); radix bounds; a tweak.
    // Self-consistency only — see the module docstring.
    for key_len in [16usize, 24, 32] {
        let key: Vec<u8> = (0..key_len).map(|i| (i * 7 + 3) as u8).collect();
        for (radix, n) in [
            (10u32, 6usize),
            (10, 7),
            (2, 20),
            (36, 10),
            (256, 5),
            (10, 60),
            (65_535, 13),
        ] {
            let x: Vec<u16> = (0..n)
                .map(|i| ((i as u32 * 7919 + 17) % radix) as u16)
                .collect();
            let ct = ff1(&key, radix, &x, b"kat", true)
                .unwrap_or_else(|e| panic!("encrypt failed: {e}"));
            assert_eq!(ct.len(), n, "length preservation");
            assert!(ct.iter().all(|&c| (c as u32) < radix), "range preservation");
            let pt = ff1(&key, radix, &ct, b"kat", false)
                .unwrap_or_else(|e| panic!("decrypt failed: {e}"));
            assert_eq!(pt, x, "round trip, radix {radix} n {n} key_len {key_len}");
        }
    }
}

/// `[n]^4` and `[t]^4` (Algorithm 7 step 5) are checked, never narrowed
/// (review 00007 MED-01). Python validation rejects both lengths first;
/// this is the defence-in-depth layer, so it must fail closed on its own.
#[test]
fn encode_len_u32_is_big_endian_at_the_boundary() {
    assert_eq!(encode_len_u32(0), Ok([0, 0, 0, 0]));
    assert_eq!(encode_len_u32(10), Ok([0, 0, 0, 10]));
    assert_eq!(
        encode_len_u32(u32::MAX as usize),
        Ok([0xff, 0xff, 0xff, 0xff])
    );
}

#[cfg(target_pointer_width = "64")]
#[test]
fn encode_len_u32_rejects_rather_than_wraps() {
    // `2**32 as u32` is 0: the old cast would have encoded this as a
    // zero-length field and produced non-conformant ciphertext.
    assert!(encode_len_u32(u32::MAX as usize + 1).is_err());
    assert!(encode_len_u32(1usize << 40).is_err());
}

// ---------------------------------------------------------------------------
// Plan 00007 STEP-04: the subquadratic conversion must equal the retained
// SP 800-38G reference loops exactly -- the Rust counterpart of
// `tests/test_conversion_equivalence.py`. Every production conversion goes
// through `num_radix`/`str_radix`; the `_reference` functions are the spec
// text, and these tests are what licenses the dispatch.
// ---------------------------------------------------------------------------

/// Radices spanning the supported range: minimum, NIST sample, common
/// alphabet, power of two, and the supported maximum (mirrors the Python
/// module's `_REPRESENTATIVE_RADICES`).
const REPRESENTATIVE_RADICES: [u32; 5] = [2, 10, 36, 256, 65_535];

/// Degenerate lengths, both sides of `D_C_THRESHOLD`, odd and even splits,
/// and lengths deep enough for several recursion levels.
const EQUIVALENCE_LENGTHS: [usize; 12] = [0, 1, 2, 63, 64, 65, 128, 129, 131, 257, 1000, 2049];

/// Deterministic numerals: a leading run of zeros (leading zeros must not
/// change a value), then a pseudo-random body, then a trailing run of the
/// maximum numeral.
fn sample_numerals(radix: u32, length: usize, seed: u64) -> Vec<u16> {
    let mut state = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
    (0..length)
        .map(|i| {
            if i < length / 8 {
                0
            } else if i >= length - length / 8 {
                (radix - 1) as u16
            } else {
                state = state
                    .wrapping_mul(6_364_136_223_846_793_005)
                    .wrapping_add(1_442_695_040_888_963_407);
                ((state >> 33) % radix as u64) as u16
            }
        })
        .collect()
}

#[test]
fn d_c_threshold_mirrors_the_python_reference() {
    // `_ff1.py::_D_C_THRESHOLD`; both cores must dispatch at the same length.
    assert_eq!(D_C_THRESHOLD, 64);
}

#[test]
fn num_radix_equals_reference_at_representative_radices() {
    for &radix in &REPRESENTATIVE_RADICES {
        for &length in &EQUIVALENCE_LENGTHS {
            let numerals = sample_numerals(radix, length, (radix as u64) << 16 | length as u64);
            assert_eq!(
                num_radix(radix, &numerals),
                num_radix_reference(radix, &numerals),
                "radix {radix} length {length}"
            );
        }
    }
}

#[test]
fn str_radix_equals_reference_at_representative_radices() {
    for &radix in &REPRESENTATIVE_RADICES {
        for &length in &EQUIVALENCE_LENGTHS {
            let numerals = sample_numerals(radix, length, (length as u64) << 16 | radix as u64);
            let value = num_radix_reference(radix, &numerals);
            let expected = str_radix_reference(&value, radix, length);
            assert_eq!(expected, numerals, "reference self-check, radix {radix}");
            assert_eq!(
                str_radix(&value, radix, length),
                expected,
                "radix {radix} length {length}"
            );
        }
    }
}

#[test]
fn every_supported_radix_is_equivalent_above_the_threshold() {
    // One length that takes the fast path (power-of-two packing or the
    // divide-and-conquer split) for every radix the package accepts.
    let length = D_C_THRESHOLD + 1;
    for radix in 2u32..65_536 {
        let numerals = sample_numerals(radix, length, radix as u64);
        let value = num_radix_reference(radix, &numerals);
        assert_eq!(num_radix(radix, &numerals), value, "NUM radix {radix}");
        assert_eq!(
            str_radix(&value, radix, length),
            str_radix_reference(&value, radix, length),
            "STR radix {radix}"
        );
    }
}

#[test]
fn every_power_of_two_radix_is_equivalent_at_every_chunk_alignment() {
    // The packing path groups numerals into byte-aligned chunks; lengths
    // 65..=81 cover every remainder modulo the largest chunk size (8).
    for k in 1u32..16 {
        let radix = 1u32 << k;
        for length in 65usize..=81 {
            let numerals = sample_numerals(radix, length, (k as u64) << 8 | length as u64);
            let value = num_radix_reference(radix, &numerals);
            assert_eq!(
                num_radix(radix, &numerals),
                value,
                "NUM k {k} length {length}"
            );
            assert_eq!(
                str_radix(&value, radix, length),
                str_radix_reference(&value, radix, length),
                "STR k {k} length {length}"
            );
        }
    }
}

#[test]
fn str_radix_truncation_contract_matches_reference() {
    // Values >= radix**length silently drop their high digits in the
    // reference; the fast paths must drop exactly the same digits.
    for &radix in &REPRESENTATIVE_RADICES {
        for length in [65usize, 129, 257] {
            let base = BigUint::from(radix);
            let over = base.pow(length as u32) * BigUint::from(12_345u32) + BigUint::from(678u32);
            let far = base.pow((2 * length) as u32) - BigUint::one();
            for value in [over, far] {
                assert_eq!(
                    str_radix(&value, radix, length),
                    str_radix_reference(&value, radix, length),
                    "radix {radix} length {length}"
                );
            }
        }
    }
}