mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 08:35:28 -05:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a81722ec5 | |||
| ecc68d9e57 | |||
| c42dc66209 | |||
| 045613216a | |||
| 98666285db | |||
| ba320a29de | |||
| 9890a6dbac | |||
|
c92a8096ea
|
|||
|
99a55a3de5
|
|||
| 4a3e9f8e7f | |||
| 8eeebd1d75 | |||
|
3b7a2d09b0
|
|||
|
a85beac602
|
|||
| 338446efcb | |||
|
ccbc2e262b
|
|||
|
|
17a894dc0d | ||
| 60918f1a39 | |||
|
fa6c9eb740
|
|||
| b3b10f9315 | |||
|
e85eae567a
|
|||
| 7c631454a1 | |||
|
b9edeb160d
|
|||
|
57fb0576e9
|
|||
|
640ca91716
|
|||
|
142a121fa9
|
|||
|
8026fc4fa3
|
|||
|
379359c089
|
|||
|
b5aab5ad22
|
|||
|
8244d79fba
|
|||
|
7b6703cf36
|
|||
|
c2b7724fdd
|
|||
|
bfe937f58f
|
|||
|
0cb37487b6
|
|||
|
9598d6d434
|
|||
| 67c5a4b59e | |||
| 614e7d87f9 | |||
|
230095e199
|
|||
|
748dbfd6d5
|
|||
| 34354ad557 | |||
| 522f370971 | |||
|
74192f8feb
|
|||
| 9751ea0d2e | |||
| 67e93d7341 | |||
|
73d684a74f
|
|||
|
3fd5bfd5df
|
|||
| 817fe1b499 | |||
| 7ca46372b0 | |||
| b3923cfe40 | |||
| 413b7a371f | |||
| a2c4c850a8 | |||
| 1d127b09e6 | |||
| 8eb54ab961 | |||
| 6c5c045bb1 | |||
| b0e48eb63c | |||
|
|
80c37b9b55 | ||
|
|
548d9d4098 | ||
|
|
f9b5e7aa16 | ||
| b241124240 | |||
| 270a7095ac | |||
| bfec2af0ac | |||
| 3c5f8b6763 | |||
|
|
c9c4aa5b62 | ||
|
|
c5025348dd | ||
| d86c754ebf | |||
| ed21fc0704 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -164,3 +164,9 @@ cython_debug/
|
|||||||
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
|
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
|
||||||
*.tmd
|
*.tmd
|
||||||
*.wad
|
*.wad
|
||||||
|
out_prod/
|
||||||
|
remakewad.pl
|
||||||
|
|
||||||
|
# Also awful macOS files
|
||||||
|
*._*
|
||||||
|
*.DS_Store
|
||||||
|
|||||||
51
README.md
51
README.md
@@ -1,40 +1,61 @@
|
|||||||
|

