11 Commits

Author SHA1 Message Date
1b6e0db26d Revert changes related to processing content indices
Changes released in libWiiPy v0.5.0 and v0.5.1 to how indices were handled ended up way overcomplicating things, resulting in lots of issues now that I'm working with the content module again in WiiPy. These changes have mostly been reverted.
The issues were related to handling WADs where the content indices don't align with the actual index of the content, like in cases where content has bene removed. This issue has been fixed again with a new and much simpler patch that should not introduce new bugs.
2024-10-20 19:03:26 -04:00
9ae059b797 Add support for extracting/packing/otherwise handling dev WADs 2024-10-13 21:39:52 -04:00
c604c195d2 Correct line endings when dumping setting.txt 2024-10-09 20:38:00 -04:00
0c2e13f18a Remove leftover debugging print 2024-10-08 14:06:09 -04:00
7fed039fdc Added methods to get a content index from a CID and add content from a Title() 2024-09-13 14:56:37 -04:00
0d306076a2 Added methods to content module to remove contents by index or CID 2024-09-11 11:13:01 -04:00
a1773b9a02 Improved adding new content to title, fixed minor bugs
Dumping a title now properly updates the "number of contents" field in the TMD, so you're able to add more content than there was previously, and that new content will be added correctly.
2024-09-08 13:15:52 -04:00
7c2f0fb21f Fix getting a title's type 2024-09-05 11:07:14 -04:00
0edd4fa6bb Update how TMD regions are handled 2024-09-04 14:28:13 -04:00
e163d34f0b Allow for calculating title size with and without shared content 2024-09-04 14:23:24 -04:00
9fb0fdbc17 Added setting.txt parser, moved some modules under a new "nand" subpackage 2024-08-14 01:26:46 -04:00
16 changed files with 439 additions and 193 deletions

View File

@@ -6,6 +6,7 @@
:maxdepth: 4 :maxdepth: 4
libWiiPy.archive libWiiPy.archive
libWiiPy.nand
libWiiPy.title libWiiPy.title
``` ```

View File

@@ -0,0 +1,27 @@
# libWiiPy.nand package
## Submodules
### libWiiPy.nand.emunand module
```{eval-rst}
.. automodule:: libWiiPy.nand.emunand
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.nand.setting module
```{eval-rst}
.. automodule:: libWiiPy.nand.setting
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.nand.sys module
```{eval-rst}
.. automodule:: libWiiPy.nand.sys
:members:
:undoc-members:
:show-inheritance:
```

View File

@@ -26,14 +26,6 @@
:show-inheritance: :show-inheritance:
``` ```
### libWiiPy.title.emunand module
```{eval-rst}
.. automodule:: libWiiPy.title.emunand
:members:
:undoc-members:
:show-inheritance:
```
### libWiipy.title.iospatcher module ### libWiipy.title.iospatcher module
```{eval-rst} ```{eval-rst}
.. automodule:: libWiiPy.title.iospatcher .. automodule:: libWiiPy.title.iospatcher
@@ -50,14 +42,6 @@
:show-inheritance: :show-inheritance:
``` ```
### libWiiPy.title.sys module
```{eval-rst}
.. automodule:: libWiiPy.title.sys
:members:
:undoc-members:
:show-inheritance:
```
### libWiiPy.title.ticket module ### libWiiPy.title.ticket module
```{eval-rst} ```{eval-rst}
.. automodule:: libWiiPy.title.ticket .. automodule:: libWiiPy.title.ticket

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "libWiiPy" name = "libWiiPy"
version = "0.5.0" version = "0.5.2"
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" }

View File

@@ -3,7 +3,8 @@
# #
# These are the essential submodules from libWiiPy that you'd probably want imported by default. # These are the essential submodules from libWiiPy that you'd probably want imported by default.
__all__ = ["archive", "title"] __all__ = ["archive", "nand", "title"]
from . import archive from . import archive
from . import nand
from . import title from . import title

View File

@@ -0,0 +1,6 @@
# "nand/__init__.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
from .emunand import *
from .setting import *
from .sys import *

View File

@@ -1,4 +1,4 @@
# "title/emunand.py" from libWiiPy by NinjaCheetah & Contributors # "nand/emunand.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
# #
# Code for handling setting up and modifying a Wii EmuNAND. # Code for handling setting up and modifying a Wii EmuNAND.
@@ -6,8 +6,8 @@
import os import os
import pathlib import pathlib
import shutil import shutil
from .title import Title from ..title.title import Title
from .content import SharedContentMap as _SharedContentMap from ..title.content import SharedContentMap as _SharedContentMap
from .sys import UidSys as _UidSys from .sys import UidSys as _UidSys

View File

