From 5f4fa8827c3ec435dc18161d1514d7f567a70837 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:42:04 -0400 Subject: [PATCH] Added new methods to TMD/Ticket/Title modules for changing title versions --- src/libWiiPy/title/__init__.py | 1 + src/libWiiPy/title/content.py | 2 +- src/libWiiPy/title/crypto.py | 2 +- src/libWiiPy/title/ticket.py | 38 ++++++++++++++++--- src/libWiiPy/title/title.py | 16 +++++++- src/libWiiPy/title/tmd.py | 48 +++++++++++++++++++++--- src/libWiiPy/title/util.py | 68 ++++++++++++++++++++++++++++++++++ 7 files changed, 161 insertions(+), 14 deletions(-) create mode 100644 src/libWiiPy/title/util.py diff --git a/src/libWiiPy/title/__init__.py b/src/libWiiPy/title/__init__.py index 0c95020..37e7c7f 100644 --- a/src/libWiiPy/title/__init__.py +++ b/src/libWiiPy/title/__init__.py @@ -7,4 +7,5 @@ from .nus import * from .ticket import * from .title import * from .tmd import * +from .util import * from .wad import * diff --git a/src/libWiiPy/title/content.py b/src/libWiiPy/title/content.py index e7ae048..783da79 100644 --- a/src/libWiiPy/title/content.py +++ b/src/libWiiPy/title/content.py @@ -357,7 +357,7 @@ class ContentRegion: # If the hash matches, encrypt the content and set it where it belongs. # This uses the index from the content records instead of just the index given, because there are some strange # circumstances where the actual index in the array and the assigned content index don't match up, and this - # needs to accommodate that. + # needs to accommodate that. Seems to only apply to cIOS WADs? enc_content = encrypt_content(dec_content, title_key, self.content_records[index].index) if (index + 1) > len(self.content_list): self.content_list.append(enc_content) diff --git a/src/libWiiPy/title/crypto.py b/src/libWiiPy/title/crypto.py index 56a02f6..5ce05f0 100644 --- a/src/libWiiPy/title/crypto.py +++ b/src/libWiiPy/title/crypto.py @@ -7,7 +7,7 @@ from .commonkeys import get_common_key from Crypto.Cipher import AES as _AES -def _convert_tid_to_iv(title_id: str) -> bytes: +def _convert_tid_to_iv(title_id: str | bytes) -> bytes: # Converts a Title ID in various formats into the format required to act as an IV. Private function used by other # crypto functions. title_key_iv = b'' diff --git a/src/libWiiPy/title/ticket.py b/src/libWiiPy/title/ticket.py index d55f344..c757628 100644 --- a/src/libWiiPy/title/ticket.py +++ b/src/libWiiPy/title/ticket.py @@ -9,6 +9,7 @@ import hashlib from dataclasses import dataclass as _dataclass from .crypto import decrypt_title_key from typing import List +from .util import title_ver_standard_to_dec @_dataclass @@ -66,7 +67,6 @@ class Ticket: self.ticket_id: bytes = b'' # Used as the IV when decrypting the title key for console-specific title installs. self.console_id: int = 0 # ID of the console that the ticket was issued for. self.title_id: bytes = b'' # TID/IV used for AES-CBC encryption. - self.title_id_str: str = "" # TID in string form for comparing against the TMD. self.unknown1: bytes = b'' # Some unknown data, not always the same so reading it just in case. self.title_version: int = 0 # Version of the ticket's associated title. self.permitted_titles: bytes = b'' # Permitted titles mask @@ -125,8 +125,6 @@ class Ticket: # Title ID. ticket_data.seek(0x1DC) self.title_id = binascii.hexlify(ticket_data.read(8)) - # Title ID (as a string). - self.title_id_str = str(self.title_id.decode()) # Unknown data 1. ticket_data.seek(0x1E4) self.unknown1 = ticket_data.read(2) @@ -307,7 +305,8 @@ class Ticket: def set_title_id(self, title_id) -> None: """ - Sets the Title ID of the title in the Ticket. + Sets the Title ID property of the Ticket. Recommended over setting the property directly because of input + validation. Parameters ---------- @@ -316,5 +315,34 @@ class Ticket: """ if len(title_id) != 16: raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.") - self.title_id_str = title_id self.title_id = binascii.unhexlify(title_id) + + def set_title_version(self, new_version: str | int) -> None: + """ + Sets the version of the title in the Ticket. Recommended over setting the data directly because of input + validation. + + Accepts either standard form (vX.X) as a string or decimal form (vXXX) as an integer. + + Parameters + ---------- + new_version : str, int + The new version of the title. See description for valid formats. + """ + if type(new_version) is str: + # Validate string input is in the correct format, then validate that the version isn't higher than v255.0. + # If checks pass, convert to decimal form and set that as the title version. + version_str_split = new_version.split(".") + if len(version_str_split) != 2: + raise ValueError("Title version is not valid! String version must be entered in format \"X.X\".") + if int(version_str_split[0]) > 255 or (int(version_str_split[0]) == 255 and int(version_str_split[1]) > 0): + raise ValueError("Title version is not valid! String version number cannot exceed v255.0.") + version_converted = title_ver_standard_to_dec(new_version, str(self.title_id.decode())) + self.title_version = version_converted + elif type(new_version) is int: + # Validate that the version isn't higher than v65280. If the check passes, set that as the title version. + if new_version > 65280: + raise ValueError("Title version is not valid! Integer version number cannot exceed v65280.") + self.title_version = new_version + else: + raise TypeError("Title version type is not valid! Type must be either integer or string.") diff --git a/src/libWiiPy/title/title.py b/src/libWiiPy/title/title.py index 6836160..a98ca36 100644 --- a/src/libWiiPy/title/title.py +++ b/src/libWiiPy/title/title.py @@ -56,7 +56,7 @@ class Title: self.content.load(self.wad.get_content_data(), self.tmd.content_records) # Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an # error because clearly something strange has gone on with the WAD and editing it probably won't work. - if self.tmd.title_id != self.ticket.title_id_str: + if self.tmd.title_id != str(self.ticket.title_id.decode()): raise ValueError("The Title IDs of the TMD and Ticket in this WAD do not match. This WAD appears to be " "invalid.") @@ -131,6 +131,20 @@ class Title: self.tmd.set_title_id(title_id) self.ticket.set_title_id(title_id) + def set_title_version(self, title_version: str | int) -> None: + """ + Sets the version of the title in both the TMD and Ticket. + + Accepts either standard form (vX.X) as a string or decimal form (vXXX) as an integer. + + Parameters + ---------- + title_version : str, int + The new version of the title. See description for valid formats. + """ + self.tmd.set_title_version(title_version) + self.ticket.set_title_version(title_version) + def get_content_by_index(self, index: id) -> bytes: """ Gets an individual content from the content region based on the provided index, in decrypted form. diff --git a/src/libWiiPy/title/tmd.py b/src/libWiiPy/title/tmd.py index d398da1..9fa4926 100644 --- a/src/libWiiPy/title/tmd.py +++ b/src/libWiiPy/title/tmd.py @@ -9,6 +9,7 @@ import hashlib import struct from typing import List from ..types import _ContentRecord +from .util import title_ver_dec_to_standard, title_ver_standard_to_dec class TMD: @@ -134,11 +135,11 @@ class TMD: # Version number straight from the TMD. tmd_data.seek(0x1DC) self.title_version = int.from_bytes(tmd_data.read(2)) - # Calculate the converted version number by multiplying 0x1DC by 256 and adding 0x1DD. - tmd_data.seek(0x1DC) - title_version_high = int.from_bytes(tmd_data.read(1)) * 256 - title_version_low = int.from_bytes(tmd_data.read(1)) - self.title_version_converted = title_version_high + title_version_low + # Calculate the converted version number via util module. + try: + self.title_version_converted = title_ver_dec_to_standard(self.title_version, self.title_id) + except ValueError: + self.title_version_converted = "" # The number of contents listed in the TMD. tmd_data.seek(0x1DE) self.num_contents = int.from_bytes(tmd_data.read(2)) @@ -374,7 +375,8 @@ class TMD: def set_title_id(self, title_id) -> None: """ - Sets the Title ID of the title in the ticket. + Sets the Title ID property of the TMD. Recommended over setting the property directly because of input + validation. Parameters ---------- @@ -384,3 +386,37 @@ class TMD: if len(title_id) != 16: raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.") self.title_id = title_id + + def set_title_version(self, new_version: str | int) -> None: + """ + Sets the version of the title in the TMD. Recommended over setting the data directly because of input + validation. + + Accepts either standard form (vX.X) as a string or decimal form (vXXX) as an integer. + + Parameters + ---------- + new_version : str, int + The new version of the title. See description for valid formats. + """ + if type(new_version) is str: + # Validate string input is in the correct format, then validate that the version isn't higher than v255.0. + # If checks pass, set that as the converted version, then convert to decimal form and set that as well. + version_str_split = new_version.split(".") + if len(version_str_split) != 2: + raise ValueError("Title version is not valid! String version must be entered in format \"X.X\".") + if int(version_str_split[0]) > 255 or (int(version_str_split[0]) == 255 and int(version_str_split[1]) > 0): + raise ValueError("Title version is not valid! String version number cannot exceed v255.0.") + self.title_version_converted = new_version + version_converted = title_ver_standard_to_dec(new_version, self.title_id) + self.title_version = version_converted + elif type(new_version) is int: + # Validate that the version isn't higher than v65280. If the check passes, set that as the title version, + # then convert to standard form and set that as well. + if new_version > 65280: + raise ValueError("Title version is not valid! Integer version number cannot exceed v65280.") + self.title_version = new_version + version_converted = title_ver_dec_to_standard(new_version, self.title_id) + self.title_version_converted = version_converted + else: + raise TypeError("Title version type is not valid! Type must be either integer or string.") diff --git a/src/libWiiPy/title/util.py b/src/libWiiPy/title/util.py new file mode 100644 index 0000000..59ebbd3 --- /dev/null +++ b/src/libWiiPy/title/util.py @@ -0,0 +1,68 @@ +# "title/util.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# +# General title-related utilities that don't fit within a specific module. + +import math + + +def title_ver_dec_to_standard(version: int, title_id: str) -> str: + """ + Converts a title's version from decimal form (vXXX, the way the version is stored in the TMD/Ticket) to its standard + and human-readable form (vX.X). The Title ID is required as some titles handle this version differently from others. + For the System Menu, the returned version will include the region code (ex. 4.3U). + + Parameters + ---------- + version : int + The version of the title, in decimal form. + title_id : str + The Title ID that the version is associated with. + + Returns + ------- + str + The version of the title, in standard form. + """ + version_out = "" + if title_id == "0000000100000002": + raise ValueError("The System Menu's version cannot currently be converted.") + else: + # For most channels, we need to get the floored value of version / 256 for the major version, and the version % + # 256 as the minor version. Minor versions > 9 are intended, as Nintendo themselves frequently used them. + version_upper = math.floor(version / 256) + version_lower = version % 256 + version_out = f"{version_upper}.{version_lower}" + + return version_out + + +def title_ver_standard_to_dec(version: str, title_id: str) -> int: + """ + Converts a title's version from its standard and human-readable form (vX.X) to its decimal form (vXXX, the way the + version is stored in the TMD/Ticket). The Title ID is required as some titles handle this version differently from + others. For the System Menu, the supplied version must include the region code (ex. 4.3U) for the conversion to + work correctly. + + Parameters + ---------- + version : str + The version of the title, in standard form. + title_id : str + The Title ID that the version is associated with. + + Returns + ------- + int + The version of the title, in decimal form. + """ + version_out = 0 + if title_id == "0000000100000002": + raise ValueError("The System Menu's version cannot currently be converted.") + else: + version_str_split = version.split(".") + version_upper = int(version_str_split[0]) * 256 + version_lower = int(version_str_split[1]) + version_out = version_upper + version_lower + + return version_out