Partially working code for decrypting content

This commit is contained in:
Campbell 2024-03-02 23:59:02 -05:00
parent 1d127b09e6
commit a2c4c850a8
6 changed files with 254 additions and 115 deletions

88
src/libWiiPy/content.py Normal file
View File

@ -0,0 +1,88 @@
# "content.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
#
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
import io
import sys
import hashlib
from typing import List
from .types import ContentRecord
from .crypto import decrypt_content
class ContentRegion:
"""Creates a ContentRegion object to parse the continuous content region of a WAD.
Attributes:
----------
content_region : bytes
A bytes object containing the content region of a WAD file.
content_records : list[ContentRecord]
A list of ContentRecord objects detailing all contents contained in the region.
"""
def __init__(self, content_region, content_records: List[ContentRecord]):
self.content_region = content_region
self.content_records = content_records
self.content_region_size: int # Size of the content region.
self.num_contents: int # Number of contents in the content region.
self.content_start_offsets: List[int] = [0] # The start offsets of each content in the content region.
with io.BytesIO(content_region) as content_region_data:
# Get the total size of the content region.
self.content_region_size = sys.getsizeof(content_region_data)
self.num_contents = len(self.content_records)
# Calculate the offsets of each content in the content region.
for content in self.content_records[:-1]:
start_offset = content.content_size + self.content_start_offsets[-1]
self.content_start_offsets.append(start_offset)
def get_enc_content(self, index: int) -> bytes:
"""Gets an individual content from the content region based on the provided content record, in encrypted form.
Parameters
----------
index : int
The index of the content you want to get.
Returns
-------
bytes
The encrypted content listed in the content record.
"""
with io.BytesIO(self.content_region) as content_region_data:
# Seek to the start of the requested content based on the list of offsets.
content_region_data.seek(self.content_start_offsets[index])
# Read the file based on the size of the content in the associated record.
content_enc = content_region_data.read(self.content_records[index].content_size)
return content_enc
def get_content(self, index: int, title_key: bytes) -> bytes:
"""Gets an individual content from the content region based on the provided content record, in decrypted form.
Parameters
----------
index : int
The index of the content you want to get.
title_key : bytes
The Title Key for the title the content is from.
Returns
-------
bytes
The decrypted content listed in the content record.
"""
# Load the encrypted content at the specified index and then decrypt it with the Title Key.
content_enc = self.get_enc_content(index)
content_dec = decrypt_content(content_enc, title_key, self.content_records[index].index)
# Hash the decrypted content and ensure that the hash matches the one in its Content Record.
# If it does not, then something has gone wrong in the decryption, and an error will be thrown.
content_dec_hash = hashlib.sha1(content_dec)
content_record_hash = str(self.content_records[index].content_hash.decode())
if content_dec_hash.hexdigest() != content_record_hash:
raise ValueError("Content hash did not match the expected hash in its record! This most likely means that "
"the incorrect Title Key was used for this content.\n"
"Expected hash is: {}\n".format(content_record_hash) +
"Actual hash is: {}".format(content_dec_hash.hexdigest()))
return content_dec

View File

@ -3,8 +3,10 @@
# #
# See https://wiibrew.org/wiki/Ticket for details about the TMD format # See https://wiibrew.org/wiki/Ticket for details about the TMD format
import struct
from .commonkeys import get_common_key from .commonkeys import get_common_key
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def decrypt_title_key(title_key_enc, common_key_index, title_id): def decrypt_title_key(title_key_enc, common_key_index, title_id):
@ -35,3 +37,42 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id):
# Decrypt the Title Key using the AES object. # Decrypt the Title Key using the AES object.
title_key = aes.decrypt(title_key_enc) title_key = aes.decrypt(title_key_enc)
return title_key return title_key
def decrypt_content(content_enc, title_key, content_index):
"""Gets the decrypted version of the encrypted content.
Requires the index of the common key to use, and the Title ID of the title that the Title Key is for.
Parameters
----------
content_enc : bytes
The encrypted content.
title_key : bytes
The Title Key for the title the content is from.
Returns
-------
bytes
The decrypted content.
"""
# Generate the IV from the Content Index of the content to be decrypted.
content_index_bin = struct.pack('>H', content_index)
while len(content_index_bin) < 16:
content_index_bin += b'\x00'
# In CBC mode, content must be padded out to a 16-byte boundary, so do that here, and then remove bytes added after.
padding_count = 0
content_enc = pad(content_enc, 16, "pkcs7")
#while (len(content_enc) % 16) != 0:
#content_enc += b'\x00'
#padding_count += 1
# Create a new AES object with the values provided, with the content's unique ID as the IV.
aes = AES.new(title_key, AES.MODE_CBC, content_index_bin)
# Decrypt the content using the AES object.
content_dec = aes.decrypt(content_enc)
# Remove padding bytes, if any were added.
#content_dec = unpad(content_dec, 128)
file = open("out", "wb")
file.write(content_dec)
file.close()
return content_dec

