18 Commits

Author SHA1 Message Date
6a81722ec5 Read/write reserved data in tmd.py, since it mattered for the DSi, it may matter here 2024-05-09 11:19:29 -04:00
ecc68d9e57 Updated definitions of TMD properties based on new information 2024-05-09 11:11:54 -04:00
c42dc66209 Update README.md 2024-05-07 21:35:22 -04:00
045613216a Replaced unnecessary BytesIO usages with standard variables 2024-05-06 19:34:18 -04:00
98666285db Improved comments, moved TID to IV conversion into a function in shared.py 2024-05-03 22:32:51 -04:00
ba320a29de Improved IV handling code for Title Keys in crypto.py 2024-05-03 17:27:52 -04:00
9890a6dbac Added function to crypto.py for encrypting a title key 2024-05-03 15:10:17 -04:00
c92a8096ea Rename fallback_endpoint to wiiu_endpoint in nus.py 2024-05-02 22:20:59 -04:00
99a55a3de5 Added the ability to use the Wii U NUS servers to nus.py 2024-05-01 22:22:30 -04:00
4a3e9f8e7f Merge remote-tracking branch 'origin/main' 2024-04-30 23:07:22 -04:00
8eeebd1d75 Change endpoint URLs for NUS, this greatly improves download speed 2024-04-30 23:07:14 -04:00
3b7a2d09b0 Corrected small error in tmd.py, GameChannel is the "correct" name 2024-04-26 21:34:40 -04:00
a85beac602 Correctly pack boot2 WADs as type "ib" and not "Is" 2024-04-14 12:58:40 -04:00
338446efcb Merge pull request #17 from NinjaCheetah/nus_download
Add NUS Downloading
2024-04-05 01:05:43 -04:00
ccbc2e262b Rewrote nus.py to be functions instead of an NUSDownloader class because the class was unnecessary and made things more complex 2024-04-05 00:09:19 -04:00
rmc
17a894dc0d Add support for boot2 WADs
This is the hard work they pay me for.
2024-04-03 23:22:23 -04:00
60918f1a39 Update README.md 2024-04-03 23:06:58 -04:00
fa6c9eb740 Added nus.py for handling NUS downloads, more or less completed already 2024-04-03 22:59:36 -04:00
12 changed files with 514 additions and 226 deletions

View File

@@ -10,6 +10,7 @@ libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSh
This list will expand as libWiiPy is developed, but these features are currently available: This list will expand as libWiiPy is developed, but these features are currently available:
- TMD, ticket, and WAD parsing - TMD, ticket, and WAD parsing
- WAD content extraction, decryption, re-encryption, and packing - WAD content extraction, decryption, re-encryption, and packing
- Downloading titles from the NUS
# Usage # 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. 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.
@@ -30,27 +31,27 @@ Please be aware that because libWiiPy is in a very early state right now, many f
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 for the Inspiration and Previous Projects ## Special Thanks for the Inspiration and Previous Projects
- Xuzz, SquidMan, megazig, Matt_P, Omega and The Lemon Man for creating Wii.py - Xuzz, SquidMan, megazig, Matt_P, Omega and The Lemon Man for creating Wii.py
- 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

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "libWiiPy" name = "libWiiPy"
version = "0.2.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" }
@@ -15,6 +15,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"pycryptodome", "pycryptodome",
"requests"
] ]
[project.urls] [project.urls]

View File

@@ -1,2 +1,3 @@
build build
pycryptodome pycryptodome
requests

View File

@@ -6,6 +6,8 @@
from .commonkeys import * from .commonkeys import *
from .content import * from .content import *
from .ticket import * from .ticket import *
from .crypto import *
from .title import * from .title import *
from .tmd import * from .tmd import *
from .wad import * from .wad import *
from .nus import *

View File

@@ -79,21 +79,18 @@ class ContentRegion:
bytes bytes
The full WAD file as bytes. The full WAD file as bytes.
""" """
# Open the stream and begin writing data to it. content_region_data = b''
with io.BytesIO() as content_region_data: for content in self.content_list:
for content in self.content_list: # Calculate padding after this content before the next one.
# Calculate padding after this content before the next one. padding_bytes = 0
padding_bytes = 0 if (len(content) % 64) != 0:
if (len(content) % 64) != 0: padding_bytes = 64 - (len(content) % 64)
padding_bytes = 64 - (len(content) % 64) # Write content data, then the padding afterward if necessary.
# Write content data, then the padding afterward if necessary. content_region_data += content
content_region_data.write(content) if padding_bytes > 0:
if padding_bytes > 0: content_region_data += b'\x00' * padding_bytes
content_region_data.write(b'\x00' * padding_bytes)
content_region_data.seek(0x0)
content_region_raw = content_region_data.read()
# Return the raw ContentRegion for the data contained in the object. # Return the raw ContentRegion for the data contained in the object.
return content_region_raw return content_region_data
def get_enc_content_by_index(self, index: int) -> bytes: def get_enc_content_by_index(self, index: int) -> bytes:
""" """

