A tool for deriving PKG packet encryption keys for ps4 written in c++
| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | RIF Generator GUI |
| 4 | Graphical interface for generating PlayStation 4 RIF files for retail games |
| 5 | """ |
| 6 | |
| 7 | import tkinter as tk |
| 8 | from tkinter import ttk, filedialog, messagebox |
| 9 | import os |
| 10 | import struct |
| 11 | import hashlib |
| 12 | from datetime import datetime |
| 13 | from pathlib import Path |
| 14 | |
| 15 | class RIFGeneratorGUI: |
| 16 | def __init__(self, root): |
| 17 | self.root = root |
| 18 | self.root.title("PlayStation 4 RIF Generator") |
| 19 | self.root.geometry("600x500") |
| 20 | |
| 21 | # Variables |
| 22 | self.content_id_var = tk.StringVar() |
| 23 | self.output_dir_var = tk.StringVar(value=os.getcwd()) |
| 24 | self.game_title_var = tk.StringVar() |
| 25 | |
| 26 | self.setup_ui() |
| 27 | |
| 28 | def setup_ui(self): |
| 29 | # Main frame |
| 30 | main_frame = ttk.Frame(self.root, padding="10") |
| 31 | main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) |
| 32 | |
| 33 | # Title |
| 34 | title_label = ttk.Label(main_frame, text="PlayStation 4 RIF File Generator", |
| 35 | font=('Arial', 16, 'bold')) |
| 36 | title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20)) |
| 37 | |
| 38 | # Content ID section |
| 39 | ttk.Label(main_frame, text="Content ID:").grid(row=1, column=0, sticky=tk.W, pady=5) |
| 40 | content_id_entry = ttk.Entry(main_frame, textvariable=self.content_id_var, width=40) |
| 41 | content_id_entry.grid(row=1, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) |
| 42 | |
| 43 | # Game title section |
| 44 | ttk.Label(main_frame, text="Game Title (optional):").grid(row=2, column=0, sticky=tk.W, pady=5) |
| 45 | game_title_entry = ttk.Entry(main_frame, textvariable=self.game_title_var, width=40) |
| 46 | game_title_entry.grid(row=2, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) |
| 47 | |
| 48 | # Output directory section |
| 49 | ttk.Label(main_frame, text="Output Directory:").grid(row=3, column=0, sticky=tk.W, pady=5) |
| 50 | output_dir_entry = ttk.Entry(main_frame, textvariable=self.output_dir_var, width=30) |
| 51 | output_dir_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=5) |
| 52 | |
| 53 | browse_btn = ttk.Button(main_frame, text="Browse", command=self.browse_directory) |
| 54 | browse_btn.grid(row=3, column=2, padx=(5, 0), pady=5) |
| 55 | |
| 56 | # Content ID examples |
| 57 | examples_frame = ttk.LabelFrame(main_frame, text="Content ID Examples", padding="10") |
| 58 | examples_frame.grid(row=4, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) |
| 59 | |
| 60 | examples_text = tk.Text(examples_frame, height=8, width=70) |
| 61 | examples_text.grid(row=0, column=0, sticky=(tk.W, tk.E)) |
| 62 | |
| 63 | examples_content = """Examples of PlayStation 4 Content IDs: |
| 64 | |
| 65 | • EP0001-CUSA00074_00-CHILDOFLIGHT0001 (Child of Light) |
| 66 | • UP0001-CUSA07346_00-EAGLEFLIGHTDEMO1 (Eagle Flight Demo) |
| 67 | • EP0002-CUSA03529_00-GHLIVERETAILDEMO (Guitar Hero Live Demo) |
| 68 | • EP0006-CUSA00276_00-FIFA2014DEMOGAME (FIFA 2014 Demo) |
| 69 | |
| 70 | Format: [Region]-[CUSA_ID]_[Version]-[Product_Code] |
| 71 | • Region: EP (Europe), UP (US), JP (Japan) |
| 72 | • CUSA_ID: Unique game identifier |
| 73 | • Version: Usually 00 |
| 74 | • Product_Code: 16-character product identifier""" |
| 75 | |
| 76 | examples_text.insert(tk.END, examples_content) |
| 77 | examples_text.config(state=tk.DISABLED) |
| 78 | |
| 79 | # Buttons frame |
| 80 | buttons_frame = ttk.Frame(main_frame) |
| 81 | buttons_frame.grid(row=5, column=0, columnspan=3, pady=20) |
| 82 | |
| 83 | generate_btn = ttk.Button(buttons_frame, text="Generate RIF File", |
| 84 | command=self.generate_rif, style='Accent.TButton') |
| 85 | generate_btn.pack(side=tk.LEFT, padx=(0, 10)) |
| 86 | |
| 87 | validate_btn = ttk.Button(buttons_frame, text="Validate Content ID", |
| 88 | command=self.validate_content_id) |
| 89 | validate_btn.pack(side=tk.LEFT, padx=(0, 10)) |
| 90 | |
| 91 | clear_btn = ttk.Button(buttons_frame, text="Clear", command=self.clear_fields) |
| 92 | clear_btn.pack(side=tk.LEFT) |
| 93 | |
| 94 | # Status frame |
| 95 | status_frame = ttk.LabelFrame(main_frame, text="Status", padding="10") |
| 96 | status_frame.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=10) |
| 97 | |
| 98 | self.status_text = tk.Text(status_frame, height=6, width=70) |
| 99 | self.status_text.grid(row=0, column=0, sticky=(tk.W, tk.E)) |
| 100 | |
| 101 | # Scrollbar for status |
| 102 | status_scrollbar = ttk.Scrollbar(status_frame, orient=tk.VERTICAL, command=self.status_text.yview) |
| 103 | status_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) |
| 104 | self.status_text.config(yscrollcommand=status_scrollbar.set) |
| 105 | |
| 106 | # Configure grid weights |
| 107 | main_frame.columnconfigure(1, weight=1) |
| 108 | self.root.columnconfigure(0, weight=1) |
| 109 | self.root.rowconfigure(0, weight=1) |
| 110 | |
| 111 | def browse_directory(self): |
| 112 | directory = filedialog.askdirectory(initialdir=self.output_dir_var.get()) |
| 113 | if directory: |
| 114 | self.output_dir_var.set(directory) |
| 115 | |
| 116 | def validate_content_id(self): |
| 117 | content_id = self.content_id_var.get().strip() |
| 118 | |
| 119 | if not content_id: |
| 120 | self.log_status("Please enter a Content ID") |
| 121 | return False |
| 122 | |
| 123 | # Basic validation - Content ID format: REGION-CUSAXXXXX_XX-PRODUCTCODE |
| 124 | parts = content_id.split('-') |
| 125 | if len(parts) != 3: |
| 126 | self.log_status("Invalid Content ID format. Expected: REGION-CUSAXXXXX_XX-PRODUCTCODE") |
| 127 | self.log_status("Example: EP0001-CUSA12345_00-TESTGAMERETAIL01") |
| 128 | return False |
| 129 | |
| 130 | region_part = parts[0] |
| 131 | cusa_version_part = parts[1] |
| 132 | product_part = parts[2] |
| 133 | |
| 134 | # Validate region (first part) |
| 135 | if not region_part.startswith(('EP', 'UP', 'JP')): |
| 136 | self.log_status(f"Invalid region '{region_part}'. Must start with EP, UP, or JP") |
| 137 | return False |
| 138 | |
| 139 | # Validate CUSA and version part |
| 140 | if '_' not in cusa_version_part: |
| 141 | self.log_status(f"Invalid format. Missing version separator in '{cusa_version_part}'") |
| 142 | return False |
| 143 | |
| 144 | cusa_part, version_part = cusa_version_part.split('_', 1) |
| 145 | |
| 146 | if not cusa_part.startswith('CUSA') or len(cusa_part) != 9: |
| 147 | self.log_status(f"Invalid CUSA format '{cusa_part}'. Must be CUSAXXXXX (5 digits)") |
| 148 | return False |
| 149 | |
| 150 | if len(version_part) != 2 or not version_part.isdigit(): |
| 151 | self.log_status(f"Invalid version format '{version_part}'. Must be 2 digits") |
| 152 | return False |
| 153 | |
| 154 | # Validate product code |
| 155 | if len(product_part) != 16: |
| 156 | self.log_status(f"Invalid product code '{product_part}'. Must be 16 characters") |
| 157 | return False |
| 158 | |
| 159 | self.log_status(f"Content ID '{content_id}' is valid!") |
| 160 | return True |
| 161 | |
| 162 | def generate_timestamp(self, content_id: str) -> int: |
| 163 | """Generate a deterministic timestamp based on content ID""" |
| 164 | hash_obj = hashlib.md5(content_id.encode()) |
| 165 | hash_int = int(hash_obj.hexdigest()[:8], 16) |
| 166 | |
| 167 | # Map to a reasonable timestamp range (2013-2024) |
| 168 | base_timestamp = 0x52000000 # Around 2013 |
| 169 | max_offset = 0x10000000 # About 11 years range |
| 170 | |
| 171 | timestamp = base_timestamp + (hash_int % max_offset) |
| 172 | return timestamp |
| 173 | |
| 174 | def generate_rif_content(self, content_id: str) -> bytes: |
| 175 | """Generate RIF file content for a given content ID""" |
| 176 | rif_data = bytearray(1024) # Initialize 1024-byte array with zeros |
| 177 | |
| 178 | offset = 0 |
| 179 | |
| 180 | # Magic number: "RIF\0" |
| 181 | rif_data[offset:offset+4] = b'RIF\x00' |
| 182 | offset += 4 |
| 183 | |
| 184 | # Version: 0x0001 |
| 185 | rif_data[offset:offset+2] = b'\x00\x01' |
| 186 | offset += 2 |
| 187 | |
| 188 | # Unknown field: 0xFFFF |
| 189 | rif_data[offset:offset+2] = b'\xFF\xFF' |
| 190 | offset += 2 |
| 191 | |
| 192 | # Padding 1: 12 bytes of zeros |
| 193 | rif_data[offset:offset+12] = b'\x00' * 12 |
| 194 | offset += 12 |
| 195 | |
| 196 | # Timestamp (big-endian) |
| 197 | timestamp = self.generate_timestamp(content_id) |
| 198 | rif_data[offset:offset+4] = struct.pack('>I', timestamp) |
| 199 | offset += 4 |
| 200 | |
| 201 | # Padding 2: specific pattern |
| 202 | rif_data[offset:offset+8] = b'\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF' |
| 203 | offset += 8 |
| 204 | |
| 205 | return bytes(rif_data) |
| 206 | |
| 207 | def generate_rif(self): |
| 208 | content_id = self.content_id_var.get().strip() |
| 209 | output_dir = self.output_dir_var.get().strip() |
| 210 | game_title = self.game_title_var.get().strip() |
| 211 | |
| 212 | if not self.validate_content_id(): |
| 213 | return |
| 214 | |
| 215 | if not output_dir or not os.path.exists(output_dir): |
| 216 | self.log_status("Please select a valid output directory") |
| 217 | return |
| 218 | |
| 219 | try: |
| 220 | # Generate RIF content |
| 221 | rif_content = self.generate_rif_content(content_id) |
| 222 | |
| 223 | # Create filename |
| 224 | filename = f"{content_id}.rif" |
| 225 | filepath = os.path.join(output_dir, filename) |
| 226 | |
| 227 | # Write file |
| 228 | with open(filepath, 'wb') as f: |
| 229 | f.write(rif_content) |
| 230 | |
| 231 | # Log success |
| 232 | self.log_status(f"Successfully generated RIF file: {filename}") |
| 233 | self.log_status(f"Location: {filepath}") |
| 234 | self.log_status(f"Size: {len(rif_content)} bytes") |
| 235 | |
| 236 | if game_title: |
| 237 | self.log_status(f"Game: {game_title}") |
| 238 | |
| 239 | # Show timestamp info |
| 240 | timestamp = self.generate_timestamp(content_id) |
| 241 | timestamp_date = datetime.fromtimestamp(timestamp) |
| 242 | self.log_status(f"Generated timestamp: {hex(timestamp)} ({timestamp_date})") |
| 243 | |
| 244 | messagebox.showinfo("Success", f"RIF file generated successfully!\n\nFile: {filename}\nLocation: {filepath}") |
| 245 | |
| 246 | except Exception as e: |
| 247 | error_msg = f"Error generating RIF file: {str(e)}" |
| 248 | self.log_status(error_msg) |
| 249 | messagebox.showerror("Error", error_msg) |
| 250 | |
| 251 | def clear_fields(self): |
| 252 | self.content_id_var.set("") |
| 253 | self.game_title_var.set("") |
| 254 | self.status_text.delete(1.0, tk.END) |
| 255 | |
| 256 | def log_status(self, message): |
| 257 | timestamp = datetime.now().strftime("%H:%M:%S") |
| 258 | self.status_text.insert(tk.END, f"[{timestamp}] {message}\n") |
| 259 | self.status_text.see(tk.END) |
| 260 | self.root.update_idletasks() |
| 261 | |
| 262 | def main(): |
| 263 | root = tk.Tk() |
| 264 | app = RIFGeneratorGUI(root) |
| 265 | root.mainloop() |
| 266 | |
| 267 | if __name__ == "__main__": |
| 268 | main() |