Last active
May 14, 2023 20:11
-
-
Save WitherOrNot/989ec8f35bc68349ea1437336be7bb71 to your computer and use it in GitHub Desktop.
Download games from Wii U NUS
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 | |
| import struct | |
| import subprocess | |
| import os | |
| import base64 | |
| import shutil | |
| import argparse | |
| import binascii | |
| import requests | |
| import hashlib | |
| from Crypto.Hash import MD5 | |
| from Crypto.Cipher import AES | |
| COMMON_KEY = b"\xd7\xb0\x04\x02e\x9b\xa2\xab\xd2\xcb\r\xb2\x7f\xa2\xb6V" | |
| SECRET = b"\xfd\x04\x01\x05\x06\x0b\x11\x1c-I" | |
| CERT = "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" | |
| unhex = binascii.unhexlify | |
| rand = os.urandom | |
| zero = lambda n: b"\x00" * n | |
| def aes_encrypt(key, iv, ctext): | |
| iv += b"\x00" * (16 - len(iv)) | |
| aes = AES.new(key, AES.MODE_CBC, iv) | |
| return aes.encrypt(ctext) | |
| def md5(s): | |
| md = MD5.new() | |
| md.update(s) | |
| return md.digest() | |
| def gen_key(titleid, pasw="mypass"): | |
| titleid = titleid[1:] | |
| salt = md5(SECRET + titleid) | |
| return hashlib.pbkdf2_hmac('sha1', pasw.encode("utf-8"), salt, 20, 16) | |
| def tikgen(tid): | |
| tid_bytes = unhex(tid) | |
| out = unhex("00010004") | |
| out += rand(0x100) | |
| out += zero(0x3C) | |
| out += unhex("526F6F742D434130303030303030332D58533030303030303063000000000000") | |
| out += zero(0x5C) | |
| out += unhex("010000") | |
| out += aes_encrypt(COMMON_KEY, tid_bytes, gen_key(tid_bytes)) | |
| out += unhex("000005") | |
| out += rand(0x06) | |
| out += zero(0x04) | |
| out += unhex(tid) | |
| out += unhex("00000011000000000000000000000005") | |
| out += zero(0xB0) | |
| out += unhex("00010014000000AC000000140001001400000000000000280000000100000084000000840003000000000000FFFFFF01") | |
| out += zero(0x7C) | |
| return out | |
| def parse_tmd(tmd_path): | |
| tmd_file = open(tmd_path, "rb") | |
| tmd_file.seek(0x1DE) | |
| nbr_conts = struct.unpack(">H", tmd_file.read(2))[0] | |
| tmd_file.seek(0xB04) | |
| contents = [] | |
| for i in range(nbr_conts): | |
| c = tmd_file.read(48) | |
| contents.append((c[:4].hex(), c[7] & 2 == 2)) | |
| tmd_file.close() | |
| return contents | |
| def mkdir(dirc): | |
| if os.path.exists(dirc): | |
| return | |
| os.mkdir(dirc) | |
| def wget(files): | |
| with open("dl.tmp", "w") as f: | |
| for url, dirc, fname in files: | |
| f.write(f"{url}\n\tout={fname}\n\tdir={dirc}\n") | |
| p = subprocess.run(["aria2c", "--no-conf", "--file-allocation=none", "--summary-interval=0", "--download-result=hide", "-x", "16", "-j", "16", "-i", "dl.tmp"]) | |
| os.remove("dl.tmp") | |
| def download_title(titleid, root=""): | |
| og_titleid = titleid.replace("0005000E", "00050000").replace("0005000C", "00050000") | |
| try: | |
| tdata = requests.get(f"https://dantheman827.github.io/nus-info/titles/{og_titleid.lower()}.json").json() | |
| if "US" in tdata: | |
| region = "USA" | |
| name = tdata["US"]["name"] | |
| elif "JP" in tdata: | |
| region = "JPN" | |
| name = tdata["JP"]["name"] | |
| else: | |
| region = "EUR" | |
| name = tdata["GB"]["name"] | |
| name = name.replace("\n", " ") | |
| if region != "JPN": | |
| name = "".join(filter(lambda c: c.isascii() and c not in "<>\"/\\|?*", name)) | |
| else: | |
| name = "".join(map(lambda c: c if c.isascii() and c not in "<>\"/\\|?*" else "_", name)) | |
| tags = "" | |
| if "0005000C" in titleid: | |
| tags += "[DLC]" | |
| elif "0005000E" in titleid: | |
| tags += "[Update]" | |
| dirname = f"{name} [{titleid}]{tags}[{region}]" | |
| except: | |
| dirname = titleid | |
| files = [] | |
| dldir = os.path.join(root, dirname) | |
| if os.path.exists(dldir): | |
| return | |
| mkdir(dldir) | |
| print(f"Downloading title {titleid}...") | |
| if titleid[:8] in ["00050000", "0005000C", "0005000E"]: | |
| with open(os.path.join(dldir, "title.tik"), "wb") as tikfile: | |
| tikfile.write(tikgen(titleid)) | |
| else: | |
| files.append((f"http://nus.cdn.wup.shop.nintendo.net/ccs/download/{titleid}/cetk", dldir, "title.tik")) | |
| files.append((f"http://nus.cdn.wup.shop.nintendo.net/ccs/download/{titleid}/tmd", dldir, "title.tmd")) | |
| wget(files) | |
| files = [] | |
| certfile = open(os.path.join(dldir, "title.cert"), "wb") | |
| certfile.write(base64.b64decode(CERT)) | |
| certfile.close() | |
| for content, h3 in parse_tmd(os.path.join(dldir, "title.tmd")): | |
| files.append((f"http://nus.cdn.wup.shop.nintendo.net/ccs/download/{titleid}/{content}", dldir, f"{content}.app")) | |
| if h3: | |
| files.append((f"http://nus.cdn.wup.shop.nintendo.net/ccs/download/{titleid}/{content}.h3", dldir, f"{content}.h3")) | |
| wget(files) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("titleids", nargs="*", help="Title IDs for game to download") | |
| parser.add_argument("--directory", "-d", help="Directory to download within", default="") | |
| args = parser.parse_args() | |
| for t in args.titleids: | |
| download_title(t.upper(), root=args.directory) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment