#!/usr/bin/env python3 """Structural analysis of the IDS-CAN command-auth challenge->response map. Black-box: decide what the captured (challenge, response) pairs can and cannot tell us about f, where response = f(challenge). Pure stdlib. Usage: ./analyze_auth.py [pairfile ...] Default: loads 2A-auth-pairs.txt + auth-pairs-multinode-2026-06-11.txt (51 pairs). Pair-file format: lines of " ", optionally prefixed with a node label (" "); '#' comments ignored. """ import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) DEFAULTS = [ os.path.join(HERE, "2A-auth-pairs.txt"), os.path.join(HERE, "auth-pairs-multinode-2026-06-11.txt"), ] def load(paths): pairs = [] for path in paths: with open(path) as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue tok = line.split() c, r = tok[-2], tok[-1] # tolerate optional node label pairs.append((int(c, 16), int(r, 16))) return pairs def reduce_vec(iv, ov, basis): for piv, bi, bo in basis: if (iv >> piv) & 1: iv ^= bi ov ^= bo return iv, ov def main(): paths = sys.argv[1:] or DEFAULTS pairs = load(paths) n = len(pairs) print(f"loaded {n} pairs from {', '.join(os.path.basename(p) for p in paths)}\n") # determinism is only testable if a challenge recurs (else the check is vacuous) chals = [c for c, _ in pairs] repeats = len(chals) - len(set(chals)) print(f"[determinism] {len(set(chals))} distinct challenges, {repeats} repeated " f"-> {'testable' if repeats else 'NOT testable (all distinct; statelessness assumed)'}") # TEST 1: GF(2)-affine. f(x)=M.x^k => f(Ci)^f(Cj)=M.(Ci^Cj) (constant cancels). # Reduce input-diffs against a growing basis; an input-diff that collapses to 0 # while its output-diff does not => NOT affine. Full rank + consistent => solved. C0, R0 = pairs[0] basis, contradiction = [], None for c, r in pairs[1:]: iv, ov = reduce_vec(c ^ C0, r ^ R0, basis) if iv == 0: if ov != 0 and contradiction is None: contradiction = ov else: basis.append((iv.bit_length() - 1, iv, ov)) rank = len(basis) print(f"\n[TEST 1: GF(2)-affine] input-difference rank = {rank}/32") if contradiction is not None: print(" -> NOT GF(2)-affine (linear fit contradicted despite full-rank data;" " obstacle is structure, not sample count)") elif rank == 32: applyM = lambda x: reduce_vec(x, 0, basis)[1] k = R0 ^ applyM(C0) bad = sum((applyM(c) ^ k) != r for c, r in pairs) print(f" -> AFFINE & SOLVED: k={k:#010x}, mismatches={bad}/{n}") else: print(f" -> consistent w/ affine but underdetermined ({rank}/32); need more pairs") # TEST 2: affine over Z/2^32. R=a*C+b mod 2^32; derive a from one odd diff, verify. MOD = 1 << 32 a = None for c, r in pairs[1:]: dc = (c - C0) % MOD if dc & 1: inv = 1 for _ in range(6): inv = (inv * (2 - dc * inv)) % MOD a = ((r - R0) * inv) % MOD break print("\n[TEST 2: affine over Z/2^32 R=a*C+b]") if a is None: print(" -> no odd input-difference; skipped") else: b = (R0 - a * C0) % MOD bad = sum((a * c + b) % MOD != r for c, r in pairs) print(f" a={a:#010x} b={b:#010x} -> {bad}/{n} miss" f" -> {'SOLVED' if bad == 0 else 'not a ring-affine map'}") # TEST 3: byte locality. Is out-byte p a pure function of a single in-byte q? byte = lambda v, i: (v >> (8 * (3 - i))) & 0xFF local = [] for p in range(4): for q in range(4): groups = {} ok = True for c, r in pairs: key = byte(c, q) if key in groups and groups[key] != byte(r, p): ok = False break groups[key] = byte(r, p) if ok and any(sum(byte(c, q) == k for c, _ in pairs) > 1 for k in groups): local.append((p, q)) print("\n[TEST 3: byte locality]", "out-byte/in-byte dependencies:" if local else "-> no out-byte is a function of any single in-byte (full diffusion)") for p, q in local: print(f" out-byte {p} may depend only on in-byte {q} (check w/ more data)") # TEST 4: per-bit balance (informative, not decisive at this sample size) ones_in = [sum((c >> b) & 1 for c, _ in pairs) for b in range(32)] ones_out = [sum((r >> b) & 1 for _, r in pairs) for b in range(32)] print(f"\n[TEST 4: per-bit balance] (ideal 0.5) " f"challenge {min(ones_in)/n:.2f}-{max(ones_in)/n:.2f}, " f"response {min(ones_out)/n:.2f}-{max(ones_out)/n:.2f}") print("\nVERDICT: keyed nonlinear cipher (TEA/XTEA-family), not recoverable from " "random known-plaintext pairs at any feasible count. SOLVED 2026-06-12 " "(REMOTE_CONTROL session key 0xB16B00B5), verified 51/51 here — see " "../ids_can_auth.py.") if __name__ == "__main__": main()