-
Notifications
You must be signed in to change notification settings - Fork 16
/
title.py
450 lines (400 loc) · 15.9 KB
/
title.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
from common import *
class TicketView:
"""Creates a ticket view from the Ticket object ``tik''."""
class TikviewStruct(Struct):
__endian__ = Struct.BE
def __format__(self):
self.view = Struct.uint32
self.ticketid = Struct.uint64
self.devicetype = Struct.uint32
self.titleid = Struct.uint64
self.accessmask = Struct.uint16
self.reserved = Struct.string(0x3C)
self.cidxmask = Struct.string(0x40)
self.padding = Struct.uint16
self.limits = Struct.string(96)
def __init__(self, tik):
self.tikview = self.TikviewStruct()
self.tikview.view = 0
self.tikview.ticketid = tik.tik.tikid
self.tikview.devicetype = tik.tik.console
self.tikview.titleid = tik.getTitleID()
self.tikview.accessmask = 0xFFFF # This needs to be changed, I'm sure...
self.tikview.reserved = "\0" * 0x3C
self.tikview.cidxmask = "\xFF" * 0x40 # This needs to be changed, I'm sure...
self.tikview.padding = 0x0000
self.tikview.limits = "\0" * 96
def __str__(self):
out = ""
out += " Ticket View:\n"
out += " Title ID: %08X-%08X\n" % (self.tikview.titleid >> 32, self.tikview.titleid & 0xFFFFFFFF)
out += " Device type: %08X\n" % self.tikview.devicetype
out += " Ticket ID: %016X\n" % self.tikview.ticketid
out += " Access Mask: %04X\n" % self.tikview.accessmask
return out
class Ticket(WiiObject):
"""Creates a ticket from the filename defined in f. This may take a longer amount of time than expected, as it also decrypts the title key. Now supports Korean tickets (but their title keys stay Korean on dump)."""
class TicketStruct(Struct):
__endian__ = Struct.BE
def __format__(self):
self.rsaexp = Struct.uint32
self.rsamod = Struct.string(256)
self.padding1 = Struct.string(60)
self.rsaid = Struct.string(64)
self.padding2 = Struct.string(63)
self.enctitlekey = Struct.string(16)
self.unk1 = Struct.uint8
self.tikid = Struct.uint64
self.console = Struct.uint32
self.titleid = Struct.uint64
self.unk2 = Struct.uint16
self.dlc = Struct.uint16
self.unk3 = Struct.uint64
self.commonkey_index = Struct.uint8
self.reserved = Struct.string(80)
self.unk3 = Struct.uint16
self.limits = Struct.string(96)
self.unk4 = Struct.uint8
def __init__(self):
self.tik = self.TicketStruct()
self.tik.rsaexp = 0x10001
self.tik.rsamod = "\x00" * 256
self.tik.padding1 = "\x00" * 60
self.tik.rsaid = "\x00" * 64
self.tik.padding2 = "\x00" * 63
self.tik.enctitlekey = "\x00" * 16
self.tik.titleid = 0x0000000100000000
self.tik.reserved = "\x00" * 80
self.tik.limits = "\x00" * 96
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
koreankey = "\x63\xB8\x2B\xB4\xF4\x61\x4E\x2E\x13\xF2\xFE\xFB\xBA\x4C\x9B\x7E"
if(self.tik.commonkey_index == 1): #korean, kekekekek!
commonkey = koreankey
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
def _load(self, data):
self.tik.unpack(data[:len(self.tik)])
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
koreankey = "\x63\xB8\x2B\xB4\xF4\x61\x4E\x2E\x13\xF2\xFE\xFB\xBA\x4C\x9B\x7E"
if(self.tik.commonkey_index == 1): #korean, kekekekek!
commonkey = koreankey
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
return self
def getTitleKey(self):
"""Returns a string containing the title key."""
return self.titlekey
def getTitleID(self):
"""Returns a long integer with the title id."""
return self.tik.titleid
def setTitleID(self, titleid):
"""Sets the title id of the ticket from the long integer passed in titleid."""
self.tik.titleid = titleid
commonkey = "\xEB\xE4\x2A\x22\x5E\x85\x93\xE4\x48\xD9\xC5\x45\x73\x81\xAA\xF7"
koreankey = "\x63\xB8\x2B\xB4\xF4\x61\x4E\x2E\x13\xF2\xFE\xFB\xBA\x4C\x9B\x7E"
if(self.tik.commonkey_index == 1): #korean, kekekekek!
commonkey = koreankey
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey) #This changes the decrypted title key!
def __str__(self):
out = ""
out += " Ticket:\n"
out += " Title ID: %08x-%08x\n" % (self.getTitleID() >> 32, self.getTitleID() & 0xFFFFFFFF)
out += " Title key IV: "
out += hexdump(struct.pack(">Q", self.getTitleID()) + "\x00\x00\x00\x00\x00\x00\x00\x00")
out += "\n"
out += " Title key (encrypted): "
out += hexdump(self.tik.enctitlekey)
out += "\n"
out += " Title key (decrypted): "
out += hexdump(self.getTitleKey())
out += "\n"
return out
def fakesign(self):
"""Fakesigns (or Trucha signs) and dumps the ticket to either fn, if not empty, or overwriting the source if empty. Returns the output filename."""
self.rsamod = self.rsamod = "\x00" * 256
for i in range(65536):
self.tik.unk2 = i
if(Crypto().createSHAHashHex(self.tik.pack())[:2] == "00"):
break
if(i == 65535):
raise ValueError("Failed to fakesign. Aborting...")
def _dump(self):
"""Dumps the ticket to either fn, if not empty, or overwriting the source if empty. **Does not fakesign.** Returns the output filename."""
return self.tik.pack()
def __len__(self):
return len(self.tik)
class TMD(WiiObject):
"""This class allows you to edit TMDs. TMD (Title Metadata) files are used in many places to hold information about titles. The parameter f to the initialization is the filename to open and create a TMD from."""
class TMDContent(Struct):
__endian__ = Struct.BE
def __format__(self):
self.cid = Struct.uint32
self.index = Struct.uint16
self.type = Struct.uint16
self.size = Struct.uint64
self.hash = Struct.string(20)
class TMDStruct(Struct):
__endian__ = Struct.BE
def __format__(self):
self.rsaexp = Struct.uint32
self.rsamod = Struct.string(256)
self.padding1 = Struct.string(60)
self.rsaid = Struct.string(64)
self.version = Struct.uint8[4]
self.iosversion = Struct.uint64
self.titleid = Struct.uint64
self.title_type = Struct.uint32
self.group_id = Struct.uint16
self.reserved = Struct.string(62)
self.access_rights = Struct.uint32
self.title_version = Struct.uint16
self.numcontents = Struct.uint16
self.boot_index = Struct.uint16
self.padding2 = Struct.uint16
#contents follow this
def _load(self, data):
self.tmd.unpack(data[:len(self.tmd)])
pos = len(self.tmd)
for i in range(self.tmd.numcontents):
cont = self.TMDContent()
cont.unpack(data[pos:pos + len(cont)])
pos += len(cont)
self.contents.append(cont)
def __init__(self):
self.tmd = self.TMDStruct()
self.tmd.titleid = 0x0000000100000000
self.contents = []
def getContents(self):
"""Returns a list of contents. Each content is an object with the members "size", the size of the content's decrypted data; "cid", the content id; "type", the type of the content (0x8001 for shared, 0x0001 for standard, more possible), and a 20 byte string called "hash"."""
return self.contents
def setContents(self, contents):
"""This sets the contents in the TMD to the contents you provide in the contents parameter. Also updates the TMD to the appropraite amount of contents."""
self.contents = contents
self.tmd.numcontents = len(contents)
def __str__(self):
out = ""
out += " TMD:\n"
out += " Versions: (todo) %u, CA CRL (todo) %u, Signer CRL (todo) %u, System %u-%u\n" % (0, 0, 0, self.getIOSVersion() >> 32, self.getIOSVersion() & 0xFFFFFFFF)
out += " Title ID: %08x-%08x\n" % (self.getTitleID() >> 32, self.getTitleID() & 0xFFFFFFFF)
out += " Title Type: %u\n" % self.tmd.title_type
out += " Group ID: '%02u'\n" % self.tmd.group_id
out += " Access Rights: 0x%08x\n" % self.tmd.access_rights
out += " Title Version: 0x%04x\n" % self.tmd.title_version
out += " Boot Index: %u\n" % self.getBootIndex()
out += " Contents: \n"
out += " ID Index Type Size Hash\n"
contents = self.getContents()
for i in range(len(contents)):
out += " %08X %-4u 0x%04x %#-12x " % (contents[i].cid, contents[i].index, contents[i].type, contents[i].size)
out += hexdump(contents[i].hash)
out += "\n"
return out
def __len__(self):
contents = self.getContents()
sz = len(self.tmd)
for i in range(len(contents)):
sz += len(contents[i])
return sz
def fakesign(self):
"""Dumps the TMD to the filename specified in fn, if not empty. If that is empty, it overwrites the original. This fakesigns the TMD, but does not update the hashes and the sizes, that is left as a job for you. Returns output filename."""
for i in range(65536):
self.tmd.padding2 = i
data = "" #gotta reset it every time
data += self.tmd.pack()
for i in range(self.tmd.numcontents):
data += self.contents[i].pack()
if(Crypto().createSHAHashHex(data)[:2] == "00"):
break
if(i == 65535):
raise ValueError("Failed to fakesign! Aborting...")
def _dump(self):
"""Same as the :dump: function, but does not fakesign the TMD. Also returns output filename."""
data = ""
data += self.tmd.pack()
for i in range(self.tmd.numcontents):
data += self.contents[i].pack()
return data
def getTitleID(self):
"""Returns the long integer title id."""
return self.tmd.titleid
def setTitleID(self, titleid):
"""Sets the title id to the long integer specified in the parameter titleid."""
self.tmd.titleid = titleid
def getIOSVersion(self):
"""Returns the IOS version the title will run off of."""
return self.tmd.iosversion
def setIOSVersion(self, version):
"""Sets the IOS version the title will run off of to the arguement version."""
self.tmd.iosverison = version
def getBootIndex(self):
"""Returns the boot index of the TMD."""
return self.tmd.boot_index
def setBootIndex(self, index):
"""Sets the boot index of the TMD to the value of index."""
self.tmd.boot_index = index
class Title(WiiArchive):
def __init__(self, boot2 = False):
self.tmd = TMD()
self.tik = Ticket()
self.contents = []
self.boot2 = False
self.cert = ""
def _load(self, data):
if(self.boot2 != True):
headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding = struct.unpack('>I4s6I32s', data[:64])
pos = 64
else:
headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', data[:32])
pos = 32
rawcert = data[pos:pos + certsize]
pos += certsize
if(self.boot2 != True):
if(certsize % 64 != 0):
pos += 64 - (certsize % 64)
self.cert = rawcert
rawtik = data[pos:pos + tiksize]
pos += tiksize
if(self.boot2 != True):
if(tiksize % 64 != 0):
pos += 64 - (tiksize % 64)
self.tik = Ticket.load(rawtik)
rawtmd = data[pos:pos + tmdsize]
pos += tmdsize
if(self.boot2 == True):
pos = data_offset
else:
pos += 64 - (tmdsize % 64)
self.tmd = TMD.load(rawtmd)
titlekey = self.tik.getTitleKey()
contents = self.tmd.getContents()
for i in range(0, len(contents)):
tmpsize = contents[i].size
if(tmpsize % 16 != 0):
tmpsize += 16 - (tmpsize % 16)
encdata = data[pos:pos + tmpsize]
pos += tmpsize
decdata = Crypto().decryptContent(titlekey, contents[i].index, encdata)
self.contents.append(decdata)
if(tmpsize % 64 != 0):
pos += 64 - (tmpsize % 64)
def _loadDir(self, dir):
origdir = os.getcwd()
os.chdir(dir)
self.tmd = TMD.loadFile("tmd")
self.tik = Ticket.loadFile("tik")
self.cert = open("cert", "rb").read()
contents = self.tmd.getContents()
for i in range(len(contents)):
self.contents.append(open("%08x.app" % i, "rb").read())
os.chdir(origdir)
def _dumpDir(self, dir, useidx = True, decrypt = True):
origdir = os.getcwd()
if not os.path.isdir(dir):
os.mkdir(dir)
os.chdir(dir)
contents = self.tmd.getContents()
titlekey = self.tik.getTitleKey()
for i, content in enumerate(contents):
if(useidx == True):
output = content.index
else:
output = content.cid
if(decrypt == True):
open("%08x.app" % output, "wb").write(self.contents[i][:contents[i].size])
else:
open("%08x.app" % output, "wb").write(Crypto.encryptContent(titlekey, content.index, self.contents[content.index]))
self.tmd.dumpFile("tmd")
self.tik.dumpFile("tik")
open("cert", "wb").write(self.cert)
os.chdir(origdir)
def _dump(self, fakesign = True):
titlekey = self.tik.getTitleKey()
contents = self.tmd.getContents()
apppack = ""
for i, content in enumerate(contents):
if(fakesign):
content.hash = str(Crypto.createSHAHash(self.contents[content.index]))
content.size = len(self.contents[content.index])
encdata = Crypto.encryptContent(titlekey, content.index, self.contents[content.index])
apppack += encdata
if(len(encdata) % 64 != 0):
apppack += "\x00" * (64 - (len(encdata) % 64))
if(fakesign):
self.tmd.setContents(contents)
self.tmd.fakesign()
self.tik.fakesign()
rawtmd = self.tmd.dump()
rawcert = self.cert
rawtik = self.tik.dump()
sz = 0
for i in range(len(contents)):
sz += contents[i].size
if(sz % 64 != 0):
sz += 64 - (contents[i].size % 64)
if(self.boot2 != True):
pack = struct.pack('>I4s6I', 32, "Is\x00\x00", len(rawcert), 0, len(rawtik), len(rawtmd), sz, 0)
pack += "\x00" * 32
else:
pack = struct.pack('>IIIII12s', 32, align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40), len(rawcert), len(rawtik), len(rawtmd), "\x00" * 12)
pack += rawcert
if(len(rawcert) % 64 != 0 and self.boot2 != True):
pack += "\x00" * (64 - (len(rawcert) % 64))
pack += rawtik
if(len(rawtik) % 64 != 0 and self.boot2 != True):
pack += "\x00" * (64 - (len(rawtik) % 64))
pack += rawtmd
if(len(rawtmd) % 64 != 0 and self.boot2 != True):
pack += "\x00" * (64 - (len(rawtmd) % 64))
if(self.boot2 == True):
pack += "\x00" * (align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40) - (len(rawcert) + len(rawtik) + len(rawtmd)))
pack += apppack
return pack
def fakesign(self):
self.tik.fakesign()
self.tmd.fakesign()
def __getitem__(self, idx):
return self.contents[idx]
def __setitem__(self, idx, value):
self.contents[idx] = value
def __str__(self):
out = ""
out += "Wii WAD:\n"
out += str(self.tmd)
out += str(self.tik)
return out
WAD = Title
class NUS:
"""This class can download titles from NUS, or Nintendo Update Server. The titleid parameter is the long integer version of the title to download. The version parameter is optional and specifies the version to download. If version is not given, it is assumed to be the latest version on NUS."""
url = "http://nus.cdn.shop.wii.com/ccs/download/"
def download(self, titleid, version = None):
certs = ""
tmd = TMD.load(urllib.urlopen("http://nus.cdn.shop.wii.com/ccs/download/0000000100000002/tmd.289").read())
tik = Ticket.load(urllib.urlopen("http://nus.cdn.shop.wii.com/ccs/download/0000000100000002/cetk").read())
rawtmd = urllib.urlopen("http://nus.cdn.shop.wii.com/ccs/download/0000000100000002/tmd.289").read()
rawtik = urllib.urlopen("http://nus.cdn.shop.wii.com/ccs/download/0000000100000002/cetk").read()
certs += rawtik[0x2A4:0x2A4 + 0x300] #XS
certs += rawtik[0x2A4 + 0x300:] #CA (tik)
certs += rawtmd[0x328:0x328 + 0x300] #CP
#certs += tik.dump()[0x2A4:0x2A4 + 0x300] #XS
#certs += tik.dump()[0x2A4 + 0x300:] #CA (tik)
#certs += tmd.dump()[0x328:0x328 + 0x300] #CP
if(Crypto.createMD5HashHex(certs) != "7ff50e2733f7a6be1677b6f6c9b625dd"):
raise ValueError("Failed to create certs! MD5 mistatch.")
if(version == None):
versionstring = ""
else:
versionstring = ".%u" % version
titleurl = self.url + "%08x%08x/" % (titleid >> 32, titleid & 0xFFFFFFFF)
tmd = TMD.load(urllib.urlopen(titleurl + "tmd" + versionstring).read())
tik = Ticket.load(urllib.urlopen(titleurl + "cetk").read())
title = Title()
title.tmd = tmd
title.tik = tik
title.cert = certs
contents = tmd.getContents()
for content in contents:
encdata = urllib.urlopen(titleurl + ("%08x" % content.cid)).read(content.size)
decdata = Crypto.decryptContent(tik.titlekey, content.index, encdata)
if(Crypto.validateSHAHash(decdata, content.hash) == 0):
raise ValueError("Decryption failed! SHA1 mismatch.")
title.contents.append(decdata)
return title
download = classmethod(download)