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 1/3] 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. # ==================================================================================== From 3c5f8b676365cb06e5448cf2dc460b96cb44488f Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:02:36 -0500 Subject: [PATCH 2/3] Added function to ticket.py to return the decrypted Title Key Also added crypto.py which will manage all crypto-related functionality in the library. Currently only offers a function to decrypt a given Title Key. ticket.py now creates a list of play limits placed on a title. --- requirements.txt | 1 + src/libWiiPy/commonkeys.py | 32 ++++++++++++++++++-------------- src/libWiiPy/crypto.py | 19 +++++++++++++++++++ src/libWiiPy/ticket.py | 28 +++++++++++++++++++++++----- src/libWiiPy/tmd.py | 18 +++++++++--------- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/requirements.txt b/requirements.txt index 378eac2..1fca23a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ build +pycryptodome diff --git a/src/libWiiPy/commonkeys.py b/src/libWiiPy/commonkeys.py index f6eee50..97f436d 100644 --- a/src/libWiiPy/commonkeys.py +++ b/src/libWiiPy/commonkeys.py @@ -1,21 +1,25 @@ # "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors # https://github.com/NinjaCheetah/libWiiPy -default_key = 'ebe42a225e8593e448d9c5457381aaf7' +import binascii + +common_key = 'ebe42a225e8593e448d9c5457381aaf7' korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e' vwii_key = '30bfc76e7c19afbb23163330ced7c28d' -def get_default_key(): - """Returns the regular Wii Common Key used to encrypt most content.""" - return default_key - - -def get_korean_key(): - """Returns the Korean Wii Common Key used to encrypt Korean content.""" - return korean_key - - -def get_vwii_key(): - """Returns the vWii Common Key used to encrypt vWii-specific content.""" - return vwii_key +def get_common_key(common_key_index): + """ + Returns the specified Wii Common Key based on the index provided. + Possible values for common_key_index: 0: Common Key, 1: Korean Key, 2: vWii Key + """ + match common_key_index: + case 0: + common_key_bin = binascii.unhexlify(common_key) + case 1: + common_key_bin = binascii.unhexlify(korean_key) + case 2: + common_key_bin = binascii.unhexlify(vwii_key) + case _: + raise ValueError("The common key index provided, " + str(common_key_index + ", does not exist.")) + return common_key_bin diff --git a/src/libWiiPy/crypto.py b/src/libWiiPy/crypto.py index 6c214d7..d8a7451 100644 --- a/src/libWiiPy/crypto.py +++ b/src/libWiiPy/crypto.py @@ -2,3 +2,22 @@ # https://github.com/NinjaCheetah/libWiiPy # # See https://wiibrew.org/wiki/Ticket for details about the TMD format + +from .commonkeys import get_common_key +from Crypto.Cipher import AES + + +def decrypt_title_key(title_key_enc, common_key_index, title_id): + """ + Returns the decrypted version of the encrypted Title Key provided. + Requires the index of the common key to use, and the Title ID of the title that the Title Key is for. + """ + # Load the correct common key for the title. + common_key = get_common_key(common_key_index) + # Calculate the IV by adding 8 bytes to the end of the Title ID. + title_key_iv = title_id + (b'\x00' * 8) + # Create a new AES object with the values provided. + aes = AES.new(common_key, AES.MODE_CBC, title_key_iv) + # Decrypt the Title Key using the AES object. + title_key = aes.decrypt(title_key_enc) + return title_key diff --git a/src/libWiiPy/ticket.py b/src/libWiiPy/ticket.py index ddf1084..cf31c8f 100644 --- a/src/libWiiPy/ticket.py +++ b/src/libWiiPy/ticket.py @@ -4,6 +4,20 @@ # See https://wiibrew.org/wiki/Ticket for details about the TMD format import io +from .crypto import decrypt_title_key +from dataclasses import dataclass +from typing import List + + +@dataclass +class TitleLimit: + """Creates a TitleLimit object that contains the type of restriction and the limit.""" + # 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: @@ -28,9 +42,7 @@ class Ticket: 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. + self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title. # v1 ticket data # TODO: Figure out v1 ticket stuff with io.BytesIO(self.ticket) as ticketdata: @@ -85,6 +97,12 @@ class Ticket: # Content access permissions ticketdata.seek(0x222) self.content_access_permissions = ticketdata.read(64) + # Content limits + ticketdata.seek(0x264) + for limit in range(0, 8): + limit_type = int.from_bytes(ticketdata.read(4)) + limit_value = int.from_bytes(ticketdata.read(4)) + self.title_limits_list.append(TitleLimit(limit_type, limit_value)) def get_signature(self): """Returns the signature of the ticket.""" @@ -131,6 +149,6 @@ class Ticket: def get_title_key(self): """Returns the decrypted title key contained in the ticket.""" - # TODO - return b'\x00' + title_key = decrypt_title_key(self.title_key_enc, self.common_key_index, self.title_id) + return title_key diff --git a/src/libWiiPy/tmd.py b/src/libWiiPy/tmd.py index 5a8e838..e626356 100644 --- a/src/libWiiPy/tmd.py +++ b/src/libWiiPy/tmd.py @@ -29,17 +29,17 @@ class TMD: self.version: int # This seems to always be 0 no matter what? self.ca_crl_version: int self.signer_crl_version: int - self.vwii: int - self.ios_tid: str - self.ios_version: int - self.title_id: str - self.content_type: str - self.group_id: int # Publisher of the title - self.region: int + self.vwii: int # Whether the title is for the vWii. 0 = No, 1 = Yes + self.ios_tid: str # The Title ID of the IOS version the associated title runs on. + self.ios_version: int # The IOS version the associated title runs on. + self.title_id: str # The Title ID of the associated title. + self.content_type: str # The type of content contained within the associated title. + self.group_id: int # The ID of the publisher of the associated title. + self.region: int # The ID of the region of the associated title. self.ratings: int self.access_rights: int - self.title_version: int - self.num_contents: int + self.title_version: int # The version of the associated title. + self.num_contents: int # The number of contents contained in the associated title. self.boot_index: int self.content_record: List[ContentRecord] # Load data from TMD file From bfec2af0acdf7e293950f07a09e73f4b2cb7871f Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Thu, 29 Feb 2024 23:06:27 -0500 Subject: [PATCH 3/3] Ironically the only error left was in a line to raise an error. --- src/libWiiPy/commonkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libWiiPy/commonkeys.py b/src/libWiiPy/commonkeys.py index 97f436d..c0a6048 100644 --- a/src/libWiiPy/commonkeys.py +++ b/src/libWiiPy/commonkeys.py @@ -21,5 +21,5 @@ def get_common_key(common_key_index): case 2: common_key_bin = binascii.unhexlify(vwii_key) case _: - raise ValueError("The common key index provided, " + str(common_key_index + ", does not exist.")) + raise ValueError("The common key index provided, " + str(common_key_index) + ", does not exist.") return common_key_bin