View File

@@ -3,10 +3,12 @@
import struct import struct
from .commonkeys import get_common_key from .commonkeys import get_common_key
from .shared import convert_tid_to_iv
from Crypto.Cipher import AES from Crypto.Cipher import AES
def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes: 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. Gets the decrypted version of the encrypted Title Key provided.
@@ -17,9 +19,9 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
title_key_enc : bytes title_key_enc : bytes
The encrypted Title Key. The encrypted Title Key.
common_key_index : int common_key_index : int
The index of the common key to be returned. The index of the common key used to encrypt the Title Key.
title_id : bytes title_id : bytes, str
The title ID of the title that the key is for. The Title ID of the title that the key is for.
Returns Returns
------- -------
@@ -28,8 +30,10 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
""" """
# Load the correct common key for the title. # Load the correct common key for the title.
common_key = get_common_key(common_key_index) common_key = get_common_key(common_key_index)
# Calculate the IV by adding 8 bytes to the end of the Title ID. # Convert the IV into the correct format based on the type provided.
title_key_iv = title_id + (b'\x00' * 8) 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. # Create a new AES object with the values provided.
aes = AES.new(common_key, AES.MODE_CBC, title_key_iv) aes = AES.new(common_key, AES.MODE_CBC, title_key_iv)
# Decrypt the Title Key using the AES object. # Decrypt the Title Key using the AES object.
@@ -37,6 +41,39 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes:
return title_key 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: def decrypt_content(content_enc, title_key, content_index, content_length) -> bytes:
""" """
Gets the decrypted version of the encrypted content. Gets the decrypted version of the encrypted content.
@@ -72,8 +109,7 @@ def decrypt_content(content_enc, title_key, content_index, content_length) -> by
# Decrypt the content using the AES object. # Decrypt the content using the AES object.
content_dec = aes.decrypt(content_enc) content_dec = aes.decrypt(content_enc)
# Trim additional bytes that may have been added so the content is the correct size. # Trim additional bytes that may have been added so the content is the correct size.
while len(content_dec) > content_length: content_dec = content_dec[:content_length]
content_dec = content_dec[:-1]
return content_dec return content_dec

233
src/libWiiPy/nus.py Normal file
View 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

View File

@@ -4,6 +4,9 @@
# This file defines general functions that may be useful in other modules of libWiiPy. Putting them here cuts down on # This file defines general functions that may be useful in other modules of libWiiPy. Putting them here cuts down on
# clutter in other files. # clutter in other files.
import binascii
def align_value(value, alignment=64) -> int: def align_value(value, alignment=64) -> int:
""" """
Aligns the provided value to the set alignment (defaults to 64). Aligns the provided value to the set alignment (defaults to 64).
@@ -26,22 +29,43 @@ def align_value(value, alignment=64) -> int:
return value return value
def pad_bytes_stream(data, alignment=64) -> bytes: def pad_bytes(data, alignment=64) -> bytes:
""" """
Pads the provided bytes stream to the provided alignment (defaults to 64). Pads the provided bytes object to the provided alignment (defaults to 64).
Parameters Parameters
---------- ----------
data : BytesIO data : bytes
The data to align. The data to align.
alignment : int alignment : int
The number to align to. Defaults to 64. The number to align to. Defaults to 64.
Returns Returns
------- -------
BytesIO bytes
The aligned data. The aligned data.
""" """
while (data.getbuffer().nbytes % alignment) != 0: while (len(data) % alignment) != 0:
data.write(b'\x00') data += b'\x00'
return data 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

View File

