36 Commits
main ... main

Author SHA1 Message Date
624aeaac5e Minor fix for libWiiPy v1.0.0 compatibility
ContentRecord is now a public type in libWiiPy.title.types rather than a private type in libWiiPy.types
2026-01-05 19:07:12 -05:00
502542a471 Merge remote-tracking branch 'upstream/main'
merge upstream
2026-01-05 18:02:28 -05:00
9539ed58a9 merge upstream 2025-07-04 16:02:40 -04:00
f674c8904d merge upstream 2025-05-31 00:00:37 -04:00
9cb11f2f68 Style update for After Dark theme 2025-05-25 01:12:46 -04:00
dee71e03d0 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	modules/download_wii.py
#	qt/py/ui_AboutDialog.py
2025-05-25 01:09:32 -04:00
72b7ae5789 merge upstream 2025-05-18 23:51:04 -04:00
db3947a100 Fix some remaining blue 2025-05-12 19:26:08 -04:00
c2e17bece7 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	resources/style.qss
2025-05-12 19:20:20 -04:00
47431c8834 merge upstream 2025-05-09 13:03:18 -04:00
ce099365cd merge upstream 2025-05-09 11:16:16 -04:00
0b6551d50d Merge remote-tracking branch 'origin/main' 2025-05-08 22:32:57 -04:00
9a9775348b After Dark color scheme B) 2025-05-08 22:32:20 -04:00
e1f8a23919 Merge remote-tracking branch 'refs/remotes/upstream/main' 2025-05-08 22:31:12 -04:00
ee5012383f Merge remote-tracking branch 'upstream/main' 2025-05-08 22:26:27 -04:00
45616f7f57 Merge remote-tracking branch 'upstream/main' 2025-05-07 08:55:41 -04:00
7feeeefc87 Merge remote-tracking branch 'upstream/main' 2025-05-04 19:23:08 -04:00
6689eaae70 Merge remote-tracking branch 'upstream/main' 2025-04-22 21:53:52 -04:00
d63acba656 Merge remote-tracking branch 'upstream/main' 2025-04-21 23:34:34 -04:00
0f96dc75a2 Merge remote-tracking branch 'upstream/main' 2025-02-23 19:33:48 -05:00
220fcc5e91 Merge remote-tracking branch 'upstream/main' 2025-02-18 21:31:54 -05:00
ab28a7bf1a Merge remote-tracking branch 'origin/main' 2025-02-18 21:31:06 -05:00
0bb87bf75f Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	README.md
#	modules/download_wii.py
2025-02-18 21:27:16 -05:00
92bfeb2374 Merge remote-tracking branch 'upstream/main' 2025-01-13 14:24:38 -05:00
86da2d62b0 Merge remote-tracking branch 'upstream/main' 2025-01-05 01:31:20 -05:00
1e88c22f7c Merge remote-tracking branch 'upstream/main' 2024-12-23 18:04:18 -05:00
c4ed6e6ee6 Merge remote-tracking branch 'origin/main' 2024-12-23 12:56:45 -05:00
b337be8c08 Merge remote-tracking branch 'upstream/main' 2024-12-23 12:56:27 -05:00
76911db12d Merge remote-tracking branch 'upstream/main' 2024-12-22 21:53:12 -05:00
a361a45314 Merge remote-tracking branch 'upstream/main' 2024-12-22 17:16:37 -05:00
724c7e554b Merge remote-tracking branch 'upstream/main' 2024-12-21 20:08:03 -05:00
469cd96392 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	README.md
2024-12-19 20:34:19 -05:00
398654609b Merge changes from upstream 2024-12-18 16:43:35 -05:00
78f98b2c73 Fix minor issue with Ticket forging 2024-12-13 23:17:20 -05:00
5872ca4676 Merge major improvements from upstream 2024-12-13 23:09:23 -05:00
147e72c8c9 Added Title Key generation code 2024-12-13 16:56:15 -05:00
6 changed files with 133 additions and 38 deletions

View File

@@ -1,3 +1,5 @@
# NUSGet After Dark
A modern and supercharged NUS downloader built with Python and Qt6. Powered by libWiiPy and libTWLPy. Fork with features not acceptable for prod.
<div align="center">
<img src="https://github.com/user-attachments/assets/156eb949-93aa-4453-b7a0-99b784ec0c8c" alt="The icon for NUSGet" width=256 height=256>
<h1>NUSGet</h1>
@@ -17,11 +19,12 @@ NUSGet also offers the ability to create vWii WADs that can be installed from wi
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.
- 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.
- Scripting support, allowing you to perform batch downloads of any titles for the Wii, vWii, or DSi in one script. (See `example-script.json` for an example of the scripting format.)
**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:**
- "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

