WADs can now be packed properly via title.py or low-level methods

This commit is contained in:
Campbell 2024-04-01 22:16:42 -04:00
parent 640ca91716
commit 57fb0576e9
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
3 changed files with 99 additions and 11 deletions

View File

@ -157,14 +157,14 @@ class ContentRegion:
self.content_records[index].content_size) self.content_records[index].content_size)
# Hash the decrypted content and ensure that the hash matches the one in its Content Record. # Hash the decrypted content and ensure that the hash matches the one in its Content Record.
# If it does not, then something has gone wrong in the decryption, and an error will be thrown. # If it does not, then something has gone wrong in the decryption, and an error will be thrown.
content_dec_hash = hashlib.sha1(content_dec) content_dec_hash = hashlib.sha1(content_dec).hexdigest()
content_record_hash = str(self.content_records[index].content_hash.decode()) content_record_hash = str(self.content_records[index].content_hash.decode())
# Compare the hash and throw a ValueError if the hash doesn't match. # Compare the hash and throw a ValueError if the hash doesn't match.
if content_dec_hash.hexdigest() != content_record_hash: if content_dec_hash != content_record_hash:
raise ValueError("Content hash did not match the expected hash in its record! The incorrect Title Key may " raise ValueError("Content hash did not match the expected hash in its record! The incorrect Title Key may "
"have been used!.\n" "have been used!.\n"
"Expected hash is: {}\n".format(content_record_hash) + "Expected hash is: {}\n".format(content_record_hash) +
"Actual hash is: {}".format(content_dec_hash.hexdigest())) "Actual hash is: {}".format(content_dec_hash))
return content_dec return content_dec
def get_content_by_cid(self, cid: int, title_key: bytes) -> bytes: def get_content_by_cid(self, cid: int, title_key: bytes) -> bytes:
@ -282,10 +282,52 @@ class ContentRegion:
# Pass values to set_enc_content() # Pass values to set_enc_content()
self.set_enc_content(enc_content, cid, index, content_type, dec_content_size, dec_content_hash) self.set_enc_content(enc_content, cid, index, content_type, dec_content_size, dec_content_hash)
def load_enc_content(self, enc_content: bytes, index: int) -> bytes: def load_enc_content(self, enc_content: bytes, index: int) -> None:
"""Loads the provided encrypted content into the content region at the specified index, with the assumption that """Loads the provided encrypted content into the content region at the specified index, with the assumption that
it matches the record at that index. it matches the record at that index. Not recommended for most use cases, use decrypted content and
load_content() instead.
:param index: Parameters
:return: ----------
enc_content : bytes
The encrypted content to load.
index : int
The content index to load the content at.
""" """
if (index + 1) > len(self.content_records) or len(self.content_records) == 0:
raise IndexError("No content records have been loaded, or that index is higher than the highest entry in "
"the content records.")
if (index + 1) > len(self.content_list):
self.content_list.append(enc_content)
else:
self.content_list[index] = enc_content
def load_content(self, dec_content: bytes, index: int, title_key: bytes) -> None:
"""Loads the provided decrypted content into the content region at the specified index, but first checks to make
sure it matches the record at that index before loading. This content will be encrypted when loaded.
Parameters
----------
dec_content : bytes
The decrypted content to load.
index : int
The content index to load the content at.
title_key: bytes
The Title Key that matches the decrypted content.
"""
# Make sure that content records exist and that the provided index exists in them.
if (index + 1) > len(self.content_records) or len(self.content_records) == 0:
raise IndexError("No content records have been loaded, or that index is higher than the highest entry in "
"the content records.")
# Check the hash of the content against the hash stored in the record to ensure it matches.
content_hash = hashlib.sha1(dec_content).hexdigest()
if content_hash != self.content_records[index].content_hash.decode():
raise ValueError("The decrypted content provided does not match the record at the provided index. \n"
"Expected hash is: {}\n".format(self.content_records[index].content_hash.decode()) +
"Actual hash is: {}".format(content_hash))
# If the hash matches, encrypt the content and set it where it belongs.
enc_content = encrypt_content(dec_content, title_key, index)
if (index + 1) > len(self.content_list):
self.content_list.append(enc_content)
else:
self.content_list[index] = enc_content

View File

@ -27,7 +27,7 @@ class Title:
self.ticket: Ticket = Ticket() self.ticket: Ticket = Ticket()
self.content: ContentRegion = ContentRegion() self.content: ContentRegion = ContentRegion()
def set_wad(self, wad: bytes) -> None: 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 """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. to allow you to modify that data. Note that this will overwrite any existing data for this title.
@ -54,7 +54,7 @@ class Title:
raise ValueError("The Title IDs of the TMD and Ticket in this WAD do not match. This WAD appears to be " raise ValueError("The Title IDs of the TMD and Ticket in this WAD do not match. This WAD appears to be "
"invalid.") "invalid.")
def dump(self) -> bytes: def dump_wad(self) -> bytes:
"""Dumps all title components (TMD, Ticket, and contents) back into the WAD object, and then dumps the WAD back """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. into raw data and returns it.
@ -73,6 +73,38 @@ class Title:
wad_data = self.wad.dump() wad_data = self.wad.dump()
return wad_data return wad_data
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: def set_title_id(self, title_id: str) -> None:
"""Sets the Title ID of the title in both the TMD and Ticket. """Sets the Title ID of the title in both the TMD and Ticket.
@ -170,3 +202,17 @@ class Title:
self.content.set_content(dec_content, cid, index, content_type, self.ticket.get_title_key()) self.content.set_content(dec_content, cid, index, content_type, self.ticket.get_title_key())
# Update the TMD to match. # Update the TMD to match.
self.tmd.content_records = self.content.content_records 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 content region at the specified index, but first checks to make
sure it matches the record at that index before loading. This content will be encrypted when loaded.
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())

View File

@ -14,8 +14,8 @@ class WAD:
""" """
def __init__(self): def __init__(self):
self.wad_hdr_size: int = 64 self.wad_hdr_size: int = 64
self.wad_type: str = "" self.wad_type: str = "Is"
self.wad_version: bytes = b'' self.wad_version: bytes = b'\x00\x00'
# === Sizes === # === Sizes ===
self.wad_cert_size: int = 0 self.wad_cert_size: int = 0
self.wad_crl_size: int = 0 self.wad_crl_size: int = 0