Added Title Key generation code

This commit is contained in:
Campbell 2024-12-13 16:56:15 -05:00
parent 08c2bd27f5
commit 147e72c8c9
Signed by: NinjaCheetah
GPG Key ID: 670C282B3291D63D
5 changed files with 98 additions and 10 deletions

View File

@ -1,5 +1,5 @@
# NUSGet # NUSGet After Dark
A modern and supercharged NUS downloader built with Python and Qt6. Powered by libWiiPy and libTWLPy. A modern and supercharged NUS downloader built with Python and Qt6. Powered by libWiiPy and libTWLPy. Fork with features not acceptable for prod.
[![Python application](https://github.com/NinjaCheetah/NUSGet/actions/workflows/python-build.yml/badge.svg)](https://github.com/NinjaCheetah/NUSGet/actions/workflows/python-build.yml) [![Python application](https://github.com/NinjaCheetah/NUSGet/actions/workflows/python-build.yml/badge.svg)](https://github.com/NinjaCheetah/NUSGet/actions/workflows/python-build.yml)
@ -12,10 +12,11 @@ NUSGet also offers the ability to create vWii WADs that can be installed from wi
The following features are available for all supported consoles: The following features are available for all supported consoles:
- Downloading encrypted contents (files like `00000000`, `00000001`, etc.) directly from the update servers for any title. - Downloading encrypted contents (files like `00000000`, `00000001`, etc.) directly from the update servers for any title.
- Creating decrypted contents (*.app files) from the encrypted contents on the servers. Only supported for free titles. - Creating decrypted contents (*.app files) from the encrypted contents on the servers.
**For Wii and vWii titles only:** **For Wii and vWii titles only:**
- "Pack installable archive (WAD/TAD)": Pack the encrypted contents, TMD, and Ticket into a WAD file that can be installed on a Wii or in Dolphin Emulator. Only supported for free titles. - "Pack installable archive (WAD/TAD)": Pack the encrypted contents, TMD, and Ticket into a WAD file that can be installed on a Wii or in Dolphin Emulator.
- Forging Tickets for titles without a common Ticket available on the NUS by using the Title Key algorithm to derive the key needed to decrypt the title.
**For vWii titles only:** **For vWii titles only:**
- "Re-encrypt title using the Wii Common Key": Re-encrypt the Title Key in a vWii title's Ticket before packing the WAD, so that the WAD can be installed via a normal WAD manager on the vWii, and can be extracted with legacy tools. **This also creates WADs that can be installed directly in Dolphin, allowing for running the vWii System Menu in Dolphin without a vWii NAND dump!** - "Re-encrypt title using the Wii Common Key": Re-encrypt the Title Key in a vWii title's Ticket before packing the WAD, so that the WAD can be installed via a normal WAD manager on the vWii, and can be extracted with legacy tools. **This also creates WADs that can be installed directly in Dolphin, allowing for running the vWii System Menu in Dolphin without a vWii NAND dump!**

View File

@ -4,8 +4,9 @@
import os import os
import pathlib import pathlib
from typing import List, Tuple from typing import List, Tuple
from .tkey import find_tkey
import libWiiPy import libWiiPy
from libWiiPy.title.ticket import _TitleLimit
def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool, def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool,
@ -58,6 +59,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
tmd_out.write(title.tmd.dump()) tmd_out.write(title.tmd.dump())
tmd_out.close() tmd_out.close()
# Use a local ticket, if one exists and "use local files" is enabled. # Use a local ticket, if one exists and "use local files" is enabled.
forge_ticket = False
if use_local_chkbox is True and os.path.exists(os.path.join(version_dir, "tik")): if use_local_chkbox is True and os.path.exists(os.path.join(version_dir, "tik")):
progress_callback.emit(" - Parsing local copy of Ticket...") progress_callback.emit(" - Parsing local copy of Ticket...")
local_ticket = open(os.path.join(version_dir, "tik"), "rb") local_ticket = open(os.path.join(version_dir, "tik"), "rb")
@ -70,11 +72,10 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
ticket_out.write(title.ticket.dump()) ticket_out.write(title.ticket.dump())
ticket_out.close() ticket_out.close()
except ValueError: except ValueError:
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a # If libWiiPy returns an error, then no ticket is available. Try to forge a ticket after we download the
# ticket so that they aren't attempted later. # content.
progress_callback.emit(" - No Ticket is available!") progress_callback.emit(" - No Ticket is available! Will try forging a Ticket.")
pack_wad_enabled = False forge_ticket = True
decrypt_contents_enabled = False
# Load the content records from the TMD, and begin iterating over the records. # Load the content records from the TMD, and begin iterating over the records.
title.load_content_records() title.load_content_records()
content_list = [] content_list = []
@ -105,6 +106,39 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
enc_content_out.write(content_list[content]) enc_content_out.write(content_list[content])
enc_content_out.close() enc_content_out.close()
title.content.content_list = content_list title.content.content_list = content_list
# Try to forge a Ticket, if a common one wasn't available.
if forge_ticket is True:
progress_callback.emit(" - Attempting to forge Ticket...")
try:
title_key = find_tkey(tid, title.content.content_list[0], title.tmd.content_records[0])
title_key_enc = libWiiPy.title.encrypt_title_key(title_key, 0, tid)
ticket = libWiiPy.title.Ticket()
ticket.common_key_index = 0
ticket.console_id = 0
ticket.content_access_permissions = b'\xff' * 64
ticket.ecdh_data = b'\x00' * 60
ticket.permit_mask = b'\x00' * 4
ticket.permitted_titles = b'\x00' * 4
ticket.signature = b'\x00' * 256
ticket.signature_issuer = "Root-CA00000001-XS00000003" + ("\x00" * 38)
ticket.signature_type = b'\x00\x01' * 2
ticket.ticket_id = b'\x00' * 8
ticket.ticket_version = 0
ticket.title_export_allowed = 0
ticket.title_id = tid.encode()
ticket.title_key_enc = title_key_enc
ticket.title_limits_list = [_TitleLimit(0, 0) for _ in range(0, 8)]
ticket.title_version = 0
ticket.unknown1 = b'\xff' * 2
ticket.unknown2 = b'\x00' * 48
ticket.fakesign()
title.ticket = ticket
open(os.path.join(version_dir, "tik"), "wb").write(title.ticket.dump())
progress_callback.emit(" - Successfully forged Ticket!")
except Exception:
progress_callback.emit(" - Ticket could not be forged!")
pack_wad_enabled = False
decrypt_contents_enabled = False
# If decrypt local contents is still true, decrypt each content and write out the decrypted file. # If decrypt local contents is still true, decrypt each content and write out the decrypted file.
if decrypt_contents_enabled is True: if decrypt_contents_enabled is True:
try: try:

53
modules/tkey.py Normal file
View File

@ -0,0 +1,53 @@
# "tkey-gen.py", licensed under the MIT license
# Copyright 2024 NinjaCheetah
import binascii
import hashlib
import libWiiPy
from libWiiPy.types import _ContentRecord
def _secret(start, length):
ret = b''
add = start + length
for _ in range(length):
unsigned_start = start & 0xFF # Compensates for how Python handles negative values vs PHP.
ret += bytes.fromhex(f"{unsigned_start:02x}"[-2:])
nxt = start + add
add = start
start = nxt
return ret
def _mungetid(tid):
# Remove leading zeroes from the TID.
while tid.startswith("00"):
tid = tid[2:]
if tid == "":
tid = "00"
# In PHP, the last character just gets dropped if you make a hex string from an odd-length input, so this
# replicates that functionality.
if len(tid) % 2 != 0:
tid = tid[:-1]
return bytes.fromhex(tid)
def _derive_key(tid, passwd):
key_secret = _secret(-3, 10)
salt = hashlib.md5(key_secret + _mungetid(tid)).digest()
# Had to reduce the length here from 32 to 16 when converting to get the same length keys.
return hashlib.pbkdf2_hmac("sha1", passwd.encode(), salt, 20, 16).hex()
def find_tkey(tid: str, banner_enc: bytes, content_record: _ContentRecord) -> bytes:
# Find a working Title Key by generating a key with a password, then decrypting content 0 and comparing it to the
# expected hash. If the hash matches, then we generated the correct key.
passwds = ["nintendo", "mypass"]
for passwd in passwds:
key = binascii.unhexlify(_derive_key(tid, passwd).encode())
banner_dec = libWiiPy.title.decrypt_content(banner_enc, key, content_record.index, content_record.content_size)
banner_dec_hash = hashlib.sha1(banner_dec).hexdigest()
content_record_hash = content_record.content_hash.decode()
if banner_dec_hash == content_record_hash:
return key
raise Exception("Valid Title Key could not be generated")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB