Merge branch 'dev'

merge blackbox cube attack
This commit is contained in:
2026-07-13 15:30:54 +02:00
4 changed files with 363 additions and 11 deletions
+5 -4
View File
@@ -48,10 +48,11 @@ Modes are:
| 5. Variable XOR | XOR chosen bits inside a register and check the evolution during n steps |
| 6. F31, F32 analysis | - |
| 7. Exhaustive XOR search | XOR bits inside a register, returns the best combination (least number of monomials)|
| 8. Cube attack search | offline part of the cube attack (a precomputed model can be used) |
| 9. Precompute and save model | precompute n steps and save the model |
| 10. Load model | load a saved model |
| 8. Cube attack search (symbolic) | offline part of the cube attack (a precomputed model can be used) |
| 9. Cube attack search (oracle) | offline part of the cube attack (uses TEA-3 cipher as an oracle) |
| 10. Precompute and save model | precompute n steps and save the model |
| 11. Load model | load a saved model |
n steps can be precomputed and then loaded for the offline part of the cube attack with the menu entries 9 and 10.
n steps can be precomputed and then loaded for the offline part of the cube attack with the menu entries 10 and 11.
+99 -7
View File
@@ -1,12 +1,13 @@
from tea3.pretty_print import pretty_print
from tea3.cliutils import prompt_int, prompt_choice, prompt_list
from tea3.tea3model import Tea3Model
from tea3.utils import set_bits
from tea3.utils import set_bits, set_int_bits
from tea3.variable_search import run_exhaustive, run_exhaustive_staircase, run_exhaustive_staircase2, run_exhaustive_bp
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
from tea3.cube_attack_blackbox import run_cube_attack_offline_tea3
from tea3.precompute import run_precompute_cli, run_load_cli
@@ -200,6 +201,94 @@ def run_cube_attack_cli(model = None):
print("Done.")
def run_cube_attack_oracle_cli():
print("\nBlack-box cube attack search on TEA3.")
print("This mode varies frame-number bits and queries the cipher directly.")
print("Frame-number bits are indexed 031, with 0 = least significant bit.")
base_frame_number = prompt_int("Base frame number (04294967295): ", 0, 0xFFFFFFFF)
raw = input("Frame bits to set to 0 (space-separated): ").strip()
fixed_zero_bits = raw.split() if raw else []
raw = input("Frame bits to set to 1 (space-separated): ").strip()
fixed_one_bits = raw.split() if raw else []
output_byte = prompt_int("Output keystream byte (063): ", 0, 63)
output_bit = prompt_int("Output bit within that byte (07): ", 0, 7)
cube_size = prompt_int("Cube size (132): ", 1, 32)
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)
raw = input(
"Enter 10 key bytes in hex or decimal, separated by spaces "
"(leave empty for all-zero key): "
).strip()
if raw:
try:
key_register = [int(x, 0) & 0xFF for x in raw.split()]
except ValueError as e:
print(f"Error: invalid key byte: {e}")
return
if len(key_register) != 10:
print("Error: TEA3 key register must contain exactly 10 bytes.")
return
else:
key_register = [0] * 10
try:
fixed_bits = {}
for b in fixed_zero_bits:
fixed_bits[int(b)] = 0
for b in fixed_one_bits:
fixed_bits[int(b)] = 1
except ValueError:
print("Error: frame bit indices must be integers.")
return
public_bits = [i for i in range(32) if i not in fixed_bits]
if not public_bits:
print("Error: no unfixed frame bits left to use as public variables.")
return
base_frame_number = set_int_bits(base_frame_number, fixed_zero_bits, 0)
base_frame_number = set_int_bits(base_frame_number, fixed_one_bits, 1)
print("-" * 50)
try:
run_cube_attack_offline_tea3(
key_register=key_register,
base_frame_number=base_frame_number,
public_bits=public_bits,
cube_size=cube_size,
mode=mode,
samples=samples,
limit=limit,
output_byte=output_byte,
output_bit=output_bit,
)
except ValueError as e:
print(f"Error: {e}")
print("\n" + "=" * 50)
print("Done.")
def main():
model = None
@@ -216,14 +305,15 @@ def main():
print(" 5) Variable XOR")
print(" 6) F31, F32 analysis")
print(" 7) Exhaustive XOR search")
print(" 8) Cube attack search")
print(" 9) Precompute and save model")
print(" 10) Load model")
print(" 8) Cube attack search (symbolic)")
print(" 9) Cube attack search (oracle)")
print(" 10) Precompute and save model")
print(" 11) Load model")
print(" 0) Exit")
mode = prompt_choice(
"Your choice (0-10): ",
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
"Your choice (0-11): ",
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
)
if mode == 0:
@@ -246,8 +336,10 @@ def main():
elif mode == 8:
run_cube_attack_cli(model)
elif mode == 9:
run_precompute_cli()
run_cube_attack_oracle_cli()
elif mode == 10:
run_precompute_cli()
elif mode == 11:
model = run_load_cli()
input("\nPress Enter to return to the main menu...")
+242
View File
@@ -0,0 +1,242 @@
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
+17
View File
@@ -46,3 +46,20 @@ def set_bits(model, bit_names, value):
raise ValueError(
f"Invalid bit name '{name}'. Expected R, x, or r."
)
def set_int_bits(value: int, bit_names, bit_value: int) -> int:
x = value & 0xFFFFFFFF
for raw in bit_names:
name = raw.strip()
if not name:
continue
if not name.isdigit():
raise ValueError(f"Invalid bit index '{name}'. Expected integers 031.")
idx = int(name)
if idx < 0 or idx > 31:
raise ValueError(f"Bit index out of range: {idx}. Expected 031.")
if bit_value == 0:
x &= ~(1 << idx)
else:
x |= 1 << idx
return x & 0xFFFFFFFF