cube attack offline phase

This commit is contained in:
2026-07-06 16:54:19 +02:00
parent bfe0f6c356
commit b91f01c7e3
3 changed files with 312 additions and 4 deletions
+55 -2
View File
@@ -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? (1100): ", 1, 100)
target_reg = prompt_int("Target R register (07): ", 0, 7)
target_bit = prompt_int("Target bit of that register (07): ", 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 (164): ", 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? (1100): ", 1, 100)
else:
mode = "random"
samples = prompt_int("How many random cubes to test? (1200000): ", 1, 200000)
limit = prompt_int("How many results to keep? (1100): ", 1, 100)
max_degree = prompt_int("Maximum accepted degree (05): ", 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()
+253
View File
@@ -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()
+4 -2
View File
@@ -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