cube attack offline phase
This commit is contained in:
+55
-2
@@ -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? (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():
|
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()
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user