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 io
import binascii import binascii
from .crypto import decrypt_title_key from .crypto import decrypt_title_key
from dataclasses import dataclass from .types import TitleLimit
from typing import List 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: class Ticket:
"""Creates a Ticket object to parse a Ticket file to retrieve the Title Key needed to decrypt it. """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): def __init__(self, ticket):
self.ticket = ticket self.ticket = ticket
# Signature blob header # Signature blob header
self.signature_type: bytes # Type of signature, always 0x10001 for RSA-2048 self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048
self.signature: bytes # Actual signature data self.signature: bytes = b'' # Actual signature data
# v0 ticket data # v0 ticket data
self.signature_issuer: str # Who issued the signature for the ticket 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.ecdh_data: bytes = b'' # Involved in created one-time keys for console-specific title installs.
self.ticket_version: int # The version of the current ticket file. self.ticket_version: int = 0 # 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.title_key_enc: bytes = b'' # 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.ticket_id: bytes = b'' # 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.console_id: int = 0 # ID of the console that the ticket was issued for.
self.title_id: bytes # TID/IV used for AES-CBC encryption. 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.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.unknown1: bytes = b'' # Some unknown data, not always the same so reading it just in case.
self.permitted_titles: bytes # Permitted titles mask self.title_version: int = 0 # Version of the ticket's associated title.
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.permitted_titles: bytes = b'' # Permitted titles mask
self.title_export_allowed: int # Whether title export is allowed with a PRNG key or not. 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.common_key_index: int # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not.
self.content_access_permissions: bytes # "Content access permissions (one bit for each content)" 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. self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title.
# v1 ticket data # 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: with io.BytesIO(self.ticket) as ticket_data:
# ==================================================================================== # ====================================================================================
# Parses each of the keys contained in the Ticket. # Parses each of the keys contained in the Ticket.
# ==================================================================================== # ====================================================================================
# Signature type # Signature type.
ticket_data.seek(0x0) ticket_data.seek(0x0)
self.signature_type = ticket_data.read(4) self.signature_type = ticket_data.read(4)
# Signature data # Signature data.
ticket_data.seek(0x04) ticket_data.seek(0x04)
self.signature = ticket_data.read(256) self.signature = ticket_data.read(256)
# Signature issuer # Signature issuer.
ticket_data.seek(0x140) ticket_data.seek(0x140)
self.signature_issuer = str(ticket_data.read(64).decode()) self.signature_issuer = str(ticket_data.read(64).decode())
# ECDH data # ECDH data.
ticket_data.seek(0x180) ticket_data.seek(0x180)
self.ecdh_data = ticket_data.read(60) self.ecdh_data = ticket_data.read(60)
# Ticket version # Ticket version.
ticket_data.seek(0x1BC) ticket_data.seek(0x1BC)
self.ticket_version = int.from_bytes(ticket_data.read(1)) 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) ticket_data.seek(0x1BF)
self.title_key_enc = ticket_data.read(16) self.title_key_enc = ticket_data.read(16)
# Ticket ID # Ticket ID.
ticket_data.seek(0x1D0) ticket_data.seek(0x1D0)
self.ticket_id = ticket_data.read(8) self.ticket_id = ticket_data.read(8)
# Console ID # Console ID.
ticket_data.seek(0x1D8) ticket_data.seek(0x1D8)
self.console_id = int.from_bytes(ticket_data.read(4)) self.console_id = int.from_bytes(ticket_data.read(4))
# Title ID # Title ID.
ticket_data.seek(0x1DC) ticket_data.seek(0x1DC)
self.title_id = ticket_data.read(8) 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) title_id_hex = binascii.hexlify(self.title_id)
self.title_id_str = str(title_id_hex.decode()) 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) ticket_data.seek(0x1E6)
title_version_high = int.from_bytes(ticket_data.read(1)) * 256 title_version_high = int.from_bytes(ticket_data.read(1)) * 256
ticket_data.seek(0x1E7) ticket_data.seek(0x1E7)
title_version_low = int.from_bytes(ticket_data.read(1)) title_version_low = int.from_bytes(ticket_data.read(1))
self.title_version = title_version_high + title_version_low self.title_version = title_version_high + title_version_low
# Permitted titles mask # Permitted titles mask.
ticket_data.seek(0x1E8) ticket_data.seek(0x1E8)
self.permitted_titles = ticket_data.read(4) self.permitted_titles = ticket_data.read(4)
# Permit mask # Permit mask.
ticket_data.seek(0x1EC) ticket_data.seek(0x1EC)
self.permit_mask = ticket_data.read(4) 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) ticket_data.seek(0x1F0)
self.title_export_allowed = int.from_bytes(ticket_data.read(1)) self.title_export_allowed = int.from_bytes(ticket_data.read(1))
# Common key index # Common key index.
ticket_data.seek(0x1F1) ticket_data.seek(0x1F1)
self.common_key_index = int.from_bytes(ticket_data.read(1)) 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) ticket_data.seek(0x222)
self.content_access_permissions = ticket_data.read(64) self.content_access_permissions = ticket_data.read(64)
# Content limits # Content limits.
ticket_data.seek(0x264) ticket_data.seek(0x264)
for limit in range(0, 8): for limit in range(0, 8):
limit_type = int.from_bytes(ticket_data.read(4)) limit_type = int.from_bytes(ticket_data.read(4))
limit_value = 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)) 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): def get_title_id(self):
"""Gets the Title ID of the ticket's associated title. """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)) tmd_data.write(int.to_bytes(self.region, 2))
# Ratings. # Ratings.
tmd_data.write(self.ratings) tmd_data.write(self.ratings)
# Reserved (All \x00). # Reserved (all \x00).
tmd_data.write(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') tmd_data.write(b'\x00' * 12)
# IPC mask. # IPC mask.
tmd_data.write(self.ipc_mask) tmd_data.write(self.ipc_mask)
# Reserved (ALl \x00). # 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') tmd_data.write(b'\x00' * 18)
# Access rights. # Access rights.
tmd_data.write(self.access_rights) tmd_data.write(self.access_rights)
# Title version. # Title version.
@ -211,7 +211,7 @@ class TMD:
content_data.seek(0x0) content_data.seek(0x0)
tmd_data.write(content_data.read()) tmd_data.write(content_data.read())
content_data.close() content_data.close()
# Set the TMD attribute of the object to the new raw TMD.
tmd_data.seek(0x0) tmd_data.seek(0x0)
self.tmd = tmd_data.read() self.tmd = tmd_data.read()
# Reload object's attributes to ensure the raw data and object match. # 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_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared.
content_size: int # Size of the content when decrypted. content_size: int # Size of the content when decrypted.
content_hash: bytes # SHA-1 hash 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