From d86c754ebf3066d4d5f377592fbf620217b53a55 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:51:17 -0500 Subject: [PATCH] Created ticket.py, with a basic structure for reading a ticket Reads all keys within a v0 ticket, and exposes functions to retrieve all the keys that may be useful to an end user. Other keys remain internal for decryption in the future. --- pyproject.toml | 2 +- src/libWiiPy/crypto.py | 4 ++ src/libWiiPy/shared.py | 9 --- src/libWiiPy/ticket.py | 136 +++++++++++++++++++++++++++++++++++++++++ src/libWiiPy/tmd.py | 5 +- src/libWiiPy/wad.py | 2 +- 6 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 src/libWiiPy/crypto.py create mode 100644 src/libWiiPy/ticket.py diff --git a/pyproject.toml b/pyproject.toml index b652676..e09d5a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "libWiiPy" -version = "0.1.0" +version = "0.2.0" authors = [ { name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" }, { name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" } diff --git a/src/libWiiPy/crypto.py b/src/libWiiPy/crypto.py new file mode 100644 index 0000000..6c214d7 --- /dev/null +++ b/src/libWiiPy/crypto.py @@ -0,0 +1,4 @@ +# "crypto.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# +# See https://wiibrew.org/wiki/Ticket for details about the TMD format diff --git a/src/libWiiPy/shared.py b/src/libWiiPy/shared.py index 7172771..e69de29 100644 --- a/src/libWiiPy/shared.py +++ b/src/libWiiPy/shared.py @@ -1,9 +0,0 @@ -from typing import List -from binascii import unhexlify - - -def hex_string_to_byte_array(hex_string: str) -> List[int]: - byte_string = unhexlify(hex_string) - byte_array = list(byte_string) - - return byte_array diff --git a/src/libWiiPy/ticket.py b/src/libWiiPy/ticket.py new file mode 100644 index 0000000..ddf1084 --- /dev/null +++ b/src/libWiiPy/ticket.py @@ -0,0 +1,136 @@ +# "ticket.py" from libWiiPy by NinjaCheetah & Contributors +# https://github.com/NinjaCheetah/libWiiPy +# +# See https://wiibrew.org/wiki/Ticket for details about the TMD format + +import io + + +class Ticket: + """Creates a Ticket object to parse a Ticket file to retrieve the Title Key needed to decrypt it.""" + + 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 + # 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 ticket format. + 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_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.limit_type: int # Type of play limit applied to the title. + # 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count + #self.maximum_launches: int # Maximum for the selected limit type, being either minutes or launches. + # v1 ticket data + # TODO: Figure out v1 ticket stuff + with io.BytesIO(self.ticket) as ticketdata: + # ==================================================================================== + # Parses each of the keys contained in the Ticket. + # ==================================================================================== + # Signature type + ticketdata.seek(0x0) + self.signature_type = ticketdata.read(4) + # Signature data + ticketdata.seek(0x04) + self.signature = ticketdata.read(256) + # Signature issuer + ticketdata.seek(0x140) + self.signature_issuer = str(ticketdata.read(64).decode()) + # ECDH data + ticketdata.seek(0x180) + self.ecdh_data = ticketdata.read(60) + # Ticket version + ticketdata.seek(0x1BC) + self.ticket_version = int.from_bytes(ticketdata.read(1)) + # Title Key (Encrypted by a common key) + ticketdata.seek(0x1BF) + self.title_key_enc = ticketdata.read(16) + # Ticket ID + ticketdata.seek(0x1D0) + self.ticket_id = ticketdata.read(8) + # Console ID + ticketdata.seek(0x1D8) + self.console_id = int.from_bytes(ticketdata.read(4)) + # Title ID + ticketdata.seek(0x1DC) + self.title_id = ticketdata.read(8) + # Title version + ticketdata.seek(0x1E6) + title_version_high = int.from_bytes(ticketdata.read(1)) * 256 + ticketdata.seek(0x1E7) + title_version_low = int.from_bytes(ticketdata.read(1)) + self.title_version = title_version_high + title_version_low + # Permitted titles mask + ticketdata.seek(0x1E8) + self.permitted_titles = ticketdata.read(4) + # Permit mask + ticketdata.seek(0x1EC) + self.permit_mask = ticketdata.read(4) + # Whether title export with a PRNG key is allowed + ticketdata.seek(0x1F0) + self.title_export_allowed = int.from_bytes(ticketdata.read(1)) + # Common key index + ticketdata.seek(0x1F1) + self.common_key_index = int.from_bytes(ticketdata.read(1)) + # Content access permissions + ticketdata.seek(0x222) + self.content_access_permissions = ticketdata.read(64) + + def get_signature(self): + """Returns the signature of the ticket.""" + return self.signature + + def get_ticket_version(self): + """Returns the version of the ticket.""" + return self.ticket_version + + def get_title_key_enc(self): + """Returns the title key contained in the ticket, in encrypted form.""" + return self.title_key_enc + + def get_ticket_id(self): + """Returns the ID of the ticket.""" + return self.ticket_id + + def get_console_id(self): + """Returns the ID of the console this ticket is designed for, if the ticket is console-specific.""" + return self.console_id + + def get_title_id(self): + """Returns the Title ID of the ticket's associated title.""" + title_id_str = str(self.title_id.decode()) + return title_id_str + + def get_title_version(self): + """Returns the version of the ticket's associated title that this ticket is designed for.""" + return self.title_version + + def get_common_key_index(self): + """Returns the index of the common key used to encrypt the Title Key contained in the ticket.""" + return self.common_key_index + + def get_common_key_type(self): + """Returns the name of the common key used to encrypt the Title Key contained in the ticket.""" + match self.common_key_index: + case 0: + return "Common" + case 1: + return "Korean" + case 2: + return "vWii" + + def get_title_key(self): + """Returns the decrypted title key contained in the ticket.""" + # TODO + return b'\x00' + diff --git a/src/libWiiPy/tmd.py b/src/libWiiPy/tmd.py index 31155b0..5a8e838 100644 --- a/src/libWiiPy/tmd.py +++ b/src/libWiiPy/tmd.py @@ -43,7 +43,10 @@ class TMD: self.boot_index: int self.content_record: List[ContentRecord] # Load data from TMD file - with io.BytesIO(tmd) as tmddata: + with io.BytesIO(self.tmd) as tmddata: + # ==================================================================================== + # Parses each of the keys contained in the TMD. + # ==================================================================================== # Signing certificate issuer tmddata.seek(0x140) self.issuer = tmddata.read(64) diff --git a/src/libWiiPy/wad.py b/src/libWiiPy/wad.py index 19b52d8..61af8fd 100644 --- a/src/libWiiPy/wad.py +++ b/src/libWiiPy/wad.py @@ -30,7 +30,7 @@ class WAD: self.wad_content_offset: int self.wad_meta_offset: int # Load header data from WAD stream - with io.BytesIO(wad) as waddata: + with io.BytesIO(self.wad) as waddata: # ==================================================================================== # Get the sizes of each data region contained within the WAD. # ====================================================================================