Added option to patch downloaded IOSes

This commit is contained in:
Campbell 2024-08-17 23:36:45 -04:00
parent adc8be9cbb
commit cbbac55743
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
5 changed files with 399 additions and 298 deletions

325
NUSGet.py
View File

@ -1,5 +1,6 @@
# NUSGet.py, licensed under the MIT license # "NUSGet.py", licensed under the MIT license
# Copyright 2024 NinjaCheetah # Copyright 2024 NinjaCheetah
import sys import sys
import os import os
import json import json
@ -7,17 +8,16 @@ import pathlib
import platform import platform
from importlib.metadata import version from importlib.metadata import version
import libWiiPy
import libTWLPy
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (QApplication, QMainWindow, QMessageBox, QTreeWidgetItem, QHeaderView, QStyle, from PySide6.QtWidgets import (QApplication, QMainWindow, QMessageBox, QTreeWidgetItem, QHeaderView, QStyle,
QStyleFactory) QStyleFactory)
from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, QLibraryInfo, QTranslator, QLocale from PySide6.QtCore import QRunnable, Slot, QThreadPool, Signal, QObject, QLibraryInfo, QTranslator, QLocale
from qt.py.ui_MainMenu import Ui_MainWindow from qt.py.ui_MainMenu import Ui_MainWindow
from modules.download_wii import run_nus_download_wii
from modules.download_dsi import run_nus_download_dsi
nusget_version = "1.2.0" nusget_version = "1.2.0"
regions = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": ["4A"], "Korea": ["4B"], "China": ["43"], regions = {"World": ["41"], "USA/NTSC": ["45"], "Europe/PAL": ["50"], "Japan": ["4A"], "Korea": ["4B"], "China": ["43"],
@ -32,9 +32,10 @@ class WorkerSignals(QObject):
# Worker class used to thread the downloads. # Worker class used to thread the downloads.
class Worker(QRunnable): class Worker(QRunnable):
def __init__(self, fn, **kwargs): def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__() super(Worker, self).__init__()
self.fn = fn self.fn = fn
self.args = args
self.kwargs = kwargs self.kwargs = kwargs
self.signals = WorkerSignals() self.signals = WorkerSignals()
@ -46,7 +47,7 @@ class Worker(QRunnable):
# unlikely event that an unexpected error happens, it can only possibly be a ValueError, so handle that and # unlikely event that an unexpected error happens, it can only possibly be a ValueError, so handle that and
# return code 1. # return code 1.
try: try:
result = self.fn(**self.kwargs) result = self.fn(*self.args, **self.kwargs)
except ValueError: except ValueError:
self.signals.result.emit(1) self.signals.result.emit(1)
else: else:
@ -62,6 +63,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.threadpool = QThreadPool() self.threadpool = QThreadPool()
self.ui.download_btn.clicked.connect(self.download_btn_pressed) self.ui.download_btn.clicked.connect(self.download_btn_pressed)
self.ui.pack_archive_chkbox.clicked.connect(self.pack_wad_chkbox_toggled) self.ui.pack_archive_chkbox.clicked.connect(self.pack_wad_chkbox_toggled)
self.ui.tid_entry.textChanged.connect(self.tid_updated)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
self.ui.wii_title_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents) self.ui.wii_title_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents)
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@ -141,6 +143,14 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.console_select_dropdown.setCurrentIndex(self.ui.platform_tabs.currentIndex()) self.ui.console_select_dropdown.setCurrentIndex(self.ui.platform_tabs.currentIndex())
self.load_title_data(selected_title, selected_version, selected_region) self.load_title_data(selected_title, selected_version, selected_region)
def tid_updated(self):
tid = self.ui.tid_entry.text()
if len(tid) == 16:
if tid[:8] == "00000001" and int(tid[-2:], 16) > 2:
self.ui.patch_ios_chkbox.setEnabled(True)
return
self.ui.patch_ios_chkbox.setEnabled(False)
def update_log_text(self, new_text): def update_log_text(self, new_text):
# This method primarily exists to be the handler for the progress signal emitted by the worker thread. # This method primarily exists to be the handler for the progress signal emitted by the worker thread.
self.log_text += new_text + "\n" self.log_text += new_text + "\n"
@ -218,9 +228,17 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.ui.log_text_browser.setText(self.log_text) self.ui.log_text_browser.setText(self.log_text)
# Create a new worker object to handle the download in a new thread. # Create a new worker object to handle the download in a new thread.
if self.ui.console_select_dropdown.currentText() == "DSi": if self.ui.console_select_dropdown.currentText() == "DSi":
worker = Worker(self.run_nus_download_dsi) worker = Worker(run_nus_download_dsi, out_folder, self.ui.tid_entry.text(),
self.ui.version_entry.text(), self.ui.pack_archive_chkbox.isChecked(),
self.ui.keep_enc_chkbox.isChecked(), self.ui.create_dec_chkbox.isChecked(),
self.ui.use_local_chkbox.isChecked(), self.ui.archive_file_entry.text())
else: else:
worker = Worker(self.run_nus_download_wii) worker = Worker(run_nus_download_wii, out_folder, self.ui.tid_entry.text(),
self.ui.version_entry.text(), self.ui.pack_archive_chkbox.isChecked(),
self.ui.keep_enc_chkbox.isChecked(), self.ui.create_dec_chkbox.isChecked(),
self.ui.use_wiiu_nus_chkbox.isChecked(), self.ui.use_local_chkbox.isChecked(),
self.ui.pack_vwii_mode_chkbox.isChecked(), self.ui.patch_ios_chkbox.isChecked(),
self.ui.archive_file_entry.text())
worker.signals.result.connect(self.check_download_result) worker.signals.result.connect(self.check_download_result)
worker.signals.progress.connect(self.update_log_text) worker.signals.progress.connect(self.update_log_text)
self.threadpool.start(worker) self.threadpool.start(worker)
@ -280,283 +298,6 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# on the currently selected console, and saves on duplicate code. # on the currently selected console, and saves on duplicate code.
self.selected_console_changed() self.selected_console_changed()
def run_nus_download_wii(self, progress_callback):
# Actual NUS download function that runs in a separate thread.
tid = self.ui.tid_entry.text()
# Immediately knock out any invalidly formatted Title IDs.
if len(tid) != 16:
return -1
# An error here is acceptable, because it may just mean the box is empty. Or the version string is nonsense.
# Either way, just fall back on downloading the latest version of the title.
try:
title_version = int(self.ui.version_entry.text())
except ValueError:
title_version = None
# Set variables for these two options so that their state can be compared against the user's choices later.
pack_wad_enabled = self.ui.pack_archive_chkbox.isChecked()
decrypt_contents_enabled = self.ui.create_dec_chkbox.isChecked()
# Check whether we're going to be using the (faster) Wii U NUS or not.
wiiu_nus_enabled = self.ui.use_wiiu_nus_chkbox.isChecked()
# Create a new libWiiPy Title.
title = libWiiPy.title.Title()
# Make a directory for this title if it doesn't exist.
title_dir = pathlib.Path(os.path.join(out_folder, tid))
if not title_dir.is_dir():
title_dir.mkdir()
# Announce the title being downloaded, and the version if applicable.
if title_version is not None:
progress_callback.emit("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
else:
progress_callback.emit("Downloading title " + tid + " vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...")
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
try:
if title_version is not None:
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
else:
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
title_version = title.tmd.title_version
# If libWiiPy returns an error, that means that either the TID or version doesn't exist, so return code -2.
except ValueError:
return -2
# Make a directory for this version if it doesn't exist.
version_dir = pathlib.Path(os.path.join(title_dir, str(title_version)))
if not version_dir.is_dir():
version_dir.mkdir()
# Write out the TMD to a file.
tmd_out = open(os.path.join(version_dir, "tmd." + str(title_version)), "wb")
tmd_out.write(title.tmd.dump())
tmd_out.close()
# Use a local ticket, if one exists and "use local files" is enabled.
if self.ui.use_local_chkbox.isChecked() is True and os.path.exists(os.path.join(version_dir, "tik")):
progress_callback.emit(" - Parsing local copy of Ticket...")
local_ticket = open(os.path.join(version_dir, "tik"), "rb")
title.load_ticket(local_ticket.read())
else:
progress_callback.emit(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
ticket_out = open(os.path.join(version_dir, "tik"), "wb")
ticket_out.write(title.ticket.dump())
ticket_out.close()
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(" - No Ticket is available!")
pack_wad_enabled = False
decrypt_contents_enabled = False
# Load the content records from the TMD, and begin iterating over the records.
title.load_content_records()
content_list = []
for content in range(len(title.tmd.content_records)):
# Generate the correct file name by converting the content ID into hex, minus the 0x, and then appending
# that to the end of 000000. I refuse to believe there isn't a better way to do this here and in libWiiPy.
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
# Check for a local copy of the current content if "use local files" is enabled, and use it.
if self.ui.use_local_chkbox.isChecked() is True and os.path.exists(os.path.join(version_dir,
content_file_name)):
progress_callback.emit(" - Using local copy of content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)))
local_file = open(os.path.join(version_dir, content_file_name), "rb")
content_list.append(local_file.read())
else:
progress_callback.emit(" - Downloading content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (Content ID: " +
str(title.tmd.content_records[content].content_id) + ", Size: " +
str(title.tmd.content_records[content].content_size) + " bytes)...")
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
wiiu_endpoint=wiiu_nus_enabled))
progress_callback.emit(" - Done!")
# If keep encrypted contents is on, write out each content after its downloaded.
if self.ui.keep_enc_chkbox.isChecked() is True:
enc_content_out = open(os.path.join(version_dir, content_file_name), "wb")
enc_content_out.write(content_list[content])
enc_content_out.close()
title.content.content_list = content_list
# If decrypt local contents is still true, decrypt each content and write out the decrypted file.
if decrypt_contents_enabled is True:
try:
for content in range(len(title.tmd.content_records)):
progress_callback.emit(" - Decrypting content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (Content ID: " +
str(title.tmd.content_records[content].content_id) + ")...")
dec_content = title.get_content_by_index(content)
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
content_file_name = content_file_name + ".app"
dec_content_out = open(os.path.join(version_dir, content_file_name), "wb")
dec_content_out.write(dec_content)
dec_content_out.close()
except ValueError:
# If libWiiPy throws an error during decryption, return code -3. This should only be possible if using
# local encrypted contents that have been altered at present.
return -3
# If pack WAD is still true, pack the TMD, ticket, and contents all into a WAD.
if pack_wad_enabled is True:
# If the option to pack for vWii mode instead of Wii U mode is enabled, then the Title Key needs to be
# re-encrypted with the common key instead of the vWii key, so that the title can be installed from within
# vWii mode. (vWii mode does not have access to the vWii key, only Wii U mode has that.)
if self.ui.pack_vwii_mode_chkbox.isChecked() is True and (tid[3] == "7" or tid[7] == "7"):
progress_callback.emit(" - Re-encrypting Title Key with the common key...")
title_key_dec = title.ticket.get_title_key()
title_key_common = libWiiPy.title.encrypt_title_key(title_key_dec, 0, title.tmd.title_id)
title.ticket.common_key_index = 0
title.ticket.title_key_enc = title_key_common
# Get the WAD certificate chain, courtesy of libWiiPy.
progress_callback.emit(" - Building certificate...")
title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled))
# Use a typed WAD name if there is one, and auto generate one based on the TID and version if there isn't.
progress_callback.emit("Packing WAD...")
if self.ui.archive_file_entry.text() != "":
wad_file_name = self.ui.archive_file_entry.text()
if wad_file_name[-4:] != ".wad":
wad_file_name = wad_file_name + ".wad"
else:
wad_file_name = tid + "-v" + str(title_version) + ".wad"
# Have libWiiPy dump the WAD, and write that data out.
file = open(os.path.join(version_dir, wad_file_name), "wb")
file.write(title.dump_wad())
file.close()
progress_callback.emit("Download complete!")
# This is where the variables come in. If the state of these variables doesn't match the user's choice by this
# point, it means that they enabled decryption or WAD packing for a title that doesn't have a ticket. Return
# code 1 so that a warning popup is shown informing them of this.
if ((not pack_wad_enabled and self.ui.pack_archive_chkbox.isChecked()) or
(not decrypt_contents_enabled and self.ui.create_dec_chkbox.isChecked())):
return 1
return 0
def run_nus_download_dsi(self, progress_callback):
# Actual NUS download function that runs in a separate thread, but DSi flavored.
tid = self.ui.tid_entry.text()
# Immediately knock out any invalidly formatted Title IDs.
if len(tid) != 16:
return -1
# An error here is acceptable, because it may just mean the box is empty. Or the version string is nonsense.
# Either way, just fall back on downloading the latest version of the title.
try:
title_version = int(self.ui.version_entry.text())
except ValueError:
title_version = None
# Set variables for these two options so that their state can be compared against the user's choices later.
pack_tad_enabled = self.ui.pack_archive_chkbox.isChecked()
decrypt_contents_enabled = self.ui.create_dec_chkbox.isChecked()
# Create a new libTWLPy Title.
title = libTWLPy.Title()
# Make a directory for this title if it doesn't exist.
title_dir = pathlib.Path(os.path.join(out_folder, tid))
if not title_dir.is_dir():
title_dir.mkdir()
# Announce the title being downloaded, and the version if applicable.
if title_version is not None:
progress_callback.emit("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
else:
progress_callback.emit("Downloading title " + tid + " vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...")
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
try:
if title_version is not None:
title.load_tmd(libTWLPy.download_tmd(tid, title_version))
else:
title.load_tmd(libTWLPy.download_tmd(tid))
title_version = title.tmd.title_version
# If libTWLPy returns an error, that means that either the TID or version doesn't exist, so return code -2.
except ValueError:
return -2
# Make a directory for this version if it doesn't exist.
version_dir = pathlib.Path(os.path.join(title_dir, str(title_version)))
if not version_dir.is_dir():
version_dir.mkdir()
# Write out the TMD to a file.
tmd_out = open(os.path.join(version_dir, "tmd." + str(title_version)), "wb")
tmd_out.write(title.tmd.dump())
tmd_out.close()
# Use a local ticket, if one exists and "use local files" is enabled.
if self.ui.use_local_chkbox.isChecked() is True and os.path.exists(os.path.join(version_dir, "tik")):
progress_callback.emit(" - Parsing local copy of Ticket...")
local_ticket = open(os.path.join(version_dir, "tik"), "rb")
title.load_ticket(local_ticket.read())
else:
progress_callback.emit(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libTWLPy.download_ticket(tid))
ticket_out = open(os.path.join(version_dir, "tik"), "wb")
ticket_out.write(title.ticket.dump())
ticket_out.close()
except ValueError:
# If libTWLPy 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(" - No Ticket is available!")
pack_tad_enabled = False
decrypt_contents_enabled = False
# Load the content record from the TMD, and download the content it lists. DSi titles only have one content.
title.load_content_records()
content_file_name = hex(title.tmd.content_record.content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
# Check for a local copy of the current content if "use local files" is enabled, and use it.
if self.ui.use_local_chkbox.isChecked() is True and os.path.exists(os.path.join(version_dir,
content_file_name)):
progress_callback.emit(" - Using local copy of content")
local_file = open(os.path.join(version_dir, content_file_name), "rb")
content = local_file.read()
else:
progress_callback.emit(" - Downloading content (Content ID: " + str(title.tmd.content_record.content_id) +
", Size: " + str(title.tmd.content_record.content_size) + " bytes)...")
content = libTWLPy.download_content(tid, title.tmd.content_record.content_id)
progress_callback.emit(" - Done!")
# If keep encrypted contents is on, write out each content after its downloaded.
if self.ui.keep_enc_chkbox.isChecked() is True:
enc_content_out = open(os.path.join(version_dir, content_file_name), "wb")
enc_content_out.write(content)
enc_content_out.close()
title.content.content = content
# If decrypt local contents is still true, decrypt each content and write out the decrypted file.
if decrypt_contents_enabled is True:
try:
progress_callback.emit(" - Decrypting content (Content ID: " + str(title.tmd.content_record.content_id)
+ ")...")
dec_content = title.get_content()
content_file_name = hex(title.tmd.content_record.content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
content_file_name = content_file_name + ".app"
dec_content_out = open(os.path.join(version_dir, content_file_name), "wb")
dec_content_out.write(dec_content)
dec_content_out.close()
except ValueError:
# If libWiiPy throws an error during decryption, return code -3. This should only be possible if using
# local encrypted contents that have been altered at present.
return -3
# If pack TAD is still true, pack the TMD, ticket, and content into a TAD.
if pack_tad_enabled is True:
# Get the TAD certificate chain, courtesy of libTWLPy.
progress_callback.emit(" - Building certificate...")
title.tad.set_cert_data(libTWLPy.download_cert())
# Use a typed TAD name if there is one, and auto generate one based on the TID and version if there isn't.
progress_callback.emit("Packing TAD...")
if self.ui.archive_file_entry.text() != "":
tad_file_name = self.ui.archive_file_entry.text()
if tad_file_name[-4:] != ".tad":
tad_file_name = tad_file_name + ".tad"
else:
tad_file_name = tid + "-v" + str(title_version) + ".tad"
# Have libTWLPy dump the TAD, and write that data out.
file = open(os.path.join(version_dir, tad_file_name), "wb")
file.write(title.dump_tad())
file.close()
progress_callback.emit("Download complete!")
# This is where the variables come in. If the state of these variables doesn't match the user's choice by this
# point, it means that they enabled decryption or TAD packing for a title that doesn't have a ticket. Return
# code 1 so that a warning popup is shown informing them of this.
if ((not pack_tad_enabled and self.ui.pack_archive_chkbox.isChecked()) or
(not decrypt_contents_enabled and self.ui.create_dec_chkbox.isChecked())):
return 1
return 0
def pack_wad_chkbox_toggled(self): def pack_wad_chkbox_toggled(self):
# Simple function to catch when the WAD/TAD checkbox is toggled and enable/disable the file name entry box # Simple function to catch when the WAD/TAD checkbox is toggled and enable/disable the file name entry box
# accordingly. # accordingly.
@ -590,15 +331,15 @@ if __name__ == "__main__":
sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}' downloads_guid = '{374DE290-123F-4565-9164-39C4925E467B}'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key:
location = winreg.QueryValueEx(key, downloads_guid)[0] location = pathlib.Path(winreg.QueryValueEx(key, downloads_guid)[0])
else: else:
location = os.path.join(os.path.expanduser('~'), 'Downloads') location = pathlib.Path(os.path.expanduser('~')).joinpath('Downloads')
# Build the path by combining the path to the Downloads photo with "NUSGet". # Build the path by combining the path to the Downloads photo with "NUSGet".
out_folder = os.path.join(location, "NUSGet") out_folder = location.joinpath("NUSGet")
# Create the "NUSGet" directory if it doesn't exist. In the future, this will be user-customizable, but this works # Create the "NUSGet" directory if it doesn't exist. In the future, this will be user-customizable, but this works
# for now, and avoids the issues from when it used to use a directory next to the binary (mostly on macOS). # for now, and avoids the issues from when it used to use a directory next to the binary (mostly on macOS).
if not os.path.isdir(out_folder): if not out_folder.is_dir():
os.mkdir(out_folder) out_folder.mkdir()
# Load the system plugins directory on Linux for system styles, if it exists. Try using Breeze if available, because # Load the system plugins directory on Linux for system styles, if it exists. Try using Breeze if available, because
# it looks nice, but fallback on kvantum if it isn't, since kvantum is likely to exist. If all else fails, fusion. # it looks nice, but fallback on kvantum if it isn't, since kvantum is likely to exist. If all else fails, fusion.
@ -616,7 +357,7 @@ if __name__ == "__main__":
if translator.load(QLocale.system(), 'qtbase', '_', path): if translator.load(QLocale.system(), 'qtbase', '_', path):
app.installTranslator(translator) app.installTranslator(translator)
translator = QTranslator(app) translator = QTranslator(app)
path = 'resources/translations' path = os.path.join(os.path.dirname(__file__), "resources/translations")
if translator.load(QLocale.system(), 'nusget', '_', path): if translator.load(QLocale.system(), 'nusget', '_', path):
app.installTranslator(translator) app.installTranslator(translator)

134
modules/download_dsi.py Normal file
View File

@ -0,0 +1,134 @@
# "modules/download_dsi.py", licensed under the MIT license
# Copyright 2024 NinjaCheetah
import os
import pathlib
import libTWLPy
def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_tad_chkbox: bool, keep_enc_chkbox: bool,
decrypt_contents_chkbox: bool, use_local_chkbox: bool, tad_file_name: str,
progress_callback=None):
# Actual NUS download function that runs in a separate thread, but DSi flavored.
# Immediately knock out any invalidly formatted Title IDs.
if len(tid) != 16:
return -1
# An error here is acceptable, because it may just mean the box is empty. Or the version string is nonsense.
# Either way, just fall back on downloading the latest version of the title.
try:
title_version = int(version)
except ValueError:
title_version = None
# Set variables for these two options so that their state can be compared against the user's choices later.
pack_tad_enabled = pack_tad_chkbox
decrypt_contents_enabled = decrypt_contents_chkbox
# Create a new libTWLPy Title.
title = libTWLPy.Title()
# Make a directory for this title if it doesn't exist.
title_dir = pathlib.Path(os.path.join(out_folder, tid))
if not title_dir.is_dir():
title_dir.mkdir()
# Announce the title being downloaded, and the version if applicable.
if title_version is not None:
progress_callback.emit("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
else:
progress_callback.emit("Downloading title " + tid + " vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...")
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
try:
if title_version is not None:
title.load_tmd(libTWLPy.download_tmd(tid, title_version))
else:
title.load_tmd(libTWLPy.download_tmd(tid))
title_version = title.tmd.title_version
# If libTWLPy returns an error, that means that either the TID or version doesn't exist, so return code -2.
except ValueError:
return -2
# Make a directory for this version if it doesn't exist.
version_dir = pathlib.Path(os.path.join(title_dir, str(title_version)))
if not version_dir.is_dir():
version_dir.mkdir()
# Write out the TMD to a file.
tmd_out = open(os.path.join(version_dir, "tmd." + str(title_version)), "wb")
tmd_out.write(title.tmd.dump())
tmd_out.close()
# Use a local ticket, if one exists and "use local files" is enabled.
if use_local_chkbox is True and os.path.exists(os.path.join(version_dir, "tik")):
progress_callback.emit(" - Parsing local copy of Ticket...")
local_ticket = open(os.path.join(version_dir, "tik"), "rb")
title.load_ticket(local_ticket.read())
else:
progress_callback.emit(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libTWLPy.download_ticket(tid))
ticket_out = open(os.path.join(version_dir, "tik"), "wb")
ticket_out.write(title.ticket.dump())
ticket_out.close()
except ValueError:
# If libTWLPy 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(" - No Ticket is available!")
pack_tad_enabled = False
decrypt_contents_enabled = False
# Load the content record from the TMD, and download the content it lists. DSi titles only have one content.
title.load_content_records()
content_file_name = hex(title.tmd.content_record.content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
# Check for a local copy of the current content if "use local files" is enabled, and use it.
if use_local_chkbox is True and os.path.exists(os.path.join(version_dir, content_file_name)):
progress_callback.emit(" - Using local copy of content")
local_file = open(os.path.join(version_dir, content_file_name), "rb")
content = local_file.read()
else:
progress_callback.emit(" - Downloading content (Content ID: " + str(title.tmd.content_record.content_id) +
", Size: " + str(title.tmd.content_record.content_size) + " bytes)...")
content = libTWLPy.download_content(tid, title.tmd.content_record.content_id)
progress_callback.emit(" - Done!")
# If keep encrypted contents is on, write out each content after its downloaded.
if keep_enc_chkbox is True:
enc_content_out = open(os.path.join(version_dir, content_file_name), "wb")
enc_content_out.write(content)
enc_content_out.close()
title.content.content = content
# If decrypt local contents is still true, decrypt each content and write out the decrypted file.
if decrypt_contents_enabled is True:
try:
progress_callback.emit(" - Decrypting content (Content ID: " + str(title.tmd.content_record.content_id)
+ ")...")
dec_content = title.get_content()
content_file_name = hex(title.tmd.content_record.content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
content_file_name = content_file_name + ".app"
dec_content_out = open(os.path.join(version_dir, content_file_name), "wb")
dec_content_out.write(dec_content)
dec_content_out.close()
except ValueError:
# If libWiiPy throws an error during decryption, return code -3. This should only be possible if using
# local encrypted contents that have been altered at present.
return -3
# If pack TAD is still true, pack the TMD, ticket, and content into a TAD.
if pack_tad_enabled is True:
# Get the TAD certificate chain, courtesy of libTWLPy.
progress_callback.emit(" - Building certificate...")
title.tad.set_cert_data(libTWLPy.download_cert())
# Use a typed TAD name if there is one, and auto generate one based on the TID and version if there isn't.
progress_callback.emit("Packing TAD...")
if tad_file_name != "":
if tad_file_name[-4:] != ".tad":
tad_file_name = tad_file_name + ".tad"
else:
tad_file_name = tid + "-v" + str(title_version) + ".tad"
# Have libTWLPy dump the TAD, and write that data out.
file = open(os.path.join(version_dir, tad_file_name), "wb")
file.write(title.dump_tad())
file.close()
progress_callback.emit("Download complete!")
# This is where the variables come in. If the state of these variables doesn't match the user's choice by this
# point, it means that they enabled decryption or TAD packing for a title that doesn't have a ticket. Return
# code 1 so that a warning popup is shown informing them of this.
if (not pack_tad_enabled and pack_tad_chkbox) or (not decrypt_contents_enabled and decrypt_contents_chkbox):
return 1
return 0

167
modules/download_wii.py Normal file
View File

@ -0,0 +1,167 @@
# "modules/download_wii.py", licensed under the MIT license
# Copyright 2024 NinjaCheetah
import os
import pathlib
import libWiiPy
def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool,
decrypt_contents_chkbox: bool, wiiu_nus_chkbox: bool, use_local_chkbox: bool,
repack_vwii_chkbox: bool, patch_ios: bool, wad_file_name: str, progress_callback=None):
# Actual NUS download function that runs in a separate thread.
# Immediately knock out any invalidly formatted Title IDs.
if len(tid) != 16:
return -1
# An error here is acceptable, because it may just mean the box is empty. Or the version string is nonsense.
# Either way, just fall back on downloading the latest version of the title.
try:
title_version = int(version)
except ValueError:
title_version = None
# Set variables for these two options so that their state can be compared against the user's choices later.
pack_wad_enabled = pack_wad_chkbox
decrypt_contents_enabled = decrypt_contents_chkbox
# Check whether we're going to be using the (faster) Wii U NUS or not.
wiiu_nus_enabled = wiiu_nus_chkbox
# Create a new libWiiPy Title.
title = libWiiPy.title.Title()
# Make a directory for this title if it doesn't exist.
title_dir = pathlib.Path(os.path.join(out_folder, tid))
if not title_dir.is_dir():
title_dir.mkdir()
# Announce the title being downloaded, and the version if applicable.
if title_version is not None:
progress_callback.emit("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
else:
progress_callback.emit("Downloading title " + tid + " vLatest, please wait...")
progress_callback.emit(" - Downloading and parsing TMD...")
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
try:
if title_version is not None:
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
else:
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
title_version = title.tmd.title_version
# If libWiiPy returns an error, that means that either the TID or version doesn't exist, so return code -2.
except ValueError:
return -2
# Make a directory for this version if it doesn't exist.
version_dir = pathlib.Path(os.path.join(title_dir, str(title_version)))
if not version_dir.is_dir():
version_dir.mkdir()
# Write out the TMD to a file.
tmd_out = open(os.path.join(version_dir, "tmd." + str(title_version)), "wb")
tmd_out.write(title.tmd.dump())
tmd_out.close()
# Use a local ticket, if one exists and "use local files" is enabled.
if use_local_chkbox is True and os.path.exists(os.path.join(version_dir, "tik")):
progress_callback.emit(" - Parsing local copy of Ticket...")
local_ticket = open(os.path.join(version_dir, "tik"), "rb")
title.load_ticket(local_ticket.read())
else:
progress_callback.emit(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
ticket_out = open(os.path.join(version_dir, "tik"), "wb")
ticket_out.write(title.ticket.dump())
ticket_out.close()
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(" - No Ticket is available!")
pack_wad_enabled = False
decrypt_contents_enabled = False
# Load the content records from the TMD, and begin iterating over the records.
title.load_content_records()
content_list = []
for content in range(len(title.tmd.content_records)):
# Generate the correct file name by converting the content ID into hex, minus the 0x, and then appending
# that to the end of 000000. I refuse to believe there isn't a better way to do this here and in libWiiPy.
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
# Check for a local copy of the current content if "use local files" is enabled, and use it.
if use_local_chkbox is True and os.path.exists(os.path.join(version_dir,
content_file_name)):
progress_callback.emit(" - Using local copy of content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)))
local_file = open(os.path.join(version_dir, content_file_name), "rb")
content_list.append(local_file.read())
else:
progress_callback.emit(" - Downloading content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (Content ID: " +
str(title.tmd.content_records[content].content_id) + ", Size: " +
str(title.tmd.content_records[content].content_size) + " bytes)...")
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
wiiu_endpoint=wiiu_nus_enabled))
progress_callback.emit(" - Done!")
# If keep encrypted contents is on, write out each content after its downloaded.
if keep_enc_chkbox is True:
enc_content_out = open(os.path.join(version_dir, content_file_name), "wb")
enc_content_out.write(content_list[content])
enc_content_out.close()
title.content.content_list = content_list
# If decrypt local contents is still true, decrypt each content and write out the decrypted file.
if decrypt_contents_enabled is True:
try:
for content in range(len(title.tmd.content_records)):
progress_callback.emit(" - Decrypting content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (Content ID: " +
str(title.tmd.content_records[content].content_id) + ")...")
dec_content = title.get_content_by_index(content)
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
content_file_name = content_file_name + ".app"
dec_content_out = open(os.path.join(version_dir, content_file_name), "wb")
dec_content_out.write(dec_content)
dec_content_out.close()
except ValueError:
# If libWiiPy throws an error during decryption, return code -3. This should only be possible if using
# local encrypted contents that have been altered at present.
return -3
# If pack WAD is still true, pack the TMD, ticket, and contents all into a WAD.
if pack_wad_enabled is True:
# If the option to pack for vWii mode instead of Wii U mode is enabled, then the Title Key needs to be
# re-encrypted with the common key instead of the vWii key, so that the title can be installed from within
# vWii mode. (vWii mode does not have access to the vWii key, only Wii U mode has that.)
if repack_vwii_chkbox is True and (tid[3] == "7" or tid[7] == "7"):
progress_callback.emit(" - Re-encrypting Title Key with the common key...")
title_key_dec = title.ticket.get_title_key()
title_key_common = libWiiPy.title.encrypt_title_key(title_key_dec, 0, title.tmd.title_id)
title.ticket.common_key_index = 0
title.ticket.title_key_enc = title_key_common
# Get the WAD certificate chain, courtesy of libWiiPy.
progress_callback.emit(" - Building certificate...")
title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled))
# Use a typed WAD name if there is one, and auto generate one based on the TID and version if there isn't.
progress_callback.emit(" - Packing WAD...")
if wad_file_name != "":
if wad_file_name[-4:] != ".wad":
wad_file_name = wad_file_name + ".wad"
else:
wad_file_name = tid + "-v" + str(title_version) + ".wad"
# If enabled (after we make sure it's an IOS), apply all main IOS patches.
if patch_ios and (tid[:8] == "00000001" and int(tid[-2:], 16) > 2):
progress_callback.emit(" - Patching IOS...")
ios_patcher = libWiiPy.title.IOSPatcher()
ios_patcher.load(title)
patch_count = ios_patcher.patch_all()
if patch_count > 0:
progress_callback.emit(f" - Applied {patch_count} patches!")
else:
progress_callback.emit(" - No patches could be applied! Is this a stub IOS?")
title = ios_patcher.dump()
# Have libWiiPy dump the WAD, and write that data out.
file = open(os.path.join(version_dir, wad_file_name), "wb")
file.write(title.dump_wad())
file.close()
progress_callback.emit("Download complete!")
# This is where the variables come in. If the state of these variables doesn't match the user's choice by this
# point, it means that they enabled decryption or WAD packing for a title that doesn't have a ticket. Return
# code 1 so that a warning popup is shown informing them of this.
if (not pack_wad_enabled and pack_wad_chkbox) or (not decrypt_contents_enabled and decrypt_contents_chkbox):
return 1
return 0

View File

@ -268,6 +268,27 @@ class Ui_MainWindow(object):
self.verticalLayout_7.addLayout(self.use_wiiu_nus_row) self.verticalLayout_7.addLayout(self.use_wiiu_nus_row)
self.patch_ios_row = QHBoxLayout()
self.patch_ios_row.setObjectName(u"patch_ios_row")
self.patch_ios_chkbox = QCheckBox(self.centralwidget)
self.patch_ios_chkbox.setObjectName(u"patch_ios_chkbox")
self.patch_ios_chkbox.setEnabled(False)
sizePolicy1.setHeightForWidth(self.patch_ios_chkbox.sizePolicy().hasHeightForWidth())
self.patch_ios_chkbox.setSizePolicy(sizePolicy1)
self.patch_ios_chkbox.setText(u"")
self.patch_ios_row.addWidget(self.patch_ios_chkbox)
self.patch_ios_lbl = QLabel(self.centralwidget)
self.patch_ios_lbl.setObjectName(u"patch_ios_lbl")
self.patch_ios_lbl.setEnabled(True)
self.patch_ios_lbl.setWordWrap(True)
self.patch_ios_row.addWidget(self.patch_ios_lbl)
self.verticalLayout_7.addLayout(self.patch_ios_row)
self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.verticalSpacer_2 = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.verticalLayout_7.addItem(self.verticalSpacer_2) self.verticalLayout_7.addItem(self.verticalSpacer_2)
@ -298,11 +319,12 @@ class Ui_MainWindow(object):
self.pack_vwii_mode_row.addWidget(self.pack_vwii_mode_chkbox) self.pack_vwii_mode_row.addWidget(self.pack_vwii_mode_chkbox)
self.label_8 = QLabel(self.centralwidget) self.pack_vwii_mode_lbl = QLabel(self.centralwidget)
self.label_8.setObjectName(u"label_8") self.pack_vwii_mode_lbl.setObjectName(u"pack_vwii_mode_lbl")
self.label_8.setWordWrap(True) self.pack_vwii_mode_lbl.setEnabled(True)
self.pack_vwii_mode_lbl.setWordWrap(True)
self.pack_vwii_mode_row.addWidget(self.label_8) self.pack_vwii_mode_row.addWidget(self.pack_vwii_mode_lbl)
self.verticalLayout_8.addLayout(self.pack_vwii_mode_row) self.verticalLayout_8.addLayout(self.pack_vwii_mode_row)
@ -365,8 +387,9 @@ class Ui_MainWindow(object):
self.create_dec_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Create decrypted contents (*.app)", None)) self.create_dec_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Create decrypted contents (*.app)", None))
self.use_local_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Use local files, if they exist", None)) self.use_local_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Use local files, if they exist", None))
self.use_wiiu_nus_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Use the Wii U NUS (faster, only effects Wii/vWii)", None)) self.use_wiiu_nus_chkbox_lbl.setText(QCoreApplication.translate("MainWindow", u"Use the Wii U NUS (faster, only effects Wii/vWii)", None))
self.patch_ios_lbl.setText(QCoreApplication.translate("MainWindow", u"Apply patches to IOS (Applies to WADs only)", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"vWii Title Settings", None)) self.label_4.setText(QCoreApplication.translate("MainWindow", u"vWii Title Settings", None))
self.label_8.setText(QCoreApplication.translate("MainWindow", u"Re-encrypt title using the Wii Common Key", None)) self.pack_vwii_mode_lbl.setText(QCoreApplication.translate("MainWindow", u"Re-encrypt title using the Wii Common Key", None))
self.log_text_browser.setMarkdown("") self.log_text_browser.setMarkdown("")
self.log_text_browser.setHtml(QCoreApplication.translate("MainWindow", u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n" self.log_text_browser.setHtml(QCoreApplication.translate("MainWindow", u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n" "<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"

View File

@ -402,6 +402,39 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="patch_ios_row">
<item>
<widget class="QCheckBox" name="patch_ios_chkbox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="patch_ios_lbl">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Apply patches to IOS (Applies to WADs only)</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
@ -467,7 +500,10 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="pack_vwii_mode_lbl">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text"> <property name="text">
<string>Re-encrypt title using the Wii Common Key</string> <string>Re-encrypt title using the Wii Common Key</string>
</property> </property>