Merge branch 'dev'
merge blackbox cube attack
This commit is contained in:
@@ -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
@@ -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 0–31, with 0 = least significant bit.")
|
||||
|
||||
base_frame_number = prompt_int("Base frame number (0–4294967295): ", 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 (0–63): ", 0, 63)
|
||||
output_bit = prompt_int("Output bit within that byte (0–7): ", 0, 7)
|
||||
|
||||
cube_size = prompt_int("Cube size (1–32): ", 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? (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)
|
||||
|
||||
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...")
|
||||
|
||||
@@ -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
|
||||
@@ -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 0–31.")
|
||||
idx = int(name)
|
||||
if idx < 0 or idx > 31:
|
||||
raise ValueError(f"Bit index out of range: {idx}. Expected 0–31.")
|
||||
if bit_value == 0:
|
||||
x &= ~(1 << idx)
|
||||
else:
|
||||
x |= 1 << idx
|
||||
return x & 0xFFFFFFFF
|
||||
|
||||
Reference in New Issue
Block a user