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 |
|
| 5. Variable XOR | XOR chosen bits inside a register and check the evolution during n steps |
|
||||||
| 6. F31, F32 analysis | - |
|
| 6. F31, F32 analysis | - |
|
||||||
| 7. Exhaustive XOR search | XOR bits inside a register, returns the best combination (least number of monomials)|
|
| 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) |
|
| 8. Cube attack search (symbolic) | offline part of the cube attack (a precomputed model can be used) |
|
||||||
| 9. Precompute and save model | precompute n steps and save the model |
|
| 9. Cube attack search (oracle) | offline part of the cube attack (uses TEA-3 cipher as an oracle) |
|
||||||
| 10. Load model | load a saved model |
|
| 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.pretty_print import pretty_print
|
||||||
from tea3.cliutils import prompt_int, prompt_choice, prompt_list
|
from tea3.cliutils import prompt_int, prompt_choice, prompt_list
|
||||||
from tea3.tea3model import Tea3Model
|
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.variable_search import run_exhaustive, run_exhaustive_staircase, run_exhaustive_staircase2, run_exhaustive_bp
|
||||||
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
|
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
|
from tea3.precompute import run_precompute_cli, run_load_cli
|
||||||
|
|
||||||
|
|
||||||
@@ -200,6 +201,94 @@ def run_cube_attack_cli(model = None):
|
|||||||
print("Done.")
|
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():
|
def main():
|
||||||
model = None
|
model = None
|
||||||
@@ -216,14 +305,15 @@ 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(" 8) Cube attack search (symbolic)")
|
||||||
print(" 9) Precompute and save model")
|
print(" 9) Cube attack search (oracle)")
|
||||||
print(" 10) Load model")
|
print(" 10) Precompute and save model")
|
||||||
|
print(" 11) Load model")
|
||||||
print(" 0) Exit")
|
print(" 0) Exit")
|
||||||
|
|
||||||
mode = prompt_choice(
|
mode = prompt_choice(
|
||||||
"Your choice (0-10): ",
|
"Your choice (0-11): ",
|
||||||
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||||
)
|
)
|
||||||
|
|
||||||
if mode == 0:
|
if mode == 0:
|
||||||
@@ -246,8 +336,10 @@ def main():
|
|||||||
elif mode == 8:
|
elif mode == 8:
|
||||||
run_cube_attack_cli(model)
|
run_cube_attack_cli(model)
|
||||||
elif mode == 9:
|
elif mode == 9:
|
||||||
run_precompute_cli()
|
run_cube_attack_oracle_cli()
|
||||||
elif mode == 10:
|
elif mode == 10:
|
||||||
|
run_precompute_cli()
|
||||||
|
elif mode == 11:
|
||||||
model = run_load_cli()
|
model = run_load_cli()
|
||||||
|
|
||||||
input("\nPress Enter to return to the main menu...")
|
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(
|
raise ValueError(
|
||||||
f"Invalid bit name '{name}'. Expected R, x, or r."
|
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