lotsa shit but should all work internally nowwith U8, TMD, ticket on the new API;

This commit is contained in:
Xuzz 2009-07-03 01:57:24 -07:00
parent 126d9f5913
commit 66dea29df1
5 changed files with 184 additions and 229 deletions

1
Wii.py
View File

@ -2,7 +2,6 @@ __all__ = []
from common import *
from formats import *
from banner import *
from title import *
from disc import *
from image import *

View File

@ -2,7 +2,7 @@ from common import *
from title import *
import zlib
class U8():
class U8(WiiArchive):
"""This class can unpack and pack U8 archives, which are used all over the Wii. They are often used in Banners and contents in Downloadable Titles. Please remove all headers and compression first, kthx.
The f parameter is either the source folder to pack, or the source file to unpack."""
@ -21,48 +21,9 @@ class U8():
self.name_offset = Struct.uint16
self.data_offset = Struct.uint32
self.size = Struct.uint32
def __init__(self, f):
self.f = f
def _pack(self, file, recursion, is_root = 0): #internal
node = self.U8Node()
node.name_offset = len(self.strings)
if(is_root != 1):
self.strings += (file)
self.strings += ("\x00")
if(os.path.isdir(file)):
node.type = 0x0100
node.data_offset = recursion - 1
recursion += 1
files = sorted(os.listdir(file))
#if(sorted(files) == ["banner.bin", "icon.bin", "sound.bin"]):
# files = ["icon.bin", "banner.bin", "sound.bin"]
oldsz = len(self.nodes)
if(is_root != 1):
self.nodes.append(node)
os.chdir(file)
for entry in files:
if(entry != ".DS_Store" and entry[len(entry) - 4:] != "_out"):
self._pack(entry, recursion)
os.chdir("..")
self.nodes[oldsz].size = len(self.nodes) + 1
else:
f = open(file, "rb")
data = f.read()
f.close()
sz = len(data)
data += "\x00" * (align(sz, 32) - sz) #32 seems to work best for fuzzyness? I'm still really not sure
node.data_offset = len(self.data)
self.data += data
node.size = sz
node.type = 0x0000
if(is_root != 1):
self.nodes.append(node)
def pack(self, fn = ""):
def __init__(self):
self.files = []
def _dump(self):
"""This function will pack a folder into a U8 archive. The output file name is specified in the parameter fn. If fn is an empty string, the filename is deduced from the input folder name. Returns the output filename.
This creates valid U8 archives for all purposes."""
@ -73,56 +34,94 @@ class U8():
header.rootnode_offset = 0x20
header.zeroes = "\x00" * 16
self.nodes = []
self.strings = "\x00"
self.data = ""
origdir = os.getcwd()
os.chdir(self.f)
self._pack(".", 0, 1)
os.chdir(origdir)
nodes = []
strings = "\x00"
data = ''
header.header_size = (len(self.nodes) + 1) * len(rootnode) + len(self.strings)
for item, value in self.files:
node = self.U8Node()
node.name_offset = len(strings)
recursion = item.count('/')
if(recursion < 0):
recursion = 0
name = item[item.rfind('/') + 1:]
strings += name + '\x00'
if(value == None):
node.type = 0x0100
node.data_offset = recursion
this_length = 0
for one, two in self.files:
subdirs = one
if(subdirs.find(item) != -1):
this_length += 1
node.size = len(nodes) + this_length + 1
else:
sz = len(value)
value += "\x00" * (align(sz, 32) - sz) #32 seems to work best for fuzzyness? I'm still really not sure
node.data_offset = len(data)
data += value
node.size = sz
node.type = 0x0000
nodes.append(node)
header.header_size = (len(nodes) + 1) * len(rootnode) + len(strings)
header.data_offset = align(header.header_size + header.rootnode_offset, 64)
rootnode.size = len(self.nodes) + 1
rootnode.size = len(nodes) + 1
rootnode.type = 0x0100
for i in range(len(self.nodes)):
if(self.nodes[i].type == 0x0000):
self.nodes[i].data_offset += header.data_offset
if(fn == ""):
if(self.f[len(self.f) - 4:] == "_out"):
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f)[:-4].replace("_", ".")
for i in range(len(nodes)):
if(nodes[i].type == 0x0000):
nodes[i].data_offset += header.data_offset
fd = ''
fd += header.pack()
fd += rootnode.pack()
for nodeobj in nodes:
fd += nodeobj.pack()
fd += strings
fd += "\x00" * (header.data_offset - header.rootnode_offset - header.header_size)
fd += data
return fd
def _dumpDir(self, dir):
if(not os.path.isdir(dir)):
os.mkdir(dir)
os.chdir(dir)
for item, data in self.files:
if(data == None):
if(not os.path.isdir(item)):
os.mkdir(item)
else:
fn = self.f
fd = open(fn, "wb")
fd.write(header.pack())
fd.write(rootnode.pack())
for node in self.nodes:
fd.write(node.pack())
fd.write(self.strings)
fd.write("\x00" * (header.data_offset - header.rootnode_offset - header.header_size))
fd.write(self.data)
fd.close()
return fn
def unpack(self, fn = ""):
"""This will unpack the U8 archive specified by the initialization parameter into either the folder specified by the parameter fn, or into a folder created with this formula:
``filename_extension_out``
This will recreate the directory structure, including the initial root folder in the U8 archive (such as "arc" or "meta"). Returns the output directory name."""
data = open(self.f, "rb").read()
open(item, "wb").write(data)
os.chdir("..")
def _loadDir(self, dir):
try:
self._tmpPath += ''
except:
self._tmpPath = ''
os.chdir(dir)
entries = os.listdir(".")
for entry in entries:
if(os.path.isdir(entry)):
self.files.append((self._tmpPath + entry, None))
self._tmpPath += entry + '/'
self._loadDir(entry)
elif(os.path.isfile(entry)):
data = open(entry, "rb").read()
self.files.append((self._tmpPath + entry, data))
os.chdir("..")
self._tmpPath = self._tmpPath[:self._tmpPath.find('/') + 1]
def _load(self, data):
offset = 0
header = self.U8Header()
header.unpack(data[offset:offset + len(header)])
offset += len(header)
if(header.tag != "U\xAA8-"):
raise NameError("Bad U8 Tag")
assert header.tag == "U\xAA8-"
offset = header.rootnode_offset
rootnode = self.U8Node()
@ -139,95 +138,52 @@ class U8():
strings = data[offset:offset + header.data_offset - len(header) - (len(rootnode) * rootnode.size)]
offset += len(strings)
if(fn == ""):
fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f).replace(".", "_") + "_out"
try:
origdir = os.getcwd()
os.mkdir(fn)
except:
pass
os.chdir(fn)
recursion = [rootnode.size]
recursiondir = []
counter = 0
for node in nodes:
counter += 1
name = strings[node.name_offset:].split('\0', 1)[0]
if(node.type == 0x0100): #folder
if(node.type == 0x0100): # folder
recursion.append(node.size)
try:
os.mkdir(name)
except:
pass
os.chdir(name)
continue
elif(node.type == 0): #file
file = open(name, "wb")
file.write(data[node.data_offset:node.data_offset + node.size])
recursiondir.append(name)
assert len(recursion) == node.data_offset + 2 #bad idea?
self.files.append(('/'.join(recursiondir), None))
elif(node.type == 0): # file
self.files.append(('/'.join(recursiondir) + '/' + name, data[node.data_offset:node.data_offset + node.size]))
offset += node.size
else: #unknown
pass #ignore
else: # unknown
pass
sz = recursion.pop()
if(sz == counter + 1):
os.chdir("..")
else:
if(sz != counter + 1):
recursion.append(sz)
os.chdir("..")
os.chdir(origdir)
return fn
else:
recursiondir.pop()
def __str__(self):
data = open(self.f, "rb").read()
offset = 0
header = self.U8Header()
header.unpack(data[offset:offset + len(header)])
offset += len(header)
if(header.tag != "U\xAA8-"):
raise NameError("Bad U8 Tag")
offset = header.rootnode_offset
rootnode = self.U8Node()
rootnode.unpack(data[offset:offset + len(rootnode)])
offset += len(rootnode)
nodes = []
for i in xrange(rootnode.size - 1):
node = self.U8Node()
node.unpack(data[offset:offset + len(node)])
offset += len(node)
nodes.append(node)
strings = data[offset:offset + header.data_offset - len(header) - (len(rootnode) * rootnode.size)]
offset += len(strings)
out = "[root]\n"
recursion = [rootnode.size]
counter = 0
for node in nodes:
counter += 1
name = strings[node.name_offset:].split('\0', 1)[0]
out += " " * len(recursion)
if(node.type == 0x0100): #folder
recursion.append(node.size)
out += "[%s]\n" % name
continue
elif(node.type == 0): #file
out += "%s\n" % name
offset += node.size
else: #unknown, ignore
pass
sz = recursion.pop()
if(sz == counter + 1):
pass
ret = ''
for key, value in self.files:
name = key[key.rfind('/') + 1:]
recursion = key.count('/')
ret += ' ' * recursion
if(value == None):
ret += '[' + name + ']'
else:
recursion.append(sz)
return out
ret += name
ret += '\n'
return ret
def __getitem__(self, key):
for item, val in self.files:
if(item == key):
return val
raise KeyError
def __setitem__(self, key, val):
for i in self.files:
if(self.files[i][0] == key):
self.files[i][1] = val
raise KeyError
class WAD:
"""This class is to pack and unpack WAD files, which store a single title. You pass the input filename or input directory name to the parameter f.
@ -240,8 +196,8 @@ class WAD:
"""Packs a WAD into the filename specified by fn, if it is not empty. If it is empty, it packs into a filename generated from the folder's name. If fakesign is True, it will fakesign the Ticket and TMD, and update them as needed. If decrypted is true, it will assume the contents are already decrypted. For now, fakesign can not be True if decrypted is False, however fakesign can be False if decrypted is True. Title ID is a long integer of the destination title id."""
os.chdir(self.f)
tik = Ticket().loadFile("tik")
tmd = TMD().loadFile("tmd")
tik = Ticket.loadFile("tik")
tmd = TMD.loadFile("tmd")
titlekey = tik.getTitleKey()
contents = tmd.getContents()
@ -264,8 +220,10 @@ class WAD:
if(fakesign):
tmd.setContents(contents)
tmd.dump()
tik.dump()
tmd.fakesign()
tik.fakesign()
tmd.dumpFile("tmd")
tik.dumpFile("tik")
rawtmd = open("tmd", "rb").read()
rawcert = open("cert", "rb").read()
@ -341,8 +299,8 @@ class WAD:
fd.seek(64 - (tmdsize % 64), 1)
open('tmd', 'wb').write(rawtmd)
titlekey = Ticket().loadFile("tik").getTitleKey()
contents = TMD().loadFile("tmd").getContents()
titlekey = Ticket.loadFile("tik").getTitleKey()
contents = TMD.loadFile("tmd").getContents()
for i in range(0, len(contents)):
tmpsize = contents[i].size
if(tmpsize % 16 != 0):
@ -380,8 +338,8 @@ class WAD:
else:
out += " Header %02x Type 'boot2' Certs %x Tiket %x TMD %x Data @ %x\n" % (headersize, certsize, tiksize, tmdsize, data_offset)
out += str(Ticket().load(rawtik))
out += str(TMD().load(rawtmd))
out += str(Ticket.load(rawtik))
out += str(TMD.load(rawtmd))
return out