|
||||||
# libWiiPy
|
# libWiiPy
|
||||||
libWiiPy is a port of the project [libWiiSharp](https://github.com/TheShadowEevee/libWiiSharp), originally created by `Leathl`, now maintained by [@TheShadowEevee](https://github.com/TheShadowEevee), back to Python after it was created by porting [Wii.py](https://github.com/grp/Wii.py) to C#.
|
libWiiPy is a modern Python 3 library for interacting with and editing files from the Wii. It aims to be simple to use, well maintained, and offer as many features as reasonably possible in one library, so that a newly-written Python program could reasonably do 100% of its Wii-related work with just one library. It also aims to be fully cross-platform, so that any tools written with it can also be cross-platform.
|
||||||
|
|
||||||
### Why port this again instead of just updating Wii.py?
|
libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSharp), originally created by `Leathl`, now maintained by [@TheShadowEevee](https://github.com/TheShadowEevee). libWiiSharp is absolutely the way to go if you need a C# library for Wii files.
|
||||||
This is a really good question. Frankly, a lot of it comes from the fact that I am inexperienced with all of this Wii stuff. Attempting to recreate the features of libWiiSharp in Python with all of the freedom to do that however I see fit makes it a lot easier for someone like me to make this work. The code for Wii.py is also on the older side and is just written in a way that I can't easily understand. It's helpful as a reference here and there, but I mostly want to write this library in a unique way.
|
|
||||||
|
|
||||||
I also want to package this as a proper PyPI package, and starting with a clean slate will make that a lot easier as well.
|
**Note:** While libWiiPy is directly inspired by libWiiSharp and aims to have feature parity with it, no code from either libWiiSharp or Wii.py was used in the making of this library. All code is original and is written by [@NinjaCheetah](https://github.com/NinjaCheetah), [@rvtr](https://github.com/rvtr), and any other GitHub contributors.
|
||||||
|
|
||||||
|
# Features
|
||||||
|
This list will expand as libWiiPy is developed, but these features are currently available:
|
||||||
|
- TMD, ticket, and WAD parsing
|
||||||
|
- WAD content extraction, decryption, re-encryption, and packing
|
||||||
|
- Downloading titles from the NUS
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
A wiki, and in the future a potential documenation site, is being worked on, and can be accessed [here](https://github.com/NinjaCheetah/libWiiPy/wiki). It is currently fairly barebones, but it will be improved in the future.
|
||||||
|
|
||||||
|
The easiest way to get libWiiPy for your project is to install the latest version of the library from PyPI, as shown below.
|
||||||
|
```sh
|
||||||
|
pip install -U libWiiPy
|
||||||
|
```
|
||||||
|
Our PyPI project page can be found [here](https://pypi.org/project/libWiiPy/).
|
||||||
|
|
||||||
|
Because libWiiPy is very early in development, you may want to use the latest version of the package via git instead, so that you have the latest features available. You can do that like this:
|
||||||
|
```sh
|
||||||
|
pip install -U git+https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
```
|
||||||
|
Please be aware that because libWiiPy is in a very early state right now, many features may be subject to change, and methods and properties available now have the potential to disappear in the future.
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
To build this package locally, the steps are quite simple, and should apply to all platforms. Make sure you've set up your `venv` first!
|
To build this package locally, the steps are quite simple, and should apply to all platforms. Make sure you've set up your `venv` first!
|
||||||
|
|
||||||
First, install the dependencies from `requirements.txt`:
|
First, install the dependencies from `requirements.txt`:
|
||||||
```py
|
```sh
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, build the package using the Python `build` module:
|
Then, build the package using the Python `build` module:
|
||||||
```py
|
```sh
|
||||||
python -m build
|
python -m build
|
||||||
```
|
```
|
||||||
|
|
||||||
And that's all! You'll find your compiled pip package in `dist/`.
|
And that's all! You'll find your compiled pip package in `dist/`.
|
||||||
|
|
||||||
# Special Thanks
|
# Special Thanks
|
||||||
This project wouldn't be possible without the amazing people behind its predecessors and all of the people who have contributed to the documentation of the Wii's inner workings over at [Wiibrew](https://wiibrew.org).
|
This project wouldn't be possible without the amazing people behind its predecessors and all of the people who have contributed to the documentation of the Wii's inner workings over at [WiiBrew](https://wiibrew.org).
|
||||||
|
|
||||||
## Special Thanks from libWiiSharp
|
## Special Thanks for the Inspiration and Previous Projects
|
||||||
- Xuzz, SquidMan, megazig, Matt_P, Omega and The Lemon Man for Wii.py
|
- Xuzz, SquidMan, megazig, Matt_P, Omega and The Lemon Man for creating Wii.py
|
||||||
- megazig for his bns conversion code (bns.py)
|
|
||||||
- SquidMan for Zetsubou
|
|
||||||
- Arikado and Lunatik for Dop-Mii
|
|
||||||
- Andre Perrot for gbalzss
|
|
||||||
- Leathl for creating libWiiSharp
|
- Leathl for creating libWiiSharp
|
||||||
- TheShadowEevee for maintaining libWiiSharp
|
- TheShadowEevee for maintaining libWiiSharp
|
||||||
|
|
||||||
## Special Thanks to Wiibrew Contributors
|
## Special Thanks to WiiBrew Contributors
|
||||||
Thank you to all of the contributors to the documentation on the Wiibrew pages that make this all understandable! Some of the key articles referenced are as follows:
|
Thank you to all of the contributors to the documentation on the WiiBrew pages that make this all understandable! Some of the key articles referenced are as follows:
|
||||||
- [Title metadata](https://wiibrew.org/wiki/Title_metadata), for the documentation on how a TMD is structured
|
- [Title metadata](https://wiibrew.org/wiki/Title_metadata), for the documentation on how a TMD is structured
|
||||||
- [WAD files](https://wiibrew.org/wiki/WAD_files), for the documentation on how a WAD is structured
|
- [WAD files](https://wiibrew.org/wiki/WAD_files), for the documentation on how a WAD is structured
|
||||||
- [IOS history](https://wiibrew.org/wiki/IOS_history), for the documentation on IOS TIDs and how IOS is versioned
|
- [IOS history](https://wiibrew.org/wiki/IOS_history), for the documentation on IOS TIDs and how IOS is versioned
|
||||||
|
|
||||||
|
### One additional special thanks to [@DamiDoop](https://github.com/DamiDoop)!
|
||||||
|
She made the very cool banner you can see at the top of this README, and has also helped greatly with my sanity throughout debugging this library.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "libWiiPy"
|
name = "libWiiPy"
|
||||||
version = "0.1.0"
|
version = "0.2.3"
|
||||||
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" }
|
||||||
]
|
]
|
||||||
description = "A Wii-related library for Python"
|
description = "A modern Python library for handling files used by the Wii"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -13,6 +13,10 @@ classifiers = [
|
|||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
|
dependencies = [
|
||||||
|
"pycryptodome",
|
||||||
|
"requests"
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/NinjaCheetah/libWiiPy"
|
Homepage = "https://github.com/NinjaCheetah/libWiiPy"
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
build
|
build
|
||||||
|
pycryptodome
|
||||||
|
requests
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# "__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# These are the essential modules from libWiiPy that you'd probably want imported by default.
|
||||||
|
|
||||||
|
from .commonkeys import *
|
||||||
|
from .content import *
|
||||||
|
from .ticket import *
|
||||||
|
from .crypto import *
|
||||||
|
from .title import *
|
||||||
|
from .tmd import *
|
||||||
|
from .wad import *
|
||||||
|
from .nus import *
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
# "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors
|
# "commonkeys.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
# https://github.com/NinjaCheetah/libWiiPy
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
default_key = 'ebe42a225e8593e448d9c5457381aaf7'
|
import binascii
|
||||||
|
|
||||||
|
common_key = 'ebe42a225e8593e448d9c5457381aaf7'
|
||||||
korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e'
|
korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e'
|
||||||
vwii_key = '30bfc76e7c19afbb23163330ced7c28d'
|
vwii_key = '30bfc76e7c19afbb23163330ced7c28d'
|
||||||
|
|
||||||
|
|
||||||
def get_default_key():
|
def get_common_key(common_key_index) -> bytes:
|
||||||
"""Returns the regular Wii Common Key used to encrypt most content."""
|
"""
|
||||||
return default_key
|
Gets the specified Wii Common Key based on the index provided.
|
||||||
|
|
||||||
|
Possible values for common_key_index: 0: Common Key, 1: Korean Key, 2: vWii Key
|
||||||
|
|
||||||
def get_korean_key():
|
Parameters
|
||||||
"""Returns the Korean Wii Common Key used to encrypt Korean content."""
|
----------
|
||||||
return korean_key
|
common_key_index : int
|
||||||
|
The index of the common key to be returned.
|
||||||
|
|
||||||
|
Returns
|
||||||
def get_vwii_key():
|
-------
|
||||||
"""Returns the vWii Common Key used to encrypt vWii-specific content."""
|
bytes
|
||||||
return vwii_key
|
The specified common key, in binary format.
|
||||||
|
"""
|
||||||
|
match common_key_index:
|
||||||
|
case 0:
|
||||||
|
common_key_bin = binascii.unhexlify(common_key)
|
||||||
|
case 1:
|
||||||
|
common_key_bin = binascii.unhexlify(korean_key)
|
||||||
|
case 2:
|
||||||
|
common_key_bin = binascii.unhexlify(vwii_key)
|
||||||
|
case _:
|
||||||
|
raise ValueError("The common key index provided, " + str(common_key_index) + ", does not exist.")
|
||||||
|
return common_key_bin
|
||||||
|
|||||||
351
src/libWiiPy/content.py
Normal file
351
src/libWiiPy/content.py
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# "content.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
||||||
|
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import hashlib
|
||||||
|
from typing import List
|
||||||
|
from .types import ContentRecord
|
||||||
|
from .crypto import decrypt_content, encrypt_content
|
||||||
|
|
||||||
|
|
||||||
|
class ContentRegion:
|
||||||
|
"""
|
||||||
|
A ContentRegion object to parse the continuous content region of a WAD. Allows for retrieving content from the
|
||||||
|
region in both encrypted or decrypted form, and setting new content.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
content_records : List[ContentRecord]
|
||||||
|
The content records for the content stored in the region.
|
||||||
|
num_contents : int
|
||||||
|
The total number of contents stored in the region.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.content_records: List[ContentRecord] = []
|
||||||
|
self.content_region_size: int = 0 # Size of the content region.
|
||||||
|
self.num_contents: int = 0 # Number of contents in the content region.
|
||||||
|
self.content_start_offsets: List[int] = [0] # The start offsets of each content in the content region.
|
||||||
|
self.content_list: List[bytes] = []
|
||||||
|
|
||||||
|
def load(self, content_region: bytes, content_records: List[ContentRecord]) -> None:
|
||||||
|
"""
|
||||||
|
Loads the raw content region and builds a list of all the contents.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
content_region : bytes
|
||||||
|
The raw data for the content region being loaded.
|
||||||
|
content_records : list[ContentRecord]
|
||||||
|
A list of ContentRecord objects detailing all contents contained in the region.
|
||||||
|
"""
|
||||||
|
self.content_records = content_records
|
||||||
|
with io.BytesIO(content_region) as content_region_data:
|
||||||
|
# Get the total size of the content region.
|
||||||
|
self.content_region_size = sys.getsizeof(content_region_data)
|
||||||
|
self.num_contents = len(self.content_records)
|
||||||
|
# Calculate the offsets of each content in the content region.
|
||||||
|
# Content is aligned to 16 bytes, however a new content won't start until the next multiple of 64 bytes.
|
||||||
|
# Because of this, we need to add bytes to the next 64 byte offset if the previous content wasn't that long.
|
||||||
|
for content in self.content_records[:-1]:
|
||||||
|
start_offset = content.content_size + self.content_start_offsets[-1]
|
||||||
|
if (content.content_size % 64) != 0:
|
||||||
|
start_offset += 64 - (content.content_size % 64)
|
||||||
|
self.content_start_offsets.append(start_offset)
|
||||||
|
# Build a list of all the encrypted content data.
|
||||||
|
for content in range(len(self.content_start_offsets)):
|
||||||
|
# Seek to the start of the content based on the list of offsets.
|
||||||
|
content_region_data.seek(self.content_start_offsets[content])
|
||||||
|
# Calculate the number of bytes we need to read by adding bytes up the nearest multiple of 16 if needed.
|
||||||
|
bytes_to_read = self.content_records[content].content_size
|
||||||
|
if (bytes_to_read % 16) != 0:
|
||||||
|
bytes_to_read += 16 - (bytes_to_read % 16)
|
||||||
|
# Read the file based on the size of the content in the associated record, then append that data to
|
||||||
|
# the list of content.
|
||||||
|
content_enc = content_region_data.read(bytes_to_read)
|
||||||
|
self.content_list.append(content_enc)
|
||||||
|
|
||||||
|
def dump(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Takes the list of contents and assembles them back into one content region. Returns this content region as a
|
||||||
|
bytes object and sets the raw content region variable to this result, then calls load() again to make sure the
|
||||||
|
content list matches the raw data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The full WAD file as bytes.
|
||||||
|
"""
|
||||||
|
content_region_data = b''
|
||||||
|
for content in self.content_list:
|
||||||
|
# Calculate padding after this content before the next one.
|
||||||
|
padding_bytes = 0
|
||||||
|
if (len(content) % 64) != 0:
|
||||||
|
padding_bytes = 64 - (len(content) % 64)
|
||||||
|
# Write content data, then the padding afterward if necessary.
|
||||||
|
content_region_data += content
|
||||||
|
if padding_bytes > 0:
|
||||||
|
content_region_data += b'\x00' * padding_bytes
|
||||||
|
# Return the raw ContentRegion for the data contained in the object.
|
||||||
|
return content_region_data
|
||||||
|
|
||||||
|
def get_enc_content_by_index(self, index: int) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets an individual content from the content region based on the provided index, in encrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : int
|
||||||
|
The index of the content you want to get.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The encrypted content listed in the content record.
|
||||||
|
"""
|
||||||
|
content_enc = self.content_list[index]
|
||||||
|
return content_enc
|
||||||
|
|
||||||
|
def get_enc_content_by_cid(self, cid: int) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets an individual content from the content region based on the provided Content ID, in encrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cid : int
|
||||||
|
The Content ID of the content you want to get. Expected to be in decimal form.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The encrypted content listed in the content record.
|
||||||
|
"""
|
||||||
|
# Find the index of the requested Content ID.
|
||||||
|
content_index = None
|
||||||
|
for content in self.content_records:
|
||||||
|
if content.content_id == cid:
|
||||||
|
content_index = content.index
|
||||||
|
# If finding a matching ID was unsuccessful, that means that no content with that ID is in the TMD, so
|
||||||
|
# return a Value Error.
|
||||||
|
if content_index is None:
|
||||||
|
raise ValueError("The Content ID requested does not exist in the TMD's content records.")
|
||||||
|
# Call get_enc_content_by_index() using the index we just found.
|
||||||
|
content_enc = self.get_enc_content_by_index(content_index)
|
||||||
|
return content_enc
|
||||||
|
|
||||||
|
def get_enc_contents(self) -> List[bytes]:
|
||||||
|
"""
|
||||||
|
Gets a list of all encrypted contents from the content region.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[bytes]
|
||||||
|
A list containing all encrypted contents.
|
||||||
|
"""
|
||||||
|
return self.content_list
|
||||||
|
|
||||||
|
def get_content_by_index(self, index: int, title_key: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets an individual content from the content region based on the provided index, in decrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : int
|
||||||
|
The index of the content you want to get.
|
||||||
|
title_key : bytes
|
||||||
|
The Title Key for the title the content is from.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted content listed in the content record.
|
||||||
|
"""
|
||||||
|
# Load the encrypted content at the specified index and then decrypt it with the Title Key.
|
||||||
|
content_enc = self.get_enc_content_by_index(index)
|
||||||
|
content_dec = decrypt_content(content_enc, title_key, self.content_records[index].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[index].content_hash.decode())
|
||||||
|
# Compare the hash and throw a ValueError if the hash doesn't match.
|
||||||
|
if content_dec_hash != content_record_hash:
|
||||||
|
raise ValueError("Content hash did not match the expected hash in its record! The incorrect Title Key may "
|
||||||
|
"have been used!.\n"
|
||||||
|
"Expected hash is: {}\n".format(content_record_hash) +
|
||||||
|
"Actual hash is: {}".format(content_dec_hash))
|
||||||
|
return content_dec
|
||||||
|
|
||||||
|
def get_content_by_cid(self, cid: int, title_key: bytes) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets an individual content from the content region based on the provided Content ID, in decrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cid : int
|
||||||
|
The Content ID of the content you want to get. Expected to be in decimal form.
|
||||||
|
title_key : bytes
|
||||||
|
The Title Key for the title the content is from.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted content listed in the content record.
|
||||||
|
"""
|
||||||
|
# Find the index of the requested Content ID.
|
||||||
|
content_index = None
|
||||||
|
for content in self.content_records:
|
||||||
|
if content.content_id == cid:
|
||||||
|
content_index = content.index
|
||||||
|
# If finding a matching ID was unsuccessful, that means that no content with that ID is in the TMD, so
|
||||||
|
# return a Value Error.
|
||||||
|
if content_index is None:
|
||||||
|
raise ValueError("The Content ID requested does not exist in the TMD's content records.")
|
||||||
|
# Call get_content_by_index() using the index we just found.
|
||||||
|
content_dec = self.get_content_by_index(content_index, title_key)
|
||||||
|
return content_dec
|
||||||
|
|
||||||
|
def get_contents(self, title_key: bytes) -> List[bytes]:
|
||||||
|
"""
|
||||||
|
Gets a list of all contents from the content region, in decrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_key : bytes
|
||||||
|
The Title Key for the title the content is from.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[bytes]
|
||||||
|
A list containing all decrypted contents.
|
||||||
|
"""
|
||||||
|
dec_contents: List[bytes] = []
|
||||||
|
# Iterate over every content, get the decrypted version of it, then add it to a list and return it.
|
||||||
|
for content in range(self.num_contents):
|
||||||
|
dec_contents.append(self.get_content_by_index(content, title_key))
|
||||||
|
return dec_contents
|
||||||
|
|
||||||
|
def set_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
|
||||||
|
content_hash: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are
|
||||||
|
set in the content record, with a new record being added if necessary.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
enc_content : bytes
|
||||||
|
The new encrypted content to set.
|
||||||
|
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.
|
||||||
|
content_size : int
|
||||||
|
The size of the new encrypted content when decrypted.
|
||||||
|
content_hash : bytes
|
||||||
|
The hash of the new encrypted content when decrypted.
|
||||||
|
"""
|
||||||
|
# Save the number of contents currently in the content region and records.
|
||||||
|
num_contents = len(self.content_records)
|
||||||
|
# Check if a record already exists for this index. If it doesn't, create it.
|
||||||
|
if (index + 1) > num_contents:
|
||||||
|
# Ensure that you aren't attempting to create a gap before appending.
|
||||||
|
if (index + 1) > num_contents + 1:
|
||||||
|
raise ValueError("You are trying to set the content at position " + str(index) + ", but no content "
|
||||||
|
"exists at position " + str(index - 1) + "!")
|
||||||
|
self.content_records.append(ContentRecord(cid, index, content_type, content_size, content_hash))
|
||||||
|
# If it does, reassign the values in it.
|
||||||
|
else:
|
||||||
|
self.content_records[index].content_id = cid
|
||||||
|
self.content_records[index].content_type = content_type
|
||||||
|
self.content_records[index].content_size = content_size
|
||||||
|
self.content_records[index].content_hash = content_hash
|
||||||
|
# Check if a content already occupies the provided index. If it does, reassign it to the new content, if it
|
||||||
|
# doesn't, then append a new entry.
|
||||||
|
if (index + 1) > num_contents:
|
||||||
|
self.content_list.append(enc_content)
|
||||||
|
else:
|
||||||
|
self.content_list[index] = enc_content
|
||||||
|
|
||||||
|
def set_content(self, dec_content: bytes, cid: int, index: int, content_type: int, title_key: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are
|
||||||
|
set in the content record, with a new record being added if necessary.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dec_content : bytes
|
||||||
|
The new decrypted content to set.
|
||||||
|
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 new decrypted content.
|
||||||
|
"""
|
||||||
|
# Store the size of the new content.
|
||||||
|
dec_content_size = len(dec_content)
|
||||||
|
# Calculate the hash of the new content.
|
||||||
|
dec_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)
|
||||||
|
# Pass values to set_enc_content()
|
||||||
|
self.set_enc_content(enc_content, cid, index, content_type, dec_content_size, dec_content_hash)
|
||||||
|
|
||||||
|
def load_enc_content(self, enc_content: bytes, index: int) -> None:
|
||||||
|
"""
|
||||||
|
Loads the provided encrypted content into the content region at the specified index, with the assumption that
|
||||||
|
it matches the record at that index. Not recommended for most use cases, use decrypted content and
|
||||||
|
load_content() instead.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
enc_content : bytes
|
||||||
|
The encrypted content to load.
|
||||||
|
index : int
|
||||||
|
The content index to load the content at.
|
||||||
|
"""
|
||||||
|
if (index + 1) > len(self.content_records) or len(self.content_records) == 0:
|
||||||
|
raise IndexError("No content records have been loaded, or that index is higher than the highest entry in "
|
||||||
|
"the content records.")
|
||||||
|
if (index + 1) > len(self.content_list):
|
||||||
|
self.content_list.append(enc_content)
|
||||||
|
else:
|
||||||
|
self.content_list[index] = enc_content
|
||||||
|
|
||||||
|
def load_content(self, dec_content: bytes, index: int, title_key: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Loads the provided decrypted content into the content region at the specified index, but first checks to make
|
||||||
|
sure it matches the record at that index before loading. This content will be encrypted when loaded.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dec_content : bytes
|
||||||
|
The decrypted content to load.
|
||||||
|
index : int
|
||||||
|
The content index to load the content at.
|
||||||
|
title_key: bytes
|
||||||
|
The Title Key that matches the decrypted content.
|
||||||
|
"""
|
||||||
|
# Make sure that content records exist and that the provided index exists in them.
|
||||||
|
if (index + 1) > len(self.content_records) or len(self.content_records) == 0:
|
||||||
|
raise IndexError("No content records have been loaded, or that index is higher than the highest entry in "
|
||||||
|
"the content records.")
|
||||||
|
# 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[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))
|
||||||
|
# If the hash matches, encrypt the content and set it where it belongs.
|
||||||
|
enc_content = encrypt_content(dec_content, title_key, index)
|
||||||
|
if (index + 1) > len(self.content_list):
|
||||||
|
self.content_list.append(enc_content)
|
||||||
|
else:
|
||||||
|
self.content_list[index] = enc_content
|
||||||
152
src/libWiiPy/crypto.py
Normal file
152
src/libWiiPy/crypto.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# "crypto.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from .commonkeys import get_common_key
|
||||||
|
from .shared import convert_tid_to_iv
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_title_key(title_key_enc: bytes, common_key_index: int, title_id: bytes | str) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets the decrypted version of the encrypted Title Key provided.
|
||||||
|
|
||||||
|
Requires the index of the common key to use, and the Title ID of the title that the Title Key is for.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_key_enc : bytes
|
||||||
|
The encrypted Title Key.
|
||||||
|
common_key_index : int
|
||||||
|
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.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted Title Key.
|
||||||
|
"""
|
||||||
|
# Load the correct common key for the title.
|
||||||
|
common_key = get_common_key(common_key_index)
|
||||||
|
# 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.
|
||||||
|
title_key_iv = title_key_iv + (b'\x00' * 8)
|
||||||
|
# Create a new AES object with the values provided.
|
||||||
|
aes = AES.new(common_key, AES.MODE_CBC, title_key_iv)
|
||||||
|
# Decrypt the Title Key using the AES object.
|
||||||
|
title_key = aes.decrypt(title_key_enc)
|
||||||
|
return title_key
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_title_key(title_key_dec: bytes, common_key_index: int, title_id: bytes | str) -> bytes:
|
||||||
|
"""
|
||||||
|
Encrypts the provided Title Key with the selected common key.
|
||||||
|
|
||||||
|
Requires the index of the common key to use, and the Title ID of the title that the Title Key is for.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_key_dec : bytes
|
||||||
|
The decrypted Title Key.
|
||||||
|
common_key_index : int
|
||||||
|
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.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
An encrypted Title Key.
|
||||||
|
"""
|
||||||
|
# Load the correct common key for the title.
|
||||||
|
common_key = get_common_key(common_key_index)
|
||||||
|
# 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.
|
||||||
|
title_key_iv = title_key_iv + (b'\x00' * 8)
|
||||||
|
# Create a new AES object with the values provided.
|
||||||
|
aes = AES.new(common_key, AES.MODE_CBC, title_key_iv)
|
||||||
|
# Encrypt Title Key using the AES object.
|
||||||
|
title_key = aes.encrypt(title_key_dec)
|
||||||
|
return title_key
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_content(content_enc, title_key, content_index, content_length) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets the decrypted version of the encrypted content.
|
||||||
|
|
||||||
|
This requires the index of the content to decrypt as it is used as the IV, as well as the content length to adjust
|
||||||
|
padding as necessary.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
content_enc : bytes
|
||||||
|
The encrypted content.
|
||||||
|
title_key : bytes
|
||||||
|
The Title Key for the title the content is from.
|
||||||
|
content_index : int
|
||||||
|
The index in the TMD's content record of the content being decrypted.
|
||||||
|
content_length : int
|
||||||
|
The length in the TMD's content record of the content being decrypted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted content.
|
||||||
|
"""
|
||||||
|
# Generate the IV from the Content Index of the content to be decrypted.
|
||||||
|
content_index_bin = struct.pack('>H', content_index)
|
||||||
|
while len(content_index_bin) < 16:
|
||||||
|
content_index_bin += b'\x00'
|
||||||
|
# Align content to 16 bytes to ensure that it works with AES encryption.
|
||||||
|
if (len(content_enc) % 16) != 0:
|
||||||
|
content_enc = content_enc + (b'\x00' * (16 - (len(content_enc) % 16)))
|
||||||
|
# Create a new AES object with the values provided, with the content's unique ID as the IV.
|
||||||
|
aes = AES.new(title_key, AES.MODE_CBC, content_index_bin)
|
||||||
|
# Decrypt the content using the AES object.
|
||||||
|
content_dec = aes.decrypt(content_enc)
|
||||||
|
# Trim additional bytes that may have been added so the content is the correct size.
|
||||||
|
content_dec = content_dec[:content_length]
|
||||||
|
return content_dec
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_content(content_dec, title_key, content_index) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets the encrypted version of the decrypted content.
|
||||||
|
|
||||||
|
This requires the index of the content to encrypt as it is used as the IV, as well as the content length to adjust
|
||||||
|
padding as necessary.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
content_dec : bytes
|
||||||
|
The decrypted content.
|
||||||
|
title_key : bytes
|
||||||
|
The Title Key for the title the content is from.
|
||||||
|
content_index : int
|
||||||
|
The index in the TMD's content record of the content being decrypted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The encrypted content.
|
||||||
|
"""
|
||||||
|
# Generate the IV from the Content Index of the content to be decrypted.
|
||||||
|
content_index_bin = struct.pack('>H', content_index)
|
||||||
|
while len(content_index_bin) < 16:
|
||||||
|
content_index_bin += b'\x00'
|
||||||
|
# Calculate the intended size of the encrypted content.
|
||||||
|
enc_size = len(content_dec) + (16 - (len(content_dec) % 16))
|
||||||
|
# Align content to 16 bytes to ensure that it works with AES encryption.
|
||||||
|
if (len(content_dec) % 16) != 0:
|
||||||
|
content_dec = content_dec + (b'\x00' * (16 - (len(content_dec) % 16)))
|
||||||
|
# Create a new AES object with the values provided, with the content's unique ID as the IV.
|
||||||
|
aes = AES.new(title_key, AES.MODE_CBC, content_index_bin)
|
||||||
|
# Encrypt the content using the AES object.
|
||||||
|
content_enc = aes.encrypt(content_dec)
|
||||||
|
# Trim down the encrypted content.
|
||||||
|
content_enc = content_enc[:enc_size]
|
||||||
|
return content_enc
|
||||||
233
src/libWiiPy/nus.py
Normal file
233
src/libWiiPy/nus.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# "nus.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki/NUS for details about the NUS
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
from typing import List
|
||||||
|
from .title import Title
|
||||||
|
from .tmd import TMD
|
||||||
|
from .ticket import Ticket
|
||||||
|
|
||||||
|
nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/", "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/"]
|
||||||
|
|
||||||
|
|
||||||
|
def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> Title:
|
||||||
|
"""
|
||||||
|
Download an entire title and all of its contents, then load the downloaded components into a Title object for
|
||||||
|
further use. This method is NOT recommended for general use, as it has absolutely no verbosity. It is instead
|
||||||
|
recommended to call the individual download methods instead to provide more flexibility and output.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download.
|
||||||
|
title_version : int, option
|
||||||
|
The version of the title to download. Defaults to latest if not set.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Title
|
||||||
|
A Title object containing all the data from the downloaded title.
|
||||||
|
"""
|
||||||
|
# First, create the new title.
|
||||||
|
title = Title()
|
||||||
|
# Download and load the TMD, Ticket, and certs.
|
||||||
|
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint))
|
||||||
|
title.load_ticket(download_ticket(title_id, wiiu_endpoint))
|
||||||
|
title.wad.set_cert_data(download_cert(wiiu_endpoint))
|
||||||
|
# Download all contents
|
||||||
|
title.load_content_records()
|
||||||
|
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint)
|
||||||
|
# Return the completed title.
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
|
||||||
|
version if it was manually specified in the object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download the TMD for.
|
||||||
|
title_version : int, option
|
||||||
|
The version of the TMD to download. Defaults to latest if not set.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The TMD file from the NUS.
|
||||||
|
"""
|
||||||
|
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
|
||||||
|
# when a specific version is requested.
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
tmd_url = nus_endpoint[0] + title_id + "/tmd"
|
||||||
|
else:
|
||||||
|
tmd_url = nus_endpoint[1] + title_id + "/tmd"
|
||||||
|
# Add the version to the URL if one was specified.
|
||||||
|
if title_version is not None:
|
||||||
|
tmd_url += "." + str(title_version)
|
||||||
|
# Make the request.
|
||||||
|
tmd_request = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
|
# Handle a 404 if the TID/version doesn't exist.
|
||||||
|
if tmd_request.status_code != 200:
|
||||||
|
raise ValueError("The requested Title ID or TMD version does not exist. Please check the Title ID and Title"
|
||||||
|
" version and then try again.")
|
||||||
|
# Save the raw TMD.
|
||||||
|
raw_tmd = tmd_request.content
|
||||||
|
# Use a TMD object to load the data and then return only the actual TMD.
|
||||||
|
tmd_temp = TMD()
|
||||||
|
tmd_temp.load(raw_tmd)
|
||||||
|
tmd = tmd_temp.dump()
|
||||||
|
return tmd
|
||||||
|
|
||||||
|
|
||||||
|
def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
|
||||||
|
a free title.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download the Ticket for.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The Ticket file from the NUS.
|
||||||
|
"""
|
||||||
|
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
|
||||||
|
# title.
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
ticket_url = nus_endpoint[0] + title_id + "/cetk"
|
||||||
|
else:
|
||||||
|
ticket_url = nus_endpoint[1] + title_id + "/cetk"
|
||||||
|
# Make the request.
|
||||||
|
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
|
if ticket_request.status_code != 200:
|
||||||
|
raise ValueError("The requested Title ID does not exist, or refers to a non-free title. Tickets can only"
|
||||||
|
" be downloaded for titles that are free on the NUS.")
|
||||||
|
# Save the raw cetk file.
|
||||||
|
cetk = ticket_request.content
|
||||||
|
# Use a Ticket object to load only the Ticket data from cetk and return it.
|
||||||
|
ticket_temp = Ticket()
|
||||||
|
ticket_temp.load(cetk)
|
||||||
|
ticket = ticket_temp.dump()
|
||||||
|
return ticket
|
||||||
|
|
||||||
|
|
||||||
|
def download_cert(wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads the signing certificate used by all WADs. This uses System Menu 4.3U as the source.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The cert file.
|
||||||
|
"""
|
||||||
|
# Download the TMD and cetk for the System Menu 4.3U.
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
tmd_url = nus_endpoint[0] + "0000000100000002/tmd.513"
|
||||||
|
cetk_url = nus_endpoint[0] + "0000000100000002/cetk"
|
||||||
|
else:
|
||||||
|
tmd_url = nus_endpoint[1] + "0000000100000002/tmd.513"
|
||||||
|
cetk_url = nus_endpoint[1] + "0000000100000002/cetk"
|
||||||
|
tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
|
||||||
|
cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
|
||||||
|
# Assemble the certificate.
|
||||||
|
cert = b''
|
||||||
|
# Certificate Authority data.
|
||||||
|
cert += cetk[0x2A4 + 768:]
|
||||||
|
# Certificate Policy data.
|
||||||
|
cert += tmd[0x328:0x328 + 768]
|
||||||
|
# XS data.
|
||||||
|
cert += cetk[0x2A4:0x2A4 + 768]
|
||||||
|
# Since the cert is always the same, check the hash to make sure nothing went wildly wrong.
|
||||||
|
if hashlib.sha1(cert).hexdigest() != "ace0f15d2a851c383fe4657afc3840d6ffe30ad0":
|
||||||
|
raise Exception("An unknown error has occurred downloading and creating the certificate.")
|
||||||
|
return cert
|
||||||
|
|
||||||
|
|
||||||
|
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False) -> bytes:
|
||||||
|
"""
|
||||||
|
Downloads a specified content for the title specified in the object.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download content from.
|
||||||
|
content_id : int
|
||||||
|
The Content ID of the content you wish to download.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The downloaded content.
|
||||||
|
"""
|
||||||
|
# Build the download URL. The structure is download/<TID>/<Content ID>.
|
||||||
|
content_id_hex = hex(content_id)[2:]
|
||||||
|
if len(content_id_hex) < 2:
|
||||||
|
content_id_hex = "0" + content_id_hex
|
||||||
|
if wiiu_endpoint is False:
|
||||||
|
content_url = nus_endpoint[0] + title_id + "/000000" + content_id_hex
|
||||||
|
else:
|
||||||
|
content_url = nus_endpoint[1] + title_id + "/000000" + content_id_hex
|
||||||
|
# Make the request.
|
||||||
|
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||||
|
if content_request.status_code != 200:
|
||||||
|
raise ValueError("The requested Title ID does not exist, or an invalid Content ID is present in the"
|
||||||
|
" content records provided.\n Failed while downloading Content ID: 000000" +
|
||||||
|
content_id_hex)
|
||||||
|
content_data = content_request.content
|
||||||
|
return content_data
|
||||||
|
|
||||||
|
|
||||||
|
def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False) -> List[bytes]:
|
||||||
|
"""
|
||||||
|
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
|
||||||
|
so that the content records can be accessed.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The Title ID of the title to download content from.
|
||||||
|
tmd : TMD
|
||||||
|
The TMD that matches the title that the contents being downloaded are from.
|
||||||
|
wiiu_endpoint : bool, option
|
||||||
|
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
List[bytes]
|
||||||
|
A list of all the downloaded contents.
|
||||||
|
"""
|
||||||
|
# Retrieve the content records from the TMD.
|
||||||
|
content_records = tmd.content_records
|
||||||
|
# Create a list of Content IDs to download.
|
||||||
|
content_ids = []
|
||||||
|
for content_record in content_records:
|
||||||
|
content_ids.append(content_record.content_id)
|
||||||
|
# Iterate over that list and download each content in it, then add it to the array of contents.
|
||||||
|
content_list = []
|
||||||
|
for content_id in content_ids:
|
||||||
|
# Call self.download_content() for each Content ID.
|
||||||
|
content = download_content(title_id, content_id, wiiu_endpoint)
|
||||||
|
content_list.append(content)
|
||||||
|
return content_list
|
||||||
@@ -1,9 +1,71 @@
|
|||||||
from typing import List
|
# "shared.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
from binascii import unhexlify
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# This file defines general functions that may be useful in other modules of libWiiPy. Putting them here cuts down on
|
||||||
|
# clutter in other files.
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
def hex_string_to_byte_array(hex_string: str) -> List[int]:
|
def align_value(value, alignment=64) -> int:
|
||||||
byte_string = unhexlify(hex_string)
|
"""
|
||||||
byte_array = list(byte_string)
|
Aligns the provided value to the set alignment (defaults to 64).
|
||||||
|
|
||||||
return byte_array
|
Parameters
|
||||||
|
----------
|
||||||
|
value : int
|
||||||
|
The value to align.
|
||||||
|
alignment : int
|
||||||
|
The number to align to. Defaults to 64.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The aligned value.
|
||||||
|
"""
|
||||||
|
if (value % alignment) != 0:
|
||||||
|
aligned_value = value + (alignment - (value % alignment))
|
||||||
|
return aligned_value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def pad_bytes(data, alignment=64) -> bytes:
|
||||||
|
"""
|
||||||
|
Pads the provided bytes object to the provided alignment (defaults to 64).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : bytes
|
||||||
|
The data to align.
|
||||||
|
alignment : int
|
||||||
|
The number to align to. Defaults to 64.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The aligned data.
|
||||||
|
"""
|
||||||
|
while (len(data) % alignment) != 0:
|
||||||
|
data += b'\x00'
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def convert_tid_to_iv(title_id: str) -> bytes:
|
||||||
|
title_key_iv = b''
|
||||||
|
if type(title_id) is bytes:
|
||||||
|
# This catches the format b'0000000100000002'
|
||||||
|
if len(title_id) == 16:
|
||||||
|
title_key_iv = binascii.unhexlify(title_id)
|
||||||
|
# This catches the format b'\x00\x00\x00\x01\x00\x00\x00\x02'
|
||||||
|
elif len(title_id) == 8:
|
||||||
|
pass
|
||||||
|
# If it isn't one of those lengths, it cannot possibly be valid, so reject it.
|
||||||
|
else:
|
||||||
|
raise ValueError("Title ID is not valid!")
|
||||||
|
# Allow for a string like "0000000100000002"
|
||||||
|
elif type(title_id) is str:
|
||||||
|
title_key_iv = binascii.unhexlify(title_id)
|
||||||
|
# If the Title ID isn't bytes or a string, it isn't valid and is rejected.
|
||||||
|
else:
|
||||||
|
raise TypeError("Title ID type is not valid! It must be either type str or bytes.")
|
||||||
|
return title_key_iv
|
||||||
|
|||||||
263
src/libWiiPy/ticket.py
Normal file
263
src/libWiiPy/ticket.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
# "ticket.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki/Ticket for details about the ticket format
|
||||||
|
|
||||||
|
import io
|
||||||
|
import binascii
|
||||||
|
from .crypto import decrypt_title_key
|
||||||
|
from .types import TitleLimit
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class Ticket:
|
||||||
|
"""
|
||||||
|
A Ticket object that allows for either loading and editing an existing Ticket or creating one manually if desired.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
signature : bytes
|
||||||
|
The signature applied to the ticket.
|
||||||
|
ticket_version : int
|
||||||
|
The version of the ticket.
|
||||||
|
title_key_enc : bytes
|
||||||
|
The Title Key contained in the ticket, in encrypted form.
|
||||||
|
ticket_id : bytes
|
||||||
|
The unique ID of this ticket, used for console-specific title installations.
|
||||||
|
console_id : int
|
||||||
|
The unique ID of the console this ticket was designed for, if this is a console-specific ticket.
|
||||||
|
title_version : int
|
||||||
|
The version of the title this ticket was designed for.
|
||||||
|
common_key_index : int
|
||||||
|
The index of the common key required to decrypt this ticket's Title Key.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
# Signature blob header
|
||||||
|
self.signature_type: bytes = b'' # Type of signature, always 0x10001 for RSA-2048
|
||||||
|
self.signature: bytes = b'' # Actual signature data
|
||||||
|
# v0 ticket data
|
||||||
|
self.signature_issuer: str = "" # Who issued the signature for the ticket
|
||||||
|
self.ecdh_data: bytes = b'' # Involved in created one-time keys for console-specific title installs.
|
||||||
|
self.ticket_version: int = 0 # The version of the current ticket file.
|
||||||
|
self.title_key_enc: bytes = b'' # The title key of the ticket's respective title, encrypted by a common key.
|
||||||
|
self.ticket_id: bytes = b'' # Used as the IV when decrypting the title key for console-specific title installs.
|
||||||
|
self.console_id: int = 0 # ID of the console that the ticket was issued for.
|
||||||
|
self.title_id: bytes = b'' # TID/IV used for AES-CBC encryption.
|
||||||
|
self.title_id_str: str = "" # TID in string form for comparing against the TMD.
|
||||||
|
self.unknown1: bytes = b'' # Some unknown data, not always the same so reading it just in case.
|
||||||
|
self.title_version: int = 0 # Version of the ticket's associated title.
|
||||||
|
self.permitted_titles: bytes = b'' # Permitted titles mask
|
||||||
|
self.permit_mask: bytes = b'' # "Permit mask. The current disc title is ANDed with the inverse of this mask to see if the result matches the Permitted Titles Mask."
|
||||||
|
self.title_export_allowed: int = 0 # Whether title export is allowed with a PRNG key or not.
|
||||||
|
self.common_key_index: int = 0 # Which common key should be used. 0 = Common Key, 1 = Korean Key, 2 = vWii Key
|
||||||
|
self.unknown2: bytes = b'' # More unknown data. Varies for VC/non-VC titles so reading it to ensure it matches.
|
||||||
|
self.content_access_permissions: bytes = b'' # "Content access permissions (one bit for each content)"
|
||||||
|
self.title_limits_list: List[TitleLimit] = [] # List of play limits applied to the title.
|
||||||
|
# v1 ticket data
|
||||||
|
# TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1.
|
||||||
|
|
||||||
|
def load(self, ticket: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Loads raw Ticket data and sets all attributes of the WAD object. This allows for manipulating an already
|
||||||
|
existing Ticket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ticket : bytes
|
||||||
|
The data for the Ticket you wish to load.
|
||||||
|
"""
|
||||||
|
with io.BytesIO(ticket) as ticket_data:
|
||||||
|
# ====================================================================================
|
||||||
|
# Parses each of the keys contained in the Ticket.
|
||||||
|
# ====================================================================================
|
||||||
|
# Signature type.
|
||||||
|
ticket_data.seek(0x0)
|
||||||
|
self.signature_type = ticket_data.read(4)
|
||||||
|
# Signature data.
|
||||||
|
ticket_data.seek(0x04)
|
||||||
|
self.signature = ticket_data.read(256)
|
||||||
|
# Signature issuer.
|
||||||
|
ticket_data.seek(0x140)
|
||||||
|
self.signature_issuer = str(ticket_data.read(64).decode())
|
||||||
|
# ECDH data.
|
||||||
|
ticket_data.seek(0x180)
|
||||||
|
self.ecdh_data = ticket_data.read(60)
|
||||||
|
# Ticket version.
|
||||||
|
ticket_data.seek(0x1BC)
|
||||||
|
self.ticket_version = int.from_bytes(ticket_data.read(1))
|
||||||
|
if self.ticket_version == 1:
|
||||||
|
raise ValueError("This appears to be a v1 ticket, which is not currently supported by libWiiPy. This "
|
||||||
|
"feature is planned for a later release. Only v0 tickets are supported at this time.")
|
||||||
|
# Title Key (Encrypted by a common key).
|
||||||
|
ticket_data.seek(0x1BF)
|
||||||
|
self.title_key_enc = ticket_data.read(16)
|
||||||
|
# Ticket ID.
|
||||||
|
ticket_data.seek(0x1D0)
|
||||||
|
self.ticket_id = ticket_data.read(8)
|
||||||
|
# Console ID.
|
||||||
|
ticket_data.seek(0x1D8)
|
||||||
|
self.console_id = int.from_bytes(ticket_data.read(4))
|
||||||
|
# Title ID.
|
||||||
|
ticket_data.seek(0x1DC)
|
||||||
|
self.title_id = binascii.hexlify(ticket_data.read(8))
|
||||||
|
# Title ID (as a string).
|
||||||
|
self.title_id_str = str(self.title_id.decode())
|
||||||
|
# Unknown data 1.
|
||||||
|
ticket_data.seek(0x1E4)
|
||||||
|
self.unknown1 = ticket_data.read(2)
|
||||||
|
# Title version.
|
||||||
|
ticket_data.seek(0x1E6)
|
||||||
|
title_version_high = int.from_bytes(ticket_data.read(1)) * 256
|
||||||
|
ticket_data.seek(0x1E7)
|
||||||
|
title_version_low = int.from_bytes(ticket_data.read(1))
|
||||||
|
self.title_version = title_version_high + title_version_low
|
||||||
|
# Permitted titles mask.
|
||||||
|
ticket_data.seek(0x1E8)
|
||||||
|
self.permitted_titles = ticket_data.read(4)
|
||||||
|
# Permit mask.
|
||||||
|
ticket_data.seek(0x1EC)
|
||||||
|
self.permit_mask = ticket_data.read(4)
|
||||||
|
# Whether title export with a PRNG key is allowed.
|
||||||
|
ticket_data.seek(0x1F0)
|
||||||
|
self.title_export_allowed = int.from_bytes(ticket_data.read(1))
|
||||||
|
# Common key index.
|
||||||
|
ticket_data.seek(0x1F1)
|
||||||
|
self.common_key_index = int.from_bytes(ticket_data.read(1))
|
||||||
|
# Unknown data 2.
|
||||||
|
ticket_data.seek(0x1F2)
|
||||||
|
self.unknown2 = ticket_data.read(48)
|
||||||
|
# Content access permissions.
|
||||||
|
ticket_data.seek(0x222)
|
||||||
|
self.content_access_permissions = ticket_data.read(64)
|
||||||
|
# Content limits.
|
||||||
|
ticket_data.seek(0x264)
|
||||||
|
for limit in range(0, 8):
|
||||||
|
limit_type = int.from_bytes(ticket_data.read(4))
|
||||||
|
limit_value = int.from_bytes(ticket_data.read(4))
|
||||||
|
self.title_limits_list.append(TitleLimit(limit_type, limit_value))
|
||||||
|
|
||||||
|
def dump(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Dumps the Ticket object back into bytes. This also sets the raw Ticket attribute of Ticket object to the
|
||||||
|
dumped data, and triggers load() again to ensure that the raw data and object match.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The full Ticket file as bytes.
|
||||||
|
"""
|
||||||
|
ticket_data = b''
|
||||||
|
# Signature type.
|
||||||
|
ticket_data += self.signature_type
|
||||||
|
# Signature data.
|
||||||
|
ticket_data += self.signature
|
||||||
|
# Padding to 64 bytes.
|
||||||
|
ticket_data += b'\x00' * 60
|
||||||
|
# Signature issuer.
|
||||||
|
ticket_data += str.encode(self.signature_issuer)
|
||||||
|
# ECDH data.
|
||||||
|
ticket_data += self.ecdh_data
|
||||||
|
# Ticket version.
|
||||||
|
ticket_data += int.to_bytes(self.ticket_version, 1)
|
||||||
|
# Reserved (all \0x00).
|
||||||
|
ticket_data += b'\x00\x00'
|
||||||
|
# Title Key.
|
||||||
|
ticket_data += self.title_key_enc
|
||||||
|
# Unknown (write \0x00).
|
||||||
|
ticket_data += b'\x00'
|
||||||
|
# Ticket ID.
|
||||||
|
ticket_data += self.ticket_id
|
||||||
|
# Console ID.
|
||||||
|
ticket_data += int.to_bytes(self.console_id, 4)
|
||||||
|
# Title ID.
|
||||||
|
ticket_data += binascii.unhexlify(self.title_id)
|
||||||
|
# Unknown data 1.
|
||||||
|
ticket_data += self.unknown1
|
||||||
|
# Title version.
|
||||||
|
title_version_high = round(self.title_version / 256)
|
||||||
|
ticket_data += int.to_bytes(title_version_high, 1)
|
||||||
|
title_version_low = self.title_version % 256
|
||||||
|
ticket_data += int.to_bytes(title_version_low, 1)
|
||||||
|
# Permitted titles mask.
|
||||||
|
ticket_data += self.permitted_titles
|
||||||
|
# Permit mask.
|
||||||
|
ticket_data += self.permit_mask
|
||||||
|
# Title Export allowed.
|
||||||
|
ticket_data += int.to_bytes(self.title_export_allowed, 1)
|
||||||
|
# Common Key index.
|
||||||
|
ticket_data += int.to_bytes(self.common_key_index, 1)
|
||||||
|
# Unknown data 2.
|
||||||
|
ticket_data += self.unknown2
|
||||||
|
# Content access permissions.
|
||||||
|
ticket_data += self.content_access_permissions
|
||||||
|
# Padding (always \x00).
|
||||||
|
ticket_data += b'\x00\x00'
|
||||||
|
# Iterate over Title Limit objects, write them back into raw data, then add them to the Ticket.
|
||||||
|
for title_limit in range(len(self.title_limits_list)):
|
||||||
|
title_limit_data = b''
|
||||||
|
# Write all fields from the title limit entry.
|
||||||
|
title_limit_data += int.to_bytes(self.title_limits_list[title_limit].limit_type, 4)
|
||||||
|
title_limit_data += int.to_bytes(self.title_limits_list[title_limit].maximum_usage, 4)
|
||||||
|
# Write the entry to the ticket.
|
||||||
|
ticket_data += title_limit_data
|
||||||
|
# Return the raw TMD for the data contained in the object.
|
||||||
|
return ticket_data
|
||||||
|
|
||||||
|
def get_title_id(self) -> str:
|
||||||
|
"""
|
||||||
|
Gets the Title ID of the ticket's associated title.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The Title ID of the title.
|
||||||
|
"""
|
||||||
|
title_id_str = str(self.title_id.decode())
|
||||||
|
return title_id_str
|
||||||
|
|
||||||
|
def get_common_key_type(self) -> str:
|
||||||
|
"""
|
||||||
|
Gets the name of the common key used to encrypt the Title Key contained in the ticket.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The name of the common key required.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
commonkeys.get_common_key
|
||||||
|
"""
|
||||||
|
match self.common_key_index:
|
||||||
|
case 0:
|
||||||
|
return "Common"
|
||||||
|
case 1:
|
||||||
|
return "Korean"
|
||||||
|
case 2:
|
||||||
|
return "vWii"
|
||||||
|
|
||||||
|
def get_title_key(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets the decrypted title key contained in the ticket.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted title key.
|
||||||
|
"""
|
||||||
|
title_key = decrypt_title_key(self.title_key_enc, self.common_key_index, self.title_id)
|
||||||
|
return title_key
|
||||||
|
|
||||||
|
def set_title_id(self, title_id) -> None:
|
||||||
|
"""
|
||||||
|
Sets the Title ID of the title in the Ticket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The new Title ID of the title.
|
||||||
|
"""
|
||||||
|
if len(title_id) != 16:
|
||||||
|
raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.")
|
||||||
|
self.title_id_str = title_id
|
||||||
|
self.title_id = binascii.unhexlify(title_id)
|
||||||
237
src/libWiiPy/title.py
Normal file
237
src/libWiiPy/title.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# "title.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
#
|
||||||
|
# See https://wiibrew.org/wiki/Title for details about how titles are formatted
|
||||||
|
|
||||||
|
from .content import ContentRegion
|
||||||
|
from .ticket import Ticket
|
||||||
|
from .tmd import TMD
|
||||||
|
from .wad import WAD
|
||||||
|
|
||||||
|
|
||||||
|
class Title:
|
||||||
|
"""
|
||||||
|
A Title object that contains all components of a title, and allows altering them. Provides higher-level access
|
||||||
|
than manually creating WAD, TMD, Ticket, and ContentRegion objects and ensures that any data that needs to match
|
||||||
|
between files matches.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
wad : WAD
|
||||||
|
A WAD object of a WAD containing the title's data.
|
||||||
|
tmd : TMD
|
||||||
|
A TMD object of the title's TMD.
|
||||||
|
ticket : Ticket
|
||||||
|
A Ticket object of the title's Ticket.
|
||||||
|
content: ContentRegion
|
||||||
|
A ContentRegion object containing the title's contents.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.wad: WAD = WAD()
|
||||||
|
self.tmd: TMD = TMD()
|
||||||
|
self.ticket: Ticket = Ticket()
|
||||||
|
self.content: ContentRegion = ContentRegion()
|
||||||
|
|
||||||
|
def load_wad(self, wad: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Load existing WAD data into the title and create WAD, TMD, Ticket, and ContentRegion objects based off of it
|
||||||
|
to allow you to modify that data. Note that this will overwrite any existing data for this title.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
wad : bytes
|
||||||
|
The data for the WAD you wish to load.
|
||||||
|
"""
|
||||||
|
# Create a new WAD object based on the WAD data provided.
|
||||||
|
self.wad = WAD()
|
||||||
|
self.wad.load(wad)
|
||||||
|
# Load the TMD.
|
||||||
|
self.tmd = TMD()
|
||||||
|
self.tmd.load(self.wad.get_tmd_data())
|
||||||
|
# Load the ticket.
|
||||||
|
self.ticket = Ticket()
|
||||||
|
self.ticket.load(self.wad.get_ticket_data())
|
||||||
|
# Load the content.
|
||||||
|
self.content = ContentRegion()
|
||||||
|
self.content.load(self.wad.get_content_data(), self.tmd.content_records)
|
||||||
|
# Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an
|
||||||
|
# error because clearly something strange has gone on with the WAD and editing it probably won't work.
|
||||||
|
if self.tmd.title_id != self.ticket.title_id_str:
|
||||||
|
raise ValueError("The Title IDs of the TMD and Ticket in this WAD do not match. This WAD appears to be "
|
||||||
|
"invalid.")
|
||||||
|
|
||||||
|
def dump_wad(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Dumps all title components (TMD, Ticket, and contents) back into the WAD object, and then dumps the WAD back
|
||||||
|
into raw data and returns it.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
wad_data : bytes
|
||||||
|
The raw data of the WAD.
|
||||||
|
"""
|
||||||
|
# Set WAD type to ib if the title being packed is boot2.
|
||||||
|
if self.tmd.title_id == "0000000100000001":
|
||||||
|
self.wad.wad_type = "ib"
|
||||||
|
# Dump the TMD and set it in the WAD.
|
||||||
|
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())
|
||||||
|
# Dump the ContentRegion and set it in the WAD.
|
||||||
|
self.wad.set_content_data(self.content.dump())
|
||||||
|
# Dump the WAD with the new regions back into raw data and return it.
|
||||||
|
wad_data = self.wad.dump()
|
||||||
|
return wad_data
|
||||||
|
|
||||||
|
def load_tmd(self, tmd: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tmd : bytes
|
||||||
|
The data for the WAD you wish to load.
|
||||||
|
"""
|
||||||
|
# Load TMD.
|
||||||
|
self.tmd.load(tmd)
|
||||||
|
|
||||||
|
def load_ticket(self, ticket: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Load existing Ticket data into the title. Note that this will overwrite any existing Ticket data for this
|
||||||
|
title.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ticket : bytes
|
||||||
|
The data for the WAD you wish to load.
|
||||||
|
"""
|
||||||
|
# Load Ticket.
|
||||||
|
self.ticket.load(ticket)
|
||||||
|
|
||||||
|
def load_content_records(self) -> None:
|
||||||
|
"""
|
||||||
|
Load content records from the TMD into the ContentRegion to allow loading content files based on the records.
|
||||||
|
This requires that a TMD has already been loaded and will throw an exception if it isn't.
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
self.content.content_records = self.tmd.content_records
|
||||||
|
|
||||||
|
def set_title_id(self, title_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Sets the Title ID of the title in both the TMD and Ticket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The new Title ID of the title.
|
||||||
|
"""
|
||||||
|
if len(title_id) != 16:
|
||||||
|
raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.")
|
||||||
|
self.tmd.set_title_id(title_id)
|
||||||
|
self.ticket.set_title_id(title_id)
|
||||||
|
|
||||||
|
def get_content_by_index(self, index: id) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets an individual content from the content region based on the provided index, in decrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : int
|
||||||
|
The index of the content you want to get.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted content listed in the content record.
|
||||||
|
"""
|
||||||
|
# Load the Title Key from the Ticket.
|
||||||
|
title_key = self.ticket.get_title_key()
|
||||||
|
# Get the decrypted content and return it.
|
||||||
|
dec_content = self.content.get_content_by_index(index, title_key)
|
||||||
|
return dec_content
|
||||||
|
|
||||||
|
def get_content_by_cid(self, cid: int) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets an individual content from the content region based on the provided Content ID, in decrypted form.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cid : int
|
||||||
|
The Content ID of the content you want to get. Expected to be in decimal form.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The decrypted content listed in the content record.
|
||||||
|
"""
|
||||||
|
# Load the Title Key from the Ticket.
|
||||||
|
title_key = self.ticket.get_title_key()
|
||||||
|
# Get the decrypted content and return it.
|
||||||
|
dec_content = self.content.get_content_by_cid(cid, title_key)
|
||||||
|
return dec_content
|
||||||
|
|
||||||
|
def set_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int,
|
||||||
|
content_hash: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are
|
||||||
|
set in the content record, with a new record being added if necessary. The TMD is also updated to match the new
|
||||||
|
records.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
enc_content : bytes
|
||||||
|
The new encrypted content to set.
|
||||||
|
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.
|
||||||
|
content_size : int
|
||||||
|
The size of the new encrypted content when decrypted.
|
||||||
|
content_hash : bytes
|
||||||
|
The hash of the new encrypted content when decrypted.
|
||||||
|
"""
|
||||||
|
# Set the encrypted content.
|
||||||
|
self.content.set_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 set_content(self, dec_content: bytes, cid: int, index: int, content_type: int) -> None:
|
||||||
|
"""
|
||||||
|
Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are
|
||||||
|
set in the content record, with a new record being added if necessary. The Title Key is sourced from this
|
||||||
|
title's loaded ticket. The TMD is also updated to match the new records.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dec_content : bytes
|
||||||
|
The new decrypted content to set.
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
# Set the decrypted content.
|
||||||
|
self.content.set_content(dec_content, cid, index, content_type, self.ticket.get_title_key())
|
||||||
|
# Update the TMD to match.
|
||||||
|
self.tmd.content_records = self.content.content_records
|
||||||
|
|
||||||
|
def load_content(self, dec_content: bytes, index: int) -> None:
|
||||||
|
"""
|
||||||
|
Loads the provided decrypted content into the content region at the specified index, but first checks to make
|
||||||
|
sure it matches the record at that index before loading. This content will be encrypted when loaded.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dec_content : bytes
|
||||||
|
The decrypted content to load.
|
||||||
|
index : int
|
||||||
|
The content index to load the content at.
|
||||||
|
"""
|
||||||
|
# Load the decrypted content.
|
||||||
|
self.content.load_content(dec_content, index, self.ticket.get_title_key())
|
||||||
@@ -5,113 +5,229 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import binascii
|
import binascii
|
||||||
from dataclasses import dataclass
|
import struct
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from .types import ContentRecord
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ContentRecord:
|
|
||||||
"""Creates a content record object that contains the details of a content contained in a title."""
|
|
||||||
cid: int # Content ID
|
|
||||||
index: int # Index in the list of contents
|
|
||||||
content_type: int # normal: 0x0001; dlc: 0x4001; shared: 0x8001
|
|
||||||
content_size: int
|
|
||||||
content_hash: bytearray # SHA1 hash content
|
|
||||||
|
|
||||||
|
|
||||||
class TMD:
|
class TMD:
|
||||||
"""Creates a TMD object to parse a TMD file to retrieve information about a title."""
|
"""
|
||||||
def __init__(self, tmd):
|
A TMD object that allows for either loading and editing an existing TMD or creating one manually if desired.
|
||||||
self.tmd = tmd
|
|
||||||
self.sig_type: int
|
Attributes
|
||||||
self.sig: bytearray
|
----------
|
||||||
self.issuer: bytearray # Follows the format "Root-CA%08x-CP%08x"
|
title_id : str
|
||||||
self.version: int # This seems to always be 0 no matter what?
|
The title ID of the title listed in the TMD.
|
||||||
self.ca_crl_version: int
|
title_version : int
|
||||||
self.signer_crl_version: int
|
The version of the title listed in the TMD.
|
||||||
self.vwii: int
|
tmd_version : int
|
||||||
self.ios_tid: str
|
The version of the TMD.
|
||||||
self.ios_version: int
|
ios_tid : str
|
||||||
self.title_id: str
|
The title ID of the IOS the title runs on.
|
||||||
self.content_type: str
|
ios_version : int
|
||||||
self.group_id: int # Publisher of the title
|
The IOS version the title runs on.
|
||||||
self.region: int
|
num_contents : int
|
||||||
self.ratings: int
|
The number of contents listed in the TMD.
|
||||||
self.access_rights: int
|
"""
|
||||||
self.title_version: int
|
def __init__(self):
|
||||||
self.num_contents: int
|
self.blob_header: bytes = b''
|
||||||
self.boot_index: int
|
self.sig_type: int = 0
|
||||||
self.content_record: List[ContentRecord]
|
self.sig: bytes = b''
|
||||||
# Load data from TMD file
|
self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x"
|
||||||
with io.BytesIO(tmd) as tmddata:
|
self.tmd_version: int = 0 # This seems to always be 0 no matter what?
|
||||||
# Signing certificate issuer
|
self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version
|
||||||
tmddata.seek(0x140)
|
self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version
|
||||||
self.issuer = tmddata.read(64)
|
self.vwii: int = 0 # Whether the title is for the vWii. 0 = No, 1 = Yes
|
||||||
# TMD version, seems to usually be 0, but I've seen references to other numbers
|
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
|
||||||
tmddata.seek(0x180)
|
self.ios_version: int = 0 # The IOS version the associated title runs on.
|
||||||
self.version = int.from_bytes(tmddata.read(1))
|
self.title_id: str = "" # The Title ID of the associated title.
|
||||||
# TODO: label
|
self.content_type: str = "" # The type of content contained within the associated title.
|
||||||
tmddata.seek(0x181)
|
self.group_id: int = 0 # The ID of the publisher of the associated title.
|
||||||
self.ca_crl_version = tmddata.read(1)
|
self.region: int = 0 # The ID of the region of the associated title.
|
||||||
# TODO: label
|
self.ratings: bytes = b'' # The parental controls rating of the associated title.
|
||||||
tmddata.seek(0x182)
|
self.reserved1: bytes = b'' # Unknown data labeled "Reserved" on WiiBrew.
|
||||||
self.signer_crl_version = tmddata.read(1)
|
self.ipc_mask: bytes = b''
|
||||||
# If this is a vWii title or not
|
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
|
||||||
tmddata.seek(0x183)
|
self.access_rights: bytes = b''
|
||||||
self.vwii = int.from_bytes(tmddata.read(1))
|
self.title_version: int = 0 # The version of the associated title.
|
||||||
# TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2
|
self.num_contents: int = 0 # The number of contents contained in the associated title.
|
||||||
tmddata.seek(0x184)
|
self.boot_index: int = 0 # The content index that contains the bootable executable.
|
||||||
ios_version_bin = tmddata.read(8)
|
self.content_records: List[ContentRecord] = []
|
||||||
|
|
||||||
|
def load(self, tmd: bytes) -> None:
|
||||||
|
"""
|
||||||
|
Loads raw TMD data and sets all attributes of the TMD object. This allows for manipulating an already
|
||||||
|
existing TMD.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tmd : bytes
|
||||||
|
The data for the TMD you wish to load.
|
||||||
|
"""
|
||||||
|
with io.BytesIO(tmd) as tmd_data:
|
||||||
|
# ====================================================================================
|
||||||
|
# Parses each of the keys contained in the TMD.
|
||||||
|
# ====================================================================================
|
||||||
|
tmd_data.seek(0x0)
|
||||||
|
self.blob_header = tmd_data.read(320)
|
||||||
|
# Signing certificate issuer.
|
||||||
|
tmd_data.seek(0x140)
|
||||||
|
self.issuer = tmd_data.read(64)
|
||||||
|
# TMD version, seems to usually be 0, but I've seen references to other numbers.
|
||||||
|
tmd_data.seek(0x180)
|
||||||
|
self.tmd_version = int.from_bytes(tmd_data.read(1))
|
||||||
|
# Certificate Authority CRL version.
|
||||||
|
tmd_data.seek(0x181)
|
||||||
|
self.ca_crl_version = int.from_bytes(tmd_data.read(1))
|
||||||
|
# Certificate Policy CRL version.
|
||||||
|
tmd_data.seek(0x182)
|
||||||
|
self.signer_crl_version = int.from_bytes(tmd_data.read(1))
|
||||||
|
# If this is a vWii title or not.
|
||||||
|
tmd_data.seek(0x183)
|
||||||
|
self.vwii = int.from_bytes(tmd_data.read(1))
|
||||||
|
# TID of the IOS to use for the title, set to 0 if this title is the IOS, set to boot2 version if boot2.
|
||||||
|
tmd_data.seek(0x184)
|
||||||
|
ios_version_bin = tmd_data.read(8)
|
||||||
ios_version_hex = binascii.hexlify(ios_version_bin)
|
ios_version_hex = binascii.hexlify(ios_version_bin)
|
||||||
self.ios_tid = str(ios_version_hex.decode())
|
self.ios_tid = str(ios_version_hex.decode())
|
||||||
# Get IOS version based on TID
|
# Get IOS version based on TID.
|
||||||
self.ios_version = int(self.ios_tid[-2:], 16)
|
self.ios_version = int(self.ios_tid[-2:], 16)
|
||||||
# Title ID of the title
|
# Title ID of the title.
|
||||||
tmddata.seek(0x18C)
|
tmd_data.seek(0x18C)
|
||||||
title_id_bin = tmddata.read(8)
|
title_id_bin = tmd_data.read(8)
|
||||||
title_id_hex = binascii.hexlify(title_id_bin)
|
title_id_hex = binascii.hexlify(title_id_bin)
|
||||||
self.title_id = str(title_id_hex.decode())
|
self.title_id = str(title_id_hex.decode())
|
||||||
# Type of content
|
# Type of content.
|
||||||
tmddata.seek(0x194)
|
tmd_data.seek(0x194)
|
||||||
content_type_bin = tmddata.read(4)
|
content_type_bin = tmd_data.read(4)
|
||||||
content_type_hex = binascii.hexlify(content_type_bin)
|
content_type_hex = binascii.hexlify(content_type_bin)
|
||||||
self.content_type = str(content_type_hex.decode())
|
self.content_type = str(content_type_hex.decode())
|
||||||
# Publisher of the title
|
# Publisher of the title.
|
||||||
tmddata.seek(0x198)
|
tmd_data.seek(0x198)
|
||||||
self.group_id = tmddata.read(2)
|
self.group_id = int.from_bytes(tmd_data.read(2))
|
||||||
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = NONE, 4 = KOR
|
# Region of the title, 0 = JAP, 1 = USA, 2 = EUR, 3 = WORLD, 4 = KOR.
|
||||||
tmddata.seek(0x19C)
|
tmd_data.seek(0x19C)
|
||||||
region_hex = tmddata.read(2)
|
region_hex = tmd_data.read(2)
|
||||||
self.region = int.from_bytes(region_hex)
|
self.region = int.from_bytes(region_hex)
|
||||||
# TODO: figure this one out
|
# Content rating of the title for parental controls. Likely based on ESRB, CERO, PEGI, etc. rating.
|
||||||
tmddata.seek(0x19E)
|
tmd_data.seek(0x19E)
|
||||||
self.ratings = tmddata.read(16)
|
self.ratings = tmd_data.read(16)
|
||||||
# Access rights of the title; DVD-video access and AHBPROT
|
# "Reserved" data 1.
|
||||||
tmddata.seek(0x1D8)
|
tmd_data.seek(0x1AE)
|
||||||
self.access_rights = tmddata.read(4)
|
self.reserved1 = tmd_data.read(12)
|
||||||
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD
|
# IPC mask.
|
||||||
tmddata.seek(0x1DC)
|
tmd_data.seek(0x1BA)
|
||||||
title_version_high = int.from_bytes(tmddata.read(1)) * 256
|
self.ipc_mask = tmd_data.read(12)
|
||||||
tmddata.seek(0x1DD)
|
# "Reserved" data 2.
|
||||||
title_version_low = int.from_bytes(tmddata.read(1))
|
tmd_data.seek(0x1C6)
|
||||||
|
self.reserved2 = tmd_data.read(18)
|
||||||
|
# Access rights of the title; DVD-video access and AHBPROT.
|
||||||
|
tmd_data.seek(0x1D8)
|
||||||
|
self.access_rights = tmd_data.read(4)
|
||||||
|
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD.
|
||||||
|
tmd_data.seek(0x1DC)
|
||||||
|
title_version_high = int.from_bytes(tmd_data.read(1)) * 256
|
||||||
|
tmd_data.seek(0x1DD)
|
||||||
|
title_version_low = int.from_bytes(tmd_data.read(1))
|
||||||
self.title_version = title_version_high + title_version_low
|
self.title_version = title_version_high + title_version_low
|
||||||
# The number of contents listed in the TMD
|
# The number of contents listed in the TMD.
|
||||||
tmddata.seek(0x1DE)
|
tmd_data.seek(0x1DE)
|
||||||
self.num_contents = int.from_bytes(tmddata.read(2))
|
self.num_contents = int.from_bytes(tmd_data.read(2))
|
||||||
# Content index in content list that contains the boot file
|
# The content index that contains the bootable executable.
|
||||||
tmddata.seek(0x1E0)
|
tmd_data.seek(0x1E0)
|
||||||
self.boot_index = tmddata.read(2)
|
self.boot_index = int.from_bytes(tmd_data.read(2))
|
||||||
|
# Get content records for the number of contents in num_contents.
|
||||||
|
self.content_records = []
|
||||||
|
for content in range(0, self.num_contents):
|
||||||
|
tmd_data.seek(0x1E4 + (36 * content))
|
||||||
|
content_record_hdr = struct.unpack(">LHH4x4s20s", tmd_data.read(36))
|
||||||
|
self.content_records.append(
|
||||||
|
ContentRecord(int(content_record_hdr[0]), int(content_record_hdr[1]),
|
||||||
|
int(content_record_hdr[2]), int.from_bytes(content_record_hdr[3]),
|
||||||
|
binascii.hexlify(content_record_hdr[4])))
|
||||||
|
|
||||||
def get_title_id(self):
|
def dump(self) -> bytes:
|
||||||
"""Returns the TID of the TMD's associated title."""
|
"""
|
||||||
return self.title_id
|
Dumps the TMD object back into bytes. This also sets the raw TMD attribute of TMD object to the dumped data,
|
||||||
|
and triggers load() again to ensure that the raw data and object match.
|
||||||
|
|
||||||
def get_title_version(self):
|
Returns
|
||||||
"""Returns the version of the TMD's associated title."""
|
-------
|
||||||
return self.title_version
|
bytes
|
||||||
|
The full TMD file as bytes.
|
||||||
|
"""
|
||||||
|
tmd_data = b''
|
||||||
|
# Signed blob header.
|
||||||
|
tmd_data += self.blob_header
|
||||||
|
# Signing certificate issuer.
|
||||||
|
tmd_data += self.issuer
|
||||||
|
# TMD version.
|
||||||
|
tmd_data += int.to_bytes(self.tmd_version, 1)
|
||||||
|
# Certificate Authority CRL version.
|
||||||
|
tmd_data += int.to_bytes(self.ca_crl_version, 1)
|
||||||
|
# Certificate Policy CRL version.
|
||||||
|
tmd_data += int.to_bytes(self.signer_crl_version, 1)
|
||||||
|
# If this is a vWii title or not.
|
||||||
|
tmd_data += int.to_bytes(self.vwii, 1)
|
||||||
|
# IOS Title ID.
|
||||||
|
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.content_type)
|
||||||
|
# Group ID.
|
||||||
|
tmd_data += int.to_bytes(self.group_id, 2)
|
||||||
|
# 2 bytes of zero for reasons.
|
||||||
|
tmd_data += b'\x00\x00'
|
||||||
|
# Region.
|
||||||
|
tmd_data += int.to_bytes(self.region, 2)
|
||||||
|
# Parental Controls Ratings.
|
||||||
|
tmd_data += self.ratings
|
||||||
|
# "Reserved" 1.
|
||||||
|
tmd_data += self.reserved1
|
||||||
|
# IPC mask.
|
||||||
|
tmd_data += self.ipc_mask
|
||||||
|
# "Reserved" 2.
|
||||||
|
tmd_data += self.reserved2
|
||||||
|
# Access rights.
|
||||||
|
tmd_data += self.access_rights
|
||||||
|
# Title version.
|
||||||
|
title_version_high = round(self.title_version / 256)
|
||||||
|
tmd_data += int.to_bytes(title_version_high, 1)
|
||||||
|
title_version_low = self.title_version % 256
|
||||||
|
tmd_data += int.to_bytes(title_version_low, 1)
|
||||||
|
# Number of contents.
|
||||||
|
tmd_data += int.to_bytes(self.num_contents, 2)
|
||||||
|
# Boot index.
|
||||||
|
tmd_data += int.to_bytes(self.boot_index, 2)
|
||||||
|
# Minor version. Unused so write \x00.
|
||||||
|
tmd_data += b'\x00\x00'
|
||||||
|
# Iterate over content records, write them back into raw data, then add them to the TMD.
|
||||||
|
for content_record in range(self.num_contents):
|
||||||
|
content_data = b''
|
||||||
|
# Write all fields from the content record.
|
||||||
|
content_data += int.to_bytes(self.content_records[content_record].content_id, 4)
|
||||||
|
content_data += int.to_bytes(self.content_records[content_record].index, 2)
|
||||||
|
content_data += int.to_bytes(self.content_records[content_record].content_type, 2)
|
||||||
|
content_data += int.to_bytes(self.content_records[content_record].content_size, 8)
|
||||||
|
content_data += binascii.unhexlify(self.content_records[content_record].content_hash)
|
||||||
|
# Write the record to the TMD.
|
||||||
|
tmd_data += content_data
|
||||||
|
# Return the raw TMD for the data contained in the object.
|
||||||
|
return tmd_data
|
||||||
|
|
||||||
def get_title_region(self):
|
def get_title_region(self) -> str:
|
||||||
"""Returns the region of the TMD's associated title."""
|
"""
|
||||||
|
Gets the region of the TMD's associated title.
|
||||||
|
|
||||||
|
Can be one of several possible values:
|
||||||
|
'JAP', 'USA', 'EUR', 'WORLD', or 'KOR'.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The region of the title.
|
||||||
|
"""
|
||||||
match self.region:
|
match self.region:
|
||||||
case 0:
|
case 0:
|
||||||
return "JAP"
|
return "JAP"
|
||||||
@@ -120,31 +236,36 @@ class TMD:
|
|||||||
case 2:
|
case 2:
|
||||||
return "EUR"
|
return "EUR"
|
||||||
case 3:
|
case 3:
|
||||||
return "NONE"
|
return "WORLD"
|
||||||
case 4:
|
case 4:
|
||||||
return "KOR"
|
return "KOR"
|
||||||
|
|
||||||
def get_is_vwii_title(self):
|
def get_is_vwii_title(self) -> bool:
|
||||||
"""Returns whether the TMD is designed for the vWii or not."""
|
"""
|
||||||
|
Gets whether the TMD is designed for the vWii or not.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
If the title is for vWii.
|
||||||
|
"""
|
||||||
if self.vwii == 1:
|
if self.vwii == 1:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_tmd_version(self):
|
def get_title_type(self) -> str:
|
||||||
"""Returns the version of the TMD."""
|
"""
|
||||||
return self.version
|
Gets the type of the TMD's associated title.
|
||||||
|
|
||||||
def get_required_ios_tid(self):
|
Can be one of several possible values:
|
||||||
"""Returns the TID of the required IOS for the title."""
|
'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
|
||||||
return self.ios_tid
|
|
||||||
|
|
||||||
def get_required_ios(self):
|
Returns
|
||||||
"""Returns the required IOS version for the title."""
|
-------
|
||||||
return self.ios_version
|
str
|
||||||
|
The type of the title.
|
||||||
def get_title_type(self):
|
"""
|
||||||
"""Returns the type of the TMD's associated title."""
|
|
||||||
title_id_high = self.title_id[:8]
|
title_id_high = self.title_id[:8]
|
||||||
match title_id_high:
|
match title_id_high:
|
||||||
case '00000001':
|
case '00000001':
|
||||||
@@ -156,7 +277,7 @@ class TMD:
|
|||||||
case '00010002':
|
case '00010002':
|
||||||
return "SystemChannel"
|
return "SystemChannel"
|
||||||
case '00010004':
|
case '00010004':
|
||||||
return "GameWithChannel"
|
return "GameChannel"
|
||||||
case '00010005':
|
case '00010005':
|
||||||
return "DLC"
|
return "DLC"
|
||||||
case '00010008':
|
case '00010008':
|
||||||
@@ -165,7 +286,17 @@ class TMD:
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
def get_content_type(self):
|
def get_content_type(self):
|
||||||
"""Returns the type of content contained in the TMD's associated title."""
|
"""
|
||||||
|
Gets the type of content contained in the TMD's associated title.
|
||||||
|
|
||||||
|
Can be one of several possible values:
|
||||||
|
'Normal', 'Development/Unknown', 'Hash Tree', 'DLC', or 'Shared'
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The type of content.
|
||||||
|
"""
|
||||||
match self.content_type:
|
match self.content_type:
|
||||||
case '00000001':
|
case '00000001':
|
||||||
return "Normal"
|
return "Normal"
|
||||||
@@ -180,6 +311,35 @@ class TMD:
|
|||||||
case _:
|
case _:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
def get_num_contents(self):
|
def get_content_record(self, record) -> ContentRecord:
|
||||||
"""Returns the number of contents listed in the TMD."""
|
"""
|
||||||
return self.num_contents
|
Gets the content record at the specified index.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
record : int
|
||||||
|
The content record to be retrieved.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ContentRecord
|
||||||
|
A ContentRecord object containing the data in the content record.
|
||||||
|
"""
|
||||||
|
if record < self.num_contents:
|
||||||
|
return self.content_records[record]
|
||||||
|
else:
|
||||||
|
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) +
|
||||||
|
"' contents but index was '" + str(record) + "'!")
|
||||||
|
|
||||||
|
def set_title_id(self, title_id) -> None:
|
||||||
|
"""
|
||||||
|
Sets the Title ID of the title in the ticket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
title_id : str
|
||||||
|
The new Title ID of the title.
|
||||||
|
"""
|
||||||
|
if len(title_id) != 16:
|
||||||
|
raise ValueError("Invalid Title ID! Title IDs must be 8 bytes long.")
|
||||||
|
self.title_id = title_id
|
||||||
|
|||||||
52
src/libWiiPy/types.py
Normal file
52
src/libWiiPy/types.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# "types.py" from libWiiPy by NinjaCheetah & Contributors
|
||||||
|
# https://github.com/NinjaCheetah/libWiiPy
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContentRecord:
|
||||||
|
"""
|
||||||
|
A content record object that contains the details of a content contained in a title. This information must match
|
||||||
|
the content stored at the index in the record, or else the content will not decrypt properly, as the hash of the
|
||||||
|
decrypted data will not match the hash in the content record.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
content_id : int
|
||||||
|
ID of the content.
|
||||||
|
index : int
|
||||||
|
Index of the content in the list of contents.
|
||||||
|
content_type : int
|
||||||
|
The type of the content.
|
||||||
|
content_size : int
|
||||||
|
The size of the content.
|
||||||
|
content_hash
|
||||||
|
The SHA-1 hash of the decrypted content.
|
||||||
|
"""
|
||||||
|
content_id: int # The unique ID of the content.
|
||||||
|
index: int # The index of this content in the content record.
|
||||||
|
content_type: int # Type of content, possible values of: 0x0001: Normal, 0x4001: DLC, 0x8001: Shared.
|
||||||
|
content_size: int # Size of the content when decrypted.
|
||||||
|
content_hash: bytes # SHA-1 hash of the content when decrypted.
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TitleLimit:
|
||||||
|
"""
|
||||||
|
A TitleLimit object that contains the type of restriction and the limit. The limit type can be one of the following:
|
||||||
|
0 = None, 1 = Time Limit, 3 = None, or 4 = Launch Count. The maximum usage is then either the time in minutes the
|
||||||
|
title can be played or the maximum number of launches allowed for that title, based on the type of limit applied.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
limit_type : int
|
||||||
|
The type of play limit applied.
|
||||||
|
maximum_usage : int
|
||||||
|
The maximum value for the type of play limit applied.
|
||||||
|
"""
|
||||||
|
# The type of play limit applied.
|
||||||
|
# 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count
|
||||||
|
limit_type: int
|
||||||
|
# The maximum value of the limit applied.
|
||||||
|
maximum_usage: int
|
||||||
@@ -5,128 +5,333 @@
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import binascii
|
import binascii
|
||||||
|
from .shared import align_value, pad_bytes
|
||||||
|
|
||||||
|
|
||||||
class WAD:
|
class WAD:
|
||||||
"""Creates a WAD object to parse the header of a WAD file and retrieve the data contained in it."""
|
"""
|
||||||
def __init__(self, wad):
|
A WAD object that allows for either loading and editing an existing WAD or creating a new WAD from raw data.
|
||||||
self.wad = wad
|
|
||||||
self.wad_hdr_size: int
|
Attributes
|
||||||
self.wad_type: str
|
----------
|
||||||
self.wad_version: int
|
wad_type : str
|
||||||
|
The type of WAD, either ib for boot2 or Is for normal installable WADs.
|
||||||
|
wad_cert_size : int
|
||||||
|
The size of the WAD's certificate.
|
||||||
|
wad_crl_size : int
|
||||||
|
The size of the WAD's crl.
|
||||||
|
wad_tik_size : int
|
||||||
|
The size of the WAD's Ticket.
|
||||||
|
wad_tmd_size : int
|
||||||
|
The size of the WAD's TMD.
|
||||||
|
wad_content_size : int
|
||||||
|
The size of WAD's total content region.
|
||||||
|
wad_meta_size : int
|
||||||
|
The size of the WAD's meta/footer.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.wad_hdr_size: int = 64
|
||||||
|
self.wad_type: str = "Is"
|
||||||
|
self.wad_version: bytes = b'\x00\x00'
|
||||||
# === Sizes ===
|
# === Sizes ===
|
||||||
self.wad_cert_size: int
|
self.wad_cert_size: int = 0
|
||||||
self.wad_crl_size: int
|
self.wad_crl_size: int = 0
|
||||||
self.wad_tik_size: int
|
self.wad_tik_size: int = 0
|
||||||
self.wad_tmd_size: int
|
self.wad_tmd_size: int = 0
|
||||||
# This is the size of the content region, which contains all app files combined.
|
# This is the size of the content region, which contains all app files combined.
|
||||||
self.wad_content_size: int
|
self.wad_content_size: int = 0
|
||||||
self.wad_meta_size: int
|
self.wad_meta_size: int = 0
|
||||||
# === Offsets ===
|
# === Data ===
|
||||||
self.wad_cert_offset: int
|
self.wad_cert_data: bytes = b''
|
||||||
self.wad_crl_offset: int
|
self.wad_crl_data: bytes = b''
|
||||||
self.wad_tik_offset: int
|
self.wad_tik_data: bytes = b''
|
||||||
self.wad_tmd_offset: int
|
self.wad_tmd_data: bytes = b''
|
||||||
self.wad_content_offset: int
|
self.wad_content_data: bytes = b''
|
||||||
self.wad_meta_offset: int
|
self.wad_meta_data: bytes = b''
|
||||||
# Load header data from WAD stream
|
|
||||||
with io.BytesIO(wad) as waddata:
|
def load(self, wad_data) -> None:
|
||||||
|
"""
|
||||||
|
Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already
|
||||||
|
existing WAD file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
wad_data : bytes
|
||||||
|
The data for the WAD you wish to load.
|
||||||
|
"""
|
||||||
|
with io.BytesIO(wad_data) as wad_data:
|
||||||
|
# Read the first 8 bytes of the file to ensure that it's a WAD. Has two possible valid values for the two
|
||||||
|
# different types of WADs that might be encountered.
|
||||||
|
wad_data.seek(0x0)
|
||||||
|
wad_magic_bin = wad_data.read(8)
|
||||||
|
wad_magic_hex = binascii.hexlify(wad_magic_bin)
|
||||||
|
wad_magic = str(wad_magic_hex.decode())
|
||||||
|
if wad_magic != "0000002049730000" and wad_magic != "0000002069620000":
|
||||||
|
raise TypeError("This does not appear to be a valid WAD file.")
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
# Get the sizes of each data region contained within the WAD.
|
# Get the sizes of each data region contained within the WAD.
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
# Header length, which will always be 64 bytes, as it is padded out if it is shorter.
|
# Header length, which will always be 64 bytes, as it is padded out if it is shorter.
|
||||||
self.wad_hdr_size = 64
|
self.wad_hdr_size = 64
|
||||||
# WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is").
|
# WAD type, denoting whether this WAD contains boot2 ("ib"), or anything else ("Is").
|
||||||
waddata.seek(0x04)
|
wad_data.seek(0x04)
|
||||||
self.wad_type = str(waddata.read(2).decode())
|
self.wad_type = str(wad_data.read(2).decode())
|
||||||
# WAD version, this is always 0.
|
# WAD version, this is always 0.
|
||||||
waddata.seek(0x06)
|
wad_data.seek(0x06)
|
||||||
self.wad_version = waddata.read(2)
|
self.wad_version = wad_data.read(2)
|
||||||
# WAD cert size.
|
# WAD cert size.
|
||||||
waddata.seek(0x08)
|
wad_data.seek(0x08)
|
||||||
self.wad_cert_size = int(binascii.hexlify(waddata.read(4)), 16)
|
self.wad_cert_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||||
# WAD crl size.
|
# WAD crl size.
|
||||||
waddata.seek(0x0c)
|
wad_data.seek(0x0c)
|
||||||
self.wad_crl_size = int(binascii.hexlify(waddata.read(4)), 16)
|
self.wad_crl_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||||
# WAD ticket size.
|
# WAD ticket size.
|
||||||
waddata.seek(0x10)
|
wad_data.seek(0x10)
|
||||||
self.wad_tik_size = int(binascii.hexlify(waddata.read(4)), 16)
|
self.wad_tik_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||||
# WAD TMD size.
|
# WAD TMD size.
|
||||||
waddata.seek(0x14)
|
wad_data.seek(0x14)
|
||||||
self.wad_tmd_size = int(binascii.hexlify(waddata.read(4)), 16)
|
self.wad_tmd_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||||
# WAD content size.
|
# WAD content size.
|
||||||
waddata.seek(0x18)
|
wad_data.seek(0x18)
|
||||||
self.wad_content_size = int(binascii.hexlify(waddata.read(4)), 16)
|
self.wad_content_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||||
# Publisher of the title contained in the WAD.
|
# Time/build stamp for the title contained in the WAD.
|
||||||
waddata.seek(0x1c)
|
wad_data.seek(0x1c)
|
||||||
self.wad_meta_size = int(binascii.hexlify(waddata.read(4)), 16)
|
self.wad_meta_size = int(binascii.hexlify(wad_data.read(4)), 16)
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
# Calculate file offsets from sizes. Every section of the WAD is padded out to a multiple of 0x40.
|
# Calculate file offsets from sizes. Every section of the WAD is padded out to a multiple of 0x40.
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
self.wad_cert_offset = self.wad_hdr_size
|
wad_cert_offset = self.wad_hdr_size
|
||||||
# crl isn't ever used, however an entry for its size exists in the header, so its calculated just in case.
|
# crl isn't ever used, however an entry for its size exists in the header, so its calculated just in case.
|
||||||
self.wad_crl_offset = int(64 * round((self.wad_cert_offset + self.wad_cert_size) / 64))
|
wad_crl_offset = align_value(wad_cert_offset + self.wad_cert_size)
|
||||||
self.wad_tik_offset = int(64 * round((self.wad_crl_offset + self.wad_crl_size) / 64))
|
wad_tik_offset = align_value(wad_crl_offset + self.wad_crl_size)
|
||||||
self.wad_tmd_offset = int(64 * round((self.wad_tik_offset + self.wad_tik_size) / 64))
|
wad_tmd_offset = align_value(wad_tik_offset + self.wad_tik_size)
|
||||||
self.wad_content_offset = int(64 * round((self.wad_tmd_offset + self.wad_tmd_size) / 64))
|
# meta isn't guaranteed to be used, but some older SDK titles use it, and not reading it breaks things.
|
||||||
# meta is also never used, but Nintendo's tools calculate it so we should too.
|
wad_meta_offset = align_value(wad_tmd_offset + self.wad_tmd_size)
|
||||||
self.wad_meta_offset = int(64 * round((self.wad_content_offset + self.wad_content_size) / 64))
|
wad_content_offset = align_value(wad_meta_offset + self.wad_meta_size)
|
||||||
|
# ====================================================================================
|
||||||
|
# Load data for each WAD section based on the previously calculated offsets.
|
||||||
|
# ====================================================================================
|
||||||
|
# Cert data.
|
||||||
|
wad_data.seek(wad_cert_offset)
|
||||||
|
self.wad_cert_data = wad_data.read(self.wad_cert_size)
|
||||||
|
# Crl data.
|
||||||
|
wad_data.seek(wad_crl_offset)
|
||||||
|
self.wad_crl_data = wad_data.read(self.wad_crl_size)
|
||||||
|
# Ticket data.
|
||||||
|
wad_data.seek(wad_tik_offset)
|
||||||
|
self.wad_tik_data = wad_data.read(self.wad_tik_size)
|
||||||
|
# TMD data.
|
||||||
|
wad_data.seek(wad_tmd_offset)
|
||||||
|
self.wad_tmd_data = wad_data.read(self.wad_tmd_size)
|
||||||
|
# Content data.
|
||||||
|
wad_data.seek(wad_content_offset)
|
||||||
|
self.wad_content_data = wad_data.read(self.wad_content_size)
|
||||||
|
# Meta data.
|
||||||
|
wad_data.seek(wad_meta_offset)
|
||||||
|
self.wad_meta_data = wad_data.read(self.wad_meta_size)
|
||||||
|
|
||||||
def get_cert_region(self):
|
def dump(self) -> bytes:
|
||||||
"""Returns the offset and size for the cert data."""
|
"""
|
||||||
return self.wad_cert_offset, self.wad_cert_size
|
Dumps the WAD object into the raw WAD file. This allows for creating a WAD file from the data contained in
|
||||||
|
the WAD object.
|
||||||
|
|
||||||
def get_crl_region(self):
|
Returns
|
||||||
"""Returns the offset and size for the crl data."""
|
-------
|
||||||
return self.wad_crl_offset, self.wad_crl_size
|
bytes
|
||||||
|
The full WAD file as bytes.
|
||||||
|
"""
|
||||||
|
wad_data = b''
|
||||||
|
# Lead-in data.
|
||||||
|
wad_data += b'\x00\x00\x00\x20'
|
||||||
|
# WAD type.
|
||||||
|
wad_data += str.encode(self.wad_type)
|
||||||
|
# WAD version.
|
||||||
|
wad_data += self.wad_version
|
||||||
|
# WAD cert size.
|
||||||
|
wad_data += int.to_bytes(self.wad_cert_size, 4)
|
||||||
|
# WAD crl size.
|
||||||
|
wad_data += int.to_bytes(self.wad_crl_size, 4)
|
||||||
|
# WAD ticket size.
|
||||||
|
wad_data += int.to_bytes(self.wad_tik_size, 4)
|
||||||
|
# WAD TMD size.
|
||||||
|
wad_data += int.to_bytes(self.wad_tmd_size, 4)
|
||||||
|
# WAD content size.
|
||||||
|
wad_data += int.to_bytes(self.wad_content_size, 4)
|
||||||
|
# WAD meta size.
|
||||||
|
wad_data += int.to_bytes(self.wad_meta_size, 4)
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Retrieve the cert data and write it out.
|
||||||
|
wad_data += self.get_cert_data()
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Retrieve the crl data and write it out.
|
||||||
|
wad_data += self.get_crl_data()
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Retrieve the ticket data and write it out.
|
||||||
|
wad_data += self.get_ticket_data()
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Retrieve the TMD data and write it out.
|
||||||
|
wad_data += self.get_tmd_data()
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Retrieve the meta/footer data and write it out.
|
||||||
|
wad_data += self.get_meta_data()
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Retrieve the content data and write it out.
|
||||||
|
wad_data += self.get_content_data()
|
||||||
|
wad_data = pad_bytes(wad_data)
|
||||||
|
# Return the raw WAD file for the data contained in the object.
|
||||||
|
return wad_data
|
||||||
|
|
||||||
def get_ticket_region(self):
|
def get_wad_type(self) -> str:
|
||||||
"""Returns the offset and size for the ticket data."""
|
"""
|
||||||
return self.wad_tik_offset, self.wad_tik_size
|
Gets the type of the WAD.
|
||||||
|
|
||||||
def get_tmd_region(self):
|
Returns
|
||||||
"""Returns the offset and size for the TMD data."""
|
-------
|
||||||
return self.wad_tmd_offset, self.wad_tmd_size
|
str
|
||||||
|
The type of the WAD. This is 'Is', unless the WAD contains boot2, where it is 'ib'.
|
||||||
def get_content_region(self):
|
"""
|
||||||
"""Returns the offset and size for the content of the WAD."""
|
|
||||||
return self.wad_content_offset, self.wad_tmd_size
|
|
||||||
|
|
||||||
def get_wad_type(self):
|
|
||||||
"""Returns the type of the WAD. This is 'Is' unless the WAD contains boot2 where it is 'ib'."""
|
|
||||||
return self.wad_type
|
return self.wad_type
|
||||||
|
|
||||||
def get_cert_data(self):
|
def get_cert_data(self) -> bytes:
|
||||||
"""Returns the certificate data from the WAD."""
|
"""
|
||||||
waddata = io.BytesIO(self.wad)
|
Gets the certificate data from the WAD.
|
||||||
waddata.seek(self.wad_cert_offset)
|
|
||||||
cert_data = waddata.read(self.wad_cert_size)
|
|
||||||
return cert_data
|
|
||||||
|
|
||||||
def get_crl_data(self):
|
Returns
|
||||||
"""Returns the crl data from the WAD, if it exists."""
|
-------
|
||||||
waddata = io.BytesIO(self.wad)
|
bytes
|
||||||
waddata.seek(self.wad_crl_offset)
|
The certificate data.
|
||||||
crl_data = waddata.read(self.wad_crl_size)
|
"""
|
||||||
return crl_data
|
return self.wad_cert_data
|
||||||
|
|
||||||
def get_ticket_data(self):
|
def get_crl_data(self) -> bytes:
|
||||||
"""Returns the ticket data from the WAD."""
|
"""
|
||||||
waddata = io.BytesIO(self.wad)
|
Gets the crl data from the WAD, if it exists.
|
||||||
waddata.seek(self.wad_tik_offset)
|
|
||||||
ticket_data = waddata.read(self.wad_tik_size)
|
|
||||||
return ticket_data
|
|
||||||
|
|
||||||
def get_tmd_data(self):
|
Returns
|
||||||
"""Returns the TMD data from the WAD."""
|
-------
|
||||||
waddata = io.BytesIO(self.wad)
|
bytes
|
||||||
waddata.seek(self.wad_tmd_offset)
|
The crl data.
|
||||||
tmd_data = waddata.read(self.wad_tmd_size)
|
"""
|
||||||
return tmd_data
|
return self.wad_crl_data
|
||||||
|
|
||||||
def get_content_data(self):
|
def get_ticket_data(self) -> bytes:
|
||||||
"""Returns the content of the WAD."""
|
"""
|
||||||
waddata = io.BytesIO(self.wad)
|
Gets the ticket data from the WAD.
|
||||||
waddata.seek(self.wad_content_offset)
|
|
||||||
content_data = waddata.read(self.wad_content_size)
|
Returns
|
||||||
return content_data
|
-------
|
||||||
|
bytes
|
||||||
|
The ticket data.
|
||||||
|
"""
|
||||||
|
return self.wad_tik_data
|
||||||
|
|
||||||
|
def get_tmd_data(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Returns the TMD data from the WAD.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The TMD data.
|
||||||
|
"""
|
||||||
|
return self.wad_tmd_data
|
||||||
|
|
||||||
|
def get_content_data(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets the content of the WAD.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The content data.
|
||||||
|
"""
|
||||||
|
return self.wad_content_data
|
||||||
|
|
||||||
|
def get_meta_data(self) -> bytes:
|
||||||
|
"""
|
||||||
|
Gets the meta region of the WAD, which is typically unused.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bytes
|
||||||
|
The meta region.
|
||||||
|
"""
|
||||||
|
return self.wad_meta_data
|
||||||
|
|
||||||
|
def set_cert_data(self, cert_data) -> None:
|
||||||
|
"""
|
||||||
|
Sets the certificate data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cert_data : bytes
|
||||||
|
The new certificate data.
|
||||||
|
"""
|
||||||
|
self.wad_cert_data = cert_data
|
||||||
|
# Calculate the size of the new cert data.
|
||||||
|
self.wad_cert_size = len(cert_data)
|
||||||
|
|
||||||
|
def set_crl_data(self, crl_data) -> None:
|
||||||
|
"""
|
||||||
|
Sets the crl data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
crl_data : bytes
|
||||||
|
The new crl data.
|
||||||
|
"""
|
||||||
|
self.wad_crl_data = crl_data
|
||||||
|
# Calculate the size of the new crl data.
|
||||||
|
self.wad_crl_size = len(crl_data)
|
||||||
|
|
||||||
|
def set_tmd_data(self, tmd_data) -> None:
|
||||||
|
"""
|
||||||
|
Sets the TMD data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tmd_data : bytes
|
||||||
|
The new TMD data.
|
||||||
|
"""
|
||||||
|
self.wad_tmd_data = tmd_data
|
||||||
|
# Calculate the size of the new TMD data.
|
||||||
|
self.wad_tmd_size = len(tmd_data)
|
||||||
|
|
||||||
|
def set_ticket_data(self, tik_data) -> None:
|
||||||
|
"""
|
||||||
|
Sets the Ticket data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tik_data : bytes
|
||||||
|
The new TMD data.
|
||||||
|
"""
|
||||||
|
self.wad_tik_data = tik_data
|
||||||
|
# Calculate the size of the new Ticket data.
|
||||||
|
self.wad_tik_size = len(tik_data)
|
||||||
|
|
||||||
|
def set_content_data(self, content_data) -> None:
|
||||||
|
"""
|
||||||
|
Sets the content data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
content_data : bytes
|
||||||
|
The new content data.
|
||||||
|
"""
|
||||||
|
self.wad_content_data = content_data
|
||||||
|
# Calculate the size of the new content data.
|
||||||
|
self.wad_content_size = len(content_data)
|
||||||
|
|
||||||
|
def set_meta_data(self, meta_data) -> None:
|
||||||
|
"""
|
||||||
|
Sets the meta data of the WAD. Also calculates the new size.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
meta_data : bytes
|
||||||
|
The new meta data.
|
||||||
|
"""
|
||||||
|
self.wad_meta_data = meta_data
|
||||||
|
# Calculate the size of the new meta data.
|
||||||
|
self.wad_meta_size = len(meta_data)
|
||||||
|
|||||||
Reference in New Issue
Block a user