Update to cache keys, and download it once daily.

Use --force-update-keys to redownload new keys from sources.
This commit is contained in:
SuperUserek 2026-03-05 11:19:39 +00:00
parent 6091d3257f
commit 164ed468e6

View File

@ -1,389 +1,487 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Auto-decrypt helper for LG-style blobs. SuperUserek AutoKeybox Decoder (Auto AES Decrypt Tool)
Key sources parsed (by default): What it does:
1) openlgtv/epk2extract AES.key - Downloads + parses AES keys from multiple sources (default list below)
https://raw.githubusercontent.com/openlgtv/epk2extract/refs/heads/master/keys/AES.key - Caches the merged, deduped key list to a local "keys.txt"
- Only updates the cache once per day (unless --force-update-keys)
2) MyDRMTools KnownKeys.txt (RAW) - Optionally includes/only-uses a custom user-provided AES key
https://git.drmlab.io/SuperUserek/MyDRMTools/raw/branch/main/AutoKeyboxDecoder/KnownKeys.txt - Tries AES modes (ECB/CBC/CFB/OFB/CTR) + IV strategies
- Validates by searching for b"INNER_MSTAR"
Features: - Extracts payload using CHAI/kbox logic and saves output
- Multiple key sources (repeat --keys-url, or use defaults)
- --key <hex> custom key (AES-128/192/256 only) Requires:
- --only-custom to skip downloading lists pip install requests pycryptodome
- Tries AES modes (ECB/CBC/CFB/OFB/CTR) + IV strategies, validates by b"INNER_MSTAR" """
- Extracts payload using CHAI/kbox logic and saves output
import argparse
Requires: import os
pip install requests pycryptodome import re
""" from dataclasses import dataclass
from datetime import date
import argparse from typing import Dict, List, Optional, Tuple
import os
import re import requests
from dataclasses import dataclass from Crypto.Cipher import AES
from typing import Dict, List, Optional, Tuple from Crypto.Util.Padding import pad
from Crypto.Util import Counter
import requests
from Crypto.Cipher import AES DEFAULT_KEYS_URLS = [
from Crypto.Util.Padding import pad "https://raw.githubusercontent.com/openlgtv/epk2extract/refs/heads/master/keys/AES.key",
from Crypto.Util import Counter # IMPORTANT: RAW endpoint (not /src/branch/ which is HTML)
"https://git.drmlab.io/SuperUserek/MyDRMTools/raw/branch/main/AutoKeyboxDecoder/KnownKeys.txt",
DEFAULT_KEYS_URLS = [ ]
"https://raw.githubusercontent.com/openlgtv/epk2extract/refs/heads/master/keys/AES.key",
# IMPORTANT: use RAW endpoint, not /src/branch/ (HTML) MAGIC = b"INNER_MSTAR"
"https://git.drmlab.io/SuperUserek/MyDRMTools/raw/branch/main/AutoKeyboxDecoder/KnownKeys.txt",
] # Match exactly AES key sizes in hex: 16/24/32 bytes => 32/48/64 hex chars
HEX_KEY_RE = re.compile(
MAGIC = b"INNER_MSTAR" r"(?i)(?<![0-9a-f])(?:[0-9a-f]{32}|[0-9a-f]{48}|[0-9a-f]{64})(?![0-9a-f])"
)
# Match exactly AES key sizes in hex: 16/24/32 bytes => 32/48/64 hex chars
HEX_KEY_RE = re.compile(
r"(?i)(?<![0-9a-f])(?:[0-9a-f]{32}|[0-9a-f]{48}|[0-9a-f]{64})(?![0-9a-f])" def download_text(url: str, timeout: int = 15) -> str:
) r = requests.get(url, timeout=timeout)
r.raise_for_status()
return r.text
def download_text(url: str, timeout: int = 15) -> str:
r = requests.get(url, timeout=timeout)
r.raise_for_status() def parse_keys(text: str) -> List[str]:
return r.text """
Extract AES keys from arbitrary text.
def parse_keys(text: str) -> List[str]: Works for:
""" - AES.key style lines with comments
Extract AES keys from arbitrary text. - KnownKeys.txt where keys can be 1-per-line or space-separated
Works for: It finds ALL 32/48/64-hex tokens in each (comment-stripped) line.
- AES.key style lines with comments """
- KnownKeys.txt where keys can be 1-per-line or space-separated on one line keys: List[str] = []
for line in text.splitlines():
It finds ALL 32/48/64-hex tokens in each (comment-stripped) line. line = line.strip()
""" if not line or line.startswith("#"):
keys: List[str] = [] continue
for line in text.splitlines():
line = line.strip() # strip inline comments
if not line or line.startswith("#"): line = line.split("#", 1)[0]
continue
# find every key token on the line
# strip inline comments for m in HEX_KEY_RE.findall(line):
line = line.split("#", 1)[0] keys.append(m.upper())
# find every key token on the line (handles space-separated single-line lists) return keys
for m in HEX_KEY_RE.findall(line):
keys.append(m.upper())
def normalize_custom_key(key_hex: str) -> str:
return keys k = key_hex.strip()
if k.startswith(("0x", "0X")):
k = k[2:]
def group_keys_by_aes_size(keys_hex: List[str]) -> Dict[int, List[str]]: k = re.sub(r"\s+", "", k)
grouped: Dict[int, List[str]] = {16: [], 24: [], 32: []} if not re.fullmatch(r"[0-9A-Fa-f]+", k):
for khex in keys_hex: raise ValueError("Custom key is not valid hex")
try: kb = bytes.fromhex(k)
kb = bytes.fromhex(khex) if len(kb) not in (16, 24, 32):
except ValueError: raise ValueError(
continue f"Custom key must be 16/24/32 bytes (got {len(kb)} bytes = {len(k)} hex chars)"
if len(kb) in grouped: )
grouped[len(kb)].append(khex.upper()) return k.upper()
return grouped
def group_keys_by_aes_size(keys_hex: List[str]) -> Dict[int, List[str]]:
def normalize_custom_key(key_hex: str) -> str: grouped: Dict[int, List[str]] = {16: [], 24: [], 32: []}
k = key_hex.strip() for khex in keys_hex:
if k.startswith(("0x", "0X")): try:
k = k[2:] kb = bytes.fromhex(khex)
k = re.sub(r"\s+", "", k) except ValueError:
if not re.fullmatch(r"[0-9A-Fa-f]+", k): continue
raise ValueError("Custom key is not valid hex") if len(kb) in grouped:
kb = bytes.fromhex(k) grouped[len(kb)].append(khex.upper())
if len(kb) not in (16, 24, 32): return grouped
raise ValueError(
f"Custom key must be 16/24/32 bytes (got {len(kb)} bytes = {len(k)} hex chars)"
) def _ensure_block_multiple(data: bytes) -> bytes:
return k.upper() # Keep your original behavior: pad ciphertext to 16 then decrypt for block modes
if len(data) % 16 == 0:
return data
def extract_payload(dec_data: bytes) -> bytes: return pad(data, 16)
payload = None
for offset in (64, 96):
candidate = dec_data[offset:] def extract_payload(dec_data: bytes) -> bytes:
if b"CHAI" in candidate or b"kbox" in candidate: """
payload = candidate Your payload extraction logic (unchanged).
break """
payload = None
if payload is None: for offset in (64, 96):
payload = dec_data[64:] candidate = dec_data[offset:]
if b"CHAI" in candidate or b"kbox" in candidate:
if b"CHAI" in payload: payload = candidate
out_data = payload break
elif b"kbox" in payload:
out_data = payload[:128] if payload is None:
else: payload = dec_data[64:]
out_data = payload[:32]
if b"CHAI" in payload:
return out_data out_data = payload
elif b"kbox" in payload:
out_data = payload[:128]
@dataclass else:
class AttemptResult: out_data = payload[:32]
key_hex: str
mode_name: str return out_data
iv_used: Optional[bytes]
plaintext: bytes
@dataclass
class AttemptResult:
def _ensure_block_multiple(data: bytes) -> bytes: key_hex: str
# Keep your original behavior: pad ciphertext to 16 then decrypt for block modes mode_name: str
if len(data) % 16 == 0: iv_used: Optional[bytes]
return data plaintext: bytes
return pad(data, 16)
def try_decrypt_with_key(encrypted: bytes, key_hex: str) -> List[AttemptResult]:
def try_decrypt_with_key(encrypted: bytes, key_hex: str) -> List[AttemptResult]: try:
try: key = bytes.fromhex(key_hex)
key = bytes.fromhex(key_hex) except ValueError:
except ValueError: return []
return []
successes: List[AttemptResult] = []
successes: List[AttemptResult] = []
# --- ECB ---
# --- ECB --- try:
try: data = _ensure_block_multiple(encrypted)
data = _ensure_block_multiple(encrypted) cipher = AES.new(key, AES.MODE_ECB)
cipher = AES.new(key, AES.MODE_ECB) dec = cipher.decrypt(data)
dec = cipher.decrypt(data) if MAGIC in dec:
if MAGIC in dec: successes.append(AttemptResult(key_hex, "ECB", None, dec))
successes.append(AttemptResult(key_hex, "ECB", None, dec)) except Exception:
except Exception: pass
pass
# --- IV candidates for CBC/CFB/OFB ---
# --- IV candidates for CBC/CFB/OFB --- iv_candidates: List[Tuple[str, bytes, bytes]] = []
iv_candidates: List[Tuple[str, bytes, bytes]] = [] iv_candidates.append(("IV_ZERO", b"\x00" * 16, encrypted))
iv_zero = b"\x00" * 16
iv_candidates.append(("IV_ZERO", iv_zero, encrypted)) if len(encrypted) >= 16:
iv_candidates.append(("IV_PREFIX16", encrypted[:16], encrypted[16:]))
if len(encrypted) >= 16:
iv_from_prefix = encrypted[:16] # --- CBC ---
ct_after_prefix = encrypted[16:] for iv_label, iv, ct in iv_candidates:
iv_candidates.append(("IV_PREFIX16", iv_from_prefix, ct_after_prefix)) try:
ct2 = _ensure_block_multiple(ct)
# --- CBC --- cipher = AES.new(key, AES.MODE_CBC, iv=iv)
for iv_label, iv, ct in iv_candidates: dec = cipher.decrypt(ct2)
try: if MAGIC in dec:
ct2 = _ensure_block_multiple(ct) successes.append(AttemptResult(key_hex, f"CBC({iv_label})", iv, dec))
cipher = AES.new(key, AES.MODE_CBC, iv=iv) except Exception:
dec = cipher.decrypt(ct2) pass
if MAGIC in dec:
successes.append(AttemptResult(key_hex, f"CBC({iv_label})", iv, dec)) # --- CFB (segment_size=128) ---
except Exception: for iv_label, iv, ct in iv_candidates:
pass try:
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
# --- CFB (segment_size=128) --- dec = cipher.decrypt(ct)
for iv_label, iv, ct in iv_candidates: if MAGIC in dec:
try: successes.append(AttemptResult(key_hex, f"CFB({iv_label})", iv, dec))
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128) except Exception:
dec = cipher.decrypt(ct) pass
if MAGIC in dec:
successes.append(AttemptResult(key_hex, f"CFB({iv_label})", iv, dec)) # --- OFB ---
except Exception: for iv_label, iv, ct in iv_candidates:
pass try:
cipher = AES.new(key, AES.MODE_OFB, iv=iv)
# --- OFB --- dec = cipher.decrypt(ct)
for iv_label, iv, ct in iv_candidates: if MAGIC in dec:
try: successes.append(AttemptResult(key_hex, f"OFB({iv_label})", iv, dec))
cipher = AES.new(key, AES.MODE_OFB, iv=iv) except Exception:
dec = cipher.decrypt(ct) pass
if MAGIC in dec:
successes.append(AttemptResult(key_hex, f"OFB({iv_label})", iv, dec)) # --- CTR strategies ---
except Exception: # A) whole ciphertext, counter starts at 0
pass try:
ctr = Counter.new(128, initial_value=0)
# --- CTR strategies --- cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
# A) whole ciphertext, counter starts at 0 dec = cipher.decrypt(encrypted)
try: if MAGIC in dec:
ctr = Counter.new(128, initial_value=0) successes.append(AttemptResult(key_hex, "CTR(counter=0,whole_ct)", None, dec))
cipher = AES.new(key, AES.MODE_CTR, counter=ctr) except Exception:
dec = cipher.decrypt(encrypted) pass
if MAGIC in dec:
successes.append(AttemptResult(key_hex, "CTR(counter=0,whole_ct)", None, dec)) # B) first 16 bytes is initial counter block; decrypt remaining bytes
except Exception: if len(encrypted) >= 16:
pass try:
init_val = int.from_bytes(encrypted[:16], "big")
# B) first 16 bytes is initial counter block; decrypt remaining bytes ctr = Counter.new(128, initial_value=init_val)
if len(encrypted) >= 16: cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
try: dec = cipher.decrypt(encrypted[16:])
init_val = int.from_bytes(encrypted[:16], "big") if MAGIC in dec:
ctr = Counter.new(128, initial_value=init_val) successes.append(
cipher = AES.new(key, AES.MODE_CTR, counter=ctr) AttemptResult(
dec = cipher.decrypt(encrypted[16:]) key_hex,
if MAGIC in dec: "CTR(counter=prefix16,ct_after_prefix)",
successes.append( encrypted[:16],
AttemptResult( dec,
key_hex, )
"CTR(counter=prefix16,ct_after_prefix)", )
encrypted[:16], except Exception:
dec, pass
)
) return successes
except Exception:
pass
# -------------------------
return successes # Key cache: keys.txt daily
# -------------------------
def build_key_list( def is_cache_fresh(cache_path: str) -> bool:
keys_urls: List[str], """
custom_key: Optional[str], Fresh means: cache file mtime is 'today' (local date).
only_custom: bool, """
) -> List[str]: if not os.path.exists(cache_path):
keys: List[str] = [] return False
try:
# custom key first mtime = os.path.getmtime(cache_path)
if custom_key: except OSError:
keys.append(normalize_custom_key(custom_key)) return False
return date.fromtimestamp(mtime) == date.today()
if only_custom:
return keys
def load_keys_from_cache(cache_path: str) -> List[str]:
seen = set(keys) """
for url in keys_urls: Read keys.txt. Accepts:
text = download_text(url) - one key per line
parsed = parse_keys(text) - ignores comments/blank lines
"""
# Dedup + keep AES-valid sizes only keys: List[str] = []
for k in parsed: if not os.path.exists(cache_path):
try: return keys
kb = bytes.fromhex(k) with open(cache_path, "r", encoding="utf-8", errors="ignore") as f:
except ValueError: for line in f:
continue line = line.strip()
if len(kb) not in (16, 24, 32): if not line or line.startswith("#"):
continue continue
if k not in seen: for m in HEX_KEY_RE.findall(line):
keys.append(k) keys.append(m.upper())
seen.add(k) return keys
return keys
def save_keys_to_cache(cache_path: str, keys: List[str], sources: List[str]) -> None:
os.makedirs(os.path.dirname(os.path.abspath(cache_path)) or ".", exist_ok=True)
def auto_decrypt( with open(cache_path, "w", encoding="utf-8") as f:
input_file: str, f.write("# SuperUserek AutoKeybox Decoder - Cached AES keys\n")
keys_urls: List[str], f.write(f"# Updated: {date.today().isoformat()}\n")
out_dir: Optional[str], f.write("# Sources:\n")
stop_on_first: bool, for s in sources:
custom_key: Optional[str], f.write(f"# - {s}\n")
only_custom: bool, f.write("\n")
) -> int: for k in keys:
with open(input_file, "rb") as f: f.write(k + "\n")
encrypted_data = f.read()
all_keys = build_key_list(keys_urls, custom_key, only_custom) def build_key_list(
if not all_keys: keys_urls: List[str],
print("[-] No keys to try. Provide --key or disable --only-custom.") custom_key: Optional[str],
return 3 only_custom: bool,
cache_path: str,
grouped = group_keys_by_aes_size(all_keys) force_update_keys: bool,
print("[*] Keys ready:") ) -> Tuple[List[str], bool]:
print(f" AES-128: {len(grouped[16])}") """
print(f" AES-192: {len(grouped[24])}") Returns (keys, used_cache)
print(f" AES-256: {len(grouped[32])}") """
print(f" Total: {len(all_keys)}") keys: List[str] = []
# Try in a practical order # custom key first (if any)
ordered_keys = grouped[16] + grouped[32] + grouped[24] if custom_key:
keys.append(normalize_custom_key(custom_key))
base = os.path.splitext(os.path.basename(input_file))[0]
out_dir = out_dir or os.path.dirname(os.path.abspath(input_file)) or "." if only_custom:
os.makedirs(out_dir, exist_ok=True) return keys, False
found_any = 0 # If cache is fresh and not forced, use it
if (not force_update_keys) and is_cache_fresh(cache_path):
for key_hex in ordered_keys: cached = load_keys_from_cache(cache_path)
results = try_decrypt_with_key(encrypted_data, key_hex) # Dedup but keep custom key first if present
if not results: seen = set(keys)
continue for k in cached:
if k not in seen:
for r in results: # ensure valid AES size
found_any += 1 try:
payload = extract_payload(r.plaintext) kb = bytes.fromhex(k)
except ValueError:
safe_mode = re.sub(r"[^A-Za-z0-9_.-]+", "_", r.mode_name) continue
out_path = os.path.join(out_dir, f"{base}_decrypted_{safe_mode}_{r.key_hex[:16]}.dat") if len(kb) not in (16, 24, 32):
continue
with open(out_path, "wb") as f: keys.append(k)
f.write(payload) seen.add(k)
return keys, True
iv_info = ""
if r.iv_used is not None: # Otherwise download + parse, then write cache
iv_info = f", iv={r.iv_used.hex().upper()}" seen = set(keys)
downloaded: List[str] = []
print(f"[+] MATCH: key={r.key_hex} mode={r.mode_name}{iv_info}")
print(f" Saved: {out_path} ({len(payload)} bytes)") for url in keys_urls:
text = download_text(url)
if stop_on_first: for k in parse_keys(text):
return 0 try:
kb = bytes.fromhex(k)
if found_any == 0: except ValueError:
print("[-] No working key/mode found (no INNER_MSTAR marker detected).") continue
return 2 if len(kb) not in (16, 24, 32):
continue
print(f"[*] Done. Matches found: {found_any}") if k not in seen:
return 0 downloaded.append(k)
seen.add(k)
def main(): # Save merged list (custom key not included in cache; cache is for known keys)
ap = argparse.ArgumentParser( save_keys_to_cache(cache_path, downloaded, keys_urls)
description="Try AES keys (multiple remote lists + optional custom key) across AES modes to decrypt an input file."
) # final keys list = custom (if any) + downloaded
ap.add_argument("file", help="Path to encrypted input file (e.g. file.dat)") keys.extend(downloaded)
return keys, False
ap.add_argument(
"--keys-url",
action="append", def auto_decrypt(
default=None, input_file: str,
help="Add a key list URL to download+parse (repeatable). If omitted, uses built-in defaults.", keys_urls: List[str],
) out_dir: Optional[str],
stop_on_first: bool,
ap.add_argument("--outdir", default=None, help="Output directory (default: alongside input file)") custom_key: Optional[str],
only_custom: bool,
ap.add_argument( cache_path: str,
"--key", force_update_keys: bool,
default=None, ) -> int:
help="Custom AES key in hex (32/48/64 hex chars). You can also prefix with 0x or include spaces.", with open(input_file, "rb") as f:
) encrypted_data = f.read()
ap.add_argument(
"--only-custom", all_keys, used_cache = build_key_list(
action="store_true", keys_urls=keys_urls,
help="Only try the custom key (skip downloading remote key lists).", custom_key=custom_key,
) only_custom=only_custom,
cache_path=cache_path,
ap.add_argument( force_update_keys=force_update_keys,
"--all-matches", )
action="store_true",
help="Save all matches (default: stop on first match).", if not all_keys:
) print("[-] No keys to try. Provide --key or disable --only-custom.")
return 3
args = ap.parse_args()
keys_urls = args.keys_url if args.keys_url else DEFAULT_KEYS_URLS grouped = group_keys_by_aes_size(all_keys)
cache_msg = f"(cache: {os.path.abspath(cache_path)}"
try: cache_msg += ", used cached keys)" if used_cache else ", updated/downloaded keys)"
rc = auto_decrypt( print(f"[*] Keys ready {cache_msg}")
input_file=args.file, print(f" AES-128: {len(grouped[16])}")
keys_urls=keys_urls, print(f" AES-192: {len(grouped[24])}")
out_dir=args.outdir, print(f" AES-256: {len(grouped[32])}")
stop_on_first=not args.all_matches, print(f" Total: {len(all_keys)}")
custom_key=args.key,
only_custom=args.only_custom, # Try in a practical order
) ordered_keys = grouped[16] + grouped[32] + grouped[24]
except requests.RequestException as e:
print(f"[-] Failed to download a keys list: {e}") base = os.path.splitext(os.path.basename(input_file))[0]
raise SystemExit(4) out_dir = out_dir or os.path.dirname(os.path.abspath(input_file)) or "."
except ValueError as e: os.makedirs(out_dir, exist_ok=True)
print(f"[-] {e}")
raise SystemExit(5) found_any = 0
raise SystemExit(rc) for key_hex in ordered_keys:
results = try_decrypt_with_key(encrypted_data, key_hex)
if not results:
if __name__ == "__main__": 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 (cached daily) 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(
"--keys-cache",
default="keys.txt",
help='Path to local keys cache file (default: "keys.txt")',
)
ap.add_argument(
"--force-update-keys",
action="store_true",
help="Force re-download + rebuild keys cache (otherwise updates only once per day).",
)
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 key lists / cache).",
)
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,
cache_path=args.keys_cache,
force_update_keys=args.force_update_keys,
)
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() main()