View File

@ -29,7 +29,13 @@ class TitleLimit:
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.
Attributes:
----------
ticket : bytes
A bytes object containing the contents of a ticket file.
"""
def __init__(self, ticket): def __init__(self, ticket):
self.ticket = ticket self.ticket = ticket
@ -53,63 +59,63 @@ class Ticket:
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: Figure out v1 ticket stuff
with io.BytesIO(self.ticket) as ticketdata: 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
ticketdata.seek(0x0) ticket_data.seek(0x0)
self.signature_type = ticketdata.read(4) self.signature_type = ticket_data.read(4)
# Signature data # Signature data
ticketdata.seek(0x04) ticket_data.seek(0x04)
self.signature = ticketdata.read(256) self.signature = ticket_data.read(256)
# Signature issuer # Signature issuer
ticketdata.seek(0x140) ticket_data.seek(0x140)
self.signature_issuer = str(ticketdata.read(64).decode()) self.signature_issuer = str(ticket_data.read(64).decode())
# ECDH data # ECDH data
ticketdata.seek(0x180) ticket_data.seek(0x180)
self.ecdh_data = ticketdata.read(60) self.ecdh_data = ticket_data.read(60)
# Ticket version # Ticket version
ticketdata.seek(0x1BC) ticket_data.seek(0x1BC)
self.ticket_version = int.from_bytes(ticketdata.read(1)) self.ticket_version = int.from_bytes(ticket_data.read(1))
# Title Key (Encrypted by a common key) # Title Key (Encrypted by a common key)
ticketdata.seek(0x1BF) ticket_data.seek(0x1BF)
self.title_key_enc = ticketdata.read(16) self.title_key_enc = ticket_data.read(16)
# Ticket ID # Ticket ID
ticketdata.seek(0x1D0) ticket_data.seek(0x1D0)
self.ticket_id = ticketdata.read(8) self.ticket_id = ticket_data.read(8)
# Console ID # Console ID
ticketdata.seek(0x1D8) ticket_data.seek(0x1D8)
self.console_id = int.from_bytes(ticketdata.read(4)) self.console_id = int.from_bytes(ticket_data.read(4))
# Title ID # Title ID
ticketdata.seek(0x1DC) ticket_data.seek(0x1DC)
self.title_id = ticketdata.read(8) self.title_id = ticket_data.read(8)
# Title version # Title version
ticketdata.seek(0x1E6) ticket_data.seek(0x1E6)
title_version_high = int.from_bytes(ticketdata.read(1)) * 256 title_version_high = int.from_bytes(ticket_data.read(1)) * 256
ticketdata.seek(0x1E7) ticket_data.seek(0x1E7)
title_version_low = int.from_bytes(ticketdata.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
ticketdata.seek(0x1E8) ticket_data.seek(0x1E8)
self.permitted_titles = ticketdata.read(4) self.permitted_titles = ticket_data.read(4)
# Permit mask # Permit mask
ticketdata.seek(0x1EC) ticket_data.seek(0x1EC)
self.permit_mask = ticketdata.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
ticketdata.seek(0x1F0) ticket_data.seek(0x1F0)
self.title_export_allowed = int.from_bytes(ticketdata.read(1)) self.title_export_allowed = int.from_bytes(ticket_data.read(1))
# Common key index # Common key index
ticketdata.seek(0x1F1) ticket_data.seek(0x1F1)
self.common_key_index = int.from_bytes(ticketdata.read(1)) self.common_key_index = int.from_bytes(ticket_data.read(1))
# Content access permissions # Content access permissions
ticketdata.seek(0x222) ticket_data.seek(0x222)
self.content_access_permissions = ticketdata.read(64) self.content_access_permissions = ticket_data.read(64)
# Content limits # Content limits
ticketdata.seek(0x264) ticket_data.seek(0x264)
for limit in range(0, 8): for limit in range(0, 8):
limit_type = int.from_bytes(ticketdata.read(4)) limit_type = int.from_bytes(ticket_data.read(4))
limit_value = int.from_bytes(ticketdata.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 get_signature(self): def get_signature(self):

View File

@ -6,33 +6,8 @@
import io import io
import binascii import binascii
import struct import struct
from dataclasses import dataclass
from typing import List from typing import List
from .types import ContentRecord
@dataclass
class ContentRecord:
"""
Creates a content record object that contains the details of a content contained in a title.
Attributes:
----------
cid : int
ID of the content.
index : int
Index of the content in the list of contents.
content_type : int
The type of the content.
content_size : int
The size of the content.
content_hash
The SHA-1 hash of the decrypted content.
"""
cid: int # Content ID
index: int # Index in the list of contents
content_type: int # Normal: 0x0001, DLC: 0x4001, Shared: 0x8001
content_size: int
content_hash: bytes # SHA1 hash content
class TMD: class TMD:
@ -66,71 +41,71 @@ class TMD:
self.boot_index: int self.boot_index: int
self.content_records: List[ContentRecord] = [] self.content_records: List[ContentRecord] = []
# Load data from TMD file # Load data from TMD file
with io.BytesIO(self.tmd) as tmddata: with io.BytesIO(self.tmd) as tmd_data:
# ==================================================================================== # ====================================================================================
# Parses each of the keys contained in the TMD. # Parses each of the keys contained in the TMD.
# ==================================================================================== # ====================================================================================
# Signing certificate issuer # Signing certificate issuer
tmddata.seek(0x140) tmd_data.seek(0x140)
self.issuer = tmddata.read(64) self.issuer = tmd_data.read(64)
# TMD version, seems to usually be 0, but I've seen references to other numbers # TMD version, seems to usually be 0, but I've seen references to other numbers
tmddata.seek(0x180) tmd_data.seek(0x180)
self.version = int.from_bytes(tmddata.read(1)) self.version = int.from_bytes(tmd_data.read(1))
# TODO: label # TODO: label
tmddata.seek(0x181) tmd_data.seek(0x181)
self.ca_crl_version = tmddata.read(1) self.ca_crl_version = tmd_data.read(1)
# TODO: label # TODO: label
tmddata.seek(0x182) tmd_data.seek(0x182)
self.signer_crl_version = tmddata.read(1) self.signer_crl_version = tmd_data.read(1)
# If this is a vWii title or not # If this is a vWii title or not
tmddata.seek(0x183) tmd_data.seek(0x183)
self.vwii = int.from_bytes(tmddata.read(1)) self.vwii = int.from_bytes(tmd_data.read(1))
# TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2 # TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2
tmddata.seek(0x184) tmd_data.seek(0x184)
ios_version_bin = tmddata.read(8) ios_version_bin = tmd_data.read(8)
ios_version_hex = binascii.hexlify(ios_version_bin) ios_version_hex = binascii.hexlify(ios_version_bin)
self.ios_tid = str(ios_version_hex.decode()) self.ios_tid = str(ios_version_hex.decode())
# Get IOS version based on TID # Get IOS version based on TID
self.ios_version = int(self.ios_tid[-2:], 16) self.ios_version = int(self.ios_tid[-2:], 16)
# Title ID of the title # Title ID of the title
tmddata.seek(0x18C) tmd_data.seek(0x18C)
title_id_bin = tmddata.read(8) title_id_bin = tmd_data.read(8)
title_id_hex = binascii.hexlify(title_id_bin) title_id_hex = binascii.hexlify(title_id_bin)
self.title_id = str(title_id_hex.decode()) self.title_id = str(title_id_hex.decode())
# Type of content # Type of content
tmddata.seek(0x194) tmd_data.seek(0x194)
content_type_bin = tmddata.read(4) content_type_bin = tmd_data.read(4)
content_type_hex = binascii.hexlify(content_type_bin) content_type_hex = binascii.hexlify(content_type_bin)
self.content_type = str(content_type_hex.decode()) self.content_type = str(content_type_hex.decode())
# Publisher of the title # Publisher of the title
tmddata.seek(0x198) tmd_data.seek(0x198)
self.group_id = tmddata.read(2) self.group_id = tmd_data.read(2)
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR # Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR
tmddata.seek(0x19C) tmd_data.seek(0x19C)
region_hex = tmddata.read(2) region_hex = tmd_data.read(2)
self.region = int.from_bytes(region_hex) self.region = int.from_bytes(region_hex)
# TODO: figure this one out # TODO: figure this one out
tmddata.seek(0x19E) tmd_data.seek(0x19E)
self.ratings = tmddata.read(16) self.ratings = tmd_data.read(16)
# Access rights of the title; DVD-video access and AHBPROT # Access rights of the title; DVD-video access and AHBPROT
tmddata.seek(0x1D8) tmd_data.seek(0x1D8)
self.access_rights = tmddata.read(4) self.access_rights = tmd_data.read(4)
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD # Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD
tmddata.seek(0x1DC) tmd_data.seek(0x1DC)
title_version_high = int.from_bytes(tmddata.read(1)) * 256 title_version_high = int.from_bytes(tmd_data.read(1)) * 256
tmddata.seek(0x1DD) tmd_data.seek(0x1DD)
title_version_low = int.from_bytes(tmddata.read(1)) title_version_low = int.from_bytes(tmd_data.read(1))
self.title_version = title_version_high + title_version_low self.title_version = title_version_high + title_version_low
# The number of contents listed in the TMD # The number of contents listed in the TMD
tmddata.seek(0x1DE) tmd_data.seek(0x1DE)
self.num_contents = int.from_bytes(tmddata.read(2)) self.num_contents = int.from_bytes(tmd_data.read(2))
# Content index in content list that contains the boot file # Content index in content list that contains the boot file
tmddata.seek(0x1E0) tmd_data.seek(0x1E0)
self.boot_index = tmddata.read(2) self.boot_index = tmd_data.read(2)
# Get content records for the number of contents in num_contents. # Get content records for the number of contents in num_contents.
for content in range(0, self.num_contents): for content in range(0, self.num_contents):
tmddata.seek(0x1E4 + (36 * content)) tmd_data.seek(0x1E4 + (36 * content))
content_record_hdr = struct.unpack(">LHH4x4s20s", tmddata.read(36)) content_record_hdr = struct.unpack(">LHH4x4s20s", tmd_data.read(36))
self.content_records.append( self.content_records.append(
ContentRecord(int(content_record_hdr[0]), int(content_record_hdr[1]), ContentRecord(int(content_record_hdr[0]), int(content_record_hdr[1]),
int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]), int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]),

29
src/libWiiPy/types.py Normal file
View File

@ -0,0 +1,29 @@
# "types.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
from dataclasses import dataclass
@dataclass
class ContentRecord:
"""
Creates a content record object that contains the details of a content contained in a title.
Attributes:
----------
content_id : int
ID of the content.
index : int
Index of the content in the list of contents.
content_type : int
The type of the content.
content_size : int
The size of the content.
content_hash
The SHA-1 hash of the decrypted content.
"""
content_id: int # Unique ID for the current content
index: int # Index in the list of contents
content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared
content_size: int # Size of the current content
content_hash: bytes # SHA1 hash of the current content

View File

@ -37,36 +37,36 @@ 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(self.wad) as waddata: with io.BytesIO(self.wad) as wad_data:
# ==================================================================================== # ====================================================================================
# Get the sizes of each data region contained within the WAD. # Get the sizes of each data region contained within the WAD.
# ==================================================================================== # ====================================================================================
# Header length, which will always be 64 bytes, as it is padded out if it is shorter. # Header length, which will always be 64 bytes, as it is padded out if it is shorter.
self.wad_hdr_size = 64 self.wad_hdr_size = 64
# WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is"). # WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is").
waddata.seek(0x04) wad_data.seek(0x04)
self.wad_type = str(waddata.read(2).decode()) self.wad_type = str(wad_data.read(2).decode())
# WAD version, this is always 0. # WAD version, this is always 0.
waddata.seek(0x06) wad_data.seek(0x06)
self.wad_version = waddata.read(2) self.wad_version = wad_data.read(2)
# WAD cert size. # WAD cert size.
waddata.seek(0x08) wad_data.seek(0x08)
self.wad_cert_size = int(binascii.hexlify(waddata.read(4)), 16) self.wad_cert_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD crl size. # WAD crl size.
waddata.seek(0x0c) wad_data.seek(0x0c)
self.wad_crl_size = int(binascii.hexlify(waddata.read(4)), 16) self.wad_crl_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD ticket size. # WAD ticket size.
waddata.seek(0x10) wad_data.seek(0x10)
self.wad_tik_size = int(binascii.hexlify(waddata.read(4)), 16) self.wad_tik_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD TMD size. # WAD TMD size.
waddata.seek(0x14) wad_data.seek(0x14)
self.wad_tmd_size = int(binascii.hexlify(waddata.read(4)), 16) self.wad_tmd_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD content size. # WAD content size.
waddata.seek(0x18) wad_data.seek(0x18)
self.wad_content_size = int(binascii.hexlify(waddata.read(4)), 16) self.wad_content_size = int(binascii.hexlify(wad_data.read(4)), 16)
# Publisher of the title contained in the WAD. # Publisher of the title contained in the WAD.
waddata.seek(0x1c) wad_data.seek(0x1c)
self.wad_meta_size = int(binascii.hexlify(waddata.read(4)), 16) self.wad_meta_size = int(binascii.hexlify(wad_data.read(4)), 16)
# ==================================================================================== # ====================================================================================
# Calculate file offsets from sizes. Every section of the WAD is padded out to a multiple of 0x40. # Calculate file offsets from sizes. Every section of the WAD is padded out to a multiple of 0x40.
# ==================================================================================== # ====================================================================================