View File

@ -62,3 +62,31 @@ class Crypto:
# else:
# return 0
class WiiObject(object):
@classmethod
def load(cls, data):
self = cls()
self._load(data)
return self
@classmethod
def loadFile(cls, filename):
return cls.load(open(filename, "rb").read())
def dump(self):
return self._dump()
def dumpFile(self, filename):
open(filename, "wb").write(self.dump())
return filename
class WiiArchive(WiiObject):
@classmethod
def loadDir(cls, dirname):
self = cls()
self._loadDir(dirname)
return self
def dumpDir(self, dirname):
if(not os.path.isdir(dirname)):
os.mkdir(dirname)
self._dumpDir(dirname)
return dirname

36
nand.py
View File

@ -385,12 +385,14 @@ class NAND:
tmdpth = self.f + "/title/%08x/%08x/content/title.tmd" % (title >> 32, title & 0xFFFFFFFF)
if(version != 0):
tmdpth += ".%d" % version
tmd = TMD().loadFile(tmdpth)
tmd = TMD.loadFile(tmdpth)
if(not os.path.isdir("export")):
os.mkdir("export")
tmd.dump("export/tmd")
tik = Ticket().loadFile(self.f + "/ticket/%08x/%08x.tik" % (title >> 32, title & 0xFFFFFFFF))
tik.dump("export/tik")
tmd.fakesign()
tmd.dumpFile("export/tmd")
tik = Ticket.loadFile(self.f + "/ticket/%08x/%08x.tik" % (title >> 32, title & 0xFFFFFFFF))
tik.fakesign()
tik.dumpFile("export/tik")
contents = tmd.getContents()
for i in range(tmd.tmd.numcontents):
path = ""
@ -645,14 +647,14 @@ class ESClass:
def Identify(self, id, version=0):
if(not os.path.isfile(self.f + "/ticket/%08x/%08x.tik" % (id >> 32, id & 0xFFFFFFFF))):
return None
tik = Ticket().loadFile(self.f + "/ticket/%08x/%08x.tik" % (id >> 32, id & 0xFFFFFFFF))
tik = Ticket.loadFile(self.f + "/ticket/%08x/%08x.tik" % (id >> 32, id & 0xFFFFFFFF))
titleid = tik.titleid
path = "/title/%08x/%08x/content/title.tmd" % (titleid >> 32, titleid & 0xFFFFFFFF)
if(version):
path += ".%d" % version
if(not os.path.isfile(self.f + path)):
return None
tmd = TMD().loadFile(self.f + path)
tmd = TMD.loadFile(self.f + path)
self.title = titleid
self.group = tmd.tmd.group_id
return self.title
@ -670,7 +672,7 @@ class ESClass:
path += ".%d" % version
if(not os.path.isfile(self.f + path)):
return None
return TMD().loadFile(self.f + path)
return TMD.loadFile(self.f + path)
def GetTitleContentsCount(self, titleid, version=0):
"""Gets the number of contents the title with the specified titleid and version has."""
tmd = self.GetStoredTMD(titleid, version)
@ -708,7 +710,7 @@ class ESClass:
return
def AddTicket(self, tik):
"""Adds ticket to the title being added."""
tik.rawdump(self.f + "/tmp/title.tik")
tik.dumpFile(self.f + "/tmp/title.tik")
self.ticketadded = 1
def DeleteTicket(self, tikview):
"""Deletes the ticket relating to tikview
@ -716,7 +718,7 @@ class ESClass:
return
def AddTitleTMD(self, tmd):
"""Adds TMD to the title being added."""
tmd.rawdump(self.f + "/tmp/title.tmd")
tmd.dumpFile(self.f + "/tmp/title.tmd")
self.tmdadded = 1
def AddContentStart(self, titleid, cid):
"""Starts adding a content with content id cid to the title being added with ID titleid."""
@ -724,9 +726,9 @@ class ESClass:
"Trying to start an already existing process"
return -41
if(self.tmdadded):
a = TMD().loadFile(self.f + "/tmp/title.tmd")
a = TMD.loadFile(self.f + "/tmp/title.tmd")
else:
a = TMD().loadFile(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version))
a = TMD.loadFile(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version))
x = self.getContentIndexFromCID(a, cid)
if(x == None):
"Not a valid Content ID"
@ -766,11 +768,11 @@ class ESClass:
def AddTitleFinish(self):
"""Finishes the adding of a title."""
if(self.ticketadded):
tik = Ticket().loadFile(self.f + "/tmp/title.tik")
tik = Ticket.loadFile(self.f + "/tmp/title.tik")
else:
tik = Ticket().loadFile(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF))
tik = Ticket.loadFile(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF))
if(self.tmdadded):
tmd = TMD().loadFile(self.f + "/tmp/title.tmd")
tmd = TMD.loadFile(self.f + "/tmp/title.tmd")
contents = tmd.getContents()
for i in range(self.workingcidcnt):
idx = self.getContentIndexFromCID(tmd, self.workingcids[i])
@ -805,12 +807,12 @@ class ESClass:
outfp.close()
if(self.tmdadded and self.use_version):
self.nand.newFile("/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version), "rwrw--", 0x0000)
tmd.rawdump(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version))
tmd.dumpFile(self.f + "/title/%08x/%08x/content/title.tmd.%d" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF, tmd.tmd.title_version))
elif(self.tmdadded):
self.nand.newFile("/title/%08x/%08x/content/title.tmd" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF), "rwrw--", 0x0000)
tmd.rawdump(self.f + "/title/%08x/%08x/content/title.tmd" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF))
tmd.dumpFile(self.f + "/title/%08x/%08x/content/title.tmd" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF))
if(self.ticketadded):
self.nand.newFile("/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF), "rwrw--", 0x0000)
tik.rawdump(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF))
tik.dumpFile(self.f + "/ticket/%08x/%08x.tik" % (self.wtitleid >> 32, self.wtitleid & 0xFFFFFFFF))
self.AddTitleCancel()
return 0

View File

@ -37,7 +37,7 @@ class TicketView:
return out
class Ticket:
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
@ -79,9 +79,7 @@ class Ticket:
if(self.tik.commonkey_index == 1): #korean, kekekekek!
commonkey = koreankey
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
@classmethod
def load(cls, data):
self = cls()
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"
@ -92,9 +90,6 @@ class Ticket:
self.titlekey = Crypto().decryptTitleKey(commonkey, self.tik.titleid, self.tik.enctitlekey)
return self
@classmethod
def loadFile(cls, filename):
return cls.load(open(filename, "rb").read())
def getTitleKey(self):
"""Returns a string containing the title key."""
return self.titlekey
@ -128,7 +123,7 @@ class Ticket:
out += "\n"
return out
def dump(self, fn = ""):
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):
@ -137,25 +132,13 @@ class Ticket:
break
if(i == 65535):
raise ValueError("Failed to fakesign. Aborting...")
if(fn == ""):
open(self.f, "wb").write(self.tik.pack())
return self.f
else:
open(fn, "wb").write(self.tik.pack())
return fn
def rawdump(self, fn = ""):
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."""
if(fn == ""):
open(self.f, "wb").write(self.tik.pack())
return self.f
else:
open(fn, "wb").write(self.tik.pack())
return fn
return self.tik.pack()
def __len__(self):
return len(self.tik)
class TMD:
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
@ -184,7 +167,7 @@ class TMD:
self.boot_index = Struct.uint16
self.padding2 = Struct.uint16
#contents follow this
def load(self, data):
def _load(self, data):
self.tmd.unpack(data[:len(self.tmd)])
pos = len(self.tmd)
for i in range(self.tmd.numcontents):
@ -192,9 +175,6 @@ class TMD:
cont.unpack(data[pos:pos + len(cont)])
pos += len(cont)
self.contents.append(cont)
return self
def loadFile(self, filename):
return self.load(open(filename, "rb").read())
def __init__(self):
self.tmd = self.TMDStruct()
self.tmd.titleid = 0x0000000100000000
@ -232,7 +212,7 @@ class TMD:
for i in range(len(contents)):
sz += len(contents[i])
return sz
def dump(self, fn = ""):
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
@ -245,26 +225,14 @@ class TMD:
break
if(i == 65535):
raise ValueError("Failed to fakesign! Aborting...")
if(fn == ""):
open(self.f, "wb").write(data)
return self.f
else:
open(fn, "wb").write(data)
return fn
def rawdump(self, fn = ""):
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()
if(fn == ""):
open(self.f, "wb").write(data)
return self.f
else:
open(fn, "wb").write(data)
return fn
return data
def getTitleID(self):
"""Returns the long integer title id."""
return self.tmd.titleid
@ -320,11 +288,11 @@ class NUS:
urllib.urlretrieve(self.baseurl + "tmd" + versionstring, "tmd")
tmd = TMD.loadFile("tmd")
tmd.rawdump("tmd") # strip certs
tmd.dumpFile("tmd") # strip certs
urllib.urlretrieve(self.baseurl + "cetk", "tik")
tik = Ticket.loadFile("tik")
tik.rawdump("tik") # strip certs
tik.dumpFile("tik") # strip certs
if(decrypt):
titlekey = tik.getTitleKey()