mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2025-12-13 01:20:22 -05:00
407 lines
17 KiB
Python
407 lines
17 KiB
Python
# "title/title.py" from libWiiPy by NinjaCheetah & Contributors
|
|
# https://github.com/NinjaCheetah/libWiiPy
|
|
#
|
|
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
|
|
|
import math
|
|
from .content import ContentRegion
|
|
from .ticket import Ticket
|
|
from .tmd import TMD
|
|
from .wad import WAD
|
|
from .crypto import encrypt_title_key
|
|
|
|
|
|
class Title:
|
|
"""
|
|
A Title object that contains all components of a title, and allows altering them. Provides higher-level access
|
|
than manually creating WAD, TMD, Ticket, and ContentRegion objects and ensures that any data that needs to match
|
|
between files matches.
|
|
|
|
Attributes
|
|
----------
|
|
wad : WAD
|
|
A WAD object of a WAD containing the title's data.
|
|
tmd : TMD
|
|
A TMD object of the title's TMD.
|
|
ticket : Ticket
|
|
A Ticket object of the title's Ticket.
|
|
content: ContentRegion
|
|
A ContentRegion object containing the title's contents.
|
|
"""
|
|
def __init__(self):
|
|
self.wad: WAD = WAD()
|
|
self.tmd: TMD = TMD()
|
|
self.ticket: Ticket = Ticket()
|
|
self.content: ContentRegion = ContentRegion()
|
|
|
|
def load_wad(self, wad: bytes) -> None:
|
|
"""
|
|
Load existing WAD data into the title and create WAD, TMD, Ticket, and ContentRegion objects based off of it
|
|
to allow you to modify that data. Note that this will overwrite any existing data for this title.
|
|
|
|
Parameters
|
|
----------
|
|
wad : bytes
|
|
The data for the WAD you wish to load.
|
|
"""
|
|
# Create a new WAD object based on the WAD data provided.
|
|
self.wad = WAD()
|
|
self.wad.load(wad)
|
|
# Load the TMD.
|
|
self.tmd = TMD()
|
|
self.tmd.load(self.wad.get_tmd_data())
|
|
# Load the ticket.
|
|
self.ticket = Ticket()
|
|
self.ticket.load(self.wad.get_ticket_data())
|
|
# Load the content.
|
|
self.content = ContentRegion()
|
|
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 != 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.")
|
|
|
|
def dump_wad(self) -> bytes:
|
|
"""
|
|
Dumps all title components (TMD, Ticket, and contents) back into the WAD object, and then dumps the WAD back
|
|
into raw data and returns it.
|
|
|
|
Returns
|
|
-------
|
|
wad_data : bytes
|
|
The raw data of the WAD.
|
|
"""
|
|
# Set WAD type to ib if the title being packed is boot2.
|
|
if self.tmd.title_id == "0000000100000001":
|
|
self.wad.wad_type = "ib"
|
|
# Dump the TMD and set it in the WAD.
|
|
# This requires updating the content records and number of contents in the TMD first.
|
|
self.tmd.content_records = self.content.content_records
|
|
self.tmd.num_contents = len(self.content.content_records)
|
|
self.wad.set_tmd_data(self.tmd.dump())
|
|
# Dump the Ticket and set it in the WAD.
|
|
self.wad.set_ticket_data(self.ticket.dump())
|
|
# Dump the ContentRegion and set it in the WAD.
|
|
content_data, content_size = self.content.dump()
|
|
self.wad.set_content_data(content_data, content_size)
|
|
return self.wad.dump()
|
|
|
|
def load_tmd(self, tmd: bytes) -> None:
|
|
"""
|
|
Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title.
|
|
|
|
Parameters
|
|
----------
|
|
tmd : bytes
|
|
The data for the WAD you wish to load.
|
|
"""
|
|
# Load TMD.
|
|
self.tmd.load(tmd)
|
|
|
|
def load_ticket(self, ticket: bytes) -> None:
|
|
"""
|
|
Load existing Ticket data into the title. Note that this will overwrite any existing Ticket data for this
|
|
title.
|
|
|
|
Parameters
|
|
----------
|
|
ticket : bytes
|
|
The data for the WAD you wish to load.
|
|
"""
|
|
# Load Ticket.
|
|
self.ticket.load(ticket)
|
|
|
|
def load_content_records(self) -> None:
|
|
"""
|
|
Load content records from the TMD into the ContentRegion to allow loading content files based on the records.
|
|
This requires that a TMD has already been loaded and will throw an exception if it isn't.
|
|
"""
|
|
if not self.tmd.content_records:
|
|
ValueError("No TMD appears to have been loaded, so content records cannot be read from it.")
|
|
# Load the content records into the ContentRegion object.
|
|
self.content.content_records = self.tmd.content_records
|
|
|
|
def set_title_id(self, title_id: str) -> None:
|
|
"""
|
|
Sets the Title ID of the title in both the TMD and Ticket. This also re-encrypts the Title Key as the Title Key
|
|
is used as the IV for decrypting it.
|
|
|
|
Parameters
|
|
----------
|
|
title_id : str
|
|
The new Title ID of the title.
|
|
"""
|
|
if len(title_id) != 16:
|
|
raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.")
|
|
self.tmd.set_title_id(title_id)
|
|
title_key_decrypted = self.ticket.get_title_key()
|
|
self.ticket.set_title_id(title_id)
|
|
title_key_encrypted = encrypt_title_key(title_key_decrypted, self.ticket.common_key_index, title_id,
|
|
self.ticket.is_dev)
|
|
self.ticket.title_key_enc = title_key_encrypted
|
|
|
|
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, skip_hash=False) -> bytes:
|
|
"""
|
|
Gets an individual content from the content region based on the provided index, in decrypted form.
|
|
|
|
Parameters
|
|
----------
|
|
index : int
|
|
The index of the content you want to get.
|
|
skip_hash : bool, optional
|
|
Skip the hash check and return the content regardless of its hash. Defaults to false.
|
|
|
|
Returns
|
|
-------
|
|
bytes
|
|
The decrypted content listed in the content record.
|
|
"""
|
|
dec_content = self.content.get_content_by_index(index, self.ticket.get_title_key(), skip_hash)
|
|
return dec_content
|
|
|
|
def get_content_by_cid(self, cid: int, skip_hash=False) -> bytes:
|
|
"""
|
|
Gets an individual content from the content region based on the provided Content ID, in decrypted form.
|
|
|
|
Parameters
|
|
----------
|
|
cid : int
|
|
The Content ID of the content you want to get. Expected to be in decimal form.
|
|
skip_hash : bool, optional
|
|
Skip the hash check and return the content regardless of its hash. Defaults to false.
|
|
|
|
Returns
|
|
-------
|
|
bytes
|
|
The decrypted content listed in the content record.
|
|
"""
|
|
dec_content = self.content.get_content_by_cid(cid, self.ticket.get_title_key(), skip_hash)
|
|
return dec_content
|
|
|
|
def get_title_size(self, absolute=False) -> int:
|
|
"""
|
|
Gets the installed size of the title, including the TMD and Ticket, in bytes. The "absolute" option determines
|
|
whether shared content sizes should be included in the total size or not. This option defaults to False.
|
|
|
|
Parameters
|
|
----------
|
|
absolute : bool, optional
|
|
Whether shared contents should be included in the total size or not. Defaults to False.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
The installed size of the title, in bytes.
|
|
"""
|
|
title_size = 0
|
|
# Dumping and measuring the TMD and Ticket this way to ensure that any changes to them are measured properly.
|
|
# Yes, the Ticket size should be a constant, but it's still good to check just in case.
|
|
title_size += len(self.tmd.dump())
|
|
title_size += len(self.ticket.dump())
|
|
# For contents, get their sizes from the content records, because they store the intended sizes of the decrypted
|
|
# contents, which are usually different from the encrypted sizes.
|
|
for record in self.content.content_records:
|
|
if record.content_type == 32769:
|
|
if absolute:
|
|
title_size += record.content_size
|
|
else:
|
|
title_size += record.content_size
|
|
return title_size
|
|
|
|
def get_title_size_blocks(self, absolute=False) -> int:
|
|
"""
|
|
Gets the installed size of the title, including the TMD and Ticket, in the Wii's displayed "blocks" format. The
|
|
"absolute" option determines whether shared content sizes should be included in the total size or not. This
|
|
option defaults to False.
|
|
|
|
1 Wii block is equal to 128KiB, and if any amount of a block is used, the entire block is considered used.
|
|
|
|
Parameters
|
|
----------
|
|
absolute : bool, optional
|
|
Whether shared contents should be included in the total size or not. Defaults to False.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
The installed size of the title, in blocks.
|
|
"""
|
|
title_size_bytes = self.get_title_size(absolute)
|
|
blocks = math.ceil(title_size_bytes / 131072)
|
|
return blocks
|
|
|
|
def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
|
|
content_hash: bytes) -> None:
|
|
"""
|
|
Adds a new encrypted content to the ContentRegion, and adds the provided Content ID, index, content type,
|
|
content size, and content hash to a new record in the ContentRecord list.
|
|
|
|
Parameters
|
|
----------
|
|
enc_content : bytes
|
|
The new encrypted content to add.
|
|
cid : int
|
|
The Content ID to assign the new content in the content record.
|
|
index : int
|
|
The index used when encrypting the new content.
|
|
content_type : int
|
|
The type of the new content.
|
|
content_size : int
|
|
The size of the new encrypted content when decrypted.
|
|
content_hash : bytes
|
|
The hash of the new encrypted content when decrypted.
|
|
"""
|
|
# Add the encrypted content.
|
|
self.content.add_enc_content(enc_content, cid, index, content_type, content_size, content_hash)
|
|
# Update the TMD to match.
|
|
self.tmd.content_records = self.content.content_records
|
|
|
|
def add_content(self, dec_content: bytes, cid: int, content_type: int) -> None:
|
|
"""
|
|
Adds a new decrypted content to the end of the ContentRegion, and adds the provided Content ID, content type,
|
|
content size, and content hash to a new record in the ContentRecord list. The index will be automatically
|
|
assigned by incrementing the current highest index in the records.
|
|
|
|
This first gets the content hash and size from the provided data, and then encrypts the content with the
|
|
Title Key before adding it to the ContentRegion.
|
|
|
|
Parameters
|
|
----------
|
|
dec_content : bytes
|
|
The new decrypted content to add.
|
|
cid : int
|
|
The Content ID to assign the new content in the content record.
|
|
content_type : int
|
|
The type of the new content.
|
|
"""
|
|
# Add the decrypted content.
|
|
self.content.add_content(dec_content, cid, content_type, self.ticket.get_title_key())
|
|
# Update the TMD to match.
|
|
self.tmd.content_records = self.content.content_records
|
|
|
|
def set_enc_content(self, enc_content: bytes, index: int, content_size: int, content_hash: bytes, cid: int = None,
|
|
content_type: int = None) -> None:
|
|
"""
|
|
Sets the content at the provided content index to the provided new encrypted content. The provided hash and
|
|
content size are set in the corresponding content record. A new Content ID or content type can also be
|
|
specified, but if it isn't then the current values are preserved.
|
|
|
|
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
|
|
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
|
|
while still retaining the original indices.
|
|
|
|
This also updates the content records in the TMD after the content is set.
|
|
|
|
Parameters
|
|
----------
|
|
enc_content : bytes
|
|
The new encrypted content to set.
|
|
index : int
|
|
The index to place the new content at.
|
|
content_size : int
|
|
The size of the new encrypted content when decrypted.
|
|
content_hash : bytes
|
|
The hash of the new encrypted content when decrypted.
|
|
cid : int
|
|
The Content ID to assign the new content in the content record.
|
|
content_type : int
|
|
The type of the new content.
|
|
"""
|
|
# Set the encrypted content.
|
|
self.content.set_enc_content(enc_content, index, content_size, content_hash, cid, content_type)
|
|
# Update the TMD to match.
|
|
self.tmd.content_records = self.content.content_records
|
|
|
|
def set_content(self, dec_content: bytes, index: int, cid: int = None, content_type: int = None) -> None:
|
|
"""
|
|
Sets the content at the provided content index to the provided new decrypted content. The hash and content size
|
|
of this content will be generated and then set in the corresponding content record. A new Content ID or content
|
|
type can also be specified, but if it isn't then the current values are preserved.
|
|
|
|
This also updates the content records in the TMD after the content is set.
|
|
|
|
Parameters
|
|
----------
|
|
dec_content : bytes
|
|
The new decrypted content to set.
|
|
index : int
|
|
The index to place the new content at.
|
|
cid : int, optional
|
|
The Content ID to assign the new content in the content record.
|
|
content_type : int, optional
|
|
The type of the new content.
|
|
"""
|
|
# Set the decrypted content.
|
|
self.content.set_content(dec_content, index, self.ticket.get_title_key(), cid, content_type)
|
|
# Update the TMD to match.
|
|
self.tmd.content_records = self.content.content_records
|
|
|
|
def load_content(self, dec_content: bytes, index: int) -> None:
|
|
"""
|
|
Loads the provided decrypted content into the ContentRegion at the specified index, but first checks to make
|
|
sure that it matches the corresponding record. This content will then be encrypted using the title's Title Key
|
|
before being loaded.
|
|
|
|
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
|
|
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
|
|
while still retaining the original indices.
|
|
|
|
Parameters
|
|
----------
|
|
dec_content : bytes
|
|
The decrypted content to load.
|
|
index : int
|
|
The content index to load the content at.
|
|
"""
|
|
# Load the decrypted content.
|
|
self.content.load_content(dec_content, index, self.ticket.get_title_key())
|
|
|
|
def fakesign(self) -> None:
|
|
"""
|
|
Fakesigns this Title for the trucha bug.
|
|
|
|
This is done by brute-forcing a TMD and Ticket body hash starting with 00, causing it to pass signature
|
|
verification on older IOS versions that incorrectly check the hash using strcmp() instead of memcmp(). The TMD
|
|
and Ticket signatures will also be erased and replaced with all NULL bytes.
|
|
|
|
This modifies the TMD and Ticket objects that are part of this Title in place. You will need to call this method
|
|
after any changes to the TMD or Ticket, and before dumping the Title object into a WAD to ensure that the WAD
|
|
is properly fakesigned.
|
|
"""
|
|
self.tmd.fakesign()
|
|
self.ticket.fakesign()
|
|
|
|
def get_is_fakesigned(self):
|
|
"""
|
|
Checks the Title object to see if it is currently fakesigned. This ensures that both the TMD and Ticket are
|
|
fakesigned. For a description of fakesigning, refer to the fakesign() method.
|
|
|
|
Returns
|
|
-------
|
|
bool:
|
|
True if the Title is fakesigned, False otherwise.
|
|
|
|
See Also
|
|
--------
|
|
libWiiPy.title.title.Title.fakesign()
|
|
"""
|
|
if self.tmd.get_is_fakesigned and self.ticket.get_is_fakesigned():
|
|
return True
|
|
else:
|
|
return False
|