Added new methods to TMD/Ticket/Title modules for changing title versions

This commit is contained in:
Campbell 2024-07-22 02:42:04 -04:00
parent e70b9570de
commit 5f4fa8827c
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
7 changed files with 161 additions and 14 deletions

View File

@ -7,4 +7,5 @@ from .nus import *
from .ticket import *
from .title import *
from .tmd import *
from .util import *
from .wad import *

View File

@ -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)

View File

@ -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''

View File

@ -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.")

View File

@ -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.

View File

@ -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.")

View File

@ -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