@@ -0,0 +1,134 @@
# "nand/setting.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy
#
# See https://wiibrew.org/wiki//title/00000001/00000002/data/setting.txt for information about setting.txt.
import io
from ..shared import _pad_bytes
_key = 0x73B5DBFA
class SettingTxt:
"""
A SettingTxt object that allows for decrypting and then parsing a setting.txt file from the Wii.
Attributes
----------
area : str
The region of the System Menu this file matches with.
model : str
The model of the console, usually RVL-001 or RVL-101.
dvd : int
Unknown, might have to do with indicating support for scrapped DVD playback capabilities.
mpch : str
Unknown, generally accepted value is "0x7FFE".
code : str
Unknown code, may match with manufacturer code in serial number?
serial_number : str
Serial number of the console.
video : str
Video mode, either NTSC or PAL.
game : str
Another region code, possibly set by the hidden region select channel.
"""
def __init__(self):
self.area: str = ""
self.model: str = ""
self.dvd: int = 0
self.mpch: str = "" # What does this mean, Movie Player Channel? It's also a hex string, it seems.
self.code: str = ""
self.serial_number: str = ""
self.video: str = ""
self.game: str = ""
def load(self, setting_txt: bytes) -> None:
"""
Loads the raw data of an encrypted setting.txt file and decrypts it to parse its arguments
Parameters
----------
setting_txt : bytes
The data of an encrypted setting.txt file.
"""
with io.BytesIO(setting_txt) as setting_data:
global _key # I still don't actually know what *kind* of encryption this is.
setting_txt_dec: [int] = []
for i in range(0, 256):
setting_txt_dec.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
_key = (_key << 1) | (_key >> 31)
setting_txt_dec = bytes(setting_txt_dec)
try:
setting_str = setting_txt_dec.decode('utf-8')
except UnicodeDecodeError:
last_newline_pos = setting_txt_dec.rfind(b'\n') # This makes sure we don't try to decode any garbage data.
setting_str = setting_txt_dec[:last_newline_pos + 1].decode('utf-8')
self.load_decrypted(setting_str)
def load_decrypted(self, setting_txt: str) -> None:
"""
Loads the raw data of a decrypted setting.txt file and parses its arguments
Parameters
----------
setting_txt : str
The data of a decrypted setting.txt file.
"""
setting_dict = {}
# Iterate over every key in the file to create a dictionary.
for line in setting_txt.splitlines():
line = line.strip()
if line is not None:
key, value = line.split('=', 1)
setting_dict[key.strip()] = value.strip()
# Load the values from the dictionary into the object.
self.area = setting_dict["AREA"]
self.model = setting_dict["MODEL"]
self.dvd = int(setting_dict["DVD"])
self.mpch = setting_dict["MPCH"]
self.code = setting_dict["CODE"]
self.serial_number = setting_dict["SERNO"]
self.video = setting_dict["VIDEO"]
self.game = setting_dict["GAME"]
def dump(self) -> bytes:
"""
Dumps the SettingTxt object back into an encrypted bytes that the Wii can load.
Returns
-------
bytes
The setting.txt file as encrypted bytes.
"""
setting_str = self.dump_decrypted()
setting_txt_dec = setting_str.encode()
global _key
# This could probably be made more efficient somehow.
setting_txt_enc: [int] = []
with io.BytesIO(setting_txt_dec) as setting_data:
for i in range(0, len(setting_txt_dec)):
setting_txt_enc.append(int.from_bytes(setting_data.read(1)) ^ (_key & 0xff))
_key = (_key << 1) | (_key >> 31)
setting_txt_enc = _pad_bytes(bytes(setting_txt_enc), 256)
return setting_txt_enc
def dump_decrypted(self) -> str:
"""
Dumps the SettingTxt object into a decrypted string.
Returns
-------
str
The setting.txt file as decrypted text.
"""
# Write the keys back into a text file that can then be manually edited or re-encrypted.
setting_txt = ""
setting_txt += f"AREA={self.area}\r\n"
setting_txt += f"MODEL={self.model}\r\n"
setting_txt += f"DVD={self.dvd}\r\n"
setting_txt += f"MPCH={self.mpch}\r\n"
setting_txt += f"CODE={self.code}\r\n"
setting_txt += f"SERNO={self.serial_number}\r\n"
setting_txt += f"VIDEO={self.video}\r\n"
setting_txt += f"GAME={self.game}\r\n"
return setting_txt

View File

@@ -1,4 +1,4 @@
# "title/sys.py" from libWiiPy by NinjaCheetah & Contributors # "nand/sys.py" from libWiiPy by NinjaCheetah & Contributors
# https://github.com/NinjaCheetah/libWiiPy # https://github.com/NinjaCheetah/libWiiPy
# #
# See https://wiibrew.org/wiki//sys/uid.sys for information about uid.sys. # See https://wiibrew.org/wiki//sys/uid.sys for information about uid.sys.
@@ -28,7 +28,7 @@ class _UidSysEntry:
class UidSys: class UidSys:
""" """
A UidSys object to parse and edit the uid.sys file stored in /sys/ on the Wii's NAND. This file is used to track all A UidSys object to parse and edit the uid.sys file stored in /sys/ on the Wii's NAND. This file is used to track all
the titles installed on the console. the titles that have been launched on a console.
Attributes Attributes
---------- ----------

View File

@@ -3,10 +3,8 @@
from .content import * from .content import *
from .crypto import * from .crypto import *
from .emunand import *
from .iospatcher import * from .iospatcher import *
from .nus import * from .nus import *
from .sys import *
from .ticket import * from .ticket import *
from .title import * from .title import *
from .tmd import * from .tmd import *

View File

@@ -7,11 +7,14 @@ common_key = 'ebe42a225e8593e448d9c5457381aaf7'
korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e' korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e'
vwii_key = '30bfc76e7c19afbb23163330ced7c28d' vwii_key = '30bfc76e7c19afbb23163330ced7c28d'
development_key = 'a1604a6a7123b529ae8bec32c816fcaa'
def get_common_key(common_key_index) -> bytes:
def get_common_key(common_key_index, dev=False) -> bytes:
""" """
Gets the specified Wii Common Key based on the index provided. If an invalid common key index is provided, this Gets the specified Wii Common Key based on the index provided. If an invalid common key index is provided, this
function falls back on always returning key 0 (the Common Key). function falls back on always returning key 0 (the Common Key). If the kwarg "dev" is specified, then key 0 will
point to the development common key rather than the retail one. Keys 1 and 2 are unaffected by this argument.
Possible values for common_key_index: 0: Common Key, 1: Korean Key, 2: vWii Key Possible values for common_key_index: 0: Common Key, 1: Korean Key, 2: vWii Key
@@ -19,6 +22,8 @@ def get_common_key(common_key_index) -> bytes:
---------- ----------
common_key_index : int common_key_index : int
The index of the common key to be returned. The index of the common key to be returned.
dev : bool
If the dev keys should be used in place of the retail keys. Only affects key 0.
Returns Returns
------- -------
@@ -27,7 +32,10 @@ def get_common_key(common_key_index) -> bytes:
""" """
match common_key_index: match common_key_index:
case 0: case 0:
common_key_bin = binascii.unhexlify(common_key) if dev:
common_key_bin = binascii.unhexlify(development_key)
else:
common_key_bin = binascii.unhexlify(common_key)
case 1: case 1:
common_key_bin = binascii.unhexlify(korean_key) common_key_bin = binascii.unhexlify(korean_key)
case 2: case 2:

View File

