From 6091d3257fc049b0a90037833484c298f95a9a87 Mon Sep 17 00:00:00 2001 From: SuperUserek Date: Thu, 5 Mar 2026 11:02:46 +0000 Subject: [PATCH] Uploaded autokeybox_decoder.py --- AutoKeyboxDecoder/autokeybox_decoder.py | 389 ++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 AutoKeyboxDecoder/autokeybox_decoder.py diff --git a/AutoKeyboxDecoder/autokeybox_decoder.py b/AutoKeyboxDecoder/autokeybox_decoder.py new file mode 100644 index 0000000..52fc790 --- /dev/null +++ b/AutoKeyboxDecoder/autokeybox_decoder.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +""" +Auto-decrypt helper for LG-style blobs. + +Key sources parsed (by default): +1) openlgtv/epk2extract AES.key + https://raw.githubusercontent.com/openlgtv/epk2extract/refs/heads/master/keys/AES.key + +2) MyDRMTools KnownKeys.txt (RAW) + https://git.drmlab.io/SuperUserek/MyDRMTools/raw/branch/main/AutoKeyboxDecoder/KnownKeys.txt + +Features: +- Multiple key sources (repeat --keys-url, or use defaults) +- --key custom key (AES-128/192/256 only) +- --only-custom to skip downloading lists +- Tries AES modes (ECB/CBC/CFB/OFB/CTR) + IV strategies, validates by b"INNER_MSTAR" +- Extracts payload using CHAI/kbox logic and saves output + +Requires: + pip install requests pycryptodome +""" + +import argparse +import os +import re +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +import requests +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from Crypto.Util import Counter + +DEFAULT_KEYS_URLS = [ + "https://raw.githubusercontent.com/openlgtv/epk2extract/refs/heads/master/keys/AES.key", + # IMPORTANT: use RAW endpoint, not /src/branch/ (HTML) + "https://git.drmlab.io/SuperUserek/MyDRMTools/raw/branch/main/AutoKeyboxDecoder/KnownKeys.txt", +] + +MAGIC = b"INNER_MSTAR" + +# Match exactly AES key sizes in hex: 16/24/32 bytes => 32/48/64 hex chars +HEX_KEY_RE = re.compile( + r"(?i)(? str: + r = requests.get(url, timeout=timeout) + r.raise_for_status() + return r.text + + +def parse_keys(text: str) -> List[str]: + """ + Extract AES keys from arbitrary text. + + Works for: + - AES.key style lines with comments + - KnownKeys.txt where keys can be 1-per-line or space-separated on one line + + It finds ALL 32/48/64-hex tokens in each (comment-stripped) line. + """ + keys: List[str] = [] + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + + # strip inline comments + line = line.split("#", 1)[0] + + # find every key token on the line (handles space-separated single-line lists) + for m in HEX_KEY_RE.findall(line): + keys.append(m.upper()) + + return keys + + +def group_keys_by_aes_size(keys_hex: List[str]) -> Dict[int, List[str]]: + grouped: Dict[int, List[str]] = {16: [], 24: [], 32: []} + for khex in keys_hex: + try: + kb = bytes.fromhex(khex) + except ValueError: + continue + if len(kb) in grouped: + grouped[len(kb)].append(khex.upper()) + return grouped + + +def normalize_custom_key(key_hex: str) -> str: + k = key_hex.strip() + if k.startswith(("0x", "0X")): + k = k[2:] + k = re.sub(r"\s+", "", k) + if not re.fullmatch(r"[0-9A-Fa-f]+", k): + raise ValueError("Custom key is not valid hex") + kb = bytes.fromhex(k) + if len(kb) not in (16, 24, 32): + raise ValueError( + f"Custom key must be 16/24/32 bytes (got {len(kb)} bytes = {len(k)} hex chars)" + ) + return k.upper() + + +def extract_payload(dec_data: bytes) -> bytes: + payload = None + for offset in (64, 96): + candidate = dec_data[offset:] + if b"CHAI" in candidate or b"kbox" in candidate: + payload = candidate + break + + if payload is None: + payload = dec_data[64:] + + if b"CHAI" in payload: + out_data = payload + elif b"kbox" in payload: + out_data = payload[:128] + else: + out_data = payload[:32] + + return out_data + + +@dataclass +class AttemptResult: + key_hex: str + mode_name: str + iv_used: Optional[bytes] + plaintext: bytes + + +def _ensure_block_multiple(data: bytes) -> bytes: + # Keep your original behavior: pad ciphertext to 16 then decrypt for block modes + if len(data) % 16 == 0: + return data + return pad(data, 16) + + +def try_decrypt_with_key(encrypted: bytes, key_hex: str) -> List[AttemptResult]: + try: + key = bytes.fromhex(key_hex) + except ValueError: + return [] + + successes: List[AttemptResult] = [] + + # --- ECB --- + try: + data = _ensure_block_multiple(encrypted) + cipher = AES.new(key, AES.MODE_ECB) + dec = cipher.decrypt(data) + if MAGIC in dec: + successes.append(AttemptResult(key_hex, "ECB", None, dec)) + except Exception: + pass + + # --- IV candidates for CBC/CFB/OFB --- + iv_candidates: List[Tuple[str, bytes, bytes]] = [] + iv_zero = b"\x00" * 16 + iv_candidates.append(("IV_ZERO", iv_zero, encrypted)) + + if len(encrypted) >= 16: + iv_from_prefix = encrypted[:16] + ct_after_prefix = encrypted[16:] + iv_candidates.append(("IV_PREFIX16", iv_from_prefix, ct_after_prefix)) + + # --- CBC --- + for iv_label, iv, ct in iv_candidates: + try: + ct2 = _ensure_block_multiple(ct) + cipher = AES.new(key, AES.MODE_CBC, iv=iv) + dec = cipher.decrypt(ct2) + if MAGIC in dec: + successes.append(AttemptResult(key_hex, f"CBC({iv_label})", iv, dec)) + except Exception: + pass + + # --- CFB (segment_size=128) --- + for iv_label, iv, ct in iv_candidates: + try: + cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) + dec = cipher.decrypt(ct) + if MAGIC in dec: + successes.append(AttemptResult(key_hex, f"CFB({iv_label})", iv, dec)) + except Exception: + pass + + # --- OFB --- + for iv_label, iv, ct in iv_candidates: + try: + cipher = AES.new(key, AES.MODE_OFB, iv=iv) + dec = cipher.decrypt(ct) + if MAGIC in dec: + successes.append(AttemptResult(key_hex, f"OFB({iv_label})", iv, dec)) + except Exception: + pass + + # --- CTR strategies --- + # A) whole ciphertext, counter starts at 0 + try: + ctr = Counter.new(128, initial_value=0) + cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + dec = cipher.decrypt(encrypted) + if MAGIC in dec: + successes.append(AttemptResult(key_hex, "CTR(counter=0,whole_ct)", None, dec)) + except Exception: + pass + + # B) first 16 bytes is initial counter block; decrypt remaining bytes + if len(encrypted) >= 16: + try: + init_val = int.from_bytes(encrypted[:16], "big") + ctr = Counter.new(128, initial_value=init_val) + cipher = AES.new(key, AES.MODE_CTR, counter=ctr) + dec = cipher.decrypt(encrypted[16:]) + if MAGIC in dec: + successes.append( + AttemptResult( + key_hex, + "CTR(counter=prefix16,ct_after_prefix)", + encrypted[:16], + dec, + ) + ) + except Exception: + pass + + return successes + + +def build_key_list( + keys_urls: List[str], + custom_key: Optional[str], + only_custom: bool, +) -> List[str]: + keys: List[str] = [] + + # custom key first + if custom_key: + keys.append(normalize_custom_key(custom_key)) + + if only_custom: + return keys + + seen = set(keys) + for url in keys_urls: + text = download_text(url) + parsed = parse_keys(text) + + # Dedup + keep AES-valid sizes only + for k in parsed: + try: + kb = bytes.fromhex(k) + except ValueError: + continue + if len(kb) not in (16, 24, 32): + continue + if k not in seen: + keys.append(k) + seen.add(k) + + return keys + + +def auto_decrypt( + input_file: str, + keys_urls: List[str], + out_dir: Optional[str], + stop_on_first: bool, + custom_key: Optional[str], + only_custom: bool, +) -> int: + with open(input_file, "rb") as f: + encrypted_data = f.read() + + all_keys = build_key_list(keys_urls, custom_key, only_custom) + if not all_keys: + print("[-] No keys to try. Provide --key or disable --only-custom.") + return 3 + + grouped = group_keys_by_aes_size(all_keys) + print("[*] Keys ready:") + print(f" AES-128: {len(grouped[16])}") + print(f" AES-192: {len(grouped[24])}") + print(f" AES-256: {len(grouped[32])}") + print(f" Total: {len(all_keys)}") + + # Try in a practical order + ordered_keys = grouped[16] + grouped[32] + grouped[24] + + base = os.path.splitext(os.path.basename(input_file))[0] + out_dir = out_dir or os.path.dirname(os.path.abspath(input_file)) or "." + os.makedirs(out_dir, exist_ok=True) + + found_any = 0 + + for key_hex in ordered_keys: + results = try_decrypt_with_key(encrypted_data, key_hex) + if not results: + continue + + for r in results: + found_any += 1 + payload = extract_payload(r.plaintext) + + safe_mode = re.sub(r"[^A-Za-z0-9_.-]+", "_", r.mode_name) + out_path = os.path.join(out_dir, f"{base}_decrypted_{safe_mode}_{r.key_hex[:16]}.dat") + + with open(out_path, "wb") as f: + f.write(payload) + + iv_info = "" + if r.iv_used is not None: + iv_info = f", iv={r.iv_used.hex().upper()}" + + print(f"[+] MATCH: key={r.key_hex} mode={r.mode_name}{iv_info}") + print(f" Saved: {out_path} ({len(payload)} bytes)") + + if stop_on_first: + return 0 + + if found_any == 0: + print("[-] No working key/mode found (no INNER_MSTAR marker detected).") + return 2 + + print(f"[*] Done. Matches found: {found_any}") + return 0 + + +def main(): + ap = argparse.ArgumentParser( + description="Try AES keys (multiple remote lists + optional custom key) across AES modes to decrypt an input file." + ) + ap.add_argument("file", help="Path to encrypted input file (e.g. file.dat)") + + ap.add_argument( + "--keys-url", + action="append", + default=None, + help="Add a key list URL to download+parse (repeatable). If omitted, uses built-in defaults.", + ) + + ap.add_argument("--outdir", default=None, help="Output directory (default: alongside input file)") + + ap.add_argument( + "--key", + default=None, + help="Custom AES key in hex (32/48/64 hex chars). You can also prefix with 0x or include spaces.", + ) + ap.add_argument( + "--only-custom", + action="store_true", + help="Only try the custom key (skip downloading remote key lists).", + ) + + ap.add_argument( + "--all-matches", + action="store_true", + help="Save all matches (default: stop on first match).", + ) + + args = ap.parse_args() + keys_urls = args.keys_url if args.keys_url else DEFAULT_KEYS_URLS + + try: + rc = auto_decrypt( + input_file=args.file, + keys_urls=keys_urls, + out_dir=args.outdir, + stop_on_first=not args.all_matches, + custom_key=args.key, + only_custom=args.only_custom, + ) + except requests.RequestException as e: + print(f"[-] Failed to download a keys list: {e}") + raise SystemExit(4) + except ValueError as e: + print(f"[-] {e}") + raise SystemExit(5) + + raise SystemExit(rc) + + +if __name__ == "__main__": + main() \ No newline at end of file