Skip to content

Instantly share code, notes, and snippets.

@WitherOrNot
Created May 16, 2023 18:20
Show Gist options
  • Save WitherOrNot/25cf89b59f52a9fa72e29843f75e6deb to your computer and use it in GitHub Desktop.
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
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