@@ -8,11 +8,19 @@ import io
import hashlib import hashlib
from typing import List from typing import List
from dataclasses import dataclass as _dataclass from dataclasses import dataclass as _dataclass
from enum import IntEnum as _IntEnum
from ..types import _ContentRecord from ..types import _ContentRecord
from ..shared import _pad_bytes, _align_value from ..shared import _pad_bytes, _align_value
from .crypto import decrypt_content, encrypt_content from .crypto import decrypt_content, encrypt_content
class ContentType(_IntEnum):
NORMAL = 1
HASH_TREE = 3
DLC = 16385
SHARED = 32769
class ContentRegion: class ContentRegion:
""" """
A ContentRegion object to parse the continuous content region of a WAD. Allows for retrieving content from the A ContentRegion object to parse the continuous content region of a WAD. Allows for retrieving content from the
@@ -109,10 +117,6 @@ class ContentRegion:
""" """
Gets an individual content from the content region based on the provided index, in encrypted form. Gets an individual content from the content region based on the provided index, in encrypted form.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
Parameters Parameters
---------- ----------
index : int index : int
@@ -123,17 +127,10 @@ class ContentRegion:
bytes bytes
The encrypted content listed in the content record. The encrypted content listed in the content record.
""" """
# Get a list of the current content indices, so we can make sure the target one exists. Doing it this way if index >= self.num_contents:
# ensures we can find the target, even if the highest content index is greater than the highest literal index. raise ValueError(f"You are trying to get the content at index {index}, but no content with that "
current_indices = [] f"index exists!")
for record in self.content_records: content_enc = self.content_list[index]
current_indices.append(record.index)
if index not in current_indices:
raise ValueError("You are trying to get the content at index " + str(index) + ", but no content with that "
"index exists!")
# This is the literal index in the list of content that we're going to get.
target_index = current_indices.index(index)
content_enc = self.content_list[target_index]
return content_enc return content_enc
def get_enc_content_by_cid(self, cid: int) -> bytes: def get_enc_content_by_cid(self, cid: int) -> bytes:
@@ -150,16 +147,11 @@ class ContentRegion:
bytes bytes
The encrypted content listed in the content record. The encrypted content listed in the content record.
""" """
# Get a list of the current Content IDs, so we can make sure the target one exists. try:
content_ids = [] content_index = self.get_index_from_cid(cid)
for record in self.content_records: except ValueError:
content_ids.append(record.content_id) raise ValueError(f"You are trying to get a content with Content ID {cid}, "
if cid not in content_ids: f"but no content with that ID exists!")
raise ValueError("You are trying to get a content with Content ID " + str(cid) + ", but no content with "
"that ID exists!")
# Get the content index associated with the CID we now know exists.
target_index = content_ids.index(cid)
content_index = self.content_records[target_index].index
content_enc = self.get_enc_content_by_index(content_index) content_enc = self.get_enc_content_by_index(content_index)
return content_enc return content_enc
@@ -178,14 +170,10 @@ class ContentRegion:
""" """
Gets an individual content from the content region based on the provided index, in decrypted form. Gets an individual content from the content region based on the provided index, in decrypted form.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
Parameters Parameters
---------- ----------
index : int index : int
The content index of the content you want to get. The index of the content you want to get.
title_key : bytes title_key : bytes
The Title Key for the title the content is from. The Title Key for the title the content is from.
skip_hash : bool, optional skip_hash : bool, optional
@@ -196,19 +184,14 @@ class ContentRegion:
bytes bytes
The decrypted content listed in the content record. The decrypted content listed in the content record.
""" """
# Get a list of the current content indices, so we can make sure the target one exists. Doing it this way # Get the content index in the Content Record to ensure decryption works properly.
# ensures we can find the target, even if the highest content index is greater than the highest literal index. cnt_index = self.content_records[index].index
current_indices = []
for record in self.content_records:
current_indices.append(record.index)
# This is the literal index in the list of content that we're going to get.
target_index = current_indices.index(index)
content_enc = self.get_enc_content_by_index(index) content_enc = self.get_enc_content_by_index(index)
content_dec = decrypt_content(content_enc, title_key, index, self.content_records[target_index].content_size) content_dec = decrypt_content(content_enc, title_key, cnt_index, self.content_records[index].content_size)
# Hash the decrypted content and ensure that the hash matches the one in its Content Record. # 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. # 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).hexdigest() content_dec_hash = hashlib.sha1(content_dec).hexdigest()
content_record_hash = str(self.content_records[target_index].content_hash.decode()) content_record_hash = str(self.content_records[index].content_hash.decode())
# Compare the hash and throw a ValueError if the hash doesn't match. # Compare the hash and throw a ValueError if the hash doesn't match.
if content_dec_hash != content_record_hash: if content_dec_hash != content_record_hash:
if skip_hash: if skip_hash:
@@ -238,16 +221,11 @@ class ContentRegion:
bytes bytes
The decrypted content listed in the content record. The decrypted content listed in the content record.
""" """
# Get a list of the current Content IDs, so we can make sure the target one exists. try:
content_ids = [] content_index = self.get_index_from_cid(cid)
for record in self.content_records: except ValueError:
content_ids.append(record.content_id) raise ValueError(f"You are trying to get a content with Content ID {cid}, "
if cid not in content_ids: f"but no content with that ID exists!")
raise ValueError("You are trying to get a content with Content ID " + str(cid) + ", but no content with "
"that ID exists!")
# Get the content index associated with the CID we now know exists.
target_index = content_ids.index(cid)
content_index = self.content_records[target_index].index
content_dec = self.get_content_by_index(content_index, title_key, skip_hash) content_dec = self.get_content_by_index(content_index, title_key, skip_hash)
return content_dec return content_dec
@@ -273,6 +251,29 @@ class ContentRegion:
dec_contents.append(self.get_content_by_index(content, title_key, skip_hash)) dec_contents.append(self.get_content_by_index(content, title_key, skip_hash))
return dec_contents return dec_contents
def get_index_from_cid(self, cid: int) -> int:
"""
Gets the index of a content by its Content ID.
Parameters
----------
cid : int
The Content ID to get the index of.
Returns
-------
int
The content index.
"""
# Get a list of the current Content IDs, so we can make sure the target one exists.
content_ids = []
for record in self.content_records:
content_ids.append(record.content_id)
if cid not in content_ids:
raise ValueError("The specified Content ID does not exist!")
index = content_ids.index(cid)
return index
def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int, def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
content_hash: bytes) -> None: content_hash: bytes) -> None:
""" """
@@ -286,7 +287,7 @@ class ContentRegion:
cid : int cid : int
The Content ID to assign the new content in the content record. The Content ID to assign the new content in the content record.
index : int index : int
The index to place the new content at. The index used when encrypting the new content.
content_type : int content_type : int
The type of the new content. The type of the new content.
content_size : int content_size : int
@@ -303,11 +304,13 @@ class ContentRegion:
# If we're good, then append all the data and create a new ContentRecord(). # If we're good, then append all the data and create a new ContentRecord().
self.content_list.append(enc_content) self.content_list.append(enc_content)
self.content_records.append(_ContentRecord(cid, index, content_type, content_size, content_hash)) self.content_records.append(_ContentRecord(cid, index, content_type, content_size, content_hash))
self.num_contents += 1
def add_content(self, dec_content: bytes, cid: int, index: int, content_type: int, title_key: bytes) -> None: def add_content(self, dec_content: bytes, cid: int, content_type: int, title_key: bytes) -> None:
""" """
Adds a new decrypted content to the ContentRegion, and adds the provided Content ID, index, content type, Adds a new decrypted content to the end of the ContentRegion, and adds the provided Content ID, content type,
content size, and content hash to a new record in the ContentRecord list. content size, and content hash to a new record in the ContentRecord list. The index will be automatically
assigned by incrementing the current highest index in the records.
This first gets the content hash and size from the provided data, and then encrypts the content with the This first gets the content hash and size from the provided data, and then encrypts the content with the
provided Title Key before adding it to the ContentRegion. provided Title Key before adding it to the ContentRegion.
@@ -318,13 +321,16 @@ class ContentRegion:
The new decrypted content to add. The new decrypted content to add.
cid : int cid : int
The Content ID to assign the new content in the content record. The Content ID to assign the new content in the content record.
index : int
The index to place the new content at.
content_type : int content_type : int
The type of the new content. The type of the new content.
title_key : bytes title_key : bytes
The Title Key that matches the other content in the ContentRegion. The Title Key that matches the other content in the ContentRegion.
""" """
# Find the current highest content index and increment it for this content.
content_indices = []
for record in self.content_records:
content_indices.append(record.index)
index = max(content_indices) + 1
content_size = len(dec_content) content_size = len(dec_content)
content_hash = str.encode(hashlib.sha1(dec_content).hexdigest()) content_hash = str.encode(hashlib.sha1(dec_content).hexdigest())
enc_content = encrypt_content(dec_content, title_key, index) enc_content = encrypt_content(dec_content, title_key, index)
@@ -335,18 +341,14 @@ class ContentRegion:
""" """
Sets the content at the provided content index to the provided new encrypted content. The provided hash and Sets the content at the provided content index to the provided new encrypted content. The provided hash and
content size are set in the corresponding content record. A new Content ID or content type can also be content size are set in the corresponding content record. A new Content ID or content type can also be
specified, but if it isn't than the current values are preserved. specified, but if it isn't then the current values are preserved.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
Parameters Parameters
---------- ----------
enc_content : bytes enc_content : bytes
The new encrypted content to set. The new encrypted content to set.
index : int index : int
The target content index to set the new content at. The target index to set the new content at.
content_size : int content_size : int
The size of the new encrypted content when decrypted. The size of the new encrypted content when decrypted.
content_hash : bytes content_hash : bytes
@@ -356,34 +358,27 @@ class ContentRegion:
content_type : int, optional content_type : int, optional
The type of the new content. Current value will be preserved if not set. The type of the new content. Current value will be preserved if not set.
""" """
# Get a list of the current content indices, so we can make sure the target one exists. Doing it this way if index >= self.num_contents:
# ensures we can find the target, even if the highest content index is greater than the highest literal index. raise ValueError(f"You are trying to set the content at index {index}, but no content with that "
current_indices = [] f"index currently exists!")
for record in self.content_records:
current_indices.append(record.index)
if index not in current_indices:
raise ValueError("You are trying to set the content at index " + str(index) + ", but no content with that "
"index currently exists!")
# This is the literal index in the list of content/content records that we're going to change.
target_index = current_indices.index(index)
# Reassign the values, but only set the optional ones if they were passed. # Reassign the values, but only set the optional ones if they were passed.
self.content_records[target_index].content_size = content_size self.content_records[index].content_size = content_size
self.content_records[target_index].content_hash = content_hash self.content_records[index].content_hash = content_hash
if cid is not None: if cid is not None:
self.content_records[target_index].content_id = cid self.content_records[index].content_id = cid
if content_type is not None: if content_type is not None:
self.content_records[target_index].content_type = content_type self.content_records[index].content_type = content_type
# Add blank entries to the list to ensure that its length matches the length of the content record list. # Add blank entries to the list to ensure that its length matches the length of the content record list.
while len(self.content_list) < len(self.content_records): while len(self.content_list) < len(self.content_records):
self.content_list.append(b'') self.content_list.append(b'')
self.content_list[target_index] = enc_content self.content_list[index] = enc_content
def set_content(self, dec_content: bytes, index: int, title_key: bytes, cid: int = None, def set_content(self, dec_content: bytes, index: int, title_key: bytes, cid: int = None,
content_type: int = None) -> None: content_type: int = None) -> None:
""" """
Sets the content at the provided content index to the provided new decrypted content. The hash and content size Sets the content at the provided content index to the provided new decrypted content. The hash and content size
of this content will be generated and then set in the corresponding content record. A new Content ID or content of this content will be generated and then set in the corresponding content record. A new Content ID or content
type can also be specified, but if it isn't than the current values are preserved. type can also be specified, but if it isn't then the current values are preserved.
The provided Title Key is used to encrypt the content so that it can be set in the ContentRegion. The provided Title Key is used to encrypt the content so that it can be set in the ContentRegion.
@@ -404,8 +399,9 @@ class ContentRegion:
content_size = len(dec_content) content_size = len(dec_content)
# Calculate the hash of the new content. # Calculate the hash of the new content.
content_hash = str.encode(hashlib.sha1(dec_content).hexdigest()) content_hash = str.encode(hashlib.sha1(dec_content).hexdigest())
# Encrypt the content using the provided Title Key and index. # Encrypt the content using the provided Title Key and the index from the Content Record, to ensure that
enc_content = encrypt_content(dec_content, title_key, index) # encryption will succeed even if the provided index doesn't match the content's index.
enc_content = encrypt_content(dec_content, title_key, self.content_records[index].index)
# Pass values to set_enc_content() # Pass values to set_enc_content()
self.set_enc_content(enc_content, index, content_size, content_hash, cid, content_type) self.set_enc_content(enc_content, index, content_size, content_hash, cid, content_type)
@@ -415,10 +411,6 @@ class ContentRegion:
it matches the record at that index. Not recommended for most use cases, use decrypted content and it matches the record at that index. Not recommended for most use cases, use decrypted content and
load_content() instead. load_content() instead.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
Parameters Parameters
---------- ----------
enc_content : bytes enc_content : bytes
@@ -426,20 +418,13 @@ class ContentRegion:
index : int index : int
The content index to load the content at. The content index to load the content at.
""" """
# Get a list of the current content indices, so we can make sure the target one exists. Doing it this way if index >= self.num_contents:
# ensures we can find the target, even if the highest content index is greater than the highest literal index. raise ValueError(f"You are trying to load the content at index {index}, but no content with that "
current_indices = [] f"index currently exists! Make sure the correct content records have been loaded.")
for record in self.content_records:
current_indices.append(record.index)
if index not in current_indices:
raise ValueError("You are trying to load the content at index " + str(index) + ", but no content with that "
"index currently exists! Make sure the correct content records have been loaded.")
# Add blank entries to the list to ensure that its length matches the length of the content record list. # Add blank entries to the list to ensure that its length matches the length of the content record list.
while len(self.content_list) < len(self.content_records): while len(self.content_list) < len(self.content_records):
self.content_list.append(b'') self.content_list.append(b'')
# This is the literal index in the list of content/content records that we're going to change. self.content_list[index] = enc_content
target_index = current_indices.index(index)
self.content_list[target_index] = enc_content
def load_content(self, dec_content: bytes, index: int, title_key: bytes) -> None: def load_content(self, dec_content: bytes, index: int, title_key: bytes) -> None:
""" """
@@ -447,32 +432,21 @@ class ContentRegion:
sure that it matches the corresponding record. This content will then be encrypted using the provided Title Key sure that it matches the corresponding record. This content will then be encrypted using the provided Title Key
before being loaded. before being loaded.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
Parameters Parameters
---------- ----------
dec_content : bytes dec_content : bytes
The decrypted content to load. The decrypted content to load.
index : int index : int
The content index to load the content at. The index to load the content at.
title_key: bytes title_key: bytes
The Title Key that matches the decrypted content. The Title Key that matches the decrypted content.
""" """
# Get a list of the current content indices, so we can make sure the target one exists. Doing it this way if index >= self.num_contents:
# ensures we can find the target, even if the highest content index is greater than the highest literal index. raise ValueError(f"You are trying to load the content at index {index}, but no content with that "
current_indices = [] f"index currently exists! Make sure the correct content records have been loaded.")
for record in self.content_records:
current_indices.append(record.index)
if index not in current_indices:
raise ValueError("You are trying to load the content at index " + str(index) + ", but no content with that "
"index currently exists! Make sure the correct content records have been loaded.")
# This is the literal index in the list of content/content records that we're going to change.
target_index = current_indices.index(index)
# Check the hash of the content against the hash stored in the record to ensure it matches. # Check the hash of the content against the hash stored in the record to ensure it matches.
content_hash = hashlib.sha1(dec_content).hexdigest() content_hash = hashlib.sha1(dec_content).hexdigest()
if content_hash != self.content_records[target_index].content_hash.decode(): if content_hash != self.content_records[index].content_hash.decode():
raise ValueError("The decrypted content provided does not match the record at the provided index. \n" raise ValueError("The decrypted content provided does not match the record at the provided index. \n"
"Expected hash is: {}\n".format(self.content_records[index].content_hash.decode()) + "Expected hash is: {}\n".format(self.content_records[index].content_hash.decode()) +
"Actual hash is: {}".format(content_hash)) "Actual hash is: {}".format(content_hash))
@@ -480,11 +454,47 @@ class ContentRegion:
while len(self.content_list) < len(self.content_records): while len(self.content_list) < len(self.content_records):
self.content_list.append(b'') self.content_list.append(b'')
# If the hash matches, encrypt the content and set it where it belongs. # If the hash matches, encrypt the content and set it where it belongs.
# This uses the index from the content records instead of just the index given, because there are some strange # This uses the index from the content records instead of just the index given, because there are some poorly
# circumstances where the actual index in the array and the assigned content index don't match up, and this # made custom WADs out there that don't have the contents in order, for whatever reason.
# needs to accommodate that. Seems to only apply to custom WADs ? (Like cIOS WADs?) enc_content = encrypt_content(dec_content, title_key, self.content_records[index].index)
enc_content = encrypt_content(dec_content, title_key, index) self.content_list[index] = enc_content
self.content_list[target_index] = enc_content
def remove_content_by_index(self, index: int) -> None:
"""
Removes the content at the specified index from the ContentRegion and content records.
This will allow gaps to be left in content indices, however this should not cause any issues.
Parameters
----------
index : int
The index of the content you want to remove.
"""
if index >= self.num_contents:
raise ValueError(f"You are trying to remove the content at index {index}, but no content with "
f"that index currently exists!")
# Delete the target index from both the content list and content records.
self.content_list.pop(index)
self.content_records.pop(index)
self.num_contents -= 1
def remove_content_by_cid(self, cid: int) -> None:
"""
Removes the content with the specified Content ID from the ContentRegion and content records.
This will allow gaps to be left in content indices, however this should not cause any issues.
Parameters
----------
cid : int
The Content ID of the content you want to remove.
"""
try:
index = self.get_index_from_cid(cid)
except ValueError:
raise ValueError(f"You are trying to remove content with Content ID {cid}, "
f"but no content with that ID exists!")
self.remove_content_by_index(index)
@_dataclass @_dataclass

