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.
This commit is contained in:
Campbell 2024-02-29 23:02:36 -05:00
parent d86c754ebf
commit 3c5f8b6763
5 changed files with 70 additions and 28 deletions

View File

@ -1 +1,2 @@
build
pycryptodome

View File

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

View File

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

View File

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

View File

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