Created
May 16, 2023 18:20
-
-
Save WitherOrNot/25cf89b59f52a9fa72e29843f75e6deb to your computer and use it in GitHub Desktop.
Switch Firmware Downloader. Needs hactool in same directory, decrypted PRODINFO.bin and prod.keys in "keys" directory
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
from Crypto.PublicKey import RSA | |
from anynet import tls | |
from struct import unpack, calcsize | |
from binascii import hexlify, unhexlify | |
from requests import request | |
from os import makedirs, remove | |
from subprocess import run, PIPE | |
from os.path import basename, exists | |
from shutil import rmtree | |
from glob import glob | |
from configparser import ConfigParser | |
from sys import argv | |
import hashlib | |
import warnings | |
warnings.filterwarnings("ignore") | |
ENV = "lp1" | |
VERSION = argv[1] if len(argv) > 1 else "" | |
def readdata(f, addr, size): | |
f.seek(addr) | |
return f.read(size) | |
def utf8(s): | |
return s.decode("utf-8") | |
def sha256(s): | |
return hashlib.sha256(s).digest() | |
def readint(f, addr): | |
f.seek(addr) | |
return unpack("<I", f.read(4))[0] | |
def sreadint(f): | |
return unpack("<I", f.read(4))[0] | |
def readshort(f, addr): | |
f.seek(addr) | |
return unpack("<H", f.read(2))[0] | |
def sreadshort(f): | |
return unpack("<H", f.read(2))[0] | |
def readstc(f, s, addr): | |
f.seek(addr) | |
s = "<" + s | |
sz = calcsize(s) | |
return unpack(s, f.read(sz)) | |
def sreadstc(f, s): | |
s = "<" + s | |
sz = calcsize(s) | |
return unpack(s, f.read(sz)) | |
def hexify(s): | |
return hexlify(s).decode("utf-8") | |
def ihexify(n, b): | |
return hex(n)[2:].zfill(b * 2) | |
def dlfile(url, out): | |
run(["aria2c", "--no-conf", "--console-log-level=error", "--file-allocation=none", "--summary-interval=0", "--download-result=hide", "--certificate=keys/switch_client.crt", "--private-key=keys/switch_client.key", f"--header=User-Agent: {user_agent}", "--check-certificate=false", f"--out={out}", "-c", url]) | |
def dlfiles(dltable): | |
with open("dl.tmp", "w") as f: | |
for url, dirc, fname, fhash in dltable: | |
f.write(f"{url}\n\tout={fname}\n\tdir={dirc}\n\tchecksum=sha-256={fhash}\n") | |
run(["aria2c", "--no-conf", "--console-log-level=error", "--file-allocation=none", "--summary-interval=0", "--download-result=hide", "--certificate=keys/switch_client.crt", "--private-key=keys/switch_client.key", f"--header=User-Agent: {user_agent}", "--check-certificate=false", "-x", "16", "-s", "16", "-i", "dl.tmp"]) | |
remove("dl.tmp") | |
def nin_request(method, url, headers={}): | |
headers.update({ | |
"User-Agent": user_agent | |
}) | |
resp = request(method, url, cert=("keys/switch_client.crt", "keys/switch_client.key"), headers=headers, verify=False) | |
resp.raise_for_status() | |
return resp | |
def parse_cnmt(nca): | |
ncaf = basename(nca) | |
run(["./hactool", "-k", "keys/prod.keys", nca, f"--section0dir=cnmt_{ncaf}"], stdout=PIPE, stderr=PIPE) | |
cnmt_file = glob(f"cnmt_{ncaf}/*.cnmt")[0] | |
entries = [] | |
with open(cnmt_file, "rb") as c: | |
cnmt_type = readdata(c, 0xc, 0x1) | |
if cnmt_type[0] == 0x3: | |
n_entries = readshort(c, 0x12) | |
offset = readshort(c, 0xe) | |
c.seek(0x20 + offset) | |
for i in range(n_entries): | |
title_id = ihexify(readstc(c, "Q", 0x20 + offset + i * 0x10)[0], 8) | |
version = sreadint(c) | |
entries.append((title_id, version)) | |
else: | |
n_entries = readshort(c, 0x10) | |
offset = readshort(c, 0xe) | |
c.seek(0x20 + offset) | |
for i in range(n_entries): | |
nca_hash = hexify(readstc(c, "32s", 0x20 + offset + i * 0x38)[0]) | |
nca_id = hexify(sreadstc(c, "16s")[0]) | |
entries.append((nca_id, nca_hash)) | |
rmtree(f"cnmt_{ncaf}") | |
return entries | |
def dltitle(title_id, version, is_su=False): | |
global update_files, update_dls, sv_nca | |
p = "s" if is_su else "a" | |
cnmt_id = nin_request("HEAD", f"https://atumn.hac.{ENV}.d4c.nintendo.net/t/{p}/{title_id}/{version}?device_id={device_id}").headers["X-Nintendo-Content-ID"] | |
cnmt_nca = f"Firmware_{ver_string}/{cnmt_id}.cnmt.nca" | |
update_files.append(cnmt_nca) | |
dlfile(f"https://atumn.hac.{ENV}.d4c.nintendo.net/c/{p}/{cnmt_id}?device_id={device_id}", cnmt_nca) | |
if is_su: | |
for title_id, version in parse_cnmt(cnmt_nca): | |
dltitle(title_id, version) | |
else: | |
for nca_id, nca_hash in parse_cnmt(cnmt_nca): | |
if title_id == "0100000000000809": | |
sv_nca = f"{nca_id}.nca" | |
update_files.append(f"Firmware_{ver_string}/{nca_id}.nca") | |
update_dls.append((f"https://atumn.hac.{ENV}.d4c.nintendo.net/c/c/{nca_id}?device_id={device_id}", f"Firmware_{ver_string}", f"{nca_id}.nca", nca_hash)) | |
if __name__ == "__main__": | |
prod_keys = ConfigParser(strict=False) | |
with open("keys/prod.keys") as f: | |
prod_keys.read_string("[keys]" + f.read()) | |
with open("keys/PRODINFO.bin", "rb") as pf: | |
if pf.read(4) != b"CAL0": | |
print("Invalid PRODINFO (Invalid header)! May be encrypted") | |
exit(1) | |
device_id = utf8(readdata(pf, 0x2b56, 0x10)) | |
print("Device ID:", device_id) | |
cert_size = readint(pf, 0xad0) | |
cert_data = readdata(pf, 0xae0, cert_size) | |
cert_hash = readdata(pf, 0x12e0, 0x20) | |
if hexify(sha256(cert_data)) != hexify(cert_hash): | |
print("Invalid PRODINFO (invalid SSL cert)? May be corrupted, or Incognito used") | |
cert = tls.TLSCertificate.parse(cert_data, tls.TYPE_DER) | |
cert.save("keys/switch_client.crt", tls.TYPE_PEM) | |
d = int.from_bytes(unhexlify(prod_keys["keys"]["ssl_rsa_key"]), "big") | |
pubkey = cert.public_key() | |
rsa = RSA.construct((pubkey.n, pubkey.e, d), consistency_check=True) | |
rsa_key = tls.TLSPrivateKey.parse(rsa.export_key("DER"), tls.TYPE_DER) | |
rsa_key.save("keys/switch_client.key", tls.TYPE_PEM) | |
user_agent = f"NintendoSDK Firmware/11.0.0-0 (platform:NX; did:{device_id}; eid:{ENV})" | |
if VERSION == "": | |
su_meta = nin_request("GET", f"https://sun.hac.{ENV}.d4c.nintendo.net/v1/system_update_meta?device_id={device_id}").json() | |
ver_raw = su_meta["system_update_metas"][0]["title_version"] | |
else: | |
ver_parts = list(map(int, VERSION.split("."))) | |
ver_raw = ver_parts[0] * 0x4000000 + ver_parts[1] * 0x100000 + ver_parts[2] * 0x10000 + ver_parts[3] | |
ver_major = ver_raw // 0x4000000 | |
ver_minor = (ver_raw - ver_major * 0x4000000) // 0x100000 | |
ver_sub1 = (ver_raw - ver_major * 0x4000000 - ver_minor * 0x100000) // 0x10000 | |
ver_sub2 = (ver_raw - ver_major * 0x4000000 - ver_minor * 0x100000 - ver_sub1 * 0x10000) | |
ver_string = f"{ver_major}.{ver_minor}.{ver_sub1}.{str(ver_sub2).zfill(4)}" | |
print(f"Downloading firmware {ver_string}") | |
update_files = [] | |
update_dls = [] | |
sv_nca = "" | |
dltitle("0100000000000816", ver_raw, is_su=True) | |
dlfiles(update_dls) | |
for updf in update_files: | |
if not exists(updf): | |
print("DOWNLOAD FAILED!") | |
print("Delete the firmware folder and re-run") | |
print() | |
print("DOWNLOAD COMPLETE!") | |
print("SystemVersion NCA:", sv_nca) | |
print("Make sure to check hashes!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment