Skip to content

Instantly share code, notes, and snippets.

@WitherOrNot
Last active May 14, 2023 20:11
Show Gist options
  • Select an option

  • Save WitherOrNot/989ec8f35bc68349ea1437336be7bb71 to your computer and use it in GitHub Desktop.

Select an option

Save WitherOrNot/989ec8f35bc68349ea1437336be7bb71 to your computer and use it in GitHub Desktop.
Download games from Wii U NUS
#!/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