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.sbox import run_sbox
from tea3.variable_xor import run_variable_xor, run_exhaustive_xor from tea3.variable_xor import run_variable_xor, run_exhaustive_xor
from tea3.f31f32 import run_f31f32 from tea3.f31f32 import run_f31f32
from tea3.cube_attack import run_cube_attack
def run_classic_cli(): def run_classic_cli():
@@ -188,6 +189,55 @@ def run_exhaustive_xor_cli():
print("Done.") 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(): def main():
while True: while True:
@@ -203,11 +253,12 @@ def main():
print(" 5) Variable XOR") print(" 5) Variable XOR")
print(" 6) F31, F32 analysis") print(" 6) F31, F32 analysis")
print(" 7) Exhaustive XOR search") print(" 7) Exhaustive XOR search")
print(" 8) Cube attack search")
print(" 0) Exit") print(" 0) Exit")
mode = prompt_choice( mode = prompt_choice(
"Your choice (0-7): ", "Your choice (0-8): ",
{0, 1, 2, 3, 4, 5, 6, 7} {0, 1, 2, 3, 4, 5, 6, 7, 8}
) )
if mode == 0: if mode == 0:
@@ -227,6 +278,8 @@ def main():
run_f31f32() run_f31f32()
elif mode == 7: elif mode == 7:
run_exhaustive_xor_cli() run_exhaustive_xor_cli()
elif mode == 8:
run_cube_attack_cli()
input("\nPress Enter to return to the main menu...") input("\nPress Enter to return to the main menu...")
print() 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 self.R_bits[i][j] = result
def step(self): def step(self, skip_abstract: bool = False):
R = self.R_bits.copy() R = self.R_bits.copy()
x = self.x_bits.copy() x = self.x_bits.copy()
r = self.r_bits.copy() r = self.r_bits.copy()
@@ -102,7 +102,9 @@ class Tea3Model:
self.R_bits[1] = R0 self.R_bits[1] = R0
self.R_bits[0] = xor_vec(x0, xor_vec(R7, xor_vec(BP(R4), F32(R2, R1)))) 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 self.step_count += 1
return R7 return R7