@@ -99,10 +99,9 @@ class Ticket:
self.console_id = int.from_bytes(ticket_data.read(4)) self.console_id = int.from_bytes(ticket_data.read(4))
# Title ID. # Title ID.
ticket_data.seek(0x1DC) ticket_data.seek(0x1DC)
self.title_id = ticket_data.read(8) self.title_id = binascii.hexlify(ticket_data.read(8))
# Title ID (as a string). # Title ID (as a string).
title_id_hex = binascii.hexlify(self.title_id) self.title_id_str = str(self.title_id.decode())
self.title_id_str = str(title_id_hex.decode())
# Unknown data 1. # Unknown data 1.
ticket_data.seek(0x1E4) ticket_data.seek(0x1E4)
self.unknown1 = ticket_data.read(2) self.unknown1 = ticket_data.read(2)
@@ -147,68 +146,62 @@ class Ticket:
bytes bytes
The full Ticket file as bytes. The full Ticket file as bytes.
""" """
# Open the stream and begin writing to it. ticket_data = b''
with io.BytesIO() as ticket_data: # Signature type.
# Signature type. ticket_data += self.signature_type
ticket_data.write(self.signature_type) # Signature data.
# Signature data. ticket_data += self.signature
ticket_data.write(self.signature) # Padding to 64 bytes.
# Padding to 64 bytes. ticket_data += b'\x00' * 60
ticket_data.write(b'\x00' * 60) # Signature issuer.
# Signature issuer. ticket_data += str.encode(self.signature_issuer)
ticket_data.write(str.encode(self.signature_issuer)) # ECDH data.
# ECDH data. ticket_data += self.ecdh_data
ticket_data.write(self.ecdh_data) # Ticket version.
# Ticket version. ticket_data += int.to_bytes(self.ticket_version, 1)
ticket_data.write(int.to_bytes(self.ticket_version, 1)) # Reserved (all \0x00).
# Reserved (all \0x00). ticket_data += b'\x00\x00'
ticket_data.write(b'\x00\x00') # Title Key.
# Title Key. ticket_data += self.title_key_enc
ticket_data.write(self.title_key_enc) # Unknown (write \0x00).
# Unknown (write \0x00). ticket_data += b'\x00'
ticket_data.write(b'\x00') # Ticket ID.
# Ticket ID. ticket_data += self.ticket_id
ticket_data.write(self.ticket_id) # Console ID.
# Console ID. ticket_data += int.to_bytes(self.console_id, 4)
ticket_data.write(int.to_bytes(self.console_id, 4)) # Title ID.
# Title ID. ticket_data += binascii.unhexlify(self.title_id)
ticket_data.write(self.title_id) # Unknown data 1.
# Unknown data 1. ticket_data += self.unknown1
ticket_data.write(self.unknown1) # Title version.
# Title version. title_version_high = round(self.title_version / 256)
title_version_high = round(self.title_version / 256) ticket_data += int.to_bytes(title_version_high, 1)
ticket_data.write(int.to_bytes(title_version_high, 1)) title_version_low = self.title_version % 256
title_version_low = self.title_version % 256 ticket_data += int.to_bytes(title_version_low, 1)
ticket_data.write(int.to_bytes(title_version_low, 1)) # Permitted titles mask.
# Permitted titles mask. ticket_data += self.permitted_titles
ticket_data.write(self.permitted_titles) # Permit mask.
# Permit mask. ticket_data += self.permit_mask
ticket_data.write(self.permit_mask) # Title Export allowed.
# Title Export allowed. ticket_data += int.to_bytes(self.title_export_allowed, 1)
ticket_data.write(int.to_bytes(self.title_export_allowed, 1)) # Common Key index.
# Common Key index. ticket_data += int.to_bytes(self.common_key_index, 1)
ticket_data.write(int.to_bytes(self.common_key_index, 1)) # Unknown data 2.
# Unknown data 2. ticket_data += self.unknown2
ticket_data.write(self.unknown2) # Content access permissions.
# Content access permissions. ticket_data += self.content_access_permissions
ticket_data.write(self.content_access_permissions) # Padding (always \x00).
# Padding (always \x00). ticket_data += b'\x00\x00'
ticket_data.write(b'\x00\x00') # Iterate over Title Limit objects, write them back into raw data, then add them to the Ticket.
# 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)):
for title_limit in range(len(self.title_limits_list)): title_limit_data = b''
title_limit_data = io.BytesIO() # Write all fields from the title limit entry.
# 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.write(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)
title_limit_data.write(int.to_bytes(self.title_limits_list[title_limit].maximum_usage, 4)) # Write the entry to the ticket.
# Seek to the start and write the entry to the Ticket. ticket_data += title_limit_data
title_limit_data.seek(0x0)
ticket_data.write(title_limit_data.read())
title_limit_data.close()
# Set the Ticket attribute of the object to the new raw Ticket.
ticket_data.seek(0x0)
ticket_data_raw = ticket_data.read()
# Return the raw TMD for the data contained in the object. # Return the raw TMD for the data contained in the object.
return ticket_data_raw return ticket_data
def get_title_id(self) -> str: def get_title_id(self) -> str:
""" """

