243 lines
6.7 KiB
Python
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
|