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
import struct
from .commonkeys import get_common_key
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
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.
title_key = aes.decrypt(title_key_enc)
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:
"""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):
self.ticket = ticket
@ -53,63 +59,63 @@ class Ticket:
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:
with io.BytesIO(self.ticket) as ticket_data:
# ====================================================================================
# Parses each of the keys contained in the Ticket.
# ====================================================================================
# Signature type
ticketdata.seek(0x0)
self.signature_type = ticketdata.read(4)
ticket_data.seek(0x0)
self.signature_type = ticket_data.read(4)
# Signature data
ticketdata.seek(0x04)
self.signature = ticketdata.read(256)
ticket_data.seek(0x04)
self.signature = ticket_data.read(256)
# Signature issuer
ticketdata.seek(0x140)
self.signature_issuer = str(ticketdata.read(64).decode())
ticket_data.seek(0x140)
self.signature_issuer = str(ticket_data.read(64).decode())
# ECDH data
ticketdata.seek(0x180)
self.ecdh_data = ticketdata.read(60)
ticket_data.seek(0x180)
self.ecdh_data = ticket_data.read(60)
# Ticket version
ticketdata.seek(0x1BC)
self.ticket_version = int.from_bytes(ticketdata.read(1))
ticket_data.seek(0x1BC)
self.ticket_version = int.from_bytes(ticket_data.read(1))
# Title Key (Encrypted by a common key)
ticketdata.seek(0x1BF)
self.title_key_enc = ticketdata.read(16)
ticket_data.seek(0x1BF)
self.title_key_enc = ticket_data.read(16)
# Ticket ID
ticketdata.seek(0x1D0)
self.ticket_id = ticketdata.read(8)
ticket_data.seek(0x1D0)
self.ticket_id = ticket_data.read(8)
# Console ID
ticketdata.seek(0x1D8)
self.console_id = int.from_bytes(ticketdata.read(4))
ticket_data.seek(0x1D8)
self.console_id = int.from_bytes(ticket_data.read(4))
# Title ID
ticketdata.seek(0x1DC)
self.title_id = ticketdata.read(8)
ticket_data.seek(0x1DC)
self.title_id = ticket_data.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))
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
ticketdata.seek(0x1E8)
self.permitted_titles = ticketdata.read(4)
ticket_data.seek(0x1E8)
self.permitted_titles = ticket_data.read(4)
# Permit mask
ticketdata.seek(0x1EC)
self.permit_mask = ticketdata.read(4)
ticket_data.seek(0x1EC)
self.permit_mask = ticket_data.read(4)
# Whether title export with a PRNG key is allowed
ticketdata.seek(0x1F0)
self.title_export_allowed = int.from_bytes(ticketdata.read(1))
ticket_data.seek(0x1F0)
self.title_export_allowed = int.from_bytes(ticket_data.read(1))
# Common key index
ticketdata.seek(0x1F1)
self.common_key_index = int.from_bytes(ticketdata.read(1))
ticket_data.seek(0x1F1)
self.common_key_index = int.from_bytes(ticket_data.read(1))
# Content access permissions
ticketdata.seek(0x222)
self.content_access_permissions = ticketdata.read(64)
ticket_data.seek(0x222)
self.content_access_permissions = ticket_data.read(64)
# Content limits
ticketdata.seek(0x264)
ticket_data.seek(0x264)
for limit in range(0, 8):
limit_type = int.from_bytes(ticketdata.read(4))
limit_value = int.from_bytes(ticketdata.read(4))
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 get_signature(self):

View File

@ -6,33 +6,8 @@
import io
import binascii
import struct
from dataclasses import dataclass
from typing import List
@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
from .types import ContentRecord
class TMD:
@ -66,71 +41,71 @@ class TMD:
self.boot_index: int
self.content_records: List[ContentRecord] = []
# 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.
# ====================================================================================
# Signing certificate issuer
tmddata.seek(0x140)
self.issuer = tmddata.read(64)
tmd_data.seek(0x140)
self.issuer = tmd_data.read(64)
# TMD version, seems to usually be 0, but I've seen references to other numbers
tmddata.seek(0x180)
self.version = int.from_bytes(tmddata.read(1))
tmd_data.seek(0x180)
self.version = int.from_bytes(tmd_data.read(1))
# TODO: label
tmddata.seek(0x181)
self.ca_crl_version = tmddata.read(1)
tmd_data.seek(0x181)
self.ca_crl_version = tmd_data.read(1)
# TODO: label
tmddata.seek(0x182)
self.signer_crl_version = tmddata.read(1)
tmd_data.seek(0x182)
self.signer_crl_version = tmd_data.read(1)
# If this is a vWii title or not
tmddata.seek(0x183)
self.vwii = int.from_bytes(tmddata.read(1))
tmd_data.seek(0x183)
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
tmddata.seek(0x184)
ios_version_bin = tmddata.read(8)
tmd_data.seek(0x184)
ios_version_bin = tmd_data.read(8)
ios_version_hex = binascii.hexlify(ios_version_bin)
self.ios_tid = str(ios_version_hex.decode())
# Get IOS version based on TID
self.ios_version = int(self.ios_tid[-2:], 16)
# Title ID of the title
tmddata.seek(0x18C)
title_id_bin = tmddata.read(8)
tmd_data.seek(0x18C)
title_id_bin = tmd_data.read(8)
title_id_hex = binascii.hexlify(title_id_bin)
self.title_id = str(title_id_hex.decode())
# Type of content
tmddata.seek(0x194)
content_type_bin = tmddata.read(4)
tmd_data.seek(0x194)
content_type_bin = tmd_data.read(4)
content_type_hex = binascii.hexlify(content_type_bin)
self.content_type = str(content_type_hex.decode())
# Publisher of the title
tmddata.seek(0x198)
self.group_id = tmddata.read(2)
tmd_data.seek(0x198)
self.group_id = tmd_data.read(2)
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR
tmddata.seek(0x19C)
region_hex = tmddata.read(2)
tmd_data.seek(0x19C)
region_hex = tmd_data.read(2)
self.region = int.from_bytes(region_hex)
# TODO: figure this one out
tmddata.seek(0x19E)
self.ratings = tmddata.read(16)
tmd_data.seek(0x19E)
self.ratings = tmd_data.read(16)
# Access rights of the title; DVD-video access and AHBPROT
tmddata.seek(0x1D8)
self.access_rights = tmddata.read(4)
tmd_data.seek(0x1D8)
self.access_rights = tmd_data.read(4)
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD
tmddata.seek(0x1DC)
title_version_high = int.from_bytes(tmddata.read(1)) * 256
tmddata.seek(0x1DD)
title_version_low = int.from_bytes(tmddata.read(1))
tmd_data.seek(0x1DC)
title_version_high = int.from_bytes(tmd_data.read(1)) * 256
tmd_data.seek(0x1DD)
title_version_low = int.from_bytes(tmd_data.read(1))
self.title_version = title_version_high + title_version_low
# The number of contents listed in the TMD
tmddata.seek(0x1DE)
self.num_contents = int.from_bytes(tmddata.read(2))
tmd_data.seek(0x1DE)
self.num_contents = int.from_bytes(tmd_data.read(2))
# Content index in content list that contains the boot file
tmddata.seek(0x1E0)
self.boot_index = tmddata.read(2)
tmd_data.seek(0x1E0)
self.boot_index = tmd_data.read(2)
# Get content records for the number of contents in num_contents.
for content in range(0, self.num_contents):
tmddata.seek(0x1E4 + (36 * content))
content_record_hdr = struct.unpack(">LHH4x4s20s", tmddata.read(36))
tmd_data.seek(0x1E4 + (36 * content))
content_record_hdr = struct.unpack(">LHH4x4s20s", tmd_data.read(36))
self.content_records.append(
ContentRecord(int(content_record_hdr[0]), int(content_record_hdr[1]),
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_meta_offset: int
# 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.
# ====================================================================================
# Header length, which will always be 64 bytes, as it is padded out if it is shorter.
self.wad_hdr_size = 64
# WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is").
waddata.seek(0x04)
self.wad_type = str(waddata.read(2).decode())
wad_data.seek(0x04)
self.wad_type = str(wad_data.read(2).decode())
# WAD version, this is always 0.
waddata.seek(0x06)
self.wad_version = waddata.read(2)
wad_data.seek(0x06)
self.wad_version = wad_data.read(2)
# WAD cert size.
waddata.seek(0x08)
self.wad_cert_size = int(binascii.hexlify(waddata.read(4)), 16)
wad_data.seek(0x08)
self.wad_cert_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD crl size.
waddata.seek(0x0c)
self.wad_crl_size = int(binascii.hexlify(waddata.read(4)), 16)
wad_data.seek(0x0c)
self.wad_crl_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD ticket size.
waddata.seek(0x10)
self.wad_tik_size = int(binascii.hexlify(waddata.read(4)), 16)
wad_data.seek(0x10)
self.wad_tik_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD TMD size.
waddata.seek(0x14)
self.wad_tmd_size = int(binascii.hexlify(waddata.read(4)), 16)
wad_data.seek(0x14)
self.wad_tmd_size = int(binascii.hexlify(wad_data.read(4)), 16)
# WAD content size.
waddata.seek(0x18)
self.wad_content_size = int(binascii.hexlify(waddata.read(4)), 16)
wad_data.seek(0x18)
self.wad_content_size = int(binascii.hexlify(wad_data.read(4)), 16)
# Publisher of the title contained in the WAD.
waddata.seek(0x1c)
self.wad_meta_size = int(binascii.hexlify(waddata.read(4)), 16)
wad_data.seek(0x1c)
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.
# ====================================================================================