wad.py and tmd.py now support dumping the object back into raw data

This commit is contained in:
Campbell 2024-03-20 23:11:09 -04:00
parent 7b6703cf36
commit 8244d79fba
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
3 changed files with 232 additions and 56 deletions

View File

@ -20,3 +20,23 @@ def align_value(value, alignment=64):
aligned_value = value + (alignment - (value % alignment)) aligned_value = value + (alignment - (value % alignment))
return aligned_value return aligned_value
return value return value
def pad_bytes_stream(data, alignment=64):
"""Pads the provided bytes stream to the provided alignment (defaults to 64).
Parameters
----------
data : BytesIO
The data to align.
alignment : int
The number to align to. Defaults to 64.
Returns
-------
BytesIO
The aligned data.
"""
while (data.getbuffer().nbytes % alignment) != 0:
data.write(b'\x00')
return data

View File

@ -36,87 +36,103 @@ class TMD:
""" """
def __init__(self, tmd): def __init__(self, tmd):
self.tmd = tmd self.tmd = tmd
self.sig_type: int self.blob_header: bytes = b''
self.sig: bytearray self.sig_type: int = 0
self.issuer: bytearray # Follows the format "Root-CA%08x-CP%08x" self.sig: bytes = b''
self.tmd_version: int # This seems to always be 0 no matter what? self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x"
self.ca_crl_version: int self.tmd_version: int = 0 # This seems to always be 0 no matter what?
self.signer_crl_version: int self.ca_crl_version: int = 0
self.vwii: int # Whether the title is for the vWii. 0 = No, 1 = Yes self.signer_crl_version: int = 0
self.ios_tid: str # The Title ID of the IOS version the associated title runs on. self.vwii: int = 0 # Whether the title is for the vWii. 0 = No, 1 = Yes
self.ios_version: int # The IOS version the associated title runs on. self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
self.title_id: str # The Title ID of the associated title. self.ios_version: int = 0 # The IOS version the associated title runs on.
self.content_type: str # The type of content contained within the associated title. self.title_id: str = "" # The Title ID of the associated title.
self.group_id: int # The ID of the publisher of the associated title. self.content_type: str = "" # The type of content contained within the associated title.
self.region: int # The ID of the region of the associated title. self.group_id: int = 0 # The ID of the publisher of the associated title.
self.ratings: int self.region: int = 0 # The ID of the region of the associated title.
self.access_rights: int self.ratings: bytes = b''
self.title_version: int # The version of the associated title. self.ipc_mask: bytes = b''
self.num_contents: int # The number of contents contained in the associated title. self.access_rights: bytes = b''
self.boot_index: int self.title_version: int = 0 # The version of the associated title.
self.num_contents: int = 0 # The number of contents contained in the associated title.
self.boot_index: int = 0
self.content_records: List[ContentRecord] = [] self.content_records: List[ContentRecord] = []
# Load data from TMD file # Call load() to set all of the attributes from the raw TMD data provided.
self.load()
def load(self):
"""Loads the raw TMD data and sets all attributes of the TMD object.
Returns
-------
none
"""
with io.BytesIO(self.tmd) as tmd_data: with io.BytesIO(self.tmd) as tmd_data:
# ==================================================================================== # ====================================================================================
# Parses each of the keys contained in the TMD. # Parses each of the keys contained in the TMD.
# ==================================================================================== # ====================================================================================
# Signing certificate issuer tmd_data.seek(0x0)
self.blob_header = tmd_data.read(320)
# Signing certificate issuer.
tmd_data.seek(0x140) tmd_data.seek(0x140)
self.issuer = tmd_data.read(64) self.issuer = tmd_data.read(64)
# TMD version, seems to usually be 0, but I've seen references to other numbers # TMD version, seems to usually be 0, but I've seen references to other numbers.
tmd_data.seek(0x180) tmd_data.seek(0x180)
self.tmd_version = int.from_bytes(tmd_data.read(1)) self.tmd_version = int.from_bytes(tmd_data.read(1))
# TODO: label # Root certificate crl version.
tmd_data.seek(0x181) tmd_data.seek(0x181)
self.ca_crl_version = tmd_data.read(1) self.ca_crl_version = int.from_bytes(tmd_data.read(1))
# TODO: label # Signer crl version.
tmd_data.seek(0x182) tmd_data.seek(0x182)
self.signer_crl_version = tmd_data.read(1) self.signer_crl_version = int.from_bytes(tmd_data.read(1))
# If this is a vWii title or not # If this is a vWii title or not.
tmd_data.seek(0x183) tmd_data.seek(0x183)
self.vwii = int.from_bytes(tmd_data.read(1)) self.vwii = int.from_bytes(tmd_data.read(1))
# TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2 # TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2.
tmd_data.seek(0x184) tmd_data.seek(0x184)
ios_version_bin = tmd_data.read(8) ios_version_bin = tmd_data.read(8)
ios_version_hex = binascii.hexlify(ios_version_bin) ios_version_hex = binascii.hexlify(ios_version_bin)
self.ios_tid = str(ios_version_hex.decode()) self.ios_tid = str(ios_version_hex.decode())
# Get IOS version based on TID # Get IOS version based on TID.
self.ios_version = int(self.ios_tid[-2:], 16) self.ios_version = int(self.ios_tid[-2:], 16)
# Title ID of the title # Title ID of the title.
tmd_data.seek(0x18C) tmd_data.seek(0x18C)
title_id_bin = tmd_data.read(8) title_id_bin = tmd_data.read(8)
title_id_hex = binascii.hexlify(title_id_bin) title_id_hex = binascii.hexlify(title_id_bin)
self.title_id = str(title_id_hex.decode()) self.title_id = str(title_id_hex.decode())
# Type of content # Type of content.
tmd_data.seek(0x194) tmd_data.seek(0x194)
content_type_bin = tmd_data.read(4) content_type_bin = tmd_data.read(4)
content_type_hex = binascii.hexlify(content_type_bin) content_type_hex = binascii.hexlify(content_type_bin)
self.content_type = str(content_type_hex.decode()) self.content_type = str(content_type_hex.decode())
# Publisher of the title # Publisher of the title.
tmd_data.seek(0x198) tmd_data.seek(0x198)
self.group_id = tmd_data.read(2) self.group_id = int.from_bytes(tmd_data.read(2))
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR # Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR.
tmd_data.seek(0x19C) tmd_data.seek(0x19C)
region_hex = tmd_data.read(2) region_hex = tmd_data.read(2)
self.region = int.from_bytes(region_hex) self.region = int.from_bytes(region_hex)
# TODO: figure this one out # Likely the localized content rating for the title. (ESRB, CERO, PEGI, etc.)
tmd_data.seek(0x19E) tmd_data.seek(0x19E)
self.ratings = tmd_data.read(16) self.ratings = tmd_data.read(16)
# Access rights of the title; DVD-video access and AHBPROT # IPC mask.
tmd_data.seek(0x1BA)
self.ipc_mask = tmd_data.read(12)
# Access rights of the title; DVD-video access and AHBPROT.
tmd_data.seek(0x1D8) tmd_data.seek(0x1D8)
self.access_rights = tmd_data.read(4) self.access_rights = tmd_data.read(4)
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD # Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD.
tmd_data.seek(0x1DC) tmd_data.seek(0x1DC)
title_version_high = int.from_bytes(tmd_data.read(1)) * 256 title_version_high = int.from_bytes(tmd_data.read(1)) * 256
tmd_data.seek(0x1DD) tmd_data.seek(0x1DD)
title_version_low = int.from_bytes(tmd_data.read(1)) title_version_low = int.from_bytes(tmd_data.read(1))
self.title_version = title_version_high + title_version_low self.title_version = title_version_high + title_version_low
# The number of contents listed in the TMD # The number of contents listed in the TMD.
tmd_data.seek(0x1DE) tmd_data.seek(0x1DE)
self.num_contents = int.from_bytes(tmd_data.read(2)) self.num_contents = int.from_bytes(tmd_data.read(2))
# Content index in content list that contains the boot file # Content index in content list that contains the boot file.
tmd_data.seek(0x1E0) tmd_data.seek(0x1E0)
self.boot_index = tmd_data.read(2) self.boot_index = int.from_bytes(tmd_data.read(2))
# Get content records for the number of contents in num_contents. # Get content records for the number of contents in num_contents.
for content in range(0, self.num_contents): for content in range(0, self.num_contents):
tmd_data.seek(0x1E4 + (36 * content)) tmd_data.seek(0x1E4 + (36 * content))
@ -126,6 +142,82 @@ class TMD:
int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]), int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]),
binascii.hexlify(content_record_hdr[4]))) binascii.hexlify(content_record_hdr[4])))
def dump(self) -> bytes:
"""Dumps the TMD object back into bytes. This also sets the raw TMD attribute of TMD object to the dumped data,
and triggers load() again to ensure that the raw data and object match.
Returns
-------
bytes
The full TMD file as bytes.
"""
# Open the stream and begin writing to it.
with io.BytesIO() as tmd_data:
# Signed blob header.
tmd_data.write(self.blob_header)
# Signing certificate issuer.
tmd_data.write(self.issuer)
# TMD version.
tmd_data.write(int.to_bytes(self.tmd_version, 1))
# Root certificate crl version.
tmd_data.write(int.to_bytes(self.ca_crl_version, 1))
# Signer crl version.
tmd_data.write(int.to_bytes(self.signer_crl_version, 1))
# If this is a vWii title or not.
tmd_data.write(int.to_bytes(self.vwii, 1))
# IOS Title ID.
tmd_data.write(binascii.unhexlify(self.ios_tid))
# Title's Title ID.
tmd_data.write(binascii.unhexlify(self.title_id))
# Content type.
tmd_data.write(binascii.unhexlify(self.content_type))
# Group ID.
tmd_data.write(int.to_bytes(self.group_id, 2))
# 2 bytes of zero for reasons.
tmd_data.write(b'\x00\x00')
# Region.
tmd_data.write(int.to_bytes(self.region, 2))
# Ratings.
tmd_data.write(self.ratings)
# Reserved (All \x00).
tmd_data.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
# IPC mask.
tmd_data.write(self.ipc_mask)
# Reserved (ALl \x00).
tmd_data.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
# Access rights.
tmd_data.write(self.access_rights)
# Title version.
title_version_high = round(self.title_version / 256)
tmd_data.write(int.to_bytes(title_version_high, 1))
title_version_low = self.title_version % 256
tmd_data.write(int.to_bytes(title_version_low, 1))
# Number of contents.
tmd_data.write(int.to_bytes(self.num_contents, 2))
# Boot index.
tmd_data.write(int.to_bytes(self.boot_index, 2))
# Minor version. Unused so write \x00.
tmd_data.write(b'\x00\x00')
# Iterate over content records, write them back into raw data, then add them to the TMD.
for content_record in range(self.num_contents):
content_data = io.BytesIO()
# Write all fields from the content record.
content_data.write(int.to_bytes(self.content_records[content_record].content_id, 4))
content_data.write(int.to_bytes(self.content_records[content_record].index, 2))
content_data.write(int.to_bytes(self.content_records[content_record].content_type, 2))
content_data.write(int.to_bytes(self.content_records[content_record].content_size, 8))
content_data.write(binascii.unhexlify(self.content_records[content_record].content_hash))
# Seek to the start and write the record to the TMD.
content_data.seek(0x0)
tmd_data.write(content_data.read())
content_data.close()
tmd_data.seek(0x0)
self.tmd = tmd_data.read()
# Reload object's attributes to ensure the raw data and object match.
self.load()
return self.tmd
def get_title_region(self): def get_title_region(self):
"""Gets the region of the TMD's associated title. """Gets the region of the TMD's associated title.

