feat(tools): add key 33.6 encoding analysis tool

Analyzes DH public key ↔ key 33.6 pairs from Netflix iOS appboot CBOR
captures, confirming the trivial block-XOR encoding and mapping plaintext
regions to static header, session-bound, and per-request zones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tkgstrator
2026-04-08 17:21:00 +00:00
co-authored by Claude Sonnet 4.6
parent 06fcca4373
commit b1a3a30407
+676
View File
@@ -0,0 +1,676 @@
#!/usr/bin/env python3
"""
Analyze DH public key ↔ key 33.6 pairs from Netflix iOS appboot CBOR captures.
Encoding discovery:
key 33.6 is encoded as a block-XOR stream cipher:
k6[i:i+16] = plaintext[i:i+16] XOR nonce
where nonce = key 33.9 (16 bytes), applied to each 16-byte block independently.
Decoding: plaintext[i:i+16] = k6[i:i+16] XOR nonce
Plaintext structure (352B variant):
Bytes 0- 2: d9 d9 f7 — CBOR self-describe tag (RFC 7049 tag 55799)
Byte 3: a7 — CBOR map(7), then 7 key-value pairs
Bytes 0-127: fully static header (same across all samples with the same TFIT table version)
Bytes 128-287: session-bound (15-27 distinct values across 177 captures = session identity)
Bytes 288-351: per-request unique (165 unique values for 165 requests = per-request data)
144B variant: starts with d9 d9 f7 a6 (map(6) instead of map(7)).
Session structure:
The mid section (128-287) groups samples into ~15 distinct sessions.
Each session group uses the same DH key exchange state.
The tail (288-351) is unique per request (likely a message counter or timestamp).
DH public key mapping:
The client DH pub key (128B) is NOT directly embedded in key 33.6.
The TFIT whitebox AES chain transforms the DH pub key into the session-bound
mid-section (bytes 128-287 of the plaintext). The static header (bytes 0-127)
encodes fixed CBOR map structure. The tail (bytes 288-351) carries per-request
state (message ID, counter, or timestamp).
Usage:
uv run python tools/analyze_key336_pairs.py
"""
import gzip
import json
import math
import struct
from collections import Counter, defaultdict
from pathlib import Path
import cbor2
RAWS_DIR = Path("/home/vscode/app/raws/ios/20260408/raw")
LOG_FILE = Path("/home/vscode/app/raws/appboot_kdf_fresh.log")
KEYS_FILE = Path("/home/vscode/app/raws/msl_keys.json")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def load_cbor(path: Path) -> dict | None:
"""Read a .bin file, gzip-decompress if needed, then CBOR-decode."""
raw = path.read_bytes()
if raw[:2] == b"\x1f\x8b":
try:
raw = gzip.decompress(raw)
except Exception:
return None
try:
return cbor2.loads(raw)
except Exception:
return None
def decode_inner(k33: bytes | dict) -> dict | None:
"""Decode the inner CBOR from key 33 (may be bytes or already a dict)."""
if isinstance(k33, dict):
return k33
if isinstance(k33, bytes):
try:
return cbor2.loads(k33)
except Exception:
return None
return None
def decipher_k6(k6: bytes, nonce: bytes) -> bytes:
"""
Undo the block-XOR encoding of key 33.6.
Encoding: k6[i:i+16] = plaintext[i:i+16] XOR nonce
(same 16-byte nonce applied to every 16-byte block independently)
"""
n = len(k6)
buf = bytearray(n)
for off in range(0, n, 16):
block = k6[off : off + 16]
for j, b in enumerate(block):
if off + j < n:
buf[off + j] = b ^ nonce[j % 16]
return bytes(buf)
def entropy_bits(data: bytes) -> float:
"""Shannon entropy in bits per byte."""
if not data:
return 0.0
counts = Counter(data)
n = len(data)
h = 0.0
for c in counts.values():
p = c / n
h -= p * math.log2(p)
return h
def block_entropy(data: bytes, block_size: int = 16) -> list[float]:
"""Entropy for each block_size-byte block."""
result = []
for i in range(0, len(data), block_size):
blk = data[i : i + block_size]
result.append(entropy_bits(blk))
return result
# ---------------------------------------------------------------------------
# Data collection
# ---------------------------------------------------------------------------
def collect_pairs() -> list[dict]:
"""
Collect key 33.6 + nonce pairs from all appboot request CBOR files.
Returns a list of dicts:
{fname, k6_raw, nonce, plaintext, k6_size, esn, standard_prefix}
standard_prefix: True when plaintext starts with CBOR self-describe tag d9d9f7.
"""
pairs: list[dict] = []
for path in sorted(RAWS_DIR.glob("req_*appboot*.bin")):
obj = load_cbor(path)
if obj is None:
continue
k33 = obj.get(33)
if k33 is None:
continue
inner = decode_inner(k33)
if inner is None:
continue
k6 = inner.get(6)
nonce = inner.get(9)
esn = inner.get(8)
if not isinstance(k6, bytes) or not isinstance(nonce, bytes):
continue
if len(nonce) != 16:
continue
plaintext = decipher_k6(k6, nonce)
standard_prefix = plaintext[:3] == b"\xd9\xd9\xf7"
pairs.append(
{
"fname": path.name,
"k6_raw": k6,
"nonce": nonce,
"plaintext": plaintext,
"k6_size": len(k6),
"esn": esn,
"standard_prefix": standard_prefix,
}
)
return pairs
# ---------------------------------------------------------------------------
# Analysis functions
# ---------------------------------------------------------------------------
def analyze_encoding_pattern(pairs: list[dict]) -> None:
"""Verify and explain the XOR encoding pattern."""
print("=" * 70)
print("ENCODING PATTERN ANALYSIS")
print("=" * 70)
sizes = Counter(p["k6_size"] for p in pairs)
print(f"\nkey 33.6 size distribution (REQ files):")
for sz, cnt in sorted(sizes.items()):
print(f" {sz:4d} bytes: {cnt} samples")
# Verify XOR encoding holds universally
errors = 0
for p in pairs:
k6, nonce = p["k6_raw"], p["nonce"]
# k6[0:16] XOR nonce should always equal the first block of the plaintext
computed = bytes(a ^ b for a, b in zip(k6[:16], nonce))
if computed != p["plaintext"][:16]:
errors += 1
print(
f"\nXOR encoding verification (k6[block] XOR nonce = plaintext[block]): "
f"{'OK' if errors == 0 else f'{errors} mismatches'}"
)
# Show the static header prefix
static_pairs = [p for p in pairs if p["k6_size"] == 352]
if static_pairs:
header_xor = static_pairs[0]["plaintext"][:4]
print(f"\nPlaintext prefix for 352B variant: {header_xor.hex()}")
print(f" d9d9f7 = CBOR self-describe tag (RFC7049 tag 55799)")
print(f" a7 = CBOR map(7)")
small_pairs = [p for p in pairs if p["k6_size"] == 144]
if small_pairs:
header_xor = small_pairs[0]["plaintext"][:4]
print(f"\nPlaintext prefix for 144B variant: {header_xor.hex()}")
print(f" d9d9f7 = CBOR self-describe tag")
print(f" a6 = CBOR map(6)")
def analyze_block_variance(pairs: list[dict]) -> None:
"""
For each 16-byte block position, count how many distinct XOR-decoded
values appear. Blocks with 1 unique value are static; high-count blocks
are per-request or per-session variable.
Only standard-prefix (d9d9f7) samples are analyzed, as the 12 non-standard
samples belong to a different TFIT table version and would inflate counts.
"""
print("\n" + "=" * 70)
print("BLOCK VARIANCE ANALYSIS (352B standard-prefix samples)")
print("=" * 70)
# Filter to 352B samples with the standard d9d9f7 CBOR prefix
samples = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
all_352 = [p for p in pairs if p["k6_size"] == 352]
if not samples:
print("No standard-prefix 352B samples found.")
return
non_standard = len(all_352) - len(samples)
print(
f"\nAnalyzed {len(samples)} standard-prefix samples "
f"(excluded {non_standard} non-standard-prefix samples)."
)
n_blocks = 352 // 16
block_unique: list[set] = [set() for _ in range(n_blocks)]
for p in samples:
pt = p["plaintext"]
for i in range(n_blocks):
block_unique[i].add(pt[i * 16 : (i + 1) * 16].hex())
print(f"\n{'Block':>8} {'Bytes':>12} {'Unique':>6} {'Region'}")
print("-" * 55)
for i, s in enumerate(block_unique):
offset = i * 16
if len(s) == 1:
region = "static header"
elif len(s) <= len(samples) // 5:
region = "session-bound"
else:
region = "per-request"
print(
f" {i:3d}/{n_blocks - 1} [{offset:3d}:{offset + 16:3d}] {len(s):6d} {region}"
)
# Summarize regions
static_end = next(
(i * 16 for i in range(n_blocks) if len(block_unique[i]) > 1), n_blocks * 16
)
session_start = static_end
n_samples = len(samples)
session_end = next(
(
i * 16
for i in range(session_start // 16, n_blocks)
if len(block_unique[i]) > n_samples // 5
),
n_blocks * 16,
)
print(f"\nRegion summary (standard-prefix 352B):")
print(f" Static header: bytes 0 {static_end - 1} ({static_end}B)")
print(
f" Session-bound: bytes {static_end:3d} {session_end - 1} ({session_end - static_end}B)"
)
print(f" Per-request: bytes {session_end:3d} 351 ({352 - session_end}B)")
def analyze_session_groups(pairs: list[dict]) -> None:
"""
Group samples by the session-bound region (bytes 128-287 of plaintext).
Each group corresponds to one DH key exchange session.
Only standard-prefix samples are analyzed.
"""
print("\n" + "=" * 70)
print("SESSION GROUP ANALYSIS (352B standard-prefix, bytes 128287)")
print("=" * 70)
samples = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
if not samples:
return
groups: dict[str, list[dict]] = defaultdict(list)
for p in samples:
session_key = p["plaintext"][128:288].hex()
groups[session_key].append(p)
print(f"\n{len(groups)} distinct session groups across {len(samples)} samples.")
print(f"\nTop session groups:")
for session_key, items in sorted(groups.items(), key=lambda x: -len(x[1]))[:10]:
tail_unique = len({p["plaintext"][288:352].hex() for p in items})
print(
f" count={len(items):3d} tail_unique={tail_unique:3d} "
f"session_bytes={session_key[:32]}..."
)
def analyze_cross_sample_xor(pairs: list[dict]) -> None:
"""
XOR two k6 values to reveal the XOR of their plaintexts.
Since k6[i:i+16] = plaintext[i:i+16] XOR nonce, then:
k6a[i:i+16] XOR k6b[i:i+16]
= (pt_a[i:i+16] XOR nonce_a) XOR (pt_b[i:i+16] XOR nonce_b)
= pt_a[i:i+16] XOR pt_b[i:i+16] XOR (nonce_a XOR nonce_b)
Two samples from the same session have identical static (0-127) and
session (128-287) plaintext regions. XOR of their raw k6 values therefore
equals (nonce_a XOR nonce_b) in those regions — which is constant per pair.
Only the per-request tail (288-351) carries message-specific variance.
Only standard-prefix samples are analyzed.
"""
print("\n" + "=" * 70)
print("CROSS-SAMPLE XOR ANALYSIS (352B standard-prefix, same session)")
print("=" * 70)
samples = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
if len(samples) < 2:
return
# Find two samples from the same session group
groups: dict[str, list[dict]] = defaultdict(list)
for p in samples:
sk = p["plaintext"][128:288].hex()
groups[sk].append(p)
largest_group = max(groups.values(), key=len)
pa, pb = largest_group[0], largest_group[1]
xor_pt = bytes(a ^ b for a, b in zip(pa["plaintext"], pb["plaintext"]))
# Since both samples share the same session:
# pt_a[0:288] == pt_b[0:288], so xor_pt[0:288] should be all-zeros
# For the per-request tail (288-352): xor_pt reveals the difference
print(f"\nSamples: {pa['fname']}, {pb['fname']}")
print(f"(Both from same session group)")
print(f"\npt_a XOR pt_b (after nonce decoding each sample independently):")
print(f" bytes 0-127: all-zeros = {xor_pt[:128] == bytes(128)}")
print(f" bytes 128-287: all-zeros = {xor_pt[128:288] == bytes(160)}")
print(f" bytes 288-351: {xor_pt[288:352].hex()}")
non_zero_tail = sum(1 for b in xor_pt[288:352] if b != 0)
print(f" (per-request): non-zero bytes = {non_zero_tail}/64")
print()
print(
"NOTE: XOR of raw k6 values directly (without decoding) gives:\n"
" k6a XOR k6b = (pt_a XOR pt_b) XOR (nonce_a XOR nonce_b)\n"
" In the static/session region this equals nonce_a XOR nonce_b (non-zero),\n"
" so raw k6 XOR is NOT zero even for identical plaintext regions.\n"
" Decode with nonce first, then XOR plaintexts to confirm region identity."
)
def analyze_dh_pub_key_search(pairs: list[dict]) -> None:
"""
Search for the known client DH public key within key 33.6 plaintexts.
The DH pub key (128B) is NOT expected directly in k6 (TFIT whitebox transforms it).
Report any partial matches found.
Only standard-prefix samples are analyzed.
"""
print("\n" + "=" * 70)
print("DH PUBLIC KEY SEARCH IN PLAINTEXTS")
print("=" * 70)
if not KEYS_FILE.exists():
print(" msl_keys.json not found, skipping.")
return
with open(KEYS_FILE) as f:
keys = json.load(f)
dh_pub_hex = keys.get("dh_pub_key", "")
if not dh_pub_hex:
print(" dh_pub_key not in msl_keys.json, skipping.")
return
dh_pub = bytes.fromhex(dh_pub_hex)
print(f"\nClient DH pub key (128B): {dh_pub.hex()[:40]}...")
samples = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
if not samples:
return
# Search full key in raw k6, plaintext
found_raw = sum(1 for p in samples if dh_pub in p["k6_raw"])
found_pt = sum(1 for p in samples if dh_pub in p["plaintext"])
print(f"\nFull DH pub key (128B) found in raw k6: {found_raw}/{len(samples)}")
print(f"Full DH pub key (128B) found in plaintext: {found_pt}/{len(samples)}")
# Search first 16-byte chunk
dh_chunk = dh_pub[:16]
found_chunk_raw = sum(1 for p in samples if dh_chunk in p["k6_raw"])
found_chunk_pt = sum(1 for p in samples if dh_chunk in p["plaintext"])
print(
f"DH pub[:16] found in raw k6: {found_chunk_raw}/{len(samples)}"
)
print(
f"DH pub[:16] found in plaintext: {found_chunk_pt}/{len(samples)}"
)
# Check if k6 raw directly encodes DH pub via per-sample OTP:
# hypothesis: k6[block] = DH_pub[block] XOR f(nonce, block)
# Since nonce differs per sample but DH pub is fixed, k6 XOR DH_pub should be
# different for every sample — which it is (k6 is nonce-dependent).
# The correct check: does plaintext[128:256] == DH_pub?
found_session_eq_dh = sum(1 for p in samples if p["plaintext"][128:256] == dh_pub)
print(
f"plaintext[128:256] == DH_pub: {found_session_eq_dh}/{len(samples)}"
)
# Show mid-section (session region 128-287) XOR DH pub for the most common session
most_common_session = Counter(
p["plaintext"][128:288].hex() for p in samples
).most_common(1)[0][0]
mid_bytes = bytes.fromhex(most_common_session)
xor_mid_dh = bytes(a ^ b for a, b in zip(mid_bytes[:128], dh_pub))
print(
f"\nMost common session region (128-256B, {Counter(p['plaintext'][128:288].hex() for p in samples).most_common(1)[0][1]} samples):"
)
print(f" session_region[0:128] = {most_common_session[:64]}...")
print(f" DH_pub = {dh_pub.hex()[:64]}...")
print(f" XOR = {xor_mid_dh.hex()}")
print(
" (Non-zero XOR confirms DH pub key is NOT directly embedded in the session region)"
)
def analyze_entropy(pairs: list[dict]) -> None:
"""
Compute byte frequency and per-block entropy for k6 (raw and plaintext).
Only standard-prefix samples are analyzed.
"""
print("\n" + "=" * 70)
print("ENTROPY ANALYSIS (352B standard-prefix samples)")
print("=" * 70)
samples = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
if not samples:
return
# Aggregate all raw k6 bytes
all_raw = b"".join(p["k6_raw"] for p in samples)
all_pt = b"".join(p["plaintext"] for p in samples)
print(f"\nGlobal entropy:")
print(f" Raw k6 bytes: {entropy_bits(all_raw):.3f} bits/byte (max=8.0)")
print(f" Plaintext bytes: {entropy_bits(all_pt):.3f} bits/byte")
print(
" (Raw k6 appears high-entropy because nonce XOR randomizes each block;\n"
" plaintext is structured CBOR with lower entropy in the static header)"
)
# Per-block entropy of a single sample (plaintext)
pt0 = samples[0]["plaintext"]
entropies = block_entropy(pt0, 16)
print(f"\nPer-block entropy of plaintext (sample 0):")
for i, e in enumerate(entropies):
region = "static" if i < 8 else ("session" if i < 18 else "per-req")
print(
f" block[{i * 16:3d}:{(i + 1) * 16:3d}] ({region:8s}): {e:.3f} bits/byte"
)
def analyze_asn1_tlv(pairs: list[dict]) -> None:
"""
Check if key 33.6 plaintext contains ASN.1, protobuf, or TLV structures.
Known: plaintext starts with CBOR self-describe tag d9d9f7 + map header.
"""
print("\n" + "=" * 70)
print("STRUCTURE RECOGNITION")
print("=" * 70)
samples = [p for p in pairs if p["k6_size"] in (144, 352)]
if not samples:
return
cbor_ok = 0
cbor_fail = 0
for p in samples:
pt = p["plaintext"]
# Try full CBOR decode of plaintext
try:
cbor2.loads(pt)
cbor_ok += 1
except Exception:
cbor_fail += 1
print(f"\nFull CBOR decode of plaintext:")
print(f" Success: {cbor_ok}/{len(samples)}")
print(f" Failure: {cbor_fail}/{len(samples)}")
print(f" (Partial CBOR is expected — the plaintext is a truncated CBOR stream)")
# Show the static CBOR prefix structure
print(f"\nStatic CBOR prefix breakdown (352B variant, bytes 0-3):")
print(f" d9 d9 f7 = CBOR tag 55799 (self-described CBOR, RFC7049 §2.4)")
print(f" a7 = CBOR map with 7 entries")
print(f" 144B variant uses a6 = map with 6 entries")
# Verify CBOR self-describe tag presence
tag_ok = sum(1 for p in pairs if p["plaintext"][:3] == b"\xd9\xd9\xf7")
print(f"\nSamples with CBOR self-describe tag prefix: {tag_ok}/{len(pairs)}")
not_tag = [p for p in pairs if p["plaintext"][:3] != b"\xd9\xd9\xf7"]
if not_tag:
print(
f" {len(not_tag)} samples without standard prefix "
f"(different TFIT table version or variant)"
)
sizes = Counter(p["k6_size"] for p in not_tag)
print(f" Sizes: {dict(sizes)}")
def analyze_static_xor_cancellation(pairs: list[dict]) -> None:
"""
Demonstrate the nonce XOR encoding properties.
Within a session group, two samples share identical plaintext for bytes 0-287.
After decoding (XOR with respective nonces), pt_a XOR pt_b is zero in those
regions. The per-request tail (288-351) is unique and non-zero.
Directly XOR-ing raw k6 values does NOT give zero in the static/session regions
because the nonces differ: k6a XOR k6b = (pt_a XOR pt_b) XOR (nonce_a XOR nonce_b).
For identical plaintext regions, this equals just nonce_a XOR nonce_b.
Only standard-prefix samples are analyzed.
"""
print("\n" + "=" * 70)
print("PLAINTEXT IDENTITY VERIFICATION (same-session pairs)")
print("=" * 70)
samples = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
groups: dict[str, list[dict]] = defaultdict(list)
for p in samples:
sk = p["plaintext"][128:288].hex()
groups[sk].append(p)
largest = max(groups.values(), key=len)
print(f"\nLargest session group: {len(largest)} samples")
if len(largest) >= 2:
pa, pb = largest[0], largest[1]
raw_xor = bytes(a ^ b for a, b in zip(pa["k6_raw"], pb["k6_raw"]))
pt_xor = bytes(a ^ b for a, b in zip(pa["plaintext"], pb["plaintext"]))
print(f" Pair: {pa['fname']}")
print(f" {pb['fname']}")
print()
print(f" Raw k6 XOR (NOT zero even for shared plaintext regions):")
print(f" [ 0:128]: {raw_xor[:128].hex()[:64]}...")
print(
f" Reason: k6a XOR k6b = (pt XOR nonce_a) XOR (pt XOR nonce_b) = nonce_a XOR nonce_b"
)
print()
print(f" Decoded plaintext XOR (zero where plaintext is identical):")
print(f" [ 0:128] all-zeros: {pt_xor[:128] == bytes(128)}")
print(f" [128:288] all-zeros: {pt_xor[128:288] == bytes(160)}")
print(
f" [288:352] non-zero bytes: {sum(1 for b in pt_xor[288:352] if b != 0)}/64"
)
print(f" [288:352]: {pt_xor[288:352].hex()}")
nonce_xor = bytes(a ^ b for a, b in zip(pa["nonce"], pb["nonce"]))
print()
print(f" nonce_a XOR nonce_b: {nonce_xor.hex()}")
print(f" raw k6 XOR [0:16] XOR nonce_xor = pt_xor [0:16]?")
derived = bytes(a ^ b for a, b in zip(raw_xor[:16], nonce_xor))
print(f" derived: {derived.hex()}")
print(f" pt_xor: {pt_xor[:16].hex()}")
print(f" match: {derived == pt_xor[:16]}")
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
def print_summary(pairs: list[dict]) -> None:
std_352 = [p for p in pairs if p["k6_size"] == 352 and p["standard_prefix"]]
n_sessions = len({p["plaintext"][128:288].hex() for p in std_352}) if std_352 else 0
print("\n" + "=" * 70)
print("SUMMARY OF FINDINGS")
print("=" * 70)
print(
f"""
Encoding (CONFIRMED):
key 33.6 (k6) is encoded as: k6[i:i+16] = plaintext[i:i+16] XOR nonce
where nonce = key 33.9 (16 bytes), applied to EACH 16-byte block with the
same nonce (trivial block-XOR — NOT AES, NOT a stream cipher with derived key).
To decode: plaintext[i:i+16] = k6[i:i+16] XOR nonce (key 33.9)
Verification: k6[0:16] XOR nonce = d9d9f7a71b... (constant across all samples).
nonce = k6[0:16] XOR d9d9f7a71b... (can be recovered without key 33.9).
Plaintext structure (352B standard-prefix variant, {len(std_352)} samples):
Bytes 0-127: STATIC HEADER (1 unique value → identical across all samples)
d9d9f7 = CBOR self-describe tag (RFC7049 tag 55799)
a7 = CBOR map with 7 entries
The remaining bytes are a fixed CBOR encoding scaffold.
Bytes 128-287: SESSION-BOUND ({n_sessions} distinct values across {len(std_352)} samples)
Each distinct value corresponds to one DH key exchange session.
TFIT whitebox AES transforms the DH pub key into this region.
Cannot be directly correlated with raw DH pub key bytes.
Bytes 288-351: PER-REQUEST UNIQUE ({len(std_352)} unique values)
Unique for every request within a session. Likely a message counter,
request ID, or timestamp encoded in the CBOR map.
DH public key relationship:
The client DH pub key (128B) does NOT appear verbatim anywhere in k6 (raw or decoded).
DH pub[:16] not found in any of the {len(std_352)} standard-prefix plaintexts.
The TFIT whitebox (100+ AES-256 operations) non-linearly maps DH pub key → session region.
Key 33.6 size variants observed:
144B = CBOR map(6) — compact form, 9 blocks (static 8 + per-req 1)
240B = intermediate form
352B = CBOR map(7) — full form, 22 blocks (static 8 + session 10 + per-req 4)
464B = extended form
(Non-standard 352B samples use a different TFIT table version.)
Nonce encoding property:
k6a XOR k6b = (pt_a XOR pt_b) XOR (nonce_a XOR nonce_b)
Raw k6 XOR does NOT directly reveal shared plaintext (nonce difference remains).
Decode each sample first, then XOR plaintexts to confirm identical regions.
Within a session: pt_a[0:288] == pt_b[0:288] confirmed (all-zero XOR).
"""
)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
print("Netflix iOS appboot key 33.6 analysis")
print(f"CBOR captures dir: {RAWS_DIR}")
print()
if not RAWS_DIR.exists():
print(f"ERROR: {RAWS_DIR} does not exist.")
return
pairs = collect_pairs()
if not pairs:
print("No appboot request files with key 33.6 found.")
return
print(f"Loaded {len(pairs)} appboot request files with key 33.6 + nonce.")
analyze_encoding_pattern(pairs)
analyze_block_variance(pairs)
analyze_session_groups(pairs)
analyze_cross_sample_xor(pairs)
analyze_dh_pub_key_search(pairs)
analyze_entropy(pairs)
analyze_asn1_tlv(pairs)
analyze_static_xor_cancellation(pairs)
print_summary(pairs)
if __name__ == "__main__":
main()