mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2025-04-26 13:21:01 -04:00
Partially working code for decrypting content
This commit is contained in:
parent
1d127b09e6
commit
a2c4c850a8
88
src/libWiiPy/content.py
Normal file
88
src/libWiiPy/content.py
Normal 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
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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
29
src/libWiiPy/types.py
Normal 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
|
@ -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.
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
|
Loading…
x
Reference in New Issue
Block a user