Added ability to dump tickets back to raw data

This commit is contained in:
Campbell 2024-03-21 21:09:46 -04:00
parent 8244d79fba
commit b5aab5ad22
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
3 changed files with 153 additions and 59 deletions

View File

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

View File

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

View File

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