Seregon/ShadPKG

A tool for deriving PKG packet encryption keys for ps4 written in c++

C++/47.3 KB/No license
scripts/rif_generator_gui.py
ShadPKG / scripts / rif_generator_gui.py
1#!/usr/bin/env python3
2"""
3RIF Generator GUI
4Graphical interface for generating PlayStation 4 RIF files for retail games
5"""
6 
7import tkinter as tk
8from tkinter import ttk, filedialog, messagebox
9import os
10import struct
11import hashlib
12from datetime import datetime
13from pathlib import Path
14 
15class 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 
70Format: [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 
262def main():
263 root = tk.Tk()
264 app = RIFGeneratorGUI(root)
265 root.mainloop()
266 
267if __name__ == "__main__":
268 main()