View File

@ -5,7 +5,7 @@
import io import io
import binascii import binascii
from .shared import align_value from .shared import align_value, pad_bytes_stream
class WAD: class WAD:
@ -19,25 +19,34 @@ class WAD:
""" """
def __init__(self, wad): def __init__(self, wad):
self.wad = wad self.wad = wad
self.wad_hdr_size: int self.wad_hdr_size: int = 64
self.wad_type: str self.wad_type: str = ""
self.wad_version: int self.wad_version: bytes = b''
# === Sizes === # === Sizes ===
self.wad_cert_size: int self.wad_cert_size: int = 0
self.wad_crl_size: int self.wad_crl_size: int = 0
self.wad_tik_size: int self.wad_tik_size: int = 0
self.wad_tmd_size: int self.wad_tmd_size: int = 0
# This is the size of the content region, which contains all app files combined. # This is the size of the content region, which contains all app files combined.
self.wad_content_size: int self.wad_content_size: int = 0
self.wad_meta_size: int self.wad_meta_size: int = 0
# === Offsets === # === Offsets ===
self.wad_cert_offset: int self.wad_cert_offset: int = 0
self.wad_crl_offset: int self.wad_crl_offset: int = 0
self.wad_tik_offset: int self.wad_tik_offset: int = 0
self.wad_tmd_offset: int self.wad_tmd_offset: int = 0
self.wad_content_offset: int self.wad_content_offset: int = 0
self.wad_meta_offset: int self.wad_meta_offset: int = 0
# Load header data from WAD stream # Call load() to set all of the attributes from the raw WAD data provided.
self.load()
def load(self):
"""Loads the raw WAD data and sets all attributes of the WAD object.
Returns
-------
none
"""
with io.BytesIO(self.wad) as wad_data: with io.BytesIO(self.wad) as wad_data:
# Read the first 8 bytes of the file to ensure that it's a WAD. This will currently reject boot2 WADs, but # Read the first 8 bytes of the file to ensure that it's a WAD. This will currently reject boot2 WADs, but
# this tool cannot handle them correctly right now anyway. # this tool cannot handle them correctly right now anyway.
@ -89,6 +98,61 @@ class WAD:
self.wad_meta_offset = align_value(self.wad_tmd_offset + self.wad_tmd_size) self.wad_meta_offset = align_value(self.wad_tmd_offset + self.wad_tmd_size)
self.wad_content_offset = align_value(self.wad_meta_offset + self.wad_meta_size) self.wad_content_offset = align_value(self.wad_meta_offset + self.wad_meta_size)
def dump(self) -> bytes:
"""Dumps the WAD object back into bytes. This also sets the raw WAD attribute of WAD object to the dumped data,
and triggers load() again to ensure that the raw data and object match.
Returns
-------
bytes
The full WAD file as bytes.
"""
# Open the stream and begin writing data to it.
with io.BytesIO() as wad_data:
# Lead-in data.
wad_data.write(b'\x00\x00\x00\x20')
# WAD type.
wad_data.write(str.encode(self.wad_type))
# WAD version.
wad_data.write(self.wad_version)
# WAD cert size.
wad_data.write(int.to_bytes(self.wad_cert_size, 4))
# WAD crl size.
wad_data.write(int.to_bytes(self.wad_crl_size, 4))
# WAD ticket size.
wad_data.write(int.to_bytes(self.wad_tik_size, 4))
# WAD TMD size.
wad_data.write(int.to_bytes(self.wad_tmd_size, 4))
# WAD content size.
wad_data.write(int.to_bytes(self.wad_content_size, 4))
# WAD meta size.
wad_data.write(int.to_bytes(self.wad_meta_size, 4))
wad_data = pad_bytes_stream(wad_data)
# Retrieve the cert data and write it out.
wad_data.write(self.get_cert_data())
wad_data = pad_bytes_stream(wad_data)
# Retrieve the crl data and write it out.
wad_data.write(self.get_crl_data())
wad_data = pad_bytes_stream(wad_data)
# Retrieve the ticket data and write it out.
wad_data.write(self.get_ticket_data())
wad_data = pad_bytes_stream(wad_data)
# Retrieve the TMD data and write it out.
wad_data.write(self.get_tmd_data())
wad_data = pad_bytes_stream(wad_data)
# Retrieve the meta/footer data and write it out.
wad_data.write(self.get_meta_data())
wad_data = pad_bytes_stream(wad_data)
# Retrieve the content data and write it out.
wad_data.write(self.get_content_data())
wad_data = pad_bytes_stream(wad_data)
# Seek to the beginning and save this as the WAD data for the object.
wad_data.seek(0x0)
self.wad = wad_data.read()
# Reload object's attributes to ensure the raw data and object match.
self.load()
return self.wad
def get_cert_region(self): def get_cert_region(self):
"""Gets the offset and size of the certificate data. """Gets the offset and size of the certificate data.