From b91f01c7e3b3172c267a921b3e6d586ed1ff716e Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Mon, 6 Jul 2026 16:54:19 +0200 Subject: [PATCH] cube attack offline phase --- src/tea3/cli.py | 57 ++++++++- src/tea3/cube_attack.py | 253 ++++++++++++++++++++++++++++++++++++++++ src/tea3/tea3model.py | 6 +- 3 files changed, 312 insertions(+), 4 deletions(-) create mode 100644 src/tea3/cube_attack.py diff --git a/src/tea3/cli.py b/src/tea3/cli.py index f81fa11..f3ff561 100644 --- a/src/tea3/cli.py +++ b/src/tea3/cli.py @@ -5,6 +5,7 @@ from tea3.variable_search import run_exhaustive, run_exhaustive_staircase, run_e from tea3.sbox import run_sbox from tea3.variable_xor import run_variable_xor, run_exhaustive_xor from tea3.f31f32 import run_f31f32 +from tea3.cube_attack import run_cube_attack def run_classic_cli(): @@ -188,6 +189,55 @@ def run_exhaustive_xor_cli(): print("Done.") +def run_cube_attack_cli(): + print("\nCube attack search on TEA3 model to search for low-degree superpolys.") + + rounds = prompt_int("How many rounds? (1–100): ", 1, 100) + target_reg = prompt_int("Target R register (0–7): ", 0, 7) + target_bit = prompt_int("Target bit of that register (0–7): ", 0, 7) + + raw = input("Bits to set to 0: ").strip() + fixed_zero_bits = raw.split() if raw else [] + + raw = input("Bits to set to 1: ").strip() + fixed_one_bits = raw.split() if raw else [] + + cube_size = prompt_int("Cube size (1–64): ", 1, 64) + + print("\nSearch strategy:") + print(" 1) Exhaustive") + print(" 2) Random sampling") + strategy = prompt_choice("Your choice (1 or 2): ", {1, 2}) + + if strategy == 1: + mode = "exhaustive" + samples = 0 + limit = prompt_int("How many results to keep? (1–100): ", 1, 100) + else: + mode = "random" + samples = prompt_int("How many random cubes to test? (1–200000): ", 1, 200000) + limit = prompt_int("How many results to keep? (1–100): ", 1, 100) + + max_degree = prompt_int("Maximum accepted degree (0–5): ", 0, 5) + + print("-" * 50) + run_cube_attack( + rounds=rounds, + target_reg=target_reg, + target_bit=target_bit, + cube_size=cube_size, + mode=mode, + samples=samples, + limit=limit, + max_degree=max_degree, + fixed_zero_bits=fixed_zero_bits, + fixed_one_bits=fixed_one_bits, + ) + + print("\n" + "=" * 50) + print("Done.") + + def main(): while True: @@ -203,11 +253,12 @@ def main(): print(" 5) Variable XOR") print(" 6) F31, F32 analysis") print(" 7) Exhaustive XOR search") + print(" 8) Cube attack search") print(" 0) Exit") mode = prompt_choice( - "Your choice (0-7): ", - {0, 1, 2, 3, 4, 5, 6, 7} + "Your choice (0-8): ", + {0, 1, 2, 3, 4, 5, 6, 7, 8} ) if mode == 0: @@ -227,6 +278,8 @@ def main(): run_f31f32() elif mode == 7: run_exhaustive_xor_cli() + elif mode == 8: + run_cube_attack_cli() input("\nPress Enter to return to the main menu...") print() diff --git a/src/tea3/cube_attack.py b/src/tea3/cube_attack.py new file mode 100644 index 0000000..02ef5c5 --- /dev/null +++ b/src/tea3/cube_attack.py @@ -0,0 +1,253 @@ +from __future__ import annotations + +from dataclasses import dataclass +from itertools import combinations +from random import sample +from typing import Sequence + +from tea3.tea3model import Tea3Model + + +@dataclass(frozen=True) +class CubeResult: + cube: tuple[str, ...] + cube_size: int + degree: int + monomials: int + superpoly: object + + +def flatten_bits(bits_2d: Sequence[Sequence[object]]) -> list[object]: + return [b for row in bits_2d for b in row] + +def specialize_model(model: Tea3Model, fixed_bits: dict[object, int]) -> Tea3Model: + """ + Return a copy of `model` with selected bits fixed to constants. + """ + for var, value in fixed_bits.items(): + model.R_bits = [[p.subs({var: value}) for p in row] for row in model.R_bits] + model.x_bits = [[p.subs({var: value}) for p in row] for row in model.x_bits] + model.y_bits = [[p.subs({var: value}) for p in row] for row in model.y_bits] + model.r_bits = [[p.subs({var: value}) for p in row] for row in model.r_bits] + return model + +def build_target_poly( + rounds: int = 8, + target_reg: int = 7, + target_bit: int = 0, + fixed_bits: dict[object, int] | None = None, +): + """ + Return one Boolean polynomial from the model after `rounds` steps. + The model is treated as an oracle. The returned polynomial is the chosen bit of the register state after `rounds` iterations. + """ + model = Tea3Model() + + if fixed_bits: + model = specialize_model(model, fixed_bits) + + for _ in range(rounds): + model.step(skip_abstract = True) + + return model.R_bits[target_reg][target_bit] + +def cube_sum_anf(poly, cube_vars: Sequence[object]): + """ + Compute the cube sum of an ANF polynomial by monomial filtering. + For a Boolean polynomial over GF(2), the cube sum over the chosen cube variables keeps exactly the monomials that contain all cube variables, then removes those cube variables from the monomial. + """ + R = poly.parent() + cube_names = {str(v) for v in cube_vars} + acc = R.zero() + + for monom in poly.monomials(): + vars_in_monom = list(monom.variables()) + names_in_monom = {str(v) for v in vars_in_monom} + + if not cube_names.issubset(names_in_monom): + continue + + term = R.one() + for v in vars_in_monom: + if str(v) not in cube_names: + term *= v + acc += term + + return acc + + +def poly_degree(poly) -> int: + try: + return int(poly.total_degree()) + except Exception: + return 0 if poly == 0 else 1 + + +def count_monomials(poly) -> int: + try: + return len(poly.monomials()) + except Exception: + return 0 + + +def pick_public_vars(model: Tea3Model, fixed_bits: dict[str, int] | None = None) -> list[object]: + public = flatten_bits(model.R_bits) + if not fixed_bits: + return public + + fixed_names = set(fixed_bits.keys()) + return [v for v in public if str(v) not in fixed_names] + + +def sanity_check(public_vars: Sequence[object], cube_size: int): + n = len(public_vars) + if cube_size > n: + raise ValueError("cube_size cannot exceed number of public variables") + + +def search_cubes_exhaustive( + poly, + public_vars: Sequence[object], + cube_size: int, + max_degree: int = 1, + limit: int = 50, +) -> list[CubeResult]: + results: list[CubeResult] = [] + sanity_check(public_vars, cube_size) + + n_zero = 0 + for cube in combinations(public_vars, cube_size): + sp = cube_sum_anf(poly, cube) + if sp == 0: + n_zero += 1 + continue + deg = poly_degree(sp) + if deg <= max_degree: + results.append( + CubeResult( + cube=tuple(str(v) for v in cube), + cube_size=cube_size, + degree=deg, + monomials=count_monomials(sp), + superpoly=sp, + ) + ) + if len(results) >= limit: + break + print(f" ({n_zero} zero superpolys skipped)") + return results + + +def search_cubes_random( + poly, + public_vars: Sequence[object], + cube_size: int, + samples: int = 1000, + max_degree: int = 1, +) -> list[CubeResult]: + results: list[CubeResult] = [] + sanity_check(public_vars, cube_size) + + n = len(public_vars) + indices = list(range(n)) + for _ in range(samples): + cube_idx = sorted(sample(indices, cube_size)) + cube = [public_vars[i] for i in cube_idx] + sp = cube_sum_anf(poly, cube) + deg = poly_degree(sp) + if sp != 0 and deg <= max_degree: + results.append( + CubeResult( + cube=tuple(str(v) for v in cube), + cube_size=cube_size, + degree=deg, + monomials=count_monomials(sp), + superpoly=sp, + ) + ) + return results + + +def pretty_cube(cube: Sequence[str]) -> str: + return " ".join(cube) + + +def run_cube_attack( + rounds: int = 8, + target_reg: int = 7, + target_bit: int = 0, + cube_size: int = 4, + mode: str = "random", + samples: int = 2000, + limit: int = 20, + max_degree: int = 1, + fixed_zero_bits: Sequence[str] | None = None, + fixed_one_bits: Sequence[str] | None = None, +): + fixed_bits: dict[str, int] = {} + + if fixed_zero_bits: + for name in fixed_zero_bits: + fixed_bits[name] = 0 + if fixed_one_bits: + for name in fixed_one_bits: + fixed_bits[name] = 1 + + model = Tea3Model() + public_vars = pick_public_vars(model, fixed_bits=fixed_bits) + poly = build_target_poly( + rounds=rounds, + target_reg=target_reg, + target_bit=target_bit, + fixed_bits=fixed_bits, + ) + + print("=" * 50) + print(f"Target: {rounds}-round output bit R{target_reg}[{target_bit}]") + print(f"Public variables: {len(public_vars)} R bits") + print(f"Cube size: {cube_size}") + print(f"Search mode: {mode}") + print(f"Max degree accepted: {max_degree}") + if fixed_bits: + print("Fixed bits:") + for name, value in sorted(fixed_bits.items()): + print(f" {name} = {value}") + print("=" * 50) + print(f"Target polynomial monomials: {count_monomials(poly)}") + print(f"Target polynomial degree: {poly_degree(poly)}") + print() + + if cube_size > len(public_vars): + print("Cube size is larger than the number of unfixed public variables.") + return + + if mode == "exhaustive": + results = search_cubes_exhaustive( + poly=poly, + public_vars=public_vars, + cube_size=cube_size, + max_degree=max_degree, + limit=limit, + ) + else: + results = search_cubes_random( + poly=poly, + public_vars=public_vars, + cube_size=cube_size, + samples=samples, + max_degree=max_degree, + )[:limit] + + if not results: + print("No low-degree cubes found.") + return + + print(f"Found {len(results)} candidate cube(s):") + for i, res in enumerate(results, 1): + print("-" * 50) + print(f"[{i}] cube = {pretty_cube(res.cube)}") + print(f" degree = {res.degree}") + print(f" monomials = {res.monomials}") + print(" superpoly =") + print(res.superpoly) + print() diff --git a/src/tea3/tea3model.py b/src/tea3/tea3model.py index 0ba31f3..10d7584 100644 --- a/src/tea3/tea3model.py +++ b/src/tea3/tea3model.py @@ -69,7 +69,7 @@ class Tea3Model: self.R_bits[i][j] = result - def step(self): + def step(self, skip_abstract: bool = False): R = self.R_bits.copy() x = self.x_bits.copy() r = self.r_bits.copy() @@ -102,7 +102,9 @@ class Tea3Model: self.R_bits[1] = R0 self.R_bits[0] = xor_vec(x0, xor_vec(R7, xor_vec(BP(R4), F32(R2, R1)))) - self._abstract_R() + if not skip_abstract: + self._abstract_R() + self.step_count += 1 return R7