Files
tea3-py/src/tea3/cube_attack_blackbox.py
T
2026-07-09 16:25:29 +02:00

243 lines
6.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from itertools import combinations, product
from random import sample
from typing import Sequence
from tea3.tea3 import Tea3
MASK32 = 0xFFFFFFFF
@dataclass(frozen=True)
class CubeResult:
cube: tuple[int, ...]
cube_size: int
cube_sum: int
output_byte: int
output_bit: int
def get_output_bit(
frame_number: int,
key_register: Sequence[int],
output_byte: int = 0,
output_bit: int = 0,
) -> int:
"""
Evaluate TEA3 as a black box and return one output bit from the keystream.
"""
tea = Tea3(frame_number=frame_number, key_register=key_register)
ks = tea.keystream(output_byte + 1)
return (ks[output_byte] >> output_bit) & 1
def set_bits(value: int, bit_indices: Sequence[int], bits: Sequence[int]) -> int:
"""
Set selected bit positions of `value` according to `bits`.
Bit index 0 is the least significant bit.
"""
if len(bit_indices) != len(bits):
raise ValueError("bit_indices and bits must have the same length")
x = value & MASK32
for idx, bit in zip(bit_indices, bits):
if bit not in (0, 1):
raise ValueError("bits must be 0 or 1")
if bit:
x |= 1 << idx
else:
x &= ~(1 << idx)
return x & MASK32
def cube_sum_tea3(
cube_bits: Sequence[int],
base_frame_number: int,
key_register: Sequence[int],
output_byte: int = 0,
output_bit: int = 0,
) -> int:
"""
Compute the cube sum directly on TEA3 by querying the cipher on all assignments of the chosen cube bits.
The other IV/frame bits are taken from `base_frame_number`.
"""
acc = 0
for assignment in product((0, 1), repeat=len(cube_bits)):
frame_number = set_bits(base_frame_number, cube_bits, assignment)
acc ^= get_output_bit(
frame_number=frame_number,
key_register=key_register,
output_byte=output_byte,
output_bit=output_bit,
)
return acc & 1
def search_cubes_exhaustive_tea3(
public_bits: Sequence[int],
cube_size: int,
base_frame_number: int,
key_register: Sequence[int],
output_byte: int = 0,
output_bit: int = 0,
limit: int = 20,
keep_zero: bool = False,
) -> list[CubeResult]:
if cube_size < 0:
raise ValueError("cube_size must be non-negative")
if cube_size > len(public_bits):
raise ValueError("cube_size cannot exceed the number of public bits")
results: list[CubeResult] = []
for cube in combinations(public_bits, cube_size):
s = cube_sum_tea3(
cube_bits=cube,
base_frame_number=base_frame_number,
key_register=key_register,
output_byte=output_byte,
output_bit=output_bit,
)
if s == 0 and not keep_zero:
continue
results.append(
CubeResult(
cube=tuple(cube),
cube_size=cube_size,
cube_sum=s,
output_byte=output_byte,
output_bit=output_bit,
)
)
if len(results) >= limit:
break
return results
def search_cubes_random_tea3(
public_bits: Sequence[int],
cube_size: int,
base_frame_number: int,
key_register: Sequence[int],
samples: int = 1000,
output_byte: int = 0,
output_bit: int = 0,
limit: int = 20,
keep_zero: bool = False,
) -> list[CubeResult]:
if cube_size < 0:
raise ValueError("cube_size must be non-negative")
if cube_size > len(public_bits):
raise ValueError("cube_size cannot exceed the number of public bits")
results: list[CubeResult] = []
seen: set[tuple[int, ...]] = set()
idxs = list(range(len(public_bits)))
for _ in range(samples):
cube_idx = tuple(sorted(sample(idxs, cube_size)))
if cube_idx in seen:
continue
seen.add(cube_idx)
cube = tuple(public_bits[i] for i in cube_idx)
s = cube_sum_tea3(
cube_bits=cube,
base_frame_number=base_frame_number,
key_register=key_register,
output_byte=output_byte,
output_bit=output_bit,
)
if s == 0 and not keep_zero:
continue
results.append(
CubeResult(
cube=cube,
cube_size=cube_size,
cube_sum=s,
output_byte=output_byte,
output_bit=output_bit,
)
)
if len(results) >= limit:
break
return results
def run_cube_attack_offline_tea3(
key_register: Sequence[int],
base_frame_number: int,
public_bits: Sequence[int] = tuple(range(32)),
cube_size: int = 4,
mode: str = "random",
samples: int = 2000,
limit: int = 20,
output_byte: int = 0,
output_bit: int = 0,
keep_zero: bool = False,
) -> list[CubeResult]:
"""
Blackbox offline cube search against TEA3.
This version does not use symbolic polynomials. It evaluates the cipher on all cube assignments and returns cubes whose cube sum is nonzero by default.
Note:
This is an empirical offline phase. It can identify candidate cubes, but it does not compute the exact superpoly degree.
"""
print("=" * 50)
print("TEA3 black-box cube search")
print(f"Base frame number: 0x{base_frame_number:08x}")
print(f"Public bits: {len(public_bits)}")
print(f"Cube size: {cube_size}")
print(f"Mode: {mode}")
print(f"Output byte/bit: {output_byte}/{output_bit}")
print("=" * 50)
if mode == "exhaustive":
results = search_cubes_exhaustive_tea3(
public_bits=public_bits,
cube_size=cube_size,
base_frame_number=base_frame_number,
key_register=key_register,
output_byte=output_byte,
output_bit=output_bit,
limit=limit,
keep_zero=keep_zero,
)
elif mode == "random":
results = search_cubes_random_tea3(
public_bits=public_bits,
cube_size=cube_size,
base_frame_number=base_frame_number,
key_register=key_register,
samples=samples,
output_byte=output_byte,
output_bit=output_bit,
limit=limit,
keep_zero=keep_zero,
)
else:
raise ValueError("mode must be 'random' or 'exhaustive'")
if not results:
print("No candidate cubes found.")
return []
print(f"Found {len(results)} candidate cube(s):")
for i, res in enumerate(results, 1):
cube_str = " ".join(f"b{b}" for b in res.cube)
print("-" * 50)
print(f"[{i}] cube = {cube_str}")
print(f" cube_sum = {res.cube_sum}")
return results