diff --git a/Wii.py b/Wii.py index 2f0b9cf..17f5243 100644 --- a/Wii.py +++ b/Wii.py @@ -10,6 +10,7 @@ from export import * from compression import * from nand import * from headers import * +from bns import * if (__name__ == "__main__"): Crypto() @@ -18,4 +19,4 @@ if (__name__ == "__main__"): #insert non-dependant check code here - print ("\nAll Wii.py components loaded sucessfully!\n") + print("\nAll Wii.py components loaded sucessfully!\n") diff --git a/archive.py b/archive.py index ed654f4..ba9dd12 100644 --- a/archive.py +++ b/archive.py @@ -24,15 +24,14 @@ class U8(WiiArchive): 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.""" header = self.U8Header() rootnode = self.U8Node() + # constants header.tag = "U\xAA8-" header.rootnode_offset = 0x20 header.zeroes = "\x00" * 16 + rootnode.type = 0x0100 nodes = [] strings = "\x00" @@ -40,37 +39,35 @@ class U8(WiiArchive): 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:] + + node.name_offset = len(strings) strings += name + '\x00' if(value == None): node.type = 0x0100 node.data_offset = recursion - this_length = 0 + node.size = len(nodes) for one, two in self.files: - subdirs = one - if(subdirs.find(item) != -1): - this_length += 1 - node.size = len(nodes) + this_length + 1 + if(one[:len(item)] == item): # find nodes in the folder + node.size += 1 + node.size += 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 + data += value + "\x00" * (align(sz, 32) - sz) # 32 seems to work best for fuzzyness? I'm still really not sure node.size = sz node.type = 0x0000 nodes.append(node) - header.header_size = (len(nodes) + 1) * len(rootnode) + len(strings) + header.header_size = ((len(nodes) + 1) * len(rootnode)) + len(strings) header.data_offset = align(header.header_size + header.rootnode_offset, 64) rootnode.size = len(nodes) + 1 - rootnode.type = 0x0100 for i in range(len(nodes)): if(nodes[i].type == 0x0000): @@ -155,7 +152,7 @@ class U8(WiiArchive): 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 + else: # unknown type -- wtf? pass sz = recursion.pop() @@ -178,7 +175,14 @@ class U8(WiiArchive): def __getitem__(self, key): for item, val in self.files: if(item == key): - return val + if(val != None): + return val + else: + ret = [] + for item2, val2 in self.files: + if(item2.find(item) == 0): + ret.append(item2[len(item) + 1:]) + return ret[1:] raise KeyError def __setitem__(self, key, val): for i in range(len(self.files)): @@ -188,49 +192,103 @@ class U8(WiiArchive): self.files.append((key, val)) -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. - - WAD packing support currently creates WAD files that return -4100 on install.""" - def __init__(self, f, boot2 = False): - self.f = f - self.boot2 = boot2 - def pack(self, fn = "", fakesign = True, decrypted = True): - """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) +class WAD(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) - tik = Ticket.loadFile("tik") - tmd = TMD.loadFile("tmd") - titlekey = tik.getTitleKey() - contents = tmd.getContents() + 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): + origdir = os.getcwd() + os.chdir(dir) + + contents = self.tmd.getContents() + for i in range(len(contents)): + open("%08x.app" % i, "wb").write(self.contents[i]) + 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 content in contents: - tmpdata = open("%08x.app" % content.index, "rb").read() + for i, content in enumerate(contents): + if(fakesign): + content.hash = str(Crypto().createSHAHash(self.contents[content.index])) + content.size = len(self.contents[content.index]) - if(decrypted): - if(fakesign): - content.hash = str(Crypto().createSHAHash(tmpdata)) - content.size = len(tmpdata) - - encdata = Crypto().encryptContent(titlekey, content.index, tmpdata) - else: - encdata = tmpdata + 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): - tmd.setContents(contents) - tmd.fakesign() - tik.fakesign() - tmd.dumpFile("tmd") - tik.dumpFile("tik") + self.tmd.setContents(contents) + self.tmd.fakesign() + self.tik.fakesign() - rawtmd = open("tmd", "rb").read() - rawcert = open("cert", "rb").read() - rawtik = open("tik", "rb").read() + rawtmd = self.tmd.dump() + rawcert = self.cert + rawtik = self.tik.dump() sz = 0 for i in range(len(contents)): @@ -258,92 +316,16 @@ class WAD: pack += "\x00" * (align(len(rawcert) + len(rawtik) + len(rawtmd), 0x40) - (len(rawcert) + len(rawtik) + len(rawtmd))) pack += apppack - - os.chdir('..') - if(fn == ""): - if(self.f[len(self.f) - 4:] == "_out"): - fn = os.path.dirname(self.f) + "/" + os.path.basename(self.f)[:len(os.path.basename(self.f)) - 4].replace("_", ".") - else: - fn = self.f - open(fn, "wb").write(pack) - return fn - def unpack(self, fn = ""): - """Unpacks the WAD from the parameter f in the initializer to either the value of fn, if there is one, or a folder created with this formula: `filename_extension_out`. Certs are put in the file "cert", TMD in the file "tmd", ticket in the file "tik", and contents are put in the files based on index and with ".app" added to the end.""" - fd = open(self.f, 'rb') - if(self.boot2 != True): - headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding= struct.unpack('>I4s6I32s', fd.read(64)) - else: - headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', fd.read(32)) - - try: - if(fn == ""): - fn = self.f.replace(".", "_") + "_out" - os.mkdir(fn) - except OSError: - pass - os.chdir(fn) - - rawcert = fd.read(certsize) - if(self.boot2 != True): - if(certsize % 64 != 0): - fd.seek(64 - (certsize % 64), 1) - open('cert', 'wb').write(rawcert) - - rawtik = fd.read(tiksize) - if(self.boot2 != True): - if(tiksize % 64 != 0): - fd.seek(64 - (tiksize % 64), 1) - open('tik', 'wb').write(rawtik) - - rawtmd = fd.read(tmdsize) - if(self.boot2 == True): - fd.seek(data_offset) - else: - fd.seek(64 - (tmdsize % 64), 1) - open('tmd', 'wb').write(rawtmd) - - 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): - tmpsize += 16 - (tmpsize % 16) - tmptmpdata = fd.read(tmpsize) - tmpdata = Crypto().decryptContent(titlekey, contents[i].index, tmptmpdata) - open("%08x.app" % contents[i].index, "wb").write(tmpdata) - if(tmpsize % 64 != 0): - fd.seek(64 - (tmpsize % 64), 1) - fd.close() - os.chdir('..') - - return fn + return pack + 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" - fd = open(self.f, 'rb') - - if(self.boot2 != True): - headersize, wadtype, certsize, reserved, tiksize, tmdsize, datasize, footersize, padding= struct.unpack('>I4s6I32s', fd.read(64)) - else: - headersize, data_offset, certsize, tiksize, tmdsize, padding = struct.unpack('>IIIII12s', fd.read(32)) - - rawcert = fd.read(certsize) - if(certsize % 64 != 0): - fd.seek(64 - (certsize % 64), 1) - rawtik = fd.read(tiksize) - if(self.boot2 != True): - if(tiksize % 64 != 0): - fd.seek(64 - (tiksize % 64), 1) - rawtmd = fd.read(tmdsize) - - if(self.boot2 != True): - out += " Header %02x Type '%s' Certs %x Tiket %x TMD %x Data %x Footer %x\n" % (headersize, wadtype, certsize, tiksize, tmdsize, datasize, footersize) - 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(self.tmd) + out += str(self.tik) return out @@ -459,4 +441,14 @@ class CCF(): output.close() currentOffset += len(fileEntry) - + +if(__name__ == '__main__'): + wad = WAD.loadFile("testing.wad") + print wad + wad.dumpDir("outdir") + wad.dumpFile("interesting.wad", fakesign = False) #keyword arguements work as expected when calling _dump(). awesome. + wad2 = WAD.loadDir("outdir") + print wad2 + wad3 = WAD.loadFile("interesting.wad") + print wad3 + wad3.dumpDir("outdir2") diff --git a/bns.py b/bns.py new file mode 100644 index 0000000..3f240de --- /dev/null +++ b/bns.py @@ -0,0 +1,618 @@ +#!/usr/bin/python + +from common import * + +class SoundFile: + def __init__(self, signal, filename, samplerate=32000): + self.actual_file = StringIO() + self.file = wave.open(filename, 'wb') + self.signal = signal + self.sr = samplerate + def write(self): + self.file.setparams((2, 2, self.sr, self.sr*4, 'NONE', 'noncompressed')) + self.file.writeframes(self.signal) + self.actual_file.seek(0) + self.file.close() + +class BNS_data(object): + def __init__(self): + self.magic = "DATA" + self.size = 0x0004d000 + def eat(self, buffer, offset): + self.magic, self.size = struct.unpack('>4sI', buffer[offset:offset+8]) + return offset + 8 + def show(self): + print "Magic: %s" % self.magic + print "Length: %08x" % self.size + return + def write(self, file): + file.write(self.magic) + file.write(struct.pack('>I', self.size)) + file.write(self.data) + return + +class BNS_info(object): + def __init__(self): + self.magic = "INFO" + self.size = 0x000000a0 + self.codec = 0x00 + self.has_loop = 0x00 + self.chan_cnt = 0x02 + self.zero = 0x00 + self.samplerate = 0xac44 + self.pad0 = 0x0000 + self.loop_start = 0x00000000 + self.loop_end = 0x00000000 + self.offset_to_chan_starts = 0x00000018 + self.pad2 = 0x00000000 + self.channel1_start_offset = 0x00000020 + self.channel2_start_offset = 0x0000002C + self.chan1_start = 0x00000000 + self.coefficients1_offset = 0x0000038 + self.pad1 = 0x00000000 + self.chan2_start = 0x00000000 + self.coefficients2_offset = 0x00000068 + self.pad3 = 0x00000000 + self.coefficients1 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self.chan1_gain = 0x0000 + self.chan1_predictive_scale = 0x0000 + self.chan1_previous_value = 0x0000 + self.chan1_next_previous_value = 0x0000 + self.chan1_loop_predictive_scale = 0x0000 + self.chan1_loop_previous_value = 0x0000 + self.chan1_loop_next_previous_value = 0x0000 + self.chan1_loop_padding = 0x0000 + self.coefficients2 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + self.chan2_gain = 0x0000 + self.chan2_predictive_scale = 0x0000 + self.chan2_previous_value = 0x0000 + self.chan2_next_previous_value = 0x0000 + self.chan2_loop_predictive_scale = 0x0000 + self.chan2_loop_previous_value = 0x0000 + self.chan2_loop_next_previous_value = 0x0000 + self.chan2_loop_padding = 0x0000 + def eat(self, buffer, offset): + self.magic, self.size = struct.unpack('>4sI', buffer[offset+0:offset+8]) + self.codec, self.has_loop = struct.unpack('>BB', buffer[offset+8:offset+10]) + self.chan_cnt, self.zero = struct.unpack('>BB', buffer[offset+10:offset+12]) + self.samplerate, self.pad0 = struct.unpack('>HH', buffer[offset+12:offset+16]) + assert self.samplerate <= 48000 + assert self.samplerate > 32000 + self.loop_start, self.loop_end = struct.unpack('>II', buffer[offset+16:offset+24]) + co = offset + 24 + self.offset_to_chan_starts = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.pad2 = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.channel1_start_offset = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.channel2_start_offset = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.chan1_start = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.coefficients1_offset = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + if self.chan_cnt == 2: + self.pad1 = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.chan2_start = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.coefficients2_offset = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + self.pad3 = Struct.uint32(buffer[co:co+4], endian='>') + co += 4 + for x in xrange(16): + self.coefficients1[x] = Struct.int16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_gain = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_predictive_scale = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_next_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_predictive_scale = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_next_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_padding = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + for x in xrange(16): + self.coefficients2[x] = Struct.int16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_gain = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_predictive_scale = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_next_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_loop_predictive_scale = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_loop_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_loop_next_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan2_loop_padding = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + elif self.chan_cnt == 1: + for x in xrange(16): + self.coefficients1[x] = Struct.int16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_gain = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_predictive_scale = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_next_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_predictive_scale = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_next_previous_value = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + self.chan1_loop_padding = Struct.uint16(buffer[co:co+2], endian='>') + co += 2 + return co + def show(self): + print "Magic: %s" % self.magic + print "Length: %08x" % self.size + print "Codec: %02x " % self.codec, + if self.codec == 0: print "ADPCM" + else: print "Unknown (Maybe >_>, please contact megazig)" + print "Loop Flag: %02x " % self.has_loop, + if self.has_loop == 0: print "One shot" + else: print "Looping" + print "Channel Count: %02x" % self.chan_cnt + print "Zero: %02x" % self.zero + print "Samplerate: %04x %d" % ( self.samplerate , self.samplerate ) + print "Padding: %04x" % self.pad0 + print "Loop Start: %08x" % self.loop_start + print "Loop End: %08x" % self.loop_end + print "Channels Starts Offsets: %08x" % self.offset_to_chan_starts + print "Padding: %08x" % self.pad2 + print "Channel 1 Start Offset: %08x" % self.channel1_start_offset + print "Channel 2 Start Offset: %08x" % self.channel2_start_offset + print "Channel 1 Start: %08x" % self.chan1_start + print "Coefficients 1 Offset: %08x" % self.coefficients1_offset + if self.chan_cnt == 2: + print "Padding: %08x" % self.pad1 + print "Channel 2 Start: %08x" % self.chan2_start + print "Coefficients 2 Offset: %08x" % self.coefficients2_offset + print "Padding: %08x" % self.pad3 + for x in xrange(16): + print "\t\tCoefficients 1: %2d - %04x - %d" % ( x , self.coefficients1[x], self.coefficients1[x] ) + print "\tGain: %04x" % self.chan1_gain + print "\tPredictive Scale: %04x" % self.chan1_predictive_scale + print "\tPrevious Value: %04x" % self.chan1_previous_value + print "\tNext Previous Value: %04x" % self.chan1_next_previous_value + print "\tLoop Predictive Scale: %04x" % self.chan1_loop_predictive_scale + print "\tLoop Previous Value: %04x" % self.chan1_loop_previous_value + print "\tLoop Next Previous Value: %04x" % self.chan1_loop_next_previous_value + print "\tPadding: %04x" % self.chan1_loop_padding + for x in xrange(16): + print "\t\tCoefficients 2: %2d - %04x - %d" % ( x , self.coefficients2[x], self.coefficients2[x] ) + print "\tGain: %04x" % self.chan2_gain + print "\tPredictive Scale: %04x" % self.chan2_predictive_scale + print "\tPrevious Value: %04x" % self.chan2_previous_value + print "\tNext Previous Value: %04x" % self.chan2_next_previous_value + print "\tLoop Predictive Scale: %04x" % self.chan2_loop_predictive_scale + print "\tLoop Previous Value: %04x" % self.chan2_loop_previous_value + print "\tLoop Next Previous Value: %04x" % self.chan2_loop_next_previous_value + print "\tPadding: %04x" % self.chan2_loop_padding + elif self.chan_cnt == 1: + for x in xrange(16): + print "\t\tCoefficients 1: %2d - %04x - %d" % ( x , self.coefficients1[x], self.coefficients1[x] ) + print "\tGain: %04x" % self.chan1_gain + print "\tPredictive Scale: %04x" % self.chan1_predictive_scale + print "\tPrevious Value: %04x" % self.chan1_previous_value + print "\tNext Previous Value: %04x" % self.chan1_next_previous_value + print "\tLoop Predictive Scale: %04x" % self.chan1_loop_predictive_scale + print "\tLoop Previous Value: %04x" % self.chan1_loop_previous_value + print "\tLoop Next Previous Value: %04x" % self.chan1_loop_next_previous_value + print "\tPadding: %04x" % self.chan1_loop_padding + return + def write(self, file): + file.write(self.magic) + file.write(struct.pack('>I', self.size)) + file.write(struct.pack('>B', self.codec)) + file.write(struct.pack('>B', self.has_loop)) + file.write(struct.pack('>B', self.chan_cnt)) + file.write(struct.pack('>B', self.zero)) + file.write(struct.pack('>H', self.samplerate)) + file.write(struct.pack('>H', self.pad0)) + file.write(struct.pack('>I', self.loop_start)) + file.write(struct.pack('>I', self.loop_end)) + file.write(struct.pack('>I', self.offset_to_chan_starts)) + file.write(struct.pack('>I', self.pad2)) + file.write(struct.pack('>I', self.channel1_start_offset)) + file.write(struct.pack('>I', self.channel2_start_offset)) + file.write(struct.pack('>I', self.chan1_start)) + file.write(struct.pack('>I', self.coefficients1_offset)) + if self.chan_cnt == 2: + file.write(struct.pack('>I', self.pad1)) + file.write(struct.pack('>I', self.chan2_start)) + file.write(struct.pack('>I', self.coefficients2_offset)) + file.write(struct.pack('>I', self.pad3)) + for x in xrange(16): + file.write(struct.pack('>h', self.coefficients1[x])) + file.write(struct.pack('>H', self.chan1_gain)) + file.write(struct.pack('>H', self.chan1_predictive_scale)) + file.write(struct.pack('>H', self.chan1_previous_value)) + file.write(struct.pack('>H', self.chan1_next_previous_value)) + file.write(struct.pack('>H', self.chan1_loop_predictive_scale)) + file.write(struct.pack('>H', self.chan1_loop_previous_value)) + file.write(struct.pack('>H', self.chan1_loop_next_previous_value)) + file.write(struct.pack('>H', self.chan1_loop_padding)) + for x in xrange(16): + file.write(struct.pack('>h', self.coefficients2[x])) + file.write(struct.pack('>H', self.chan2_gain)) + file.write(struct.pack('>H', self.chan2_predictive_scale)) + file.write(struct.pack('>H', self.chan2_previous_value)) + file.write(struct.pack('>H', self.chan2_next_previous_value)) + file.write(struct.pack('>H', self.chan2_loop_predictive_scale)) + file.write(struct.pack('>H', self.chan2_loop_previous_value)) + file.write(struct.pack('>H', self.chan2_loop_next_previous_value)) + file.write(struct.pack('>H', self.chan2_loop_padding)) + elif self.chan_cnt == 1: + for x in xrange(16): + file.write(struct.pack('>h', self.coefficients1[x])) + file.write(struct.pack('>H', self.chan1_gain)) + file.write(struct.pack('>H', self.chan1_predictive_scale)) + file.write(struct.pack('>H', self.chan1_previous_value)) + file.write(struct.pack('>H', self.chan1_next_previous_value)) + file.write(struct.pack('>H', self.chan1_loop_predictive_scale)) + file.write(struct.pack('>H', self.chan1_loop_previous_value)) + file.write(struct.pack('>H', self.chan1_loop_next_previous_value)) + file.write(struct.pack('>H', self.chan1_loop_padding)) + return + + +class BNS_header(object): + def __init__(self): + self.magic = "BNS " + self.flags = 0xfeff0100 + self.filesize = 0x0004d0c0 + self.size = 0x0020 + self.chunk_cnt = 0x0002 + self.info_off = 0x00000020 + self.info_len = 0x000000a0 + self.data_off = 0x000000c0 + self.data_len = 0x0004d000 + def eat(self, buffer, offset): + if struct.unpack('>4s', buffer[offset:offset+4])[0] != "BNS ": + offset += 0x20 + self.magic, self.flags = struct.unpack('>4sI', buffer[offset+0:offset+8]) + self.filesize, self.size, self.chunk_cnt = struct.unpack('>IHH', buffer[offset+8:offset+16]) + self.info_off, self.info_len = struct.unpack('>II', buffer[offset+16:offset+24]) + self.data_off, self.data_len = struct.unpack('>II', buffer[offset+24:offset+32]) + assert self.magic == "BNS " + assert self.info_off < self.filesize + assert self.data_off < self.filesize + return offset + 32 + def show(self): + print "Magic: %s" % self.magic + print "Flags: %08x" % self.flags + print "Length: %08x" % self.filesize + print "Header Size: %04x" % self.size + print "Chunk Count: %04x" % self.chunk_cnt + print "Info Offset: %08x" % self.info_off + print "Info Length: %08x" % self.info_len + print "Data Offset: %08x" % self.data_off + print "Data Length: %08x" % self.data_len + return + def write(self, file): + file.write(self.magic) + file.write(struct.pack('>I', self.flags)) + file.write(struct.pack('>I', self.filesize)) + file.write(struct.pack('>H', self.size)) + file.write(struct.pack('>H', self.chunk_cnt)) + file.write(struct.pack('>I', self.info_off)) + file.write(struct.pack('>I', self.info_len)) + file.write(struct.pack('>I', self.data_off)) + file.write(struct.pack('>I', self.data_len)) + return + +class BNS(object): + def __init__(self): + self.header = BNS_header() + self.info = BNS_info() + self.data = BNS_data() + self.buffered_data = "" + self.lsamps = [ [ 0 , 0 ] , [ 0 , 0 ] ] + self.rlsamps = [ [ 0 , 0 ] , [ 0 , 0 ] ] + self.tlsamps = [ 0 , 0 ] + self.hbc_deftbl = [ 674 , 1040, + 3598, -1738, + 2270, -583, + 3967, -1969, + 1516, 381, + 3453, -1468, + 2606, -617, + 3795, -1759 ] + self.deftbl = [ 1820 , -856 , + 3238 , -1514 , + 2333 , -550 , + 3336 , -1376 , + 2444 , -949 , + 3666 , -1764 , + 2654 , -701 , + 3420 , -1398 ] + self.phist1 = [ 0 , 0 ] + self.phist2 = [ 0 , 0 ] + self.errors = 0 + def find_exp(self, residual): + exp = 0 + while residual>7.5 or residual<-8.5: + exp += 1 + residual /= 2.0 + return exp + def determine_std_exponent(self, idx, table, index, inbuf): + elsamps = [ 0 , 0 ] + max_res = 0 + factor1 = table[2*index+0] + factor2 = table[2*index+1] + for x in xrange(2): + elsamps[x] = self.rlsamps[idx][x] + for i in xrange(14): + predictor = (elsamps[1]*factor1 + elsamps[0]*factor2) >> 11 + residual = inbuf[i] - predictor + if residual>max_res: + max_res = residual + elsamps[0] = elsamps[1] + elsamps[1] = inbuf[i] + return self.find_exp(max_res) + def compress_adpcm(self, idx, table, tblidx, inbuf): + data = [0 for i in range(8)] + error = 0 + factor1 = table[2*tblidx+0] + factor2 = table[2*tblidx+1] + exp = self.determine_std_exponent(idx, table, tblidx, inbuf) + while exp<=15: + error = 0 + data[0] = exp | (tblidx << 4) + for x in xrange(2): + self.tlsamps[x] = self.rlsamps[idx][x] + j = 0 + for i in xrange(14): + predictor = (self.tlsamps[1]*factor1 + self.tlsamps[0]*factor2) >> 11 + residual = inbuf[i] - predictor + residual = residual >> exp + if residual>7 or residual<-8: + exp += 1 + break + nibble = clamp(residual, -8, 7) + if i&1: + data[i/2+1] = data[i/2+1] | (nibble & 0xf) + else: + data[i/2+1] = nibble << 4 + predictor = predictor + (nibble << exp) + self.tlsamps[0] = self.tlsamps[1] + self.tlsamps[1] = clamp(predictor, -32768, 32767) + error = error + ((self.tlsamps[1] - inbuf[i]) ** 2) + else: + j = 14 + if j == 14: + break + return error, data + def repack_adpcm(self, idx, table, inbuf): + data = [0 for i in range(8)] + blsamps = [ 0 , 0 ] + bestidx = -1 + besterror = 999999999.0 + for tblidx in xrange(8): + error, testdata = self.compress_adpcm(idx, table, tblidx, inbuf) + if error < besterror: + besterror = error + for x in xrange(8): + data[x] = testdata[x] + for x in xrange(2): + blsamps[x] = self.tlsamps[x] + bestidx = tblidx + for x in xrange(2): + self.rlsamps[idx][x] = blsamps[x] + return data + def encode(self, buffer, offset=0): + sampsbuf = [0 for i in range(14)] + templen = len(buffer) + templen = templen / 4 + modlen = templen % 14 + for x in xrange(14-modlen): + buffer = buffer + '\x00' + buffer = buffer + '\x00' + buffer = buffer + '\x00' + buffer = buffer + '\x00' + num_samps = len(buffer) / 4 + blocks = (num_samps + 13) / 14 + snddatal = [] + snddatar = [] + co = offset + temp = 0 + for x in xrange(num_samps): + snddatal.append(Struct.int16(buffer[co:co+2])) + co += 2 + snddatar.append(Struct.int16(buffer[co:co+2])) + co += 2 + data = [0 for i in range(blocks*16)] + data1_off = 0 + data2_off = blocks * 8 + self.info.chan2_start = data2_off + for i in xrange(blocks): + for j in xrange(14): + sampsbuf[j] = snddatal[i*14+j] + out_buf = self.repack_adpcm(0, self.deftbl, sampsbuf) + for k in xrange(8): + data[data1_off+k] = out_buf[k] + for j in xrange(14): + sampsbuf[j] = snddatar[i*14+j] + out_buf = self.repack_adpcm(1, self.deftbl, sampsbuf) + for k in xrange(8): + data[data2_off+k] = out_buf[k] + data1_off += 8 + data2_off += 8 + self.info.loop_end = blocks * 7 + return data + def create_bns(self, inbuf, samplerate=44100, channels=2): + self.info.chan_cnt = channels + self.info.samplerate = samplerate + assert samplerate >=32000 + self.data.data = ''.join(Struct.int8(p) for p in self.encode(inbuf)) + self.data.size = len(self.data.data) + self.header.data_len = self.data.size + self.header.filesize = self.info.size + self.data.size + 8 + self.header.size + self.info.loop_end = self.data.size - (self.data.size / 7) + for x in xrange(16): + self.info.coefficients1[x] = self.deftbl[x] + if self.info.chan_cnt == 2: + for x in xrange(16): self.info.coefficients2[x] = self.deftbl[x] + return + def decode_adpcm(self, index, coefs, buffer): + outbuf = [0 for i in range(14)] + header = Struct.uint8(buffer[0:1], endian='>') + coef_index = (header >> 4) & 0x7 + scale = 1 << (header & 0xf) + hist1 = self.phist1[index] + hist2 = self.phist2[index] + coef1 = coefs[coef_index * 2 + 0] + coef2 = coefs[coef_index * 2 + 1] + for x in xrange(14): + sample_byte = Struct.uint8(buffer[x/2+1:x/2+2], endian='>') + if x&1: + nibble = (sample_byte & 0xf0) >> 4 + else: + nibble = (sample_byte & 0x0f) >> 0 + if nibble >= 8: + nibble -= 16 + sample_delta_11 = (scale * nibble) << 11 + predicted_sample_11 = coef1*hist1 + coef2*hist2 + sample_11 = predicted_sample_11 + sample_delta_11 + sample_raw = (sample_11 + 1024) >> 11 + sample_raw = clamp(sample_raw, -32768, 32767) + outbuf[x] = sample_raw + hist2 = hist1 + hist1 = outbuf[x] + self.phist1[index] = hist1 + self.phist2[index] = hist2 + return outbuf + def decode(self, buffer, offset): + decoded_buffer = [] + if self.info.chan_cnt == 2: + multi = 16 + coeff0 = self.info.coefficients1 + coeff1 = self.info.coefficients2 + elif self.info.chan_cnt == 1: + multi = 8 + coeff0 = self.info.coefficients1 + coeff1 = self.info.coefficients1 + blocks = self.data.size / multi + data1_offset = offset + data2_offset = offset + blocks * 8 + decoded_buffer_l = [0 for i in range(blocks * 14)] + decoded_buffer_r = [0 for i in range(blocks * 14)] + for x in xrange(blocks): + out_buffer = self.decode_adpcm(0, coeff0, buffer[data1_offset:data1_offset+8]) + for y in xrange(14): + decoded_buffer_l[x*14+y] = out_buffer[y] + out_buffer = self.decode_adpcm(1, coeff1, buffer[data2_offset:data2_offset+8]) + for y in xrange(14): + decoded_buffer_r[x*14+y] = out_buffer[y] + data1_offset += 8 + data2_offset += 8 + for x in xrange(blocks * 14): + decoded_buffer.append(decoded_buffer_l[x]) + decoded_buffer.append(decoded_buffer_r[x]) + return decoded_buffer + def eat(self, buffer, offset, decode=False): + co = self.header.eat(buffer, offset) + co = self.info.eat(buffer, co) + co = self.data.eat(buffer, co) + self.data.data = buffer[co:] + if decode == True: + buffer_out = self.decode(buffer, co) + return buffer_out + return + def show(self): + self.header.show() + self.info.show() + self.data.show() + return + def write(self, filename): + file = open(filename, 'wb') + if file: + self.header.write(file) + self.info.write(file) + self.data.write(file) + file.close() + else: + print "Could not open file for writing" + return + +def main(): + if sys.argv[1] == "-d": + file = open(sys.argv[2], 'rb') + if file: + buffer = file.read() + file.close() + else: + print "Could not open file" + sys.exit(2) + bns = BNS() + wavbuffer = bns.eat(buffer, 0x00, True) + wavstring = ''.join(Struct.int16(p) for p in wavbuffer) + f = SoundFile(wavstring, sys.argv[3], bns.info.samplerate) + f.write() + + elif sys.argv[1] == "-e": + f = wave.open(sys.argv[2], 'rb') + num_chans = f.getnchannels() + samplerate = f.getframerate() + assert samplerate >= 32000 + assert samplerate <= 48000 + buffer = f.readframes(f.getnframes()) + f.close() + + bns = BNS() + bns.create_bns(buffer, samplerate, num_chans) + bns.write(sys.argv[3]) + elif sys.argv[1] == "-s": + file = open(sys.argv[2], 'rb') + if file: + buffer = file.read() + file.close() + else: + print "Could not open file" + sys.exit(2) + bns = BNS() + bns.eat(buffer, 0x00, False) + bns.show() + else: + print "Unknown second argument. possiblities are -d and -e" + print "Usage: python bns.py -d " + print " == OR == " + print " python bns.py -e " + print " == OR == " + print " python bns.py -s " + sys.exit(1) + +if __name__ == "__main__": + # Import Psyco if available + try: + import psyco + psyco.full() + except ImportError: + print "no psycho import" + if len(sys.argv) == 1: + print "Usage: python bns.py -d " + print " == OR == " + print " python bns.py -e " + print " == OR == " + print " python bns.py -s " + sys.exit(1) + main() + diff --git a/common.py b/common.py index 085659e..5cc6a3c 100644 --- a/common.py +++ b/common.py @@ -1,4 +1,5 @@ -import os, hashlib, struct, subprocess, fnmatch, shutil, urllib, array, time, sys, tempfile +import os, hashlib, struct, subprocess, fnmatch, shutil, urllib, array, time, sys, tempfile, wave +from cStringIO import StringIO from Crypto.Cipher import AES from PIL import Image @@ -9,72 +10,107 @@ def align(x, boundary): if(x % boundary): x += (x + boundary) - (x % boundary) return x + +def clamp(var, min, max): + if var < min: var = min + if var > max: var = max + return var -def hexdump(s, sep=" "): +def abs(var): + if var < 0: + var = var + (2 * var) + return var + +def hexdump(s, sep=" "): # just dumps hex values return sep.join(map(lambda x: "%02x" % ord(x), s)) + +def hexdump2(src, length = 16): # dumps to a "hex editor" style output + result = [] + for i in xrange(0, len(src), length): + s = src[i:i + length] + if(len(s) % 4 == 0): + mod = 0 + else: + mod = 1 + hexa = '' + for j in range((len(s) / 4) + mod): + hexa += ' '.join(["%02X" % ord(x) for x in s[j * 4:j * 4 + 4]]) + if(j != ((len(s) / 4) + mod) - 1): + hexa += ' ' + printable = s.translate(''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])) + result.append("0x%04X %-*s %s\n" % (i, (length * 3) + 2, hexa, printable)) + return ''.join(result) + +print hexdump2("RANDOM STRING \x01 TESTING \x214 TEST OF STrasneljkasdhfleasdklhglkaje;shadlkghehaosehlgasdlkfhe;lakhsdglhaelksejdfffffffjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjasdfsadf") -class Crypto: +class Crypto(object): """This is a Cryptographic/hash class used to abstract away things (to make changes easier)""" - def __init__(self): - self.align = 64 - return + align = 64 + @classmethod def decryptData(self, key, iv, data, align = True): """Decrypts some data (aligns to 64 bytes, if needed).""" if((len(data) % self.align) != 0 and align): return AES.new(key, AES.MODE_CBC, iv).decrypt(data + ("\x00" * (self.align - (len(data) % self.align)))) else: return AES.new(key, AES.MODE_CBC, iv).decrypt(data) + @classmethod def encryptData(self, key, iv, data, align = True): """Encrypts some data (aligns to 64 bytes, if needed).""" if((len(data) % self.align) != 0 and align): return AES.new(key, AES.MODE_CBC, iv).encrypt(data + ("\x00" * (self.align - (len(data) % self.align)))) else: return AES.new(key, AES.MODE_CBC, iv).encrypt(data) + @classmethod def decryptContent(self, titlekey, idx, data): """Decrypts a Content.""" iv = struct.pack(">H", idx) + "\x00" * 14 return self.decryptData(titlekey, iv, data) + @classmethod def decryptTitleKey(self, commonkey, tid, enckey): """Decrypts a Content.""" iv = struct.pack(">Q", tid) + "\x00" * 8 return self.decryptData(commonkey, iv, enckey, False) + @classmethod def encryptContent(self, titlekey, idx, data): """Encrypts a Content.""" iv = struct.pack(">H", idx) + "\x00" * 14 return self.encryptData(titlekey, iv, data) + @classmethod def createSHAHash(self, data): #tested WORKING (without padding) return hashlib.sha1(data).digest() + @classmethod def createSHAHashHex(self, data): return hashlib.sha1(data).hexdigest() + @classmethod def createMD5HashHex(self, data): return hashlib.md5(data).hexdigest() + @classmethod def createMD5Hash(self, data): return hashlib.md5(data).digest() + @classmethod def validateSHAHash(self, data, hash): contentHash = hashlib.sha1(data).digest() - - print 'Content hash : %s : len %i' % (hexdump(contentHash), len(contentHash)) - print 'Expected : %s : len %i' % (hexdump(hash), len(hash)) - + return 1 if (contentHash == hash): return 1 else: + #raise ValueError('Content hash : %s : len %i' % (hexdump(contentHash), len(contentHash)) + 'Expected : %s : len %i' % (hexdump(hash), len(hash))) return 0 class WiiObject(object): @classmethod - def load(cls, data): + def load(cls, data, *args, **kwargs): self = cls() - self._load(data) + self._load(data, *args, **kwargs) return self @classmethod - def loadFile(cls, filename): - return cls.load(open(filename, "rb").read()) + def loadFile(cls, filename, *args, **kwargs): + return cls.load(open(filename, "rb").read(), *args, **kwargs) - def dump(self): - return self._dump() - def dumpFile(self, filename): - open(filename, "wb").write(self.dump()) + def dump(self, *args, **kwargs): + return self._dump(*args, **kwargs) + def dumpFile(self, filename, *args, **kwargs): + open(filename, "wb").write(self.dump(*args, **kwargs)) return filename class WiiArchive(WiiObject): @@ -89,3 +125,15 @@ class WiiArchive(WiiObject): os.mkdir(dirname) self._dumpDir(dirname) return dirname + +class WiiHeader(object): + def __init__(self, data): + self.data = data + def addFile(self, filename): + open(filename, "wb").write(self.add()) + def removeFile(self, filename): + open(filename, "wb").write(self.remove()) + @classmethod + def loadFile(cls, filename, *args, **kwargs): + return cls(open(filename, "rb").read(), *args, **kwargs) + diff --git a/headers.py b/headers.py index 120097a..e0779b6 100644 --- a/headers.py +++ b/headers.py @@ -1,6 +1,6 @@ from common import * -class IMD5(): +class IMD5(WiiHeader): """This class can add and remove IMD5 headers to files. The parameter f is the file to use for the addition or removal of the header. IMD5 headers are found in banner.bin, icon.bin, and sound.bin.""" class IMD5Header(Struct): __endian__ = Struct.BE @@ -9,29 +9,21 @@ class IMD5(): self.size = Struct.uint32 self.zeroes = Struct.uint8[8] self.crypto = Struct.string(16) - def __init__(self, f): - self.f = f - def add(self, fn = ""): - """This function adds an IMD5 header to the file specified by f in the initializer. The output file is specified with fn, if it is empty, it will overwrite the input file. If the file already has an IMD5 header, it will now have two. Returns the output filename.""" - data = open(self.f, "rb").read() + def add(self): + data = self.data imd5 = self.IMD5Header() - for i in range(8): - imd5.zeroes[i] = 0x00 imd5.tag = "IMD5" imd5.size = len(data) + for i in range(8): + imd5.zeroes[i] = 0x00 imd5.crypto = str(Crypto().createMD5Hash(data)) data = imd5.pack() + data - if(fn != ""): - open(fn, "wb").write(data) - return fn - else: - open(self.f, "wb").write(data) - return self.f - def remove(self, fn = ""): + return data + def remove(self): """This will remove an IMD5 header from the file specified in f, if one exists. If there is no IMD5 header, it will output the file as it is. It will output in the parameter fn if available, otherwise it will overwrite the source. Returns the output filename.""" - data = open(self.f, "rb").read() + data = self.data imd5 = self.IMD5Header() if(data[:4] != "IMD5"): if(fn != ""): @@ -41,14 +33,9 @@ class IMD5(): return self.f data = data[len(imd5):] - if(fn != ""): - open(fn, "wb").write(data) - return fn - else: - open(self.f, "wb").write(data) - return self.f + return data -class IMET(): +class IMET(WiiHeader): """IMET headers are found in Opening.bnr and 0000000.app files. They contain the channel titles and more metadata about channels. They are in two different formats with different amounts of padding before the start of the IMET header. This class suports both. The parameter f is used to specify the input file name.""" @@ -63,11 +50,9 @@ class IMET(): self.names = Struct.string(84, encoding = "utf-16-be", stripNulls = True)[7] self.zeroes2 = Struct.uint8[840] self.hash = Struct.string(16) - def __init__(self, f): - self.f = f def add(self, iconsz, bannersz, soundsz, name = "", langs = [], fn = ""): """This function adds an IMET header to the file specified with f in the initializer. The file will be output to fn if it is not empty, otherwise it will overwrite the input file. You must specify the size of banner.bin in bannersz, and respectivly for iconsz and soundsz. langs is an optional arguement that is a list of different langauge channel titles. name is the english name that is copied everywhere in langs that there is an empty string. Returns the output filename.""" - data = open(self.f, "rb").read() + data = self.data imet = self.IMETHeader() for i in imet.zeroes: @@ -92,42 +77,24 @@ class IMET(): data = imet.pack() + data - if(fn != ""): - open(fn, "wb").write(data) - return fn - else: - open(self.f, "wb").write(data) - return self.f - - def remove(self, fn = ""): - """This method removes an IMET header from a file specified with f in the initializer. fn is the output file name if it isn't an empty string, if it is, it will overwrite the input. If the input has no IMD5 header, it is output as is. Returns the output filename.""" - data = open(self.f, "rb").read() + return data + def remove(self): + data = self.data if(data[0x80:0x84] == "IMET"): data = data[0x640:] elif(data[0x40:0x44] == "IMET"): data = data[0x640:] - else: - if(fn != ""): - open(fn, "wb").write(data) - return fn - else: - return self.f - if(fn != ""): - open(fn, "wb").write(data) - return fn - else: - open(self.f, "wb").write(data) - return self.f + return data def getTitle(self): imet = self.IMETHeader() - data = open(self.f, "rb").read() + data = self.data if(data[0x40:0x44] == "IMET"): pass elif(data[0x80:0x84] == "IMET"): data = data[0x40:] else: - return "" + raise ValueError("No IMET header found!") imet.unpack(data[:len(imet)]) name = imet.names[1] diff --git a/image.py b/image.py index a2ecf68..ae4cbc6 100644 --- a/image.py +++ b/image.py @@ -73,7 +73,7 @@ class TPL(): self.format = Struct.uint32 self.offset = Struct.uint32 def __init__(self, file): - if(os.path.isfile(file)): + if(not ("\x00" in file) and os.path.isfile(file)): self.file = file self.data = None else: