Last active
May 14, 2023 20:11
-
-
Save WitherOrNot/bc7259ccc5496a61280e32093bcbadbb to your computer and use it in GitHub Desktop.
Decrypt Wii U NUS downloads for Loadiine/emulator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| from struct import pack, unpack, calcsize | |
| from Crypto.Cipher import AES | |
| from Crypto.Hash import SHA1 | |
| from binascii import hexlify, unhexlify | |
| from io import BytesIO | |
| from os.path import join | |
| from os import makedirs | |
| import sys | |
| COMMON_DEV_KEY = b"/\\\x1b)D\xe7\xfdo\xc3\x97\x96K\x05v\x91\xfa" | |
| COMMON_RETAIL_KEY = b"\xd7\xb0\x04\x02e\x9b\xa2\xab\xd2\xcb\r\xb2\x7f\xa2\xb6V" | |
| COMMON_KEY = b"" | |
| EXTRACT_ROOT = "" | |
| CONTENT_ROOT = "" | |
| def hexify(s): | |
| return hexlify(s).decode("utf-8") | |
| def ihexify(n, b): | |
| return hex(n)[2:].zfill(b * 2) | |
| def aes_decrypt(key, iv, ctext): | |
| iv += b"\x00" * (16 - len(iv)) | |
| aes = AES.new(key, AES.MODE_CBC, iv) | |
| return aes.decrypt(ctext) | |
| def sha1(s): | |
| sha = SHA1.new() | |
| sha.update(s) | |
| return sha.digest() | |
| def readint(f): | |
| return unpack(">I", f.read(4))[0] | |
| def readstc(f, s): | |
| s = ">" + s | |
| sz = calcsize(s) | |
| return unpack(s, f.read(sz)) | |
| def readstr(f): | |
| s = "" | |
| c = f.read(1) | |
| while c[0] != 0: | |
| s += chr(c[0]) | |
| c = f.read(1) | |
| return s | |
| def parse_ticket(ticket_path): | |
| with open(ticket_path, "rb") as ticket_file: | |
| ticket_file.seek(0x1BF) | |
| titlekey = ticket_file.read(16) | |
| ticket_file.seek(0x1DC) | |
| titleid = ticket_file.read(8) | |
| return titleid, titlekey | |
| def parse_tmd(tmd_path): | |
| global COMMON_KEY | |
| with open(tmd_path, "rb") as tmd_file: | |
| tmd_file.seek(0x140) | |
| issuer = tmd_file.read(0x40) | |
| if b"Root-CA00000003-CP0000000b" in issuer: | |
| COMMON_KEY = COMMON_RETAIL_KEY | |
| elif b"Root-CA00000004-CP00000010" in issuer: | |
| COMMON_KEY = COMMON_DEV_KEY | |
| tmd_file.seek(0x1DE) | |
| nbr_conts = unpack(">H", tmd_file.read(2))[0] | |
| tmd_file.seek(0xB04) | |
| contents = {} | |
| for i in range(nbr_conts): | |
| c = tmd_file.read(48) | |
| cid = unpack(">H", c[4:6])[0] | |
| contents[cid] = (hexify(c[:4]), c[7] & 2 == 2) | |
| return contents | |
| def extract_file(path, name, foff, flen, cid): | |
| BLOCK_SIZE = 0x8000 | |
| roffset = (foff // BLOCK_SIZE) * BLOCK_SIZE | |
| soffset = foff % BLOCK_SIZE | |
| iv = pack(">H", cid) | |
| size = flen | |
| wsize = BLOCK_SIZE | |
| aes = AES.new(titlekey, AES.MODE_CBC, iv + b"\x00" * 14) | |
| if soffset + size > wsize: | |
| wsize -= soffset | |
| makedirs(join(*path), exist_ok=True) | |
| fpath = join(*path, name) | |
| cpath = join(CONTENT_ROOT, contents[cid][0] + ".app") | |
| with open(cpath, "rb") as ef, open(fpath, "wb") as df: | |
| ef.seek(roffset) | |
| while size > 0: | |
| wsize = min(wsize, size) | |
| enc = ef.read(BLOCK_SIZE) | |
| dec = aes.decrypt(enc) | |
| size -= df.write(dec[soffset:soffset + wsize]) | |
| if soffset > 0: | |
| wsize = BLOCK_SIZE | |
| soffset = 0 | |
| def extract_hashed_file(path, name, foff, flen, cid): | |
| BLOCK_SIZE = 0x10000 | |
| HASH_BLOCK_SIZE = 0xFC00 | |
| HASHES_SIZE = 0x400 | |
| roffset = (foff // HASH_BLOCK_SIZE) * BLOCK_SIZE | |
| soffset = foff % HASH_BLOCK_SIZE | |
| size = flen | |
| wsize = HASH_BLOCK_SIZE | |
| block_num = (foff // HASH_BLOCK_SIZE) & 0x0F | |
| if soffset + size > wsize: | |
| wsize -= soffset | |
| makedirs(join(*path), exist_ok=True) | |
| fpath = join(*path, name) | |
| cpath = join(CONTENT_ROOT, contents[cid][0] + ".app") | |
| with open(cpath, "rb") as ef, open(fpath, "wb") as df: | |
| ef.seek(roffset) | |
| while size > 0: | |
| wsize = min(wsize, size) | |
| enc = ef.read(BLOCK_SIZE) | |
| iv = pack(">H", cid) | |
| hashes = aes_decrypt(titlekey, iv, enc)[:HASHES_SIZE] | |
| h0 = hashes[0x14 * block_num:0x14 * (block_num + 1)] | |
| iv = h0[:0x10] | |
| if block_num == 0: | |
| iv = bytearray(iv) | |
| iv[1] ^= cid | |
| iv = bytes(iv) | |
| dec = aes_decrypt(titlekey, iv, enc[HASHES_SIZE:]) | |
| chash = sha1(dec) | |
| if block_num == 0: | |
| chash = bytearray(chash) | |
| chash[1] ^= cid | |
| chash = bytes(chash) | |
| if chash != h0: | |
| print(f"WARN: Hash Mismatch, {hexify(chash)} != {hexify(h0)}. Continuing.") | |
| size -= df.write(dec[soffset:soffset + wsize]) | |
| block_num = (block_num + 1) % 16 | |
| if soffset > 0: | |
| wsize = HASH_BLOCK_SIZE | |
| soffset = 0 | |
| def extract_files(fst_data): | |
| f = BytesIO(fst_data) | |
| f.read(8) | |
| entry_count = readint(f) | |
| f.seek(0x20 + entry_count * 0x20 + 0x8) | |
| entries = readint(f) | |
| name_offset = 0x20 + entry_count * 0x20 + entries * 0x10 | |
| nxt = [] | |
| path = [EXTRACT_ROOT] | |
| for i in range(1, entries): | |
| eoff = 0x20 + entry_count * 0x20 + i * 0x10 | |
| f.seek(eoff) | |
| ftype, noff1, noff2 = readstc(f, "BBH") | |
| noff = (noff1 << 16) | noff2 | |
| f.seek(name_offset + noff) | |
| name = readstr(f) | |
| f.seek(eoff + 4) | |
| if len(nxt) > 0: | |
| while len(nxt) >= 1 and nxt[-1] == i: | |
| nxt.pop() | |
| path.pop() | |
| if ftype & 1: | |
| poff, nxoff = readstc(f, "II") | |
| print(f"{name} - Index: {ihexify(i, 4)} FST: {ihexify(eoff, 4)} Parent: {ihexify(poff, 4)} Next: {ihexify(nxoff, 4)}") | |
| path.append(name) | |
| nxt.append(nxoff) | |
| elif not ftype & 0x80: | |
| foff, flen, flags, cid = readstc(f, "IIHH") | |
| if not flags & 4: | |
| foff <<= 5 | |
| fpath = join(*path[1:], name) | |
| print(f"{fpath} - Index: {ihexify(i, 4)} FST: {ihexify(eoff, 4)} Offset: {ihexify(foff, 4)} Size: {ihexify(flen, 4)} Flags: {ihexify(flags, 2)} Content: {ihexify(cid, 2)}") | |
| if flags & 0x400 or flags & 0x40: | |
| extract_hashed_file(path, name, foff, flen, cid) | |
| else: | |
| extract_file(path, name, foff, flen, cid) | |
| if __name__ == "__main__": | |
| if len(sys.argv) < 3: | |
| print("Decrypts NUS files to Loadiine/emulator compatible files.") | |
| print(f"Usage: {sys.argv[0]} in_dir out_dir") | |
| exit(1) | |
| CONTENT_ROOT = sys.argv[1] | |
| EXTRACT_ROOT = sys.argv[2] | |
| contents = parse_tmd(join(CONTENT_ROOT, "title.tmd")) | |
| titleid, titlekey = parse_ticket(join(CONTENT_ROOT, "title.tik")) | |
| titlekey = aes_decrypt(COMMON_KEY, titleid, titlekey) | |
| with open(join(CONTENT_ROOT, contents[0][0] + ".app"), "rb") as f: | |
| fst_data = aes_decrypt(titlekey, b"\x00", f.read()) | |
| extract_files(fst_data) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment