From b5aab5ad22c85e86e29e2e998d18acd58d65110b Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:09:46 -0400 Subject: [PATCH] Added ability to dump tickets back to raw data --- src/libWiiPy/ticket.py | 183 +++++++++++++++++++++++++++++------------ src/libWiiPy/tmd.py | 10 +-- src/libWiiPy/types.py | 19 +++++ 3 files changed, 153 insertions(+), 59 deletions(-) diff --git a/src/libWiiPy/ticket.py b/src/libWiiPy/ticket.py index 982922c..c61a2a5 100644 --- a/src/libWiiPy/ticket.py +++ b/src/libWiiPy/ticket.py @@ -6,29 +6,10 @@ import io import binascii from .crypto import decrypt_title_key -from dataclasses import dataclass +from .types import TitleLimit from typing import List -@dataclass -class TitleLimit: - """Creates a TitleLimit object that contains the type of restriction and the limit. - - Attributes - ---------- - limit_type : int - The type of play limit applied. - maximum_usage : int - The maximum value for the type of play limit applied. - """ - # The type of play limit applied. The following types exist: - # 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count - limit_type: int - # The maximum value of the limit applied. - # This is either the number of minutes for a time limit, or the number of launches for a launch limit. - maximum_usage: int - - class Ticket: """Creates a Ticket object to parse a Ticket file to retrieve the Title Key needed to decrypt it. @@ -57,88 +38,182 @@ class Ticket: def __init__(self, ticket): self.ticket = ticket # Signature blob header - self.signature_type: bytes # Type of signature, always 0x10001 for RSA-2048 - self.signature: bytes # Actual signature data + self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048 + self.signature: bytes = b'' # Actual signature data # v0 ticket data - self.signature_issuer: str # Who issued the signature for the ticket - self.ecdh_data: bytes # Involved in created one-time keys for console-specific title installs. - self.ticket_version: int # The version of the current ticket file. - self.title_key_enc: bytes # The title key of the ticket's respective title, encrypted by a common key. - self.ticket_id: bytes # Used as the IV when decrypting the title key for console-specific title installs. - self.console_id: int # ID of the console that the ticket was issued for. - self.title_id: bytes # TID/IV used for AES-CBC encryption. - self.title_id_str: str # TID in string form for comparing against the TMD. - self.title_version: int # Version of the ticket's associated title. - self.permitted_titles: bytes # Permitted titles mask - self.permit_mask: bytes # "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the Permitted Titles Mask." - self.title_export_allowed: int # Whether title export is allowed with a PRNG key or not. - self.common_key_index: int # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key - self.content_access_permissions: bytes # "Content access permissions (one bit for each content)" + self.signature_issuer: str = "" # Who issued the signature for the ticket + self.ecdh_data: bytes = b'' # Involved in created one-time keys for console-specific title installs. + self.ticket_version: int = 0 # The version of the current ticket file. + self.title_key_enc: bytes = b'' # The title key of the ticket's respective title, encrypted by a common key. + 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 + self.permit_mask: bytes = b'' # "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the Permitted Titles Mask." + self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not. + self.common_key_index: int = 0 # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key + self.unknown2: bytes = b'' # More unknown data. Varies for VC/non-VC titles so reading it to ensure it matches. + self.content_access_permissions: bytes = b'' # "Content access permissions (one bit for each content)" self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title. # v1 ticket data - # TODO: Figure out v1 ticket stuff + # TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1. + # Call load() to set all of the attributes from the raw Ticket data provided. + self.load() + + def load(self): + """Loads the raw Ticket data and sets all attributes of the Ticket object. + + Returns + ------- + none + """ with io.BytesIO(self.ticket) as ticket_data: # ==================================================================================== # Parses each of the keys contained in the Ticket. # ==================================================================================== - # Signature type + # Signature type. ticket_data.seek(0x0) self.signature_type = ticket_data.read(4) - # Signature data + # Signature data. ticket_data.seek(0x04) self.signature = ticket_data.read(256) - # Signature issuer + # Signature issuer. ticket_data.seek(0x140) self.signature_issuer = str(ticket_data.read(64).decode()) - # ECDH data + # ECDH data. ticket_data.seek(0x180) self.ecdh_data = ticket_data.read(60) - # Ticket version + # Ticket version. ticket_data.seek(0x1BC) self.ticket_version = int.from_bytes(ticket_data.read(1)) - # Title Key (Encrypted by a common key) + if self.ticket_version == 1: + raise ValueError("This appears to be a v1 ticket, which is not currently supported by libWiiPy. This " + "feature is planned for a later release. Only v0 tickets are supported at this time.") + # Title Key (Encrypted by a common key). ticket_data.seek(0x1BF) self.title_key_enc = ticket_data.read(16) - # Ticket ID + # Ticket ID. ticket_data.seek(0x1D0) self.ticket_id = ticket_data.read(8) - # Console ID + # Console ID. ticket_data.seek(0x1D8) self.console_id = int.from_bytes(ticket_data.read(4)) - # Title ID + # Title ID. ticket_data.seek(0x1DC) self.title_id = ticket_data.read(8) - # Title ID (as a string) + # Title ID (as a string). title_id_hex = binascii.hexlify(self.title_id) self.title_id_str = str(title_id_hex.decode()) - # Title version + # Unknown data 1. + ticket_data.seek(0x1E4) + self.unknown1 = ticket_data.read(2) + # Title version. ticket_data.seek(0x1E6) title_version_high = int.from_bytes(ticket_data.read(1)) * 256 ticket_data.seek(0x1E7) title_version_low = int.from_bytes(ticket_data.read(1)) self.title_version = title_version_high + title_version_low - # Permitted titles mask + # Permitted titles mask. ticket_data.seek(0x1E8) self.permitted_titles = ticket_data.read(4) - # Permit mask + # Permit mask. ticket_data.seek(0x1EC) self.permit_mask = ticket_data.read(4) - # Whether title export with a PRNG key is allowed + # Whether title export with a PRNG key is allowed. ticket_data.seek(0x1F0) self.title_export_allowed = int.from_bytes(ticket_data.read(1)) - # Common key index + # Common key index. ticket_data.seek(0x1F1) self.common_key_index = int.from_bytes(ticket_data.read(1)) - # Content access permissions + # Unknown data 2. + ticket_data.seek(0x1F2) + self.unknown2 = ticket_data.read(48) + # Content access permissions. ticket_data.seek(0x222) self.content_access_permissions = ticket_data.read(64) - # Content limits + # Content limits. ticket_data.seek(0x264) for limit in range(0, 8): limit_type = int.from_bytes(ticket_data.read(4)) limit_value = int.from_bytes(ticket_data.read(4)) self.title_limits_list.append(TitleLimit(limit_type, limit_value)) + def dump(self) -> bytes: + """Dumps the Ticket object back into bytes. This also sets the raw Ticket attribute of Ticket object to the + dumped data, and triggers load() again to ensure that the raw data and object match. + + Returns + ------- + bytes + The full Ticket file as bytes. + """ + # Open the stream and begin writing to it. + with io.BytesIO() as ticket_data: + # Signature type. + ticket_data.write(self.signature_type) + # Signature data. + ticket_data.write(self.signature) + # Padding to 64 bytes. + ticket_data.write(b'\x00' * 60) + # Signature issuer. + ticket_data.write(str.encode(self.signature_issuer)) + # ECDH data. + ticket_data.write(self.ecdh_data) + # Ticket version. + ticket_data.write(int.to_bytes(self.ticket_version, 1)) + # Reserved (all \0x00). + ticket_data.write(b'\x00\x00') + # Title Key. + ticket_data.write(self.title_key_enc) + # Unknown (write \0x00). + ticket_data.write(b'\x00') + # Ticket ID. + ticket_data.write(self.ticket_id) + # Console ID. + ticket_data.write(int.to_bytes(self.console_id, 4)) + # Title ID. + ticket_data.write(self.title_id) + # Unknown data 1. + ticket_data.write(self.unknown1) + # Title version. + title_version_high = round(self.title_version / 256) + ticket_data.write(int.to_bytes(title_version_high, 1)) + title_version_low = self.title_version % 256 + ticket_data.write(int.to_bytes(title_version_low, 1)) + # Permitted titles mask. + ticket_data.write(self.permitted_titles) + # Permit mask. + ticket_data.write(self.permit_mask) + # Title Export allowed. + ticket_data.write(int.to_bytes(self.title_export_allowed, 1)) + # Common Key index. + ticket_data.write(int.to_bytes(self.common_key_index, 1)) + # Unknown data 2. + ticket_data.write(self.unknown2) + # Content access permissions. + ticket_data.write(self.content_access_permissions) + # Padding (always \x00). + ticket_data.write(b'\x00\x00') + # Iterate over Title Limit objects, write them back into raw data, then add them to the Ticket. + for title_limit in range(len(self.title_limits_list)): + title_limit_data = io.BytesIO() + # Write all fields from the title limit entry. + title_limit_data.write(int.to_bytes(self.title_limits_list[title_limit].limit_type, 4)) + title_limit_data.write(int.to_bytes(self.title_limits_list[title_limit].maximum_usage, 4)) + # Seek to the start and write the entry to the Ticket. + title_limit_data.seek(0x0) + ticket_data.write(title_limit_data.read()) + title_limit_data.close() + # Set the Ticket attribute of the object to the new raw Ticket. + ticket_data.seek(0x0) + self.ticket = ticket_data.read() + # Reload object's attributes to ensure the raw data and object match. + self.load() + return self.ticket + def get_title_id(self): """Gets the Title ID of the ticket's associated title. diff --git a/src/libWiiPy/tmd.py b/src/libWiiPy/tmd.py index f01e6fa..0f6bbcb 100644 --- a/src/libWiiPy/tmd.py +++ b/src/libWiiPy/tmd.py @@ -179,12 +179,12 @@ class TMD: 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') + # Reserved (all \x00). + tmd_data.write(b'\x00' * 12) # 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') + # Reserved (all \x00). + tmd_data.write(b'\x00' * 18) # Access rights. tmd_data.write(self.access_rights) # Title version. @@ -211,7 +211,7 @@ class TMD: content_data.seek(0x0) tmd_data.write(content_data.read()) content_data.close() - + # Set the TMD attribute of the object to the new raw TMD. tmd_data.seek(0x0) self.tmd = tmd_data.read() # Reload object's attributes to ensure the raw data and object match. diff --git a/src/libWiiPy/types.py b/src/libWiiPy/types.py index fc0a57e..3a9f0ff 100644 --- a/src/libWiiPy/types.py +++ b/src/libWiiPy/types.py @@ -27,3 +27,22 @@ class ContentRecord: content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared. content_size: int # Size of the content when decrypted. content_hash: bytes # SHA-1 hash of the content when decrypted. + + +@dataclass +class TitleLimit: + """Creates a TitleLimit object that contains the type of restriction and the limit. + + Attributes + ---------- + limit_type : int + The type of play limit applied. + maximum_usage : int + The maximum value for the type of play limit applied. + """ + # The type of play limit applied. The following types exist: + # 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count + limit_type: int + # The maximum value of the limit applied. + # This is either the number of minutes for a time limit, or the number of launches for a launch limit. + maximum_usage: int