View File

@@ -30,7 +30,7 @@ def _convert_tid_to_iv(title_id: str | bytes) -> bytes:
return title_key_iv return title_key_iv
def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: bytes | str) -> bytes: def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: bytes | str, dev=False) -> bytes:
""" """
Gets the decrypted version of the encrypted Title Key provided. Gets the decrypted version of the encrypted Title Key provided.
@@ -44,6 +44,8 @@ def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: byt
The index of the common key used to encrypt the Title Key. The index of the common key used to encrypt the Title Key.
title_id : bytes, str title_id : bytes, str
The Title ID of the title that the key is for. The Title ID of the title that the key is for.
dev : bool
Whether the Title Key is encrypted with the development key or not.
Returns Returns
------- -------
@@ -51,7 +53,7 @@ def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: byt
The decrypted Title Key. The decrypted Title Key.
""" """
# Load the correct common key for the title. # Load the correct common key for the title.
common_key = get_common_key(common_key_index) common_key = get_common_key(common_key_index, dev)
# Convert the IV into the correct format based on the type provided. # Convert the IV into the correct format based on the type provided.
title_key_iv = _convert_tid_to_iv(title_id) title_key_iv = _convert_tid_to_iv(title_id)
# The IV will always be in the same format by this point, so add the last 8 bytes. # The IV will always be in the same format by this point, so add the last 8 bytes.
@@ -63,7 +65,7 @@ def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: byt
return title_key return title_key
def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: bytes | str) -> bytes: def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: bytes | str, dev=False) -> bytes:
""" """
Encrypts the provided Title Key with the selected common key. Encrypts the provided Title Key with the selected common key.
@@ -77,6 +79,8 @@ def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: byt
The index of the common key used to encrypt the Title Key. The index of the common key used to encrypt the Title Key.
title_id : bytes, str title_id : bytes, str
The Title ID of the title that the key is for. The Title ID of the title that the key is for.
dev : bool
Whether the Title Key is encrypted with the development key or not.
Returns Returns
------- -------
@@ -84,7 +88,7 @@ def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: byt
An encrypted Title Key. An encrypted Title Key.
""" """
# Load the correct common key for the title. # Load the correct common key for the title.
common_key = get_common_key(common_key_index) common_key = get_common_key(common_key_index, dev)
# Convert the IV into the correct format based on the type provided. # Convert the IV into the correct format based on the type provided.
title_key_iv = _convert_tid_to_iv(title_id) title_key_iv = _convert_tid_to_iv(title_id)
# The IV will always be in the same format by this point, so add the last 8 bytes. # The IV will always be in the same format by this point, so add the last 8 bytes.

