""" PS5 PUP Decrypt Tool - CLI Entry Point Partial PC-based tool for PS5 firmware analysis. What works (Layer 3 - known keys): - BLS container parsing (outer SLB2 format) - PUP header/segment structure parsing - Inner firmware decryption (EMC IPL, EAP KBL, EAP Kernel, Titania) What's missing (Layer 1 - key derivation): - PUP segment decryption (needs KMB_AES derivation chain) - Full end-to-end PUP decryption pipeline """ import argparse import os import sys import zlib from bls import parse_bls_header, extract_bls_entry, BLS_MAGIC from pup import parse_pup_header, parse_pup_segments, try_parse_pup, PUP_MAGIC from firmware import ( detect_firmware_type, decrypt_firmware_blob, try_all_decryptions, analyze_emc_ipl, ) from crypto import show_experimental_derivation def cmd_info(args): """Show BLS and PUP structure info for a PUP file.""" filepath = args.file if not os.path.isfile(filepath): print(f"Error: File not found: {filepath}") return 1 print(f"=== PS5 PUP Info: {os.path.basename(filepath)} ===") print(f"File size: {os.path.getsize(filepath):,} bytes") print() with open(filepath, "rb") as f: # Check BLS magic magic_bytes = f.read(4) f.seek(0) import struct magic = struct.unpack_from(" {output}") # Show first 64 bytes hex dump print("\nFirst 64 bytes of decrypted output:") _hexdump(result[:64]) return 0 def cmd_bruteforce(args): """Try all known keys against a firmware blob.""" filepath = args.file output_dir = args.output or os.path.splitext(filepath)[0] + "_bruteforce" if not os.path.isfile(filepath): print(f"Error: File not found: {filepath}") return 1 with open(filepath, "rb") as f: data = f.read() print(f"Trying all known keys against {os.path.basename(filepath)}...") print(f"Output dir: {output_dir}/") print() results = try_all_decryptions(data, output_dir, os.path.splitext(os.path.basename(filepath))[0]) print(f"{'Key':<20} {'Description':<30} {'Result'}") print("-" * 80) for key_name, description, verdict in results: marker = ">>>" if "likely correct" in verdict.lower() or "POSSIBLE" in verdict else " " print(f"{marker} {key_name:<20} {description:<30} {verdict}") print() print("LOW_ENTROPY = likely correct decryption (structured data)") print("POSSIBLE_MATCH = has null runs and ASCII (worth investigating)") print("HIGH_ENTROPY = still looks encrypted or wrong key") return 0 def cmd_analyze(args): """Analyze an EMC IPL blob in detail.""" filepath = args.file if not os.path.isfile(filepath): print(f"Error: File not found: {filepath}") return 1 with open(filepath, "rb") as f: data = f.read() print(f"=== EMC IPL Analysis: {os.path.basename(filepath)} ===") print(f"Size: {len(data):,} bytes") print() info = analyze_emc_ipl(data) if "header_magic" in info: print(f"Decrypted header magic: {info['header_magic']}") if "possible_hdr_len" in info: print(f"Possible header length: 0x{info['possible_hdr_len']:X}") if "possible_body_len" in info: print(f"Possible body length: 0x{info['possible_body_len']:X}") if "header_decrypted" in info: print("\nDecrypted header (first 0x80 bytes):") _hexdump(info["header_decrypted"]) return 0 def cmd_decompress(args): """Decompress a zlib-compressed segment.""" filepath = args.file if not os.path.isfile(filepath): print(f"Error: File not found: {filepath}") return 1 with open(filepath, "rb") as f: data = f.read() try: decompressed = zlib.decompress(data) except zlib.error as e: print(f"Decompression failed: {e}") print("Data may not be zlib-compressed, or may still be encrypted.") return 1 output = args.output if not output: base, ext = os.path.splitext(filepath) output = f"{base}.decompressed{ext}" with open(output, "wb") as f: f.write(decompressed) ratio = len(decompressed) / len(data) if data else 0 print(f"Decompressed: {len(data):,} -> {len(decompressed):,} bytes ({ratio:.1f}x)") print(f"Output: {output}") return 0 def cmd_keys(args): """Show known keys and experimental derivation.""" show_experimental_derivation() def _hexdump(data: bytes, width: int = 16): """Print a hex dump of data.""" for offset in range(0, len(data), width): chunk = data[offset:offset + width] hex_part = " ".join(f"{b:02X}" for b in chunk) ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk) print(f" {offset:08X} {hex_part:<{width * 3}} {ascii_part}") def main(): parser = argparse.ArgumentParser( description="PS5 PUP Decrypt Tool - Partial PC-based firmware analyzer", epilog=( "Layer 3 (known keys) is fully functional.\n" "Layer 1 (PUP segment decryption) requires key derivation research.\n" "For security research on personally owned hardware only." ), formatter_class=argparse.RawDescriptionHelpFormatter, ) subparsers = parser.add_subparsers(dest="command", help="Command to run") # info p_info = subparsers.add_parser("info", help="Show PUP/BLS structure info") p_info.add_argument("file", help="Path to PS5UPDATE.PUP or extracted PUP") p_info.set_defaults(func=cmd_info) # extract p_extract = subparsers.add_parser("extract", help="Extract BLS entries to disk") p_extract.add_argument("file", help="Path to PS5UPDATE.PUP") p_extract.add_argument("-o", "--output", help="Output directory") p_extract.set_defaults(func=cmd_extract) # decrypt p_decrypt = subparsers.add_parser("decrypt", help="Decrypt firmware blob with known key") p_decrypt.add_argument("file", help="Path to extracted firmware blob") p_decrypt.add_argument( "-t", "--type", choices=["emc_ipl_header", "emc_ipl", "eap_kbl", "eap_kernel", "eap_self", "titania"], help="Firmware type (auto-detected from filename if omitted)", ) p_decrypt.add_argument("-o", "--output", help="Output file path") p_decrypt.add_argument("--titania-rev", type=int, default=2, choices=[1, 2, 3], help="Titania hardware revision (default: 2)") p_decrypt.set_defaults(func=cmd_decrypt) # bruteforce p_brute = subparsers.add_parser("bruteforce", help="Try all known keys against a blob") p_brute.add_argument("file", help="Path to firmware blob") p_brute.add_argument("-o", "--output", help="Output directory") p_brute.set_defaults(func=cmd_bruteforce) # analyze p_analyze = subparsers.add_parser("analyze", help="Analyze EMC IPL blob structure") p_analyze.add_argument("file", help="Path to EMC IPL blob") p_analyze.set_defaults(func=cmd_analyze) # decompress p_decomp = subparsers.add_parser("decompress", help="Zlib-decompress a segment") p_decomp.add_argument("file", help="Path to compressed segment") p_decomp.add_argument("-o", "--output", help="Output file path") p_decomp.set_defaults(func=cmd_decompress) # keys p_keys = subparsers.add_parser("keys", help="Show known keys and derivation experiments") p_keys.set_defaults(func=cmd_keys) args = parser.parse_args() if not args.command: parser.print_help() return 0 try: from Crypto.Cipher import AES except ImportError: print("Error: pycryptodome is required. Install it with:") print(" pip install pycryptodome") return 1 return args.func(args) if __name__ == "__main__": sys.exit(main() or 0)