@@ -2,8 +2,10 @@
# Copyright 2024-2025 NinjaCheetah & Contributors
import pathlib
from typing import List, Tuple
from .tkey import find_tkey
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,
@@ -53,6 +55,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
# Write out the TMD to a file.
version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump())
# Use a local ticket, if one exists and "use local files" is enabled.
forge_ticket = False
if use_local_chkbox and version_dir.joinpath("tik").exists():
progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...")
title.load_ticket(version_dir.joinpath("tik").read_bytes())
@@ -62,11 +65,10 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
except ValueError:
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a
# ticket so that they aren't attempted later.
progress_callback.emit(0, 0, " - No Ticket is available!")
pack_wad_enabled = False
decrypt_contents_enabled = False
# If libWiiPy returns an error, then no ticket is available. Try to forge a ticket after we download the
# content.
progress_callback.emit(0, 0, " - No Ticket is available! Will try forging a Ticket.")
forge_ticket = True
# Load the content records from the TMD, and begin iterating over the records.
title.load_content_records()
content_list = []
@@ -88,6 +90,39 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
if keep_enc_chkbox is True:
version_dir.joinpath(content_file_name).write_bytes(content_list[content])
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(0, 0, " - 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
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
progress_callback.emit(-1, -1, " - Successfully forged Ticket!")
except Exception:
progress_callback.emit(-1, -1, " - 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_contents_enabled is True:
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.title.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

View File

@@ -47,7 +47,7 @@ QMenuBar::item:selected {
}
QMenuBar::item:pressed {
background-color: #1a73e8;
background-color: #6c1ae8;
color: white;
}
@@ -68,7 +68,7 @@ QMenu::item {
}
QMenu::item:selected {
background-color: #1a73e8;
background-color: #6c1ae8;
color: white;
}
@@ -88,13 +88,13 @@ QRadioButton {
QRadioButton:hover {
background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8;
border-color: #9c4ae8;
}
QRadioButton:checked {
background-color: rgba(26, 115, 232, 0.08);
border: 1px solid #1a73e8;
color: #1a73e8;
border: 1px solid #6c1ae8;
color: #6c1ae8;
}
QRadioButton::indicator {
@@ -107,13 +107,13 @@ QRadioButton::indicator {
}
QRadioButton::indicator:checked {
background-color: #1a73e8;
border: 1px solid #1a73e8;
background-color: #6c1ae8;
border: 1px solid #6c1ae8;
image: url("{IMAGE_PREFIX}/rounded_square.svg");
}
QRadioButton::indicator:hover {
border-color: #1a73e8;
border-color: #6c1ae8;
}
QLineEdit {
@@ -124,11 +124,11 @@ QLineEdit {
margin: 4px 0px;
font-size: 13px;
color: #ffffff;
selection-background-color: #1a73e8;
selection-background-color: #6c1ae8;
}
QLineEdit:focus {
border-color: #1a73e8;
border-color: #6c1ae8;
}
QLineEdit:disabled {
@@ -187,11 +187,11 @@ QTreeView::item:hover {
}
QTreeView::item:focus {
background-color: rgba(26, 115, 232, 0.08);
background-color: rgba(64, 26, 232, 0.15);
}
QTreeView::item:selected {
background-color: #1a73e8;
background-color: #6c1ae8;
}
QTreeView QScrollBar:vertical {
@@ -211,7 +211,7 @@ QTreeView::branch:open:has-children:has-siblings {
QTextBrowser {
color: white;
background-color: #1a1a1a;
selection-background-color: #1a73e8;
selection-background-color: #6c1ae8;
}
QPushButton {
@@ -229,17 +229,17 @@ QPushButton {
QPushButton:hover {
background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8;
border-color: #9c4ae8;
}
QPushButton:focus {
background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8;
border-color: #9c4ae8;
}
QPushButton:pressed {
background-color: rgba(26, 115, 232, 0.15);
border: 1px solid #1a73e8;
background-color: rgba(64, 26, 232, 0.15);
border: 1px solid #6c1ae8;
}
QPushButton:disabled {
@@ -261,18 +261,18 @@ QComboBox {
}
QComboBox:on {
background-color: rgba(26, 115, 232, 0.15);
border: 1px solid #1a73e8;
background-color: rgba(64, 26, 232, 0.15);
border: 1px solid #6c1ae8;
}
QComboBox:hover {
background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8;
border-color: #9c4ae8;
}
QComboBox:focus {
background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8;
border-color: #9c4ae8;
}
QComboBox::drop-down {
@@ -301,7 +301,7 @@ QComboBox QAbstractItemView::item {
}
QComboBox QAbstractItemView::item:hover {
background-color: #1a73e8;
background-color: #6c1ae8;
}
QScrollBar:vertical {
@@ -320,7 +320,7 @@ QScrollBar::handle:vertical {
}
QScrollBar::handle:vertical:hover {
background-color: rgba(26, 115, 232, 0.4);
background-color: rgba(71, 26, 232, 0.4);
}
QScrollBar::add-line:vertical {
@@ -350,7 +350,7 @@ QScrollBar::handle:horizontal {
}
QScrollBar::handle:horizontal:hover {
background-color: rgba(26, 115, 232, 0.4);
background-color: rgba(71, 26, 232, 0.4);
}
QScrollBar::add-line:horizontal {
@@ -381,7 +381,7 @@ QProgressBar {
QProgressBar::chunk {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #1a73e8, stop: 1 #5596f4
stop: 0 #6c1ae8, stop: 1 #8941ec
);
border-radius: 5px;
margin: 0.5px;
@@ -401,7 +401,7 @@ WrapCheckboxWidget {
WrapCheckboxWidget:hover {
background-color: rgba(60, 60, 60, 1);
border-color: #4a86e8;
border-color: #9c4ae8;
}
WrapCheckboxWidget:disabled {
@@ -421,16 +421,20 @@ WrapCheckboxWidget QCheckBox::indicator {
border: 1px solid #5f6368;
}
WrapCheckboxWidget QCheckBox::indicator::focus {
background-color: rgba(64, 26, 232, 0.15);
}
WrapCheckboxWidget QCheckBox::indicator:checked {
background-color: #1a73e8;
border: 1px solid #1a73e8;
background-color: #6c1ae8;
border: 1px solid #6c1ae8;
image: url("{IMAGE_PREFIX}/check.svg");
}
WrapCheckboxWidget QCheckBox::indicator:hover {
border-color: #1a73e8;
border-color: #6c1ae8;
}
WrapCheckboxWidget QCheckBox:checked {
color: #1a73e8;
color: #6c1ae8;
}