mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2025-04-26 05:11:02 -04:00
Added ability to dump tickets back to raw data
This commit is contained in:
parent
8244d79fba
commit
b5aab5ad22
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user