A tool for deriving PKG packet encryption keys for ps4 written in c++
| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | RIF Generator CLI |
| 4 | Command-line interface for generating PlayStation 4 RIF files for retail games |
| 5 | |
| 6 | Usage: |
| 7 | python rif_generator_cli.py <content_id> [output_directory] |
| 8 | python rif_generator_cli.py --batch <file_with_content_ids> [output_directory] |
| 9 | python rif_generator_cli.py --gui # Launch GUI version |
| 10 | |
| 11 | Examples: |
| 12 | python rif_generator_cli.py EP0001-CUSA12345_00-TESTGAMERETAIL01 |
| 13 | python rif_generator_cli.py UP0001-CUSA67890_00-ANOTHERGAME12345 ./output |
| 14 | python rif_generator_cli.py --batch content_ids.txt ./rif_files |
| 15 | """ |
| 16 | |
| 17 | import sys |
| 18 | import os |
| 19 | import argparse |
| 20 | import struct |
| 21 | import hashlib |
| 22 | from datetime import datetime |
| 23 | from pathlib import Path |
| 24 | |
| 25 | class RIFGenerator: |
| 26 | def __init__(self): |
| 27 | self.base_structure = { |
| 28 | 'magic': b'RIF\x00', |
| 29 | 'version': b'\x00\x01', |
| 30 | 'unknown1': b'\xFF\xFF', |
| 31 | 'padding1': b'\x00' * 12, |
| 32 | 'padding2': b'\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF' |
| 33 | } |
| 34 | |
| 35 | def validate_content_id(self, content_id: str) -> bool: |
| 36 | """Validate PlayStation 4 Content ID format""" |
| 37 | if not content_id: |
| 38 | return False |
| 39 | |
| 40 | # Basic validation - Content ID format: REGION-CUSAXXXXX_XX-PRODUCTCODE |
| 41 | parts = content_id.split('-') |
| 42 | if len(parts) != 3: |
| 43 | print(f"Error: Invalid Content ID format. Expected: REGION-CUSAXXXXX_XX-PRODUCTCODE") |
| 44 | print(f"Example: EP0001-CUSA12345_00-TESTGAMERETAIL01") |
| 45 | return False |
| 46 | |
| 47 | region_part = parts[0] |
| 48 | cusa_version_part = parts[1] |
| 49 | product_part = parts[2] |
| 50 | |
| 51 | # Validate region (first part) |
| 52 | if not region_part.startswith(('EP', 'UP', 'JP')): |
| 53 | print(f"Error: Invalid region '{region_part}'. Must start with EP, UP, or JP") |
| 54 | return False |
| 55 | |
| 56 | # Validate CUSA and version part |
| 57 | if '_' not in cusa_version_part: |
| 58 | print(f"Error: Invalid format. Missing version separator in '{cusa_version_part}'") |
| 59 | return False |
| 60 | |
| 61 | cusa_part, version_part = cusa_version_part.split('_', 1) |
| 62 | |
| 63 | if not cusa_part.startswith('CUSA') or len(cusa_part) != 9: |
| 64 | print(f"Error: Invalid CUSA format '{cusa_part}'. Must be CUSAXXXXX (5 digits)") |
| 65 | return False |
| 66 | |
| 67 | if len(version_part) != 2 or not version_part.isdigit(): |
| 68 | print(f"Error: Invalid version format '{version_part}'. Must be 2 digits") |
| 69 | return False |
| 70 | |
| 71 | # Validate product code |
| 72 | if len(product_part) != 16: |
| 73 | print(f"Error: Invalid product code '{product_part}'. Must be 16 characters") |
| 74 | return False |
| 75 | |
| 76 | return True |
| 77 | |
| 78 | def generate_timestamp(self, content_id: str) -> int: |
| 79 | """Generate a deterministic timestamp based on content ID""" |
| 80 | hash_obj = hashlib.md5(content_id.encode()) |
| 81 | hash_int = int(hash_obj.hexdigest()[:8], 16) |
| 82 | |
| 83 | # Map to a reasonable timestamp range (2013-2024) |
| 84 | base_timestamp = 0x52000000 # Around 2013 |
| 85 | max_offset = 0x10000000 # About 11 years range |
| 86 | |
| 87 | timestamp = base_timestamp + (hash_int % max_offset) |
| 88 | return timestamp |
| 89 | |
| 90 | def generate_rif_content(self, content_id: str) -> bytes: |
| 91 | """Generate RIF file content for a given content ID""" |
| 92 | rif_data = bytearray(1024) # Initialize 1024-byte array with zeros |
| 93 | |
| 94 | offset = 0 |
| 95 | |
| 96 | # Magic number: "RIF\0" |
| 97 | rif_data[offset:offset+4] = self.base_structure['magic'] |
| 98 | offset += 4 |
| 99 | |
| 100 | # Version: 0x0001 |
| 101 | rif_data[offset:offset+2] = self.base_structure['version'] |
| 102 | offset += 2 |
| 103 | |
| 104 | # Unknown field: 0xFFFF |
| 105 | rif_data[offset:offset+2] = self.base_structure['unknown1'] |
| 106 | offset += 2 |
| 107 | |
| 108 | # Padding 1: 12 bytes of zeros |
| 109 | rif_data[offset:offset+12] = self.base_structure['padding1'] |
| 110 | offset += 12 |
| 111 | |
| 112 | # Timestamp (big-endian) |
| 113 | timestamp = self.generate_timestamp(content_id) |
| 114 | rif_data[offset:offset+4] = struct.pack('>I', timestamp) |
| 115 | offset += 4 |
| 116 | |
| 117 | # Padding 2: specific pattern |
| 118 | rif_data[offset:offset+8] = self.base_structure['padding2'] |
| 119 | offset += 8 |
| 120 | |
| 121 | return bytes(rif_data) |
| 122 | |
| 123 | def generate_rif_file(self, content_id: str, output_path: str = None) -> bool: |
| 124 | """Generate a RIF file for the given content ID""" |
| 125 | if not self.validate_content_id(content_id): |
| 126 | return False |
| 127 | |
| 128 | try: |
| 129 | rif_content = self.generate_rif_content(content_id) |
| 130 | |
| 131 | if output_path is None: |
| 132 | output_path = f"{content_id}.rif" |
| 133 | elif os.path.isdir(output_path): |
| 134 | output_path = os.path.join(output_path, f"{content_id}.rif") |
| 135 | |
| 136 | # Create output directory if it doesn't exist |
| 137 | os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) |
| 138 | |
| 139 | with open(output_path, 'wb') as f: |
| 140 | f.write(rif_content) |
| 141 | |
| 142 | timestamp = self.generate_timestamp(content_id) |
| 143 | timestamp_date = datetime.fromtimestamp(timestamp) |
| 144 | |
| 145 | print(f"✓ Generated RIF file: {output_path}") |
| 146 | print(f" Content ID: {content_id}") |
| 147 | print(f" Size: {len(rif_content)} bytes") |
| 148 | print(f" Timestamp: {hex(timestamp)} ({timestamp_date.strftime('%Y-%m-%d %H:%M:%S')})") |
| 149 | |
| 150 | return True |
| 151 | |
| 152 | except Exception as e: |
| 153 | print(f"✗ Error generating RIF file for {content_id}: {e}") |
| 154 | return False |
| 155 | |
| 156 | def generate_batch(self, content_ids_file: str, output_dir: str = ".") -> int: |
| 157 | """Generate RIF files for multiple content IDs from a file""" |
| 158 | try: |
| 159 | with open(content_ids_file, 'r') as f: |
| 160 | content_ids = [line.strip() for line in f if line.strip() and not line.startswith('#')] |
| 161 | |
| 162 | if not content_ids: |
| 163 | print(f"No valid content IDs found in {content_ids_file}") |
| 164 | return 0 |
| 165 | |
| 166 | print(f"Processing {len(content_ids)} content IDs from {content_ids_file}") |
| 167 | print(f"Output directory: {os.path.abspath(output_dir)}") |
| 168 | print("-" * 60) |
| 169 | |
| 170 | success_count = 0 |
| 171 | for i, content_id in enumerate(content_ids, 1): |
| 172 | print(f"[{i}/{len(content_ids)}] Processing: {content_id}") |
| 173 | if self.generate_rif_file(content_id, output_dir): |
| 174 | success_count += 1 |
| 175 | print() |
| 176 | |
| 177 | print("-" * 60) |
| 178 | print(f"Batch processing complete: {success_count}/{len(content_ids)} files generated successfully") |
| 179 | |
| 180 | return success_count |
| 181 | |
| 182 | except FileNotFoundError: |
| 183 | print(f"Error: File '{content_ids_file}' not found") |
| 184 | return 0 |
| 185 | except Exception as e: |
| 186 | print(f"Error processing batch file: {e}") |
| 187 | return 0 |
| 188 | |
| 189 | def print_examples(): |
| 190 | """Print usage examples""" |
| 191 | examples = """ |
| 192 | Content ID Examples: |
| 193 | EP0001-CUSA00074_00-CHILDOFLIGHT0001 (Child of Light - Europe) |
| 194 | UP0001-CUSA07346_00-EAGLEFLIGHTDEMO1 (Eagle Flight Demo - US) |
| 195 | JP0001-CUSA12345_00-TESTGAMERETAIL01 (Test Game - Japan) |
| 196 | |
| 197 | Usage Examples: |
| 198 | # Generate single RIF file in current directory |
| 199 | python rif_generator_cli.py EP0001-CUSA12345_00-TESTGAMERETAIL01 |
| 200 | |
| 201 | # Generate single RIF file in specific directory |
| 202 | python rif_generator_cli.py UP0001-CUSA67890_00-ANOTHERGAME12345 ./output |
| 203 | |
| 204 | # Generate multiple RIF files from a text file |
| 205 | python rif_generator_cli.py --batch content_ids.txt ./rif_files |
| 206 | |
| 207 | # Launch GUI version |
| 208 | python rif_generator_cli.py --gui |
| 209 | |
| 210 | Batch File Format (content_ids.txt): |
| 211 | # Lines starting with # are comments |
| 212 | EP0001-CUSA12345_00-TESTGAMERETAIL01 |
| 213 | UP0001-CUSA67890_00-ANOTHERGAME12345 |
| 214 | JP0001-CUSA11111_00-JAPANGAMETEST001 |
| 215 | """ |
| 216 | print(examples) |
| 217 | |
| 218 | def main(): |
| 219 | parser = argparse.ArgumentParser( |
| 220 | description="PlayStation 4 RIF File Generator", |
| 221 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 222 | epilog="For more examples, use: python rif_generator_cli.py --examples" |
| 223 | ) |
| 224 | |
| 225 | parser.add_argument('content_id', nargs='?', help='PlayStation 4 Content ID') |
| 226 | parser.add_argument('output_dir', nargs='?', default='.', help='Output directory (default: current directory)') |
| 227 | parser.add_argument('--batch', metavar='FILE', help='Generate RIF files for content IDs listed in a file') |
| 228 | parser.add_argument('--gui', action='store_true', help='Launch GUI version') |
| 229 | parser.add_argument('--examples', action='store_true', help='Show usage examples') |
| 230 | parser.add_argument('--validate', metavar='CONTENT_ID', help='Validate a Content ID without generating file') |
| 231 | |
| 232 | args = parser.parse_args() |
| 233 | |
| 234 | if args.examples: |
| 235 | print_examples() |
| 236 | return 0 |
| 237 | |
| 238 | if args.gui: |
| 239 | try: |
| 240 | import subprocess |
| 241 | subprocess.run([sys.executable, 'rif_generator_gui.py']) |
| 242 | except Exception as e: |
| 243 | print(f"Error launching GUI: {e}") |
| 244 | print("Make sure rif_generator_gui.py is in the same directory") |
| 245 | return 0 |
| 246 | |
| 247 | generator = RIFGenerator() |
| 248 | |
| 249 | if args.validate: |
| 250 | if generator.validate_content_id(args.validate): |
| 251 | print(f"✓ Content ID '{args.validate}' is valid") |
| 252 | return 0 |
| 253 | else: |
| 254 | return 1 |
| 255 | |
| 256 | if args.batch: |
| 257 | success_count = generator.generate_batch(args.batch, args.output_dir) |
| 258 | return 0 if success_count > 0 else 1 |
| 259 | |
| 260 | if not args.content_id: |
| 261 | parser.print_help() |
| 262 | print("\nError: Content ID is required") |
| 263 | print("Use --examples to see usage examples") |
| 264 | return 1 |
| 265 | |
| 266 | success = generator.generate_rif_file(args.content_id, args.output_dir) |
| 267 | return 0 if success else 1 |
| 268 | |
| 269 | if __name__ == "__main__": |
| 270 | sys.exit(main()) |