View File

@@ -70,6 +70,9 @@ class Title:
wad_data : bytes wad_data : bytes
The raw data of the WAD. 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. # Dump the TMD and set it in the WAD.
self.wad.set_tmd_data(self.tmd.dump()) self.wad.set_tmd_data(self.tmd.dump())
# Dump the Ticket and set it in the WAD. # Dump the Ticket and set it in the WAD.

View File

@@ -35,8 +35,8 @@ class TMD:
self.sig: bytes = b'' self.sig: bytes = b''
self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x" self.issuer: bytes = b'' # Follows the format "Root-CA%08x-CP%08x"
self.tmd_version: int = 0 # This seems to always be 0 no matter what? self.tmd_version: int = 0 # This seems to always be 0 no matter what?
self.ca_crl_version: int = 0 self.ca_crl_version: int = 0 # Certificate Authority Certificate Revocation List version
self.signer_crl_version: int = 0 self.signer_crl_version: int = 0 # Certificate Policy Certificate Revocation List version
self.vwii: int = 0 # Whether the title is for the vWii. 0 = No, 1 = Yes self.vwii: int = 0 # Whether the title is for the vWii. 0 = No, 1 = Yes
self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on. self.ios_tid: str = "" # The Title ID of the IOS version the associated title runs on.
self.ios_version: int = 0 # The IOS version the associated title runs on. self.ios_version: int = 0 # The IOS version the associated title runs on.
@@ -44,17 +44,19 @@ class TMD:
self.content_type: str = "" # The type of content contained within the associated title. self.content_type: str = "" # The type of content contained within the associated title.
self.group_id: int = 0 # The ID of the publisher of the associated title. self.group_id: int = 0 # The ID of the publisher of the associated title.
self.region: int = 0 # The ID of the region of the associated title. self.region: int = 0 # The ID of the region of the associated title.
self.ratings: bytes = b'' self.ratings: bytes = b'' # The parental controls rating of the associated title.
self.reserved1: bytes = b'' # Unknown data labeled "Reserved" on WiiBrew.
self.ipc_mask: bytes = b'' self.ipc_mask: bytes = b''
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
self.access_rights: bytes = b'' self.access_rights: bytes = b''
self.title_version: int = 0 # The version of the associated title. self.title_version: int = 0 # The version of the associated title.
self.num_contents: int = 0 # The number of contents contained in the associated title. self.num_contents: int = 0 # The number of contents contained in the associated title.
self.boot_index: int = 0 self.boot_index: int = 0 # The content index that contains the bootable executable.
self.content_records: List[ContentRecord] = [] self.content_records: List[ContentRecord] = []
def load(self, tmd: bytes) -> None: def load(self, tmd: bytes) -> None:
""" """
Loads raw TMD data and sets all attributes of the WAD object. This allows for manipulating an already Loads raw TMD data and sets all attributes of the TMD object. This allows for manipulating an already
existing TMD. existing TMD.
Parameters Parameters
@@ -74,10 +76,10 @@ class TMD:
# TMD version, seems to usually be 0, but I've seen references to other numbers. # TMD version, seems to usually be 0, but I've seen references to other numbers.
tmd_data.seek(0x180) tmd_data.seek(0x180)
self.tmd_version = int.from_bytes(tmd_data.read(1)) self.tmd_version = int.from_bytes(tmd_data.read(1))
# Root certificate crl version. # Certificate Authority CRL version.
tmd_data.seek(0x181) tmd_data.seek(0x181)
self.ca_crl_version = int.from_bytes(tmd_data.read(1)) self.ca_crl_version = int.from_bytes(tmd_data.read(1))
# Signer crl version. # Certificate Policy CRL version.
tmd_data.seek(0x182) tmd_data.seek(0x182)
self.signer_crl_version = int.from_bytes(tmd_data.read(1)) self.signer_crl_version = int.from_bytes(tmd_data.read(1))
# If this is a vWii title or not. # If this is a vWii title or not.
@@ -103,16 +105,22 @@ class TMD:
# Publisher of the title. # Publisher of the title.
tmd_data.seek(0x198) tmd_data.seek(0x198)
self.group_id = int.from_bytes(tmd_data.read(2)) self.group_id = int.from_bytes(tmd_data.read(2))
# 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.
tmd_data.seek(0x19C) tmd_data.seek(0x19C)
region_hex = tmd_data.read(2) region_hex = tmd_data.read(2)
self.region = int.from_bytes(region_hex) self.region = int.from_bytes(region_hex)
# Likely the localized content rating for the title. (ESRB, CERO, PEGI, etc.) # Content rating of the title for parental controls. Likely based on ESRB, CERO, PEGI, etc. rating.
tmd_data.seek(0x19E) tmd_data.seek(0x19E)
self.ratings = tmd_data.read(16) self.ratings = tmd_data.read(16)
# "Reserved" data 1.
tmd_data.seek(0x1AE)
self.reserved1 = tmd_data.read(12)
# IPC mask. # IPC mask.
tmd_data.seek(0x1BA) tmd_data.seek(0x1BA)
self.ipc_mask = tmd_data.read(12) self.ipc_mask = tmd_data.read(12)
# "Reserved" data 2.
tmd_data.seek(0x1C6)
self.reserved2 = tmd_data.read(18)
# Access rights of the title; DVD-video access and AHBPROT. # Access rights of the title; DVD-video access and AHBPROT.
tmd_data.seek(0x1D8) tmd_data.seek(0x1D8)
self.access_rights = tmd_data.read(4) self.access_rights = tmd_data.read(4)
@@ -125,7 +133,7 @@ class TMD:
# The number of contents listed in the TMD. # The number of contents listed in the TMD.
tmd_data.seek(0x1DE) tmd_data.seek(0x1DE)
self.num_contents = int.from_bytes(tmd_data.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.
tmd_data.seek(0x1E0) tmd_data.seek(0x1E0)
self.boot_index = int.from_bytes(tmd_data.read(2)) self.boot_index = int.from_bytes(tmd_data.read(2))
# Get content records for the number of contents in num_contents. # Get content records for the number of contents in num_contents.
@@ -148,78 +156,72 @@ class TMD:
bytes bytes
The full TMD file as bytes. The full TMD file as bytes.
""" """
# Open the stream and begin writing to it. tmd_data = b''
with io.BytesIO() as tmd_data: # Signed blob header.
# Signed blob header. tmd_data += self.blob_header
tmd_data.write(self.blob_header) # Signing certificate issuer.
# Signing certificate issuer. tmd_data += self.issuer
tmd_data.write(self.issuer) # TMD version.
# TMD version. tmd_data += int.to_bytes(self.tmd_version, 1)
tmd_data.write(int.to_bytes(self.tmd_version, 1)) # Certificate Authority CRL version.
# Root certificate crl version. tmd_data += int.to_bytes(self.ca_crl_version, 1)
tmd_data.write(int.to_bytes(self.ca_crl_version, 1)) # Certificate Policy CRL version.
# Signer crl version. tmd_data += int.to_bytes(self.signer_crl_version, 1)
tmd_data.write(int.to_bytes(self.signer_crl_version, 1)) # If this is a vWii title or not.
# If this is a vWii title or not. tmd_data += int.to_bytes(self.vwii, 1)
tmd_data.write(int.to_bytes(self.vwii, 1)) # IOS Title ID.
# IOS Title ID. tmd_data += binascii.unhexlify(self.ios_tid)
tmd_data.write(binascii.unhexlify(self.ios_tid)) # Title's Title ID.
# Title's Title ID. tmd_data += binascii.unhexlify(self.title_id)
tmd_data.write(binascii.unhexlify(self.title_id)) # Content type.
# Content type. tmd_data += binascii.unhexlify(self.content_type)
tmd_data.write(binascii.unhexlify(self.content_type)) # Group ID.
# Group ID. tmd_data += int.to_bytes(self.group_id, 2)
tmd_data.write(int.to_bytes(self.group_id, 2)) # 2 bytes of zero for reasons.
# 2 bytes of zero for reasons. tmd_data += b'\x00\x00'
tmd_data.write(b'\x00\x00') # Region.
# Region. tmd_data += int.to_bytes(self.region, 2)
tmd_data.write(int.to_bytes(self.region, 2)) # Parental Controls Ratings.
# Ratings. tmd_data += self.ratings
tmd_data.write(self.ratings) # "Reserved" 1.
# Reserved (all \x00). tmd_data += self.reserved1
tmd_data.write(b'\x00' * 12) # IPC mask.
# IPC mask. tmd_data += self.ipc_mask
tmd_data.write(self.ipc_mask) # "Reserved" 2.
# Reserved (all \x00). tmd_data += self.reserved2
tmd_data.write(b'\x00' * 18) # Access rights.
# Access rights. tmd_data += self.access_rights
tmd_data.write(self.access_rights) # Title version.
# Title version. title_version_high = round(self.title_version / 256)
title_version_high = round(self.title_version / 256) tmd_data += int.to_bytes(title_version_high, 1)
tmd_data.write(int.to_bytes(title_version_high, 1)) title_version_low = self.title_version % 256
title_version_low = self.title_version % 256 tmd_data += int.to_bytes(title_version_low, 1)
tmd_data.write(int.to_bytes(title_version_low, 1)) # Number of contents.
# Number of contents. tmd_data += int.to_bytes(self.num_contents, 2)
tmd_data.write(int.to_bytes(self.num_contents, 2)) # Boot index.
# Boot index. tmd_data += int.to_bytes(self.boot_index, 2)
tmd_data.write(int.to_bytes(self.boot_index, 2)) # Minor version. Unused so write \x00.
# Minor version. Unused so write \x00. tmd_data += b'\x00\x00'
tmd_data.write(b'\x00\x00') # Iterate over content records, write them back into raw data, then add them to the TMD.
# Iterate over content records, write them back into raw data, then add them to the TMD. for content_record in range(self.num_contents):
for content_record in range(self.num_contents): content_data = b''
content_data = io.BytesIO() # Write all fields from the content record.
# Write all fields from the content record. content_data += int.to_bytes(self.content_records[content_record].content_id, 4)
content_data.write(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.write(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.write(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.write(int.to_bytes(self.content_records[content_record].content_size, 8)) content_data += binascii.unhexlify(self.content_records[content_record].content_hash)
content_data.write(binascii.unhexlify(self.content_records[content_record].content_hash)) # Write the record to the TMD.
# Seek to the start and write the record to the TMD. tmd_data += content_data
content_data.seek(0x0)
tmd_data.write(content_data.read())
content_data.close()
# Set the TMD attribute of the object to the new raw TMD.
tmd_data.seek(0x0)
tmd_data_raw = tmd_data.read()
# Return the raw TMD for the data contained in the object. # Return the raw TMD for the data contained in the object.
return tmd_data_raw return tmd_data
def get_title_region(self) -> str: def get_title_region(self) -> str:
""" """
Gets the region of the TMD's associated title. Gets the region of the TMD's associated title.
Can be one of several possible values: Can be one of several possible values:
'JAP', 'USA', 'EUR', 'NONE', or 'KOR'. 'JAP', 'USA', 'EUR', 'WORLD', or 'KOR'.
Returns Returns
------- -------
@@ -234,7 +236,7 @@ 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"
@@ -257,7 +259,7 @@ class TMD:
Gets the type of the TMD's associated title. Gets the type of the TMD's associated title.
Can be one of several possible values: Can be one of several possible values:
'System', 'Game', 'Channel', 'SystemChannel', 'GameWithChannel', or 'HiddenChannel' 'System', 'Game', 'Channel', 'SystemChannel', 'GameChannel', or 'HiddenChannel'
Returns Returns
------- -------
@@ -275,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':

View File

@@ -5,7 +5,7 @@
import io import io
import binascii import binascii
from .shared import align_value, pad_bytes_stream from .shared import align_value, pad_bytes
class WAD: class WAD:
@@ -15,7 +15,7 @@ class WAD:
Attributes Attributes
---------- ----------
wad_type : str wad_type : str
The type of WAD, either ib for boot2 or Is for normal installable WADs. libWiiPy only supports Is currently. The type of WAD, either ib for boot2 or Is for normal installable WADs.
wad_cert_size : int wad_cert_size : int
The size of the WAD's certificate. The size of the WAD's certificate.
wad_crl_size : int wad_crl_size : int
@@ -60,15 +60,14 @@ class WAD:
The data for the WAD you wish to load. The data for the WAD you wish to load.
""" """
with io.BytesIO(wad_data) as wad_data: with io.BytesIO(wad_data) as wad_data:
# Read the first 8 bytes of the file to ensure that it's a WAD. This will currently reject boot2 WADs, but # Read the first 8 bytes of the file to ensure that it's a WAD. Has two possible valid values for the two
# this tool cannot handle them correctly right now anyway. # different types of WADs that might be encountered.
wad_data.seek(0x0) wad_data.seek(0x0)
wad_magic_bin = wad_data.read(8) wad_magic_bin = wad_data.read(8)
wad_magic_hex = binascii.hexlify(wad_magic_bin) wad_magic_hex = binascii.hexlify(wad_magic_bin)
wad_magic = str(wad_magic_hex.decode()) wad_magic = str(wad_magic_hex.decode())
if wad_magic != "0000002049730000": if wad_magic != "0000002049730000" and wad_magic != "0000002069620000":
raise TypeError("This does not appear to be a valid WAD file, or is a boot2 WAD, which is not currently" raise TypeError("This does not appear to be a valid WAD file.")
" supported by this library.")
# ==================================================================================== # ====================================================================================
# Get the sizes of each data region contained within the WAD. # Get the sizes of each data region contained within the WAD.
# ==================================================================================== # ====================================================================================
@@ -141,50 +140,46 @@ class WAD:
bytes bytes
The full WAD file as bytes. The full WAD file as bytes.
""" """
# Open the stream and begin writing data to it. wad_data = b''
with io.BytesIO() as wad_data: # Lead-in data.
# Lead-in data. wad_data += b'\x00\x00\x00\x20'
wad_data.write(b'\x00\x00\x00\x20') # WAD type.
# WAD type. wad_data += str.encode(self.wad_type)
wad_data.write(str.encode(self.wad_type)) # WAD version.
# WAD version. wad_data += self.wad_version
wad_data.write(self.wad_version) # WAD cert size.
# WAD cert size. wad_data += int.to_bytes(self.wad_cert_size, 4)
wad_data.write(int.to_bytes(self.wad_cert_size, 4)) # WAD crl size.
# WAD crl size. wad_data += int.to_bytes(self.wad_crl_size, 4)
wad_data.write(int.to_bytes(self.wad_crl_size, 4)) # WAD ticket size.
# WAD ticket size. wad_data += int.to_bytes(self.wad_tik_size, 4)
wad_data.write(int.to_bytes(self.wad_tik_size, 4)) # WAD TMD size.
# WAD TMD size. wad_data += int.to_bytes(self.wad_tmd_size, 4)
wad_data.write(int.to_bytes(self.wad_tmd_size, 4)) # WAD content size.
# WAD content size. wad_data += int.to_bytes(self.wad_content_size, 4)
wad_data.write(int.to_bytes(self.wad_content_size, 4)) # WAD meta size.
# WAD meta size. wad_data += int.to_bytes(self.wad_meta_size, 4)
wad_data.write(int.to_bytes(self.wad_meta_size, 4)) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data) # Retrieve the cert data and write it out.
# Retrieve the cert data and write it out. wad_data += self.get_cert_data()
wad_data.write(self.get_cert_data()) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data) # Retrieve the crl data and write it out.
# Retrieve the crl data and write it out. wad_data += self.get_crl_data()
wad_data.write(self.get_crl_data()) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data) # Retrieve the ticket data and write it out.
# Retrieve the ticket data and write it out. wad_data += self.get_ticket_data()
wad_data.write(self.get_ticket_data()) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data) # Retrieve the TMD data and write it out.
# Retrieve the TMD data and write it out. wad_data += self.get_tmd_data()
wad_data.write(self.get_tmd_data()) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data) # Retrieve the meta/footer data and write it out.
# Retrieve the meta/footer data and write it out. wad_data += self.get_meta_data()
wad_data.write(self.get_meta_data()) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data) # Retrieve the content data and write it out.
# Retrieve the content data and write it out. wad_data += self.get_content_data()
wad_data.write(self.get_content_data()) wad_data = pad_bytes(wad_data)
wad_data = pad_bytes_stream(wad_data)
# Seek to the beginning and save this as the WAD data for the object.
wad_data.seek(0x0)
wad_data_raw = wad_data.read()
# Return the raw WAD file for the data contained in the object. # Return the raw WAD file for the data contained in the object.
return wad_data_raw return wad_data
def get_wad_type(self) -> str: def get_wad_type(self) -> str:
""" """