View File

@@ -40,6 +40,9 @@ class Ticket:
Attributes Attributes
---------- ----------
is_dev : bool
Whether this Ticket is signed for development or not, and whether the Title Key is encrypted for development
or not.
signature : bytes signature : bytes
The signature applied to the ticket. The signature applied to the ticket.
ticket_version : int ticket_version : int
@@ -56,6 +59,8 @@ class Ticket:
The index of the common key required to decrypt this ticket's Title Key. The index of the common key required to decrypt this ticket's Title Key.
""" """
def __init__(self): def __init__(self):
# If this is a dev ticket
self.is_dev: bool = False # Defaults to false, set to true during load if this ticket is using dev certs.
# Signature blob header # Signature blob header
self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048 self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048
self.signature: bytes = b'' # Actual signature data self.signature: bytes = b'' # Actual signature data
@@ -155,6 +160,11 @@ class Ticket:
limit_type = int.from_bytes(ticket_data.read(4)) limit_type = int.from_bytes(ticket_data.read(4))
limit_value = 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)) self.title_limits_list.append(_TitleLimit(limit_type, limit_value))
# Check certs to see if this is a retail or dev ticket. Treats unknown certs as being retail for now.
if self.signature_issuer.find("Root-CA00000002-XS00000006") != -1:
self.is_dev = True
else:
self.is_dev = False
def dump(self) -> bytes: def dump(self) -> bytes:
""" """
@@ -315,7 +325,7 @@ class Ticket:
bytes bytes
The decrypted title key. The decrypted title key.
""" """
title_key = decrypt_title_key(self.title_key_enc, self.common_key_index, self.title_id) title_key = decrypt_title_key(self.title_key_enc, self.common_key_index, self.title_id, self.is_dev)
return title_key return title_key
def set_title_id(self, title_id) -> None: def set_title_id(self, title_id) -> None:

