mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 08:35:28 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1b6e0db26d
|
|||
|
9ae059b797
|
|||
|
c604c195d2
|
|||
|
0c2e13f18a
|
|||
|
7fed039fdc
|
|||
|
0d306076a2
|
|||
|
a1773b9a02
|
|||
|
7c2f0fb21f
|
|||
|
0edd4fa6bb
|
|||
|
e163d34f0b
|
|||
|
9fb0fdbc17
|
@@ -6,6 +6,7 @@
|
||||
:maxdepth: 4
|
||||
|
||||
libWiiPy.archive
|
||||
libWiiPy.nand
|
||||
libWiiPy.title
|
||||
```
|
||||
|
||||
|
||||
27
docs/source/libWiiPy.nand.md
Normal file
27
docs/source/libWiiPy.nand.md
Normal 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:
|
||||
```
|
||||
@@ -26,14 +26,6 @@
|
||||
:show-inheritance:
|
||||
```
|
||||
|
||||
### libWiiPy.title.emunand module
|
||||
```{eval-rst}
|
||||
.. automodule:: libWiiPy.title.emunand
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
```
|
||||
|
||||
### libWiipy.title.iospatcher module
|
||||
```{eval-rst}
|
||||
.. automodule:: libWiiPy.title.iospatcher
|
||||
@@ -50,14 +42,6 @@
|
||||
:show-inheritance:
|
||||
```
|
||||
|
||||
### libWiiPy.title.sys module
|
||||
```{eval-rst}
|
||||
.. automodule:: libWiiPy.title.sys
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
```
|
||||
|
||||
### libWiiPy.title.ticket module
|
||||
```{eval-rst}
|
||||
.. automodule:: libWiiPy.title.ticket
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "libWiiPy"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
authors = [
|
||||
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
|
||||
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#
|
||||
# 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 nand
|
||||
from . import title
|
||||
|
||||
6
src/libWiiPy/nand/__init__.py
Normal file
6
src/libWiiPy/nand/__init__.py
Normal 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 *
|
||||
@@ -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
|
||||
#
|
||||
# Code for handling setting up and modifying a Wii EmuNAND.
|
||||
@@ -6,8 +6,8 @@
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
from .title import Title
|
||||
from .content import SharedContentMap as _SharedContentMap
|
||||
from ..title.title import Title
|
||||
from ..title.content import SharedContentMap as _SharedContentMap
|
||||
from .sys import UidSys as _UidSys
|
||||
|
||||
|
||||
134
src/libWiiPy/nand/setting.py
Normal file
134
src/libWiiPy/nand/setting.py
Normal 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
|
||||
@@ -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
|
||||
#
|
||||
# See https://wiibrew.org/wiki//sys/uid.sys for information about uid.sys.
|
||||
@@ -28,7 +28,7 @@ class _UidSysEntry:
|
||||
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
|
||||
the titles installed on the console.
|
||||
the titles that have been launched on a console.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
@@ -3,10 +3,8 @@
|
||||
|
||||
from .content import *
|
||||
from .crypto import *
|
||||
from .emunand import *
|
||||
from .iospatcher import *
|
||||
from .nus import *
|
||||
from .sys import *
|
||||
from .ticket import *
|
||||
from .title import *
|
||||
from .tmd import *
|
||||
|
||||
@@ -7,11 +7,14 @@ common_key = 'ebe42a225e8593e448d9c5457381aaf7'
|
||||
korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e'
|
||||
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
|
||||
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
|
||||
|
||||
@@ -19,6 +22,8 @@ def get_common_key(common_key_index) -> bytes:
|
||||
----------
|
||||
common_key_index : int
|
||||
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
|
||||
-------
|
||||
@@ -27,7 +32,10 @@ def get_common_key(common_key_index) -> bytes:
|
||||
"""
|
||||
match common_key_index:
|
||||
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:
|
||||
common_key_bin = binascii.unhexlify(korean_key)
|
||||
case 2:
|
||||
|
||||
@@ -8,11 +8,19 @@ import io
|
||||
import hashlib
|
||||
from typing import List
|
||||
from dataclasses import dataclass as _dataclass
|
||||
from enum import IntEnum as _IntEnum
|
||||
from ..types import _ContentRecord
|
||||
from ..shared import _pad_bytes, _align_value
|
||||
from .crypto import decrypt_content, encrypt_content
|
||||
|
||||
|
||||
class ContentType(_IntEnum):
|
||||
NORMAL = 1
|
||||
HASH_TREE = 3
|
||||
DLC = 16385
|
||||
SHARED = 32769
|
||||
|
||||
|
||||
class ContentRegion:
|
||||
"""
|
||||
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.
|
||||
|
||||
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
|
||||
----------
|
||||
index : int
|
||||
@@ -123,17 +127,10 @@ class ContentRegion:
|
||||
bytes
|
||||
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
|
||||
# ensures we can find the target, even if the highest content index is greater than the highest literal index.
|
||||
current_indices = []
|
||||
for record in self.content_records:
|
||||
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]
|
||||
if index >= self.num_contents:
|
||||
raise ValueError(f"You are trying to get the content at index {index}, but no content with that "
|
||||
f"index exists!")
|
||||
content_enc = self.content_list[index]
|
||||
return content_enc
|
||||
|
||||
def get_enc_content_by_cid(self, cid: int) -> bytes:
|
||||
@@ -150,16 +147,11 @@ class ContentRegion:
|
||||
bytes
|
||||
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.
|
||||
content_ids = []
|
||||
for record in self.content_records:
|
||||
content_ids.append(record.content_id)
|
||||
if cid not in content_ids:
|
||||
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
|
||||
try:
|
||||
content_index = self.get_index_from_cid(cid)
|
||||
except ValueError:
|
||||
raise ValueError(f"You are trying to get a content with Content ID {cid}, "
|
||||
f"but no content with that ID exists!")
|
||||
content_enc = self.get_enc_content_by_index(content_index)
|
||||
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.
|
||||
|
||||
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
|
||||
----------
|
||||
index : int
|
||||
The content index of the content you want to get.
|
||||
The index of the content you want to get.
|
||||
title_key : bytes
|
||||
The Title Key for the title the content is from.
|
||||
skip_hash : bool, optional
|
||||
@@ -196,19 +184,14 @@ class ContentRegion:
|
||||
bytes
|
||||
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
|
||||
# ensures we can find the target, even if the highest content index is greater than the highest literal 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)
|
||||
# Get the content index in the Content Record to ensure decryption works properly.
|
||||
cnt_index = self.content_records[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.
|
||||
# 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_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.
|
||||
if content_dec_hash != content_record_hash:
|
||||
if skip_hash:
|
||||
@@ -238,16 +221,11 @@ class ContentRegion:
|
||||
bytes
|
||||
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.
|
||||
content_ids = []
|
||||
for record in self.content_records:
|
||||
content_ids.append(record.content_id)
|
||||
if cid not in content_ids:
|
||||
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
|
||||
try:
|
||||
content_index = self.get_index_from_cid(cid)
|
||||
except ValueError:
|
||||
raise ValueError(f"You are trying to get a content with Content ID {cid}, "
|
||||
f"but no content with that ID exists!")
|
||||
content_dec = self.get_content_by_index(content_index, title_key, skip_hash)
|
||||
return content_dec
|
||||
|
||||
@@ -273,6 +251,29 @@ class ContentRegion:
|
||||
dec_contents.append(self.get_content_by_index(content, title_key, skip_hash))
|
||||
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,
|
||||
content_hash: bytes) -> None:
|
||||
"""
|
||||
@@ -286,7 +287,7 @@ class ContentRegion:
|
||||
cid : int
|
||||
The Content ID to assign the new content in the content record.
|
||||
index : int
|
||||
The index to place the new content at.
|
||||
The index used when encrypting the new content.
|
||||
content_type : int
|
||||
The type of the new content.
|
||||
content_size : int
|
||||
@@ -303,11 +304,13 @@ class ContentRegion:
|
||||
# If we're good, then append all the data and create a new ContentRecord().
|
||||
self.content_list.append(enc_content)
|
||||
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,
|
||||
content size, and content hash to a new record in the ContentRecord list.
|
||||
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
|
||||
provided Title Key before adding it to the ContentRegion.
|
||||
@@ -318,13 +321,16 @@ class ContentRegion:
|
||||
The new decrypted content to add.
|
||||
cid : int
|
||||
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
|
||||
The type of the new content.
|
||||
title_key : bytes
|
||||
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_hash = str.encode(hashlib.sha1(dec_content).hexdigest())
|
||||
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
|
||||
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.
|
||||
|
||||
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.
|
||||
specified, but if it isn't then the current values are preserved.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
enc_content : bytes
|
||||
The new encrypted content to set.
|
||||
index : int
|
||||
The target content index to set the new content at.
|
||||
The target index to set the new content at.
|
||||
content_size : int
|
||||
The size of the new encrypted content when decrypted.
|
||||
content_hash : bytes
|
||||
@@ -356,34 +358,27 @@ class ContentRegion:
|
||||
content_type : int, optional
|
||||
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
|
||||
# ensures we can find the target, even if the highest content index is greater than the highest literal index.
|
||||
current_indices = []
|
||||
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)
|
||||
if index >= self.num_contents:
|
||||
raise ValueError(f"You are trying to set the content at index {index}, but no content with that "
|
||||
f"index currently exists!")
|
||||
# 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[target_index].content_hash = content_hash
|
||||
self.content_records[index].content_size = content_size
|
||||
self.content_records[index].content_hash = content_hash
|
||||
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:
|
||||
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.
|
||||
while len(self.content_list) < len(self.content_records):
|
||||
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,
|
||||
content_type: int = None) -> None:
|
||||
"""
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -404,8 +399,9 @@ class ContentRegion:
|
||||
content_size = len(dec_content)
|
||||
# Calculate the hash of the new content.
|
||||
content_hash = str.encode(hashlib.sha1(dec_content).hexdigest())
|
||||
# Encrypt the content using the provided Title Key and index.
|
||||
enc_content = encrypt_content(dec_content, title_key, index)
|
||||
# Encrypt the content using the provided Title Key and the index from the Content Record, to ensure that
|
||||
# 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()
|
||||
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
|
||||
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
|
||||
----------
|
||||
enc_content : bytes
|
||||
@@ -426,20 +418,13 @@ class ContentRegion:
|
||||
index : int
|
||||
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
|
||||
# ensures we can find the target, even if the highest content index is greater than the highest literal index.
|
||||
current_indices = []
|
||||
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.")
|
||||
if index >= self.num_contents:
|
||||
raise ValueError(f"You are trying to load the content at index {index}, but no content with that "
|
||||
f"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.
|
||||
while len(self.content_list) < len(self.content_records):
|
||||
self.content_list.append(b'')
|
||||
# This is the literal index in the list of content/content records that we're going to change.
|
||||
target_index = current_indices.index(index)
|
||||
self.content_list[target_index] = enc_content
|
||||
self.content_list[index] = enc_content
|
||||
|
||||
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
|
||||
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
|
||||
----------
|
||||
dec_content : bytes
|
||||
The decrypted content to load.
|
||||
index : int
|
||||
The content index to load the content at.
|
||||
The index to load the content at.
|
||||
title_key: bytes
|
||||
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
|
||||
# ensures we can find the target, even if the highest content index is greater than the highest literal index.
|
||||
current_indices = []
|
||||
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)
|
||||
if index >= self.num_contents:
|
||||
raise ValueError(f"You are trying to load the content at index {index}, but no content with that "
|
||||
f"index currently exists! Make sure the correct content records have been loaded.")
|
||||
# Check the hash of the content against the hash stored in the record to ensure it matches.
|
||||
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"
|
||||
"Expected hash is: {}\n".format(self.content_records[index].content_hash.decode()) +
|
||||
"Actual hash is: {}".format(content_hash))
|
||||
@@ -480,11 +454,47 @@ class ContentRegion:
|
||||
while len(self.content_list) < len(self.content_records):
|
||||
self.content_list.append(b'')
|
||||
# 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
|
||||
# circumstances where the actual index in the array and the assigned content index don't match up, and this
|
||||
# needs to accommodate that. Seems to only apply to custom WADs ? (Like cIOS WADs?)
|
||||
enc_content = encrypt_content(dec_content, title_key, index)
|
||||
self.content_list[target_index] = enc_content
|
||||
# This uses the index from the content records instead of just the index given, because there are some poorly
|
||||
# made custom WADs out there that don't have the contents in order, for whatever reason.
|
||||
enc_content = encrypt_content(dec_content, title_key, self.content_records[index].index)
|
||||
self.content_list[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
|
||||
|
||||
@@ -30,7 +30,7 @@ def _convert_tid_to_iv(title_id: str | bytes) -> bytes:
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
title_id : bytes, str
|
||||
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
|
||||
-------
|
||||
@@ -51,7 +53,7 @@ def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: byt
|
||||
The decrypted Title Key.
|
||||
"""
|
||||
# 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.
|
||||
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.
|
||||
@@ -63,7 +65,7 @@ def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: byt
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
title_id : bytes, str
|
||||
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
|
||||
-------
|
||||
@@ -84,7 +88,7 @@ def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: byt
|
||||
An encrypted Title Key.
|
||||
"""
|
||||
# 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.
|
||||
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.
|
||||
|
||||
@@ -40,6 +40,9 @@ class Ticket:
|
||||
|
||||
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
|
||||
The signature applied to the ticket.
|
||||
ticket_version : int
|
||||
@@ -56,6 +59,8 @@ class Ticket:
|
||||
The index of the common key required to decrypt this ticket's Title Key.
|
||||
"""
|
||||
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
|
||||
self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048
|
||||
self.signature: bytes = b'' # Actual signature data
|
||||
@@ -155,6 +160,11 @@ class Ticket:
|
||||
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))
|
||||
# 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:
|
||||
"""
|
||||
@@ -315,7 +325,7 @@ class Ticket:
|
||||
bytes
|
||||
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
|
||||
|
||||
def set_title_id(self, title_id) -> None:
|
||||
|
||||
@@ -76,7 +76,9 @@ class Title:
|
||||
if self.tmd.title_id == "0000000100000001":
|
||||
self.wad.wad_type = "ib"
|
||||
# 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())
|
||||
# Dump the Ticket and set it in the WAD.
|
||||
self.wad.set_ticket_data(self.ticket.dump())
|
||||
@@ -117,8 +119,9 @@ class Title:
|
||||
"""
|
||||
if not self.tmd.content_records:
|
||||
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.num_contents = self.tmd.num_contents
|
||||
|
||||
def set_title_id(self, title_id: str) -> None:
|
||||
"""
|
||||
@@ -135,7 +138,8 @@ class Title:
|
||||
self.tmd.set_title_id(title_id)
|
||||
title_key_decrypted = self.ticket.get_title_key()
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
-------
|
||||
@@ -207,34 +217,90 @@ class Title:
|
||||
# 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.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
absolute : bool, optional
|
||||
Whether shared contents should be included in the total size or not. Defaults to False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
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)
|
||||
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,
|
||||
content_type: int = None) -> None:
|
||||
"""
|
||||
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
|
||||
specified, but if it isn't than 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.
|
||||
Sets the content at the provided 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 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.
|
||||
|
||||
@@ -260,9 +326,9 @@ class Title:
|
||||
|
||||
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
|
||||
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.
|
||||
Sets the content at the provided 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 type can
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
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
|
||||
----------
|
||||
dec_content : bytes
|
||||
The decrypted content to load.
|
||||
index : int
|
||||
The content index to load the content at.
|
||||
The index to load the content at.
|
||||
"""
|
||||
# Load the decrypted content.
|
||||
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
|
||||
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.ticket.fakesign()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import binascii
|
||||
import hashlib
|
||||
import struct
|
||||
from typing import List
|
||||
from enum import IntEnum
|
||||
from enum import IntEnum as _IntEnum
|
||||
from ..types import _ContentRecord
|
||||
from ..shared import _bitmask
|
||||
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.signature_type: int = 0
|
||||
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.ca_crl_version: int = 0 # Certificate Authority 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_version: int = 0 # The IOS version the associated title runs on.
|
||||
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.region: int = 0 # The ID of the region 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)
|
||||
# Signing certificate issuer.
|
||||
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_data.seek(0x180)
|
||||
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_hex = binascii.hexlify(title_id_bin)
|
||||
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)
|
||||
content_type_bin = tmd_data.read(4)
|
||||
content_type_hex = binascii.hexlify(content_type_bin)
|
||||
self.title_type = str(content_type_hex.decode())
|
||||
self.title_type = tmd_data.read(4)
|
||||
# Publisher of the title.
|
||||
tmd_data.seek(0x198)
|
||||
self.group_id = int.from_bytes(tmd_data.read(2))
|
||||
@@ -175,7 +174,7 @@ class TMD:
|
||||
# Padding to 64 bytes.
|
||||
tmd_data += b'\x00' * 60
|
||||
# Signing certificate issuer.
|
||||
tmd_data += self.issuer
|
||||
tmd_data += str.encode(self.signature_issuer)
|
||||
# TMD version.
|
||||
tmd_data += int.to_bytes(self.tmd_version, 1)
|
||||
# Certificate Authority CRL version.
|
||||
@@ -188,8 +187,8 @@ class TMD:
|
||||
tmd_data += binascii.unhexlify(self.ios_tid)
|
||||
# Title's Title ID.
|
||||
tmd_data += binascii.unhexlify(self.title_id)
|
||||
# Content type.
|
||||
tmd_data += binascii.unhexlify(self.title_type)
|
||||
# Title type.
|
||||
tmd_data += self.title_type
|
||||
# Group ID.
|
||||
tmd_data += int.to_bytes(self.group_id, 2)
|
||||
# 2 bytes of zero for reasons.
|
||||
@@ -280,10 +279,11 @@ class TMD:
|
||||
|
||||
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:
|
||||
'Japan', 'North America', 'Europe', 'World', or 'Korea'.
|
||||
'JPN', 'USA', 'EUR', 'None', or 'KOR'.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -292,19 +292,19 @@ class TMD:
|
||||
"""
|
||||
match self.region:
|
||||
case 0:
|
||||
return "Japan"
|
||||
return "JPN"
|
||||
case 1:
|
||||
return "North America"
|
||||
return "USA"
|
||||
case 2:
|
||||
return "Europe"
|
||||
return "EUR"
|
||||
case 3:
|
||||
return "World"
|
||||
return "None"
|
||||
case 4:
|
||||
return "Korea"
|
||||
return "KOR"
|
||||
|
||||
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:
|
||||
'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
|
||||
@@ -314,7 +314,7 @@ class TMD:
|
||||
str
|
||||
The type of the title.
|
||||
"""
|
||||
match self.title_type:
|
||||
match self.title_id[:8]:
|
||||
case '00000001':
|
||||
return "System"
|
||||
case '00010000':
|
||||
@@ -390,7 +390,7 @@ class TMD:
|
||||
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) +
|
||||
"' contents but index was '" + str(record) + "'!")
|
||||
|
||||
class AccessFlags(IntEnum):
|
||||
class AccessFlags(_IntEnum):
|
||||
AHB = 0
|
||||
DVD_VIDEO = 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user