519 lines
23 KiB
Rust
519 lines
23 KiB
Rust
//! attack-f2: the mixer's round margin (attack pass row F2, docs/plans/cryptanalysis.md 4.2).
|
|
//!
|
|
//! The SAT models live beside this crate in Python (`model.py`); this binary is the ground truth they are
|
|
//! checked against. Every value here comes from `igneum_pow::memhard::mixer`, the bit-level definition that
|
|
//! ships, never from a copy of it.
|
|
//!
|
|
//! attack-f2 params --day D [--variant V] the drawn ROT, MUL, RC of the day (and the variant's)
|
|
//! attack-f2 vectors --day D [--variant V] [--n N] [--seed S] N random states and their images after 1..4
|
|
//! applications, for the Python model's value check
|
|
//! attack-f2 verify-diff --day D [--variant V] --trail FILE [--log2 L] [--rk-base R]
|
|
//! FILE: k+1 lines of 16 hex words, the XOR difference entering application 1 and the
|
|
//! difference leaving each application; per application the measured probability over
|
|
//! 2^L random states, and the whole chain's
|
|
//! attack-f2 verify-lin --day D [--variant V] --trail FILE [--log2 L] [--rk-base R]
|
|
//! FILE: k+1 lines of 16 hex masks; per application the measured correlation of
|
|
//! mask_in . x xor mask_out . y, and the whole chain's
|
|
//! attack-f2 rx --day D [--variant V] [--apps K] [--log2 L] [--rk-base R]
|
|
//! rotational-XOR: for every rotation r in 1..31 the per-bit bias of
|
|
//! rot_r(M^k(x)) xor M^k(rot_r(x)) over 2^L states, the largest |z| per k and r
|
|
//! attack-f2 rx-word --day D [--variant V] [--log2 L]
|
|
//! the single-word prologue g(x) = (x ^ C) * MUL: for every word and r the most frequent
|
|
//! value of rot_r(g(x)) xor g(rot_r(x)) and its count (the word-level RX probability)
|
|
//! attack-f2 fold --day D [--log2 L]
|
|
//! the multiply layer against the add layer: the identities a chip would need, tested
|
|
//! attack-f2 verify-mults --kind diff|lin --day D [--variant V] --file FILE [--rk-base R]
|
|
//! FILE (written by model.py beside a trail): lines `app j word i din dout` (diff) or
|
|
//! `app j word i min mout` (lin); every word transition of the multiply layer counted
|
|
//! EXACTLY over all 2^32 inputs of g(x) = (x ^ (RC + rk)) * MUL (12 threads)
|
|
//! attack-f2 word-top --day D [--variant V] --word I --din X [--log2 L]
|
|
//! the sampled top output differences of one word's prologue for input difference X
|
|
//!
|
|
//! Variants: `real` (the day's draw), `rot0` (every rotation 0: the known-fail case for the differential and
|
|
//! linear models), `nomul` (MUL 1, RC 0, rk 0: the bare double round, the known-fail case for rotational-XOR).
|
|
//! Application j of a chain uses the round key `round_key_mult(rk_base, j, 8)`: round 0's eight keys by default.
|
|
|
|
use igneum_pow::memhard::{mixer, round_key_mult, MixParams, Shape};
|
|
use igneum_pow::seed::{day_key, SplitMix64};
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::fs;
|
|
|
|
const M: usize = 8;
|
|
|
|
fn arg(args: &[String], name: &str) -> Option<String> {
|
|
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
|
|
}
|
|
|
|
fn params_for(day: &str, variant: &str) -> MixParams {
|
|
let shape = Shape { mixer_mult: M as u32, cache_log2_words: 26, derive_len: 0 };
|
|
let mut mp = MixParams::with_shape(day_key(day), shape);
|
|
match variant {
|
|
"real" => {}
|
|
"rot0" => mp.rot = [0; 8],
|
|
"nomul" => {
|
|
mp.mul = [1; 16];
|
|
mp.rc = [0; 16];
|
|
}
|
|
other => panic!("unknown variant {other}"),
|
|
}
|
|
mp
|
|
}
|
|
|
|
/// The round key of application `j` of round `rk_base` (0 under `nomul`, which drops the keys too).
|
|
fn rk_of(variant: &str, rk_base: usize, j: usize) -> u32 {
|
|
if variant == "nomul" {
|
|
0
|
|
} else {
|
|
round_key_mult(rk_base, j, M)
|
|
}
|
|
}
|
|
|
|
fn apply(s: &mut [u32; 16], mp: &MixParams, variant: &str, rk_base: usize, apps: usize) {
|
|
for j in 0..apps {
|
|
mixer(s, rk_of(variant, rk_base, j), mp);
|
|
}
|
|
}
|
|
|
|
fn rand_state(rng: &mut SplitMix64) -> [u32; 16] {
|
|
let mut s = [0u32; 16];
|
|
for w in s.iter_mut() {
|
|
*w = rng.next() as u32;
|
|
}
|
|
s
|
|
}
|
|
|
|
fn hex16(s: &[u32; 16]) -> String {
|
|
s.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" ")
|
|
}
|
|
|
|
fn parse_trail(path: &str) -> Vec<[u32; 16]> {
|
|
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
|
|
let mut out = Vec::new();
|
|
for line in text.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let words: Vec<u32> = line.split_whitespace().map(|h| u32::from_str_radix(h, 16).expect("hex word")).collect();
|
|
assert_eq!(words.len(), 16, "a trail line has 16 hex words");
|
|
let mut s = [0u32; 16];
|
|
s.copy_from_slice(&words);
|
|
out.push(s);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn rotl_state(s: &[u32; 16], r: u32) -> [u32; 16] {
|
|
let mut o = *s;
|
|
for w in o.iter_mut() {
|
|
*w = w.rotate_left(r);
|
|
}
|
|
o
|
|
}
|
|
|
|
fn parity(mask: &[u32; 16], s: &[u32; 16]) -> u32 {
|
|
let mut p = 0u32;
|
|
for i in 0..16 {
|
|
p ^= (mask[i] & s[i]).count_ones() & 1;
|
|
}
|
|
p
|
|
}
|
|
|
|
fn cmd_params(day: &str, variant: &str) {
|
|
let mp = params_for(day, variant);
|
|
println!("day {day}");
|
|
println!("variant {variant}");
|
|
println!("key {}", mp.key.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
|
|
println!("rot {}", mp.rot.iter().map(|r| r.to_string()).collect::<Vec<_>>().join(" "));
|
|
println!("mul {}", mp.mul.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
|
|
println!("rc {}", mp.rc.iter().map(|w| format!("{w:08x}")).collect::<Vec<_>>().join(" "));
|
|
let rks: Vec<String> = (0..M).map(|j| format!("{:08x}", rk_of(variant, 0, j))).collect();
|
|
println!("rk_round0 {}", rks.join(" "));
|
|
let w: Vec<String> = mp.mul.iter().map(|m| m.count_ones().to_string()).collect();
|
|
println!("mul_weight {}", w.join(" "));
|
|
}
|
|
|
|
fn cmd_vectors(day: &str, variant: &str, n: usize, seed: u64) {
|
|
let mp = params_for(day, variant);
|
|
let mut rng = SplitMix64::new(seed);
|
|
for _ in 0..n {
|
|
let x = rand_state(&mut rng);
|
|
print!("{}", hex16(&x));
|
|
let mut s = x;
|
|
for j in 0..4 {
|
|
mixer(&mut s, rk_of(variant, 0, j), &mp);
|
|
print!(" | {}", hex16(&s));
|
|
}
|
|
println!();
|
|
}
|
|
}
|
|
|
|
fn cmd_verify_diff(day: &str, variant: &str, path: &str, log2: u32, rk_base: usize) {
|
|
let mp = params_for(day, variant);
|
|
let trail = parse_trail(path);
|
|
let k = trail.len() - 1;
|
|
assert!(k >= 1, "a trail needs at least two lines");
|
|
let n = 1u64 << log2;
|
|
let mut rng = SplitMix64::new(0xf2_d1ff);
|
|
let mut per_app = vec![0u64; k];
|
|
let mut chain = 0u64;
|
|
for _ in 0..n {
|
|
// per application j: a fresh random state, the pair (x, x ^ d[j]) through application j alone
|
|
for j in 0..k {
|
|
let x = rand_state(&mut rng);
|
|
let mut a = x;
|
|
let mut b = [0u32; 16];
|
|
for i in 0..16 {
|
|
b[i] = x[i] ^ trail[j][i];
|
|
}
|
|
let rk = rk_of(variant, rk_base, j);
|
|
mixer(&mut a, rk, &mp);
|
|
mixer(&mut b, rk, &mp);
|
|
let mut ok = true;
|
|
for i in 0..16 {
|
|
ok &= (a[i] ^ b[i]) == trail[j + 1][i];
|
|
}
|
|
per_app[j] += ok as u64;
|
|
}
|
|
// the chain: one pair through all k applications, the final difference only
|
|
let x = rand_state(&mut rng);
|
|
let mut a = x;
|
|
let mut b = [0u32; 16];
|
|
for i in 0..16 {
|
|
b[i] = x[i] ^ trail[0][i];
|
|
}
|
|
apply(&mut a, &mp, variant, rk_base, k);
|
|
apply(&mut b, &mp, variant, rk_base, k);
|
|
let mut ok = true;
|
|
for i in 0..16 {
|
|
ok &= (a[i] ^ b[i]) == trail[k][i];
|
|
}
|
|
chain += ok as u64;
|
|
}
|
|
println!("day {day} variant {variant} apps {k} samples 2^{log2} rk_base {rk_base}");
|
|
for j in 0..k {
|
|
let p = per_app[j] as f64 / n as f64;
|
|
let w = if per_app[j] == 0 { f64::INFINITY } else { -p.log2() };
|
|
println!("app {} count {} prob {:.6e} weight {:.3}", j + 1, per_app[j], p, w);
|
|
}
|
|
let p = chain as f64 / n as f64;
|
|
let w = if chain == 0 { f64::INFINITY } else { -p.log2() };
|
|
println!("chain count {chain} prob {p:.6e} weight {w:.3}");
|
|
}
|
|
|
|
fn cmd_verify_lin(day: &str, variant: &str, path: &str, log2: u32, rk_base: usize) {
|
|
let mp = params_for(day, variant);
|
|
let trail = parse_trail(path);
|
|
let k = trail.len() - 1;
|
|
assert!(k >= 1, "a trail needs at least two lines");
|
|
let n = 1u64 << log2;
|
|
let mut rng = SplitMix64::new(0xf2_11ea);
|
|
let mut per_app = vec![0i64; k];
|
|
let mut chain = 0i64;
|
|
for _ in 0..n {
|
|
for j in 0..k {
|
|
let x = rand_state(&mut rng);
|
|
let mut y = x;
|
|
mixer(&mut y, rk_of(variant, rk_base, j), &mp);
|
|
let p = parity(&trail[j], &x) ^ parity(&trail[j + 1], &y);
|
|
per_app[j] += 1 - 2 * p as i64;
|
|
}
|
|
let x = rand_state(&mut rng);
|
|
let mut y = x;
|
|
apply(&mut y, &mp, variant, rk_base, k);
|
|
let p = parity(&trail[0], &x) ^ parity(&trail[k], &y);
|
|
chain += 1 - 2 * p as i64;
|
|
}
|
|
let sigma = (n as f64).sqrt();
|
|
println!("day {day} variant {variant} apps {k} samples 2^{log2} rk_base {rk_base}");
|
|
for j in 0..k {
|
|
let c = per_app[j] as f64 / n as f64;
|
|
let z = per_app[j] as f64 / sigma;
|
|
let w = if per_app[j] == 0 { f64::INFINITY } else { -c.abs().log2() };
|
|
println!("app {} sum {} corr {:+.6e} abs_log2 {:.3} z {:+.2}", j + 1, per_app[j], c, w, z);
|
|
}
|
|
let c = chain as f64 / n as f64;
|
|
let z = chain as f64 / sigma;
|
|
let w = if chain == 0 { f64::INFINITY } else { -c.abs().log2() };
|
|
println!("chain sum {chain} corr {c:+.6e} abs_log2 {w:.3} z {z:+.2}");
|
|
}
|
|
|
|
fn cmd_rx(day: &str, variant: &str, apps: usize, log2: u32, rk_base: usize) {
|
|
let mp = params_for(day, variant);
|
|
let n = 1u64 << log2;
|
|
let sigma = (n as f64 / 4.0).sqrt();
|
|
println!("day {day} variant {variant} samples 2^{log2} rk_base {rk_base}");
|
|
println!("# columns: apps r max_abs_z bit ones exact_rx_count (D == 0)");
|
|
for k in 1..=apps {
|
|
for r in 1..32u32 {
|
|
let mut rng = SplitMix64::new(0xf2_0000 + r as u64 + 100 * k as u64);
|
|
let mut ones = [0u64; 512];
|
|
let mut exact = 0u64;
|
|
for _ in 0..n {
|
|
let x = rand_state(&mut rng);
|
|
let mut a = x;
|
|
apply(&mut a, &mp, variant, rk_base, k);
|
|
let a = rotl_state(&a, r);
|
|
let mut b = rotl_state(&x, r);
|
|
apply(&mut b, &mp, variant, rk_base, k);
|
|
let mut zero = true;
|
|
for i in 0..16 {
|
|
let d = a[i] ^ b[i];
|
|
zero &= d == 0;
|
|
let mut dd = d;
|
|
while dd != 0 {
|
|
let t = dd.trailing_zeros();
|
|
ones[i * 32 + t as usize] += 1;
|
|
dd &= dd - 1;
|
|
}
|
|
}
|
|
exact += zero as u64;
|
|
}
|
|
let mut best = (0.0f64, 0usize);
|
|
for (b, &c) in ones.iter().enumerate() {
|
|
let z = (c as f64 - n as f64 / 2.0).abs() / sigma;
|
|
if z > best.0 {
|
|
best = (z, b);
|
|
}
|
|
}
|
|
println!("rx apps {} r {} max_abs_z {:.2} bit {} ones {} exact {}", k, r, best.0, best.1, ones[best.1], exact);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cmd_rx_word(day: &str, variant: &str, log2: u32) {
|
|
let mp = params_for(day, variant);
|
|
let n = 1u64 << log2;
|
|
println!("day {day} variant {variant} samples 2^{log2} (prologue word map g(x) = (x ^ (RC + rk0)) * MUL)");
|
|
println!("# columns: word r top_delta top_count top_log2prob");
|
|
let rk = rk_of(variant, 0, 0);
|
|
let mut worst_per_r = vec![0u64; 32];
|
|
for i in 0..16 {
|
|
let c = mp.rc[i].wrapping_add(rk);
|
|
let m = mp.mul[i];
|
|
let g = |x: u32| (x ^ c).wrapping_mul(m);
|
|
for r in 1..32u32 {
|
|
let mut rng = SplitMix64::new(0xf2_0f00 + i as u64 * 64 + r as u64);
|
|
let mut counts: HashMap<u32, u32> = HashMap::new();
|
|
for _ in 0..n {
|
|
let x = rng.next() as u32;
|
|
let d = g(x).rotate_left(r) ^ g(x.rotate_left(r));
|
|
*counts.entry(d).or_insert(0) += 1;
|
|
}
|
|
let (delta, cnt) = counts.iter().max_by_key(|(_, &c)| c).map(|(&d, &c)| (d, c)).unwrap();
|
|
worst_per_r[r as usize] = worst_per_r[r as usize].max(cnt as u64);
|
|
println!("rxw word {} r {} top_delta {:08x} top_count {} top_log2prob {:.2}", i, r, delta, cnt, -((cnt as f64) / (n as f64)).log2());
|
|
}
|
|
}
|
|
for r in 1..32 {
|
|
println!("rxw_worst r {} top_count {} top_log2prob {:.2}", r, worst_per_r[r], -((worst_per_r[r] as f64) / (n as f64)).log2());
|
|
}
|
|
}
|
|
|
|
fn cmd_fold(day: &str, log2: u32) {
|
|
let mp = params_for(day, "real");
|
|
let n = 1u64 << log2;
|
|
let mut rng = SplitMix64::new(0xf2_f01d);
|
|
println!("day {day} samples 2^{log2}");
|
|
// (a) the first add of each column quarter round: (xa ^ Ca) * ma + (xb ^ Cb) * mb against ((xa ^ Ca) + (xb ^ Cb)) * ma:
|
|
// the multiply folds into the add only when ma == mb (a chip could then do one multiply for two words)
|
|
let rk = rk_of("real", 0, 0);
|
|
for (a, b) in [(0usize, 4usize), (1, 5), (2, 6), (3, 7)] {
|
|
let (ca, cb) = (mp.rc[a].wrapping_add(rk), mp.rc[b].wrapping_add(rk));
|
|
let (ma, mb) = (mp.mul[a], mp.mul[b]);
|
|
let mut eq = 0u64;
|
|
for _ in 0..n {
|
|
let (xa, xb) = (rng.next() as u32, rng.next() as u32);
|
|
let lhs = (xa ^ ca).wrapping_mul(ma).wrapping_add((xb ^ cb).wrapping_mul(mb));
|
|
let rhs = ((xa ^ ca).wrapping_add(xb ^ cb)).wrapping_mul(ma);
|
|
eq += (lhs == rhs) as u64;
|
|
}
|
|
println!("fold_add words {a},{b} mul_equal {} identity_holds {eq} of {n}", ma == mb);
|
|
}
|
|
// (b) the XOR constant against the multiply: (x ^ C) * m against (x * m) ^ (C * m) and against (x * m) ^ C'
|
|
// for the best single C' (counted on word 0): the constant does not pass through the multiply
|
|
{
|
|
let (c, m) = (mp.rc[0].wrapping_add(rk), mp.mul[0]);
|
|
let mut eq = 0u64;
|
|
let mut counts: HashMap<u32, u32> = HashMap::new();
|
|
for _ in 0..n {
|
|
let x = rng.next() as u32;
|
|
let lhs = (x ^ c).wrapping_mul(m);
|
|
eq += (lhs == x.wrapping_mul(m) ^ c.wrapping_mul(m)) as u64;
|
|
*counts.entry(lhs ^ x.wrapping_mul(m)).or_insert(0) += 1;
|
|
}
|
|
let best = counts.values().max().copied().unwrap_or(0);
|
|
println!("fold_xor word 0 (x^C)*m == (x*m)^(C*m): {eq} of {n}; best single C' matches {best} of {n}");
|
|
}
|
|
// (c) the MSB passes the prologue and every add for free: (x ^ 2^31) through g and through x + y
|
|
{
|
|
let mut eq_g = 0u64;
|
|
let mut eq_add = 0u64;
|
|
for i in 0..16 {
|
|
let (c, m) = (mp.rc[i].wrapping_add(rk), mp.mul[i]);
|
|
for _ in 0..(n >> 4) {
|
|
let x = rng.next() as u32;
|
|
let y = rng.next() as u32;
|
|
eq_g += (((x ^ 0x8000_0000) ^ c).wrapping_mul(m) == (x ^ c).wrapping_mul(m) ^ 0x8000_0000) as u64;
|
|
eq_add += ((x ^ 0x8000_0000).wrapping_add(y) == x.wrapping_add(y) ^ 0x8000_0000) as u64;
|
|
}
|
|
}
|
|
println!("msb_free prologue {eq_g} of {} ; add {eq_add} of {}", (n >> 4) * 16, (n >> 4) * 16);
|
|
}
|
|
// (d) the LSB of a product is the LSB of the input (odd multiplier), and of a sum the XOR of the inputs' LSBs
|
|
{
|
|
let mut eq = 0u64;
|
|
for i in 0..16 {
|
|
let (c, m) = (mp.rc[i].wrapping_add(rk), mp.mul[i]);
|
|
for _ in 0..(n >> 4) {
|
|
let x = rng.next() as u32;
|
|
eq += (((x ^ c).wrapping_mul(m)) & 1 == (x ^ c) & 1) as u64;
|
|
}
|
|
}
|
|
println!("lsb_free prologue {eq} of {}", (n >> 4) * 16);
|
|
}
|
|
// (e) does an XOR constant on any single word commute with the bare double round (so that the next
|
|
// application's RC + rk could be folded back into the previous one)? Tested per word against the constant
|
|
// coming out on the same word under any rotation. Expected 0 everywhere: every word's value feeds an add.
|
|
{
|
|
let mut eqs = [0u64; 16];
|
|
let mut bare = mp.clone();
|
|
bare.mul = [1; 16];
|
|
bare.rc = [0; 16];
|
|
for _ in 0..(n >> 4) {
|
|
let x = rand_state(&mut rng);
|
|
let kk = rng.next() as u32;
|
|
let mut b = x;
|
|
mixer(&mut b, 0, &bare);
|
|
for w in 0..16 {
|
|
let mut a = x;
|
|
a[w] ^= kk;
|
|
mixer(&mut a, 0, &bare);
|
|
let mut any = false;
|
|
for r in 0..32u32 {
|
|
let mut c = b;
|
|
c[w] ^= kk.rotate_left(r);
|
|
any |= c == a;
|
|
}
|
|
eqs[w] += any as u64;
|
|
}
|
|
}
|
|
println!("xor_const_commutes_with_arx per word: {}", eqs.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(" "));
|
|
println!(" (of {} each; a constant that commutes would read the full count)", n >> 4);
|
|
}
|
|
}
|
|
|
|
/// Exact count over all 2^32 inputs of `g(u ^ din) ^ g(u) == dout` (diff) or the signed sum of
|
|
/// `(-1)^(min.u ^ mout.g(u))` (lin) for the word map g(u) = (u ^ k) * m, on 12 threads.
|
|
fn word_exact(k: u32, m: u32, kind: &str, a: u32, b: u32) -> i64 {
|
|
const T: u64 = 12;
|
|
let chunk = (1u64 << 32) / T;
|
|
let totals: Vec<i64> = std::thread::scope(|sc| {
|
|
let hs: Vec<_> = (0..T)
|
|
.map(|t| {
|
|
sc.spawn(move || {
|
|
let mut acc: i64 = 0;
|
|
let lo = t * chunk;
|
|
let hi = if t == T - 1 { 1u64 << 32 } else { lo + chunk };
|
|
if kind == "diff" {
|
|
for u in lo..hi {
|
|
let u = u as u32;
|
|
let y0 = (u ^ k).wrapping_mul(m);
|
|
let y1 = ((u ^ a) ^ k).wrapping_mul(m);
|
|
acc += ((y0 ^ y1) == b) as i64;
|
|
}
|
|
} else {
|
|
for u in lo..hi {
|
|
let u = u as u32;
|
|
let y = (u ^ k).wrapping_mul(m);
|
|
let par = ((a & u).count_ones() + (b & y).count_ones()) & 1;
|
|
acc += 1 - 2 * par as i64;
|
|
}
|
|
}
|
|
acc
|
|
})
|
|
})
|
|
.collect();
|
|
hs.into_iter().map(|h| h.join().unwrap()).collect()
|
|
});
|
|
totals.iter().sum()
|
|
}
|
|
|
|
fn cmd_verify_mults(day: &str, variant: &str, kind: &str, path: &str, rk_base: usize) {
|
|
let mp = params_for(day, variant);
|
|
let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("read {path}: {e}"));
|
|
println!("day {day} variant {variant} kind {kind} rk_base {rk_base} (exact over 2^32 per word)");
|
|
let mut total_weight = 0.0f64;
|
|
let mut n = 0;
|
|
for line in text.lines() {
|
|
let f: Vec<&str> = line.split_whitespace().collect();
|
|
if f.len() < 6 || f[0] != "app" {
|
|
continue;
|
|
}
|
|
let j: usize = f[1].parse().unwrap();
|
|
let i: usize = f[3].parse().unwrap();
|
|
let a = u32::from_str_radix(f[4], 16).unwrap();
|
|
let b = u32::from_str_radix(f[5], 16).unwrap();
|
|
let k = mp.rc[i].wrapping_add(rk_of(variant, rk_base, j - 1));
|
|
let cnt = word_exact(k, mp.mul[i], kind, a, b);
|
|
let w = if kind == "diff" {
|
|
if cnt == 0 { f64::INFINITY } else { -((cnt as f64) / 4294967296.0).log2() }
|
|
} else if cnt == 0 { f64::INFINITY } else { -((cnt.unsigned_abs() as f64) / 4294967296.0).log2() };
|
|
total_weight += w;
|
|
n += 1;
|
|
println!("app {j} word {i} {a:08x} -> {b:08x} count {cnt} weight {w:.3}");
|
|
}
|
|
println!("words {n} total_exact_weight {total_weight:.3}");
|
|
}
|
|
|
|
fn cmd_word_top(day: &str, variant: &str, word: usize, din: u32, log2: u32) {
|
|
let mp = params_for(day, variant);
|
|
let k = mp.rc[word].wrapping_add(rk_of(variant, 0, 0));
|
|
let m = mp.mul[word];
|
|
let n = 1u64 << log2;
|
|
let mut rng = SplitMix64::new(0xf2_70b);
|
|
let mut counts: HashMap<u32, u32> = HashMap::new();
|
|
for _ in 0..n {
|
|
let u = rng.next() as u32;
|
|
let d = (u ^ k).wrapping_mul(m) ^ ((u ^ din) ^ k).wrapping_mul(m);
|
|
*counts.entry(d).or_insert(0) += 1;
|
|
}
|
|
let mut v: Vec<(u32, u32)> = counts.into_iter().collect();
|
|
v.sort_by(|a, b| b.1.cmp(&a.1));
|
|
println!("day {day} variant {variant} word {word} din {din:08x} samples 2^{log2} distinct {}", v.len());
|
|
for (d, c) in v.iter().take(8) {
|
|
println!("top dout {d:08x} count {c} log2prob {:.2}", -((*c as f64) / (n as f64)).log2());
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let args: Vec<String> = env::args().collect();
|
|
let cmd = args.get(1).map(String::as_str).unwrap_or("");
|
|
let day = arg(&args, "--day").unwrap_or_else(|| "2026-10-03".to_string());
|
|
let variant = arg(&args, "--variant").unwrap_or_else(|| "real".to_string());
|
|
let log2: u32 = arg(&args, "--log2").map(|s| s.parse().unwrap()).unwrap_or(20);
|
|
let rk_base: usize = arg(&args, "--rk-base").map(|s| s.parse().unwrap()).unwrap_or(0);
|
|
match cmd {
|
|
"params" => cmd_params(&day, &variant),
|
|
"vectors" => {
|
|
let n: usize = arg(&args, "--n").map(|s| s.parse().unwrap()).unwrap_or(8);
|
|
let seed: u64 = arg(&args, "--seed").map(|s| s.parse().unwrap()).unwrap_or(1);
|
|
cmd_vectors(&day, &variant, n, seed)
|
|
}
|
|
"verify-diff" => cmd_verify_diff(&day, &variant, &arg(&args, "--trail").expect("--trail FILE"), log2, rk_base),
|
|
"verify-lin" => cmd_verify_lin(&day, &variant, &arg(&args, "--trail").expect("--trail FILE"), log2, rk_base),
|
|
"rx" => {
|
|
let apps: usize = arg(&args, "--apps").map(|s| s.parse().unwrap()).unwrap_or(4);
|
|
cmd_rx(&day, &variant, apps, log2, rk_base)
|
|
}
|
|
"rx-word" => cmd_rx_word(&day, &variant, log2),
|
|
"fold" => cmd_fold(&day, log2),
|
|
"verify-mults" => cmd_verify_mults(&day, &variant, &arg(&args, "--kind").expect("--kind"), &arg(&args, "--file").expect("--file FILE"), rk_base),
|
|
"word-top" => cmd_word_top(&day, &variant, arg(&args, "--word").map(|s| s.parse().unwrap()).unwrap_or(0), u32::from_str_radix(&arg(&args, "--din").expect("--din hex"), 16).unwrap(), log2),
|
|
_ => {
|
|
eprintln!("usage: attack-f2 (params|vectors|verify-diff|verify-lin|rx|rx-word|fold|verify-mults|word-top) --day D [--variant real|rot0|nomul] ...");
|
|
std::process::exit(2);
|
|
}
|
|
}
|
|
}
|