Merge pull request #5 from NinjaCheetah/ticket_work

Add ticket.py and crypto.py, for handling tickets and title keys
This commit is contained in:
Campbell 2024-02-29 23:09:39 -05:00 committed by GitHub
commit 270a7095ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 211 additions and 35 deletions

View File

@ -1,6 +1,6 @@
[project] [project]
name = "libWiiPy" name = "libWiiPy"
version = "0.1.0" version = "0.2.0"
authors = [ authors = [
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" }, { name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" } { name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }

View File

@ -1 +1,2 @@
build build
pycryptodome

View File

@ -1,21 +1,25 @@
# "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors # "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
default_key = 'ebe42a225e8593e448d9c5457381aaf7' import binascii
common_key = 'ebe42a225e8593e448d9c5457381aaf7'
korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e' korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e'
vwii_key = '30bfc76e7c19afbb23163330ced7c28d' vwii_key = '30bfc76e7c19afbb23163330ced7c28d'
def get_default_key(): def get_common_key(common_key_index):
"""Returns the regular Wii Common Key used to encrypt most content.""" """
return default_key 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
"""
def get_korean_key(): match common_key_index:
"""Returns the Korean Wii Common Key used to encrypt Korean content.""" case 0:
return korean_key common_key_bin = binascii.unhexlify(common_key)
case 1:
common_key_bin = binascii.unhexlify(korean_key)
def get_vwii_key(): case 2:
"""Returns the vWii Common Key used to encrypt vWii-specific content.""" common_key_bin = binascii.unhexlify(vwii_key)
return vwii_key case _:
raise ValueError("The common key index provided, " + str(common_key_index) + ", does not exist.")
return common_key_bin

23
src/libWiiPy/crypto.py Normal file
View File

@ -0,0 +1,23 @@
# "crypto.py" from libWiiPy by NinjaCheetah & Contributors
# 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

View File

@ -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

154
src/libWiiPy/ticket.py Normal file
View File

@ -0,0 +1,154 @@
# "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
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:
"""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.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:
# ====================================================================================
# 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)
# 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."""
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."""
title_key = decrypt_title_key(self.title_key_enc, self.common_key_index, self.title_id)
return title_key

View File

@ -29,21 +29,24 @@ class TMD:
self.version: int # This seems to always be 0 no matter what? self.version: int # This seems to always be 0 no matter what?
self.ca_crl_version: int self.ca_crl_version: int
self.signer_crl_version: int self.signer_crl_version: int
self.vwii: int self.vwii: int # Whether the title is for the vWii. 0 = No, 1 = Yes
self.ios_tid: str self.ios_tid: str # The Title ID of the IOS version the associated title runs on.
self.ios_version: int self.ios_version: int # The IOS version the associated title runs on.
self.title_id: str self.title_id: str # The Title ID of the associated title.
self.content_type: str self.content_type: str # The type of content contained within the associated title.
self.group_id: int # Publisher of the title self.group_id: int # The ID of the publisher of the associated title.
self.region: int self.region: int # The ID of the region of the associated title.
self.ratings: int self.ratings: int
self.access_rights: int self.access_rights: int
self.title_version: int self.title_version: int # The version of the associated title.
self.num_contents: int self.num_contents: int # The number of contents contained in the associated title.
self.boot_index: int self.boot_index: int
self.content_record: List[ContentRecord] self.content_record: List[ContentRecord]
# Load data from TMD file # 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 # Signing certificate issuer
tmddata.seek(0x140) tmddata.seek(0x140)
self.issuer = tmddata.read(64) self.issuer = tmddata.read(64)

View File

@ -30,7 +30,7 @@ class WAD:
self.wad_content_offset: int self.wad_content_offset: int
self.wad_meta_offset: int self.wad_meta_offset: int
# Load header data from WAD stream # 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. # Get the sizes of each data region contained within the WAD.
# ==================================================================================== # ====================================================================================