View File

@@ -76,7 +76,9 @@ class Title:
if self.tmd.title_id == "0000000100000001": if self.tmd.title_id == "0000000100000001":
self.wad.wad_type = "ib" self.wad.wad_type = "ib"
# Dump the TMD and set it in the WAD. # Dump the TMD and set it in the WAD.
self.tmd.content_records = self.content.content_records # This requires updating the content records and number of contents in the TMD first.
self.tmd.content_records = self.content.content_records # This may not be needed because it's a ref already
self.tmd.num_contents = len(self.content.content_records)
self.wad.set_tmd_data(self.tmd.dump()) self.wad.set_tmd_data(self.tmd.dump())
# Dump the Ticket and set it in the WAD. # Dump the Ticket and set it in the WAD.
self.wad.set_ticket_data(self.ticket.dump()) self.wad.set_ticket_data(self.ticket.dump())
@@ -117,8 +119,9 @@ class Title:
""" """
if not self.tmd.content_records: if not self.tmd.content_records:
ValueError("No TMD appears to have been loaded, so content records cannot be read from it.") ValueError("No TMD appears to have been loaded, so content records cannot be read from it.")
# Load the content records into the ContentRegion object. # Load the content records into the ContentRegion object, and update the number of contents.
self.content.content_records = self.tmd.content_records self.content.content_records = self.tmd.content_records
self.content.num_contents = self.tmd.num_contents
def set_title_id(self, title_id: str) -> None: def set_title_id(self, title_id: str) -> None:
""" """
@@ -135,7 +138,8 @@ class Title:
self.tmd.set_title_id(title_id) self.tmd.set_title_id(title_id)
title_key_decrypted = self.ticket.get_title_key() title_key_decrypted = self.ticket.get_title_key()
self.ticket.set_title_id(title_id) self.ticket.set_title_id(title_id)
title_key_encrypted = encrypt_title_key(title_key_decrypted, self.ticket.common_key_index, title_id) title_key_encrypted = encrypt_title_key(title_key_decrypted, self.ticket.common_key_index, title_id,
self.ticket.is_dev)
self.ticket.title_key_enc = title_key_encrypted self.ticket.title_key_enc = title_key_encrypted
def set_title_version(self, title_version: str | int) -> None: def set_title_version(self, title_version: str | int) -> None:
@@ -190,9 +194,15 @@ class Title:
dec_content = self.content.get_content_by_cid(cid, self.ticket.get_title_key(), skip_hash) dec_content = self.content.get_content_by_cid(cid, self.ticket.get_title_key(), skip_hash)
return dec_content return dec_content
def get_title_size(self) -> int: def get_title_size(self, absolute=False) -> int:
""" """
Gets the installed size of the title, including the TMD and Ticket, in bytes. Gets the installed size of the title, including the TMD and Ticket, in bytes. The "absolute" option determines
whether shared content sizes should be included in the total size or not. This option defaults to False.
Parameters
----------
absolute : bool, optional
Whether shared contents should be included in the total size or not. Defaults to False.
Returns Returns
------- -------
@@ -207,34 +217,90 @@ class Title:
# For contents, get their sizes from the content records, because they store the intended sizes of the decrypted # For contents, get their sizes from the content records, because they store the intended sizes of the decrypted
# contents, which are usually different from the encrypted sizes. # contents, which are usually different from the encrypted sizes.
for record in self.content.content_records: for record in self.content.content_records:
title_size += record.content_size if record.content_type == 32769:
if absolute:
title_size += record.content_size
else:
title_size += record.content_size
return title_size return title_size
def get_title_size_blocks(self) -> int: def get_title_size_blocks(self, absolute=False) -> int:
""" """
Gets the installed size of the title, including the TMD and Ticket, in the Wii's displayed "blocks" format. Gets the installed size of the title, including the TMD and Ticket, in the Wii's displayed "blocks" format. The
"absolute" option determines whether shared content sizes should be included in the total size or not. This
option defaults to False.
1 Wii block is equal to 128KiB, and if any amount of a block is used, the entire block is considered used. 1 Wii block is equal to 128KiB, and if any amount of a block is used, the entire block is considered used.
Parameters
----------
absolute : bool, optional
Whether shared contents should be included in the total size or not. Defaults to False.
Returns Returns
------- -------
int int
The installed size of the title, in blocks. The installed size of the title, in blocks.
""" """
title_size_bytes = self.get_title_size() title_size_bytes = self.get_title_size(absolute)
blocks = math.ceil(title_size_bytes / 131072) blocks = math.ceil(title_size_bytes / 131072)
return blocks return blocks
def add_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
content_hash: bytes) -> None:
"""
Adds a new encrypted content to the ContentRegion, and adds the provided Content ID, index, content type,
content size, and content hash to a new record in the ContentRecord list.
Parameters
----------
enc_content : bytes
The new encrypted content to add.
cid : int
The Content ID to assign the new content in the content record.
index : int
The index used when encrypting the new content.
content_type : int
The type of the new content.
content_size : int
The size of the new encrypted content when decrypted.
content_hash : bytes
The hash of the new encrypted content when decrypted.
"""
# Add the encrypted content.
self.content.add_enc_content(enc_content, cid, index, content_type, content_size, content_hash)
# Update the TMD to match.
self.tmd.content_records = self.content.content_records
def add_content(self, dec_content: bytes, cid: int, content_type: int) -> None:
"""
Adds a new decrypted content to the end of the ContentRegion, and adds the provided Content ID, content type,
content size, and content hash to a new record in the ContentRecord list. The index will be automatically
assigned by incrementing the current highest index in the records.
This first gets the content hash and size from the provided data, and then encrypts the content with the
Title Key before adding it to the ContentRegion.
Parameters
----------
dec_content : bytes
The new decrypted content to add.
cid : int
The Content ID to assign the new content in the content record.
content_type : int
The type of the new content.
"""
# Add the decrypted content.
self.content.add_content(dec_content, cid, content_type, self.ticket.get_title_key())
# Update the TMD to match.
self.tmd.content_records = self.content.content_records
def set_enc_content(self, enc_content: bytes, index: int, content_size: int, content_hash: bytes, cid: int = None, def set_enc_content(self, enc_content: bytes, index: int, content_size: int, content_hash: bytes, cid: int = None,
content_type: int = None) -> None: content_type: int = None) -> None:
""" """
Sets the content at the provided content index to the provided new encrypted content. The provided hash and Sets the content at the provided index to the provided new encrypted content. The provided hash and content size
content size are set in the corresponding content record. A new Content ID or content type can also be are set in the corresponding content record. A new Content ID or content type can also be specified, but if it
specified, but if it isn't than the current values are preserved. isn't then the current values are preserved.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
This also updates the content records in the TMD after the content is set. This also updates the content records in the TMD after the content is set.
@@ -260,9 +326,9 @@ class Title:
def set_content(self, dec_content: bytes, index: int, cid: int = None, content_type: int = None) -> None: def set_content(self, dec_content: bytes, index: int, cid: int = None, content_type: int = None) -> None:
""" """
Sets the content at the provided content index to the provided new decrypted content. The hash and content size Sets the content at the provided index to the provided new decrypted content. The hash and content size of this
of this content will be generated and then set in the corresponding content record. A new Content ID or content content will be generated and then set in the corresponding content record. A new Content ID or content type can
type can also be specified, but if it isn't than the current values are preserved. also be specified, but if it isn't then the current values are preserved.
This also updates the content records in the TMD after the content is set. This also updates the content records in the TMD after the content is set.
@@ -288,16 +354,12 @@ class Title:
sure that it matches the corresponding record. This content will then be encrypted using the title's Title Key sure that it matches the corresponding record. This content will then be encrypted using the title's Title Key
before being loaded. before being loaded.
This uses the content index, which is the value tied to each content and used as the IV for encryption, rather
than the literal index in the array of content, because sometimes the contents end up out of order in a WAD
while still retaining the original indices.
Parameters Parameters
---------- ----------
dec_content : bytes dec_content : bytes
The decrypted content to load. The decrypted content to load.
index : int index : int
The content index to load the content at. The index to load the content at.
""" """
# Load the decrypted content. # Load the decrypted content.
self.content.load_content(dec_content, index, self.ticket.get_title_key()) self.content.load_content(dec_content, index, self.ticket.get_title_key())
@@ -314,6 +376,7 @@ class Title:
after any changes to the TMD or Ticket, and before dumping the Title object into a WAD to ensure that the WAD after any changes to the TMD or Ticket, and before dumping the Title object into a WAD to ensure that the WAD
is properly fakesigned. is properly fakesigned.
""" """
self.tmd.num_contents = self.content.num_contents # This needs to be updated in case it was changed
self.tmd.fakesign() self.tmd.fakesign()
self.ticket.fakesign() self.ticket.fakesign()

