mirror of
				https://github.com/NinjaCheetah/libWiiPy.git
				synced 2025-11-04 00:16:18 -05:00 
			
		
		
		
	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:
		
						commit
						270a7095ac
					
				@ -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" }
 | 
				
			||||||
 | 
				
			|||||||
@ -1 +1,2 @@
 | 
				
			|||||||
build
 | 
					build
 | 
				
			||||||
 | 
					pycryptodome
 | 
				
			||||||
 | 
				
			|||||||
@ -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
									
								
							
							
						
						
									
										23
									
								
								src/libWiiPy/crypto.py
									
									
									
									
									
										Normal 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
 | 
				
			||||||
@ -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
									
								
							
							
						
						
									
										154
									
								
								src/libWiiPy/ticket.py
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -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)
 | 
				
			||||||
 | 
				
			|||||||
@ -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.
 | 
				
			||||||
            # ====================================================================================
 | 
					            # ====================================================================================
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user