View File

@@ -8,7 +8,7 @@ import binascii
import hashlib import hashlib
import struct import struct
from typing import List from typing import List
from enum import IntEnum from enum import IntEnum as _IntEnum
from ..types import _ContentRecord from ..types import _ContentRecord
from ..shared import _bitmask from ..shared import _bitmask
from .util import title_ver_dec_to_standard, title_ver_standard_to_dec from .util import title_ver_dec_to_standard, title_ver_standard_to_dec
@@ -37,7 +37,7 @@ class TMD:
self.blob_header: bytes = b'' self.blob_header: bytes = b''
self.signature_type: int = 0 self.signature_type: int = 0
self.signature: bytes = b'' self.signature: bytes = b''
self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x" self.signature_issuer: str = "" # Follows the format "Root-CA%08x-CP%08x"
self.tmd_version: int = 0 # This seems to always be 0 no matter what? self.tmd_version: int = 0 # This seems to always be 0 no matter what?
self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version
self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version
@@ -45,7 +45,7 @@ class TMD:
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on. self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
self.ios_version: int = 0 # The IOS version the associated title runs on. self.ios_version: int = 0 # The IOS version the associated title runs on.
self.title_id: str = "" # The Title ID of the associated title. self.title_id: str = "" # The Title ID of the associated title.
self.title_type: str = "" # The type of the associated title. self.title_type: bytes = b'' # The type of the associated title. Should always be 00000001 in a Wii TMD.
self.group_id: int = 0 # The ID of the publisher of the associated title. self.group_id: int = 0 # The ID of the publisher of the associated title.
self.region: int = 0 # The ID of the region of the associated title. self.region: int = 0 # The ID of the region of the associated title.
self.ratings: bytes = b'' # The parental controls rating of the associated title. self.ratings: bytes = b'' # The parental controls rating of the associated title.
@@ -82,7 +82,7 @@ class TMD:
self.signature = tmd_data.read(256) self.signature = tmd_data.read(256)
# Signing certificate issuer. # Signing certificate issuer.
tmd_data.seek(0x140) tmd_data.seek(0x140)
self.issuer = tmd_data.read(64) self.signature_issuer = str(tmd_data.read(64).decode())
# 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.
tmd_data.seek(0x180) tmd_data.seek(0x180)
self.tmd_version = int.from_bytes(tmd_data.read(1)) self.tmd_version = int.from_bytes(tmd_data.read(1))
@@ -107,11 +107,10 @@ class TMD:
title_id_bin = tmd_data.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 the title. This is an internal property used to show if this title is for the ill-fated
# NetCard (0), or the Wii (1), and is therefore always 1 for Wii TMDs.
tmd_data.seek(0x194) tmd_data.seek(0x194)
content_type_bin = tmd_data.read(4) self.title_type = tmd_data.read(4)
content_type_hex = binascii.hexlify(content_type_bin)
self.title_type = str(content_type_hex.decode())
# Publisher of the title. # Publisher of the title.
tmd_data.seek(0x198) tmd_data.seek(0x198)
self.group_id = int.from_bytes(tmd_data.read(2)) self.group_id = int.from_bytes(tmd_data.read(2))
@@ -175,7 +174,7 @@ class TMD:
# Padding to 64 bytes. # Padding to 64 bytes.
tmd_data += b'\x00' * 60 tmd_data += b'\x00' * 60
# Signing certificate issuer. # Signing certificate issuer.
tmd_data += self.issuer tmd_data += str.encode(self.signature_issuer)
# TMD version. # TMD version.
tmd_data += int.to_bytes(self.tmd_version, 1) tmd_data += int.to_bytes(self.tmd_version, 1)
# Certificate Authority CRL version. # Certificate Authority CRL version.
@@ -188,8 +187,8 @@ class TMD:
tmd_data += binascii.unhexlify(self.ios_tid) tmd_data += binascii.unhexlify(self.ios_tid)
# Title's Title ID. # Title's Title ID.
tmd_data += binascii.unhexlify(self.title_id) tmd_data += binascii.unhexlify(self.title_id)
# Content type. # Title type.
tmd_data += binascii.unhexlify(self.title_type) tmd_data += self.title_type
# Group ID. # Group ID.
tmd_data += int.to_bytes(self.group_id, 2) tmd_data += int.to_bytes(self.group_id, 2)
# 2 bytes of zero for reasons. # 2 bytes of zero for reasons.
@@ -280,10 +279,11 @@ class TMD:
def get_title_region(self) -> str: def get_title_region(self) -> str:
""" """
Gets the region of the TMD's associated title. Gets the system region specified in the TMD. This is not necessarily the true region of the title, but is the
hardware region that this title is designed and allowed to be run on.
Can be one of several possible values: Can be one of several possible values:
'Japan', 'North America', 'Europe', 'World', or 'Korea'. 'JPN', 'USA', 'EUR', 'None', or 'KOR'.
Returns Returns
------- -------
@@ -292,19 +292,19 @@ class TMD:
""" """
match self.region: match self.region:
case 0: case 0:
return "Japan" return "JPN"
case 1: case 1:
return "North America" return "USA"
case 2: case 2:
return "Europe" return "EUR"
case 3: case 3:
return "World" return "None"
case 4: case 4:
return "Korea" return "KOR"
def get_title_type(self) -> str: def get_title_type(self) -> str:
""" """
Gets the type of the TMD's associated title. Gets the type of the title this TMD describes. The title_type field is not related to these types.
Can be one of several possible values: Can be one of several possible values:
'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel' 'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
@@ -314,7 +314,7 @@ class TMD:
str str
The type of the title. The type of the title.
""" """
match self.title_type: match self.title_id[:8]:
case '00000001': case '00000001':
return "System" return "System"
case '00010000': case '00010000':
@@ -390,7 +390,7 @@ class TMD:
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) + raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) +
"' contents but index was '" + str(record) + "'!") "' contents but index was '" + str(record) + "'!")
class AccessFlags(IntEnum): class AccessFlags(_IntEnum):
AHB = 0 AHB = 0
DVD_VIDEO = 1 DVD_VIDEO = 1