Restructured command files, updated U8 command syntax to match others

This commit is contained in:
2024-11-07 13:57:33 -05:00
parent 33197c36f1
commit ec7cb1063f
15 changed files with 82 additions and 70 deletions

143
commands/title/ciosbuild.py Normal file
View File

@@ -0,0 +1,143 @@
# "commands/title/ciosbuild.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import io
import os
import xml.etree.ElementTree as ET
import pathlib
import libWiiPy
def build_cios(args):
base_path = pathlib.Path(args.base)
map_path = pathlib.Path(args.map)
if args.modules:
modules_path = pathlib.Path(args.modules)
else:
modules_path = pathlib.Path(os.getcwd())
output_path = pathlib.Path(args.output)
if not base_path.exists():
raise FileNotFoundError(base_path)
if not map_path.exists():
raise FileNotFoundError(map_path)
if not modules_path.exists():
raise FileNotFoundError(modules_path)
title = libWiiPy.title.Title()
title.load_wad(open(base_path, 'rb').read())
cios_tree = ET.parse(map_path)
cios_root = cios_tree.getroot()
# Iterate over all <ciosgroup> tags to find the cIOS that was requested, and return an error if it doesn't match
# any of the groups in the provided map.
target_cios = None
for child in cios_root:
cios = child.get("name")
if args.cios_ver == cios:
target_cios = child
break
if target_cios is None:
raise ValueError("The target cIOS could not be found in the provided map!")
# Iterate over all bases in the target cIOS to find a base that matches the provided WAD. If one is found, ensure
# that the version of the base in the map matches the version of the IOS WAD.
target_base = None
provided_base = int(title.tmd.title_id[-2:], 16)
for child in target_cios:
base = int(child.get("ios"))
if base == provided_base:
target_base = child
break
if target_base is None:
raise ValueError("The provided base IOS doesn't match any bases found in the provided map!")
base_version = int(target_base.get("version"))
if title.tmd.title_version != base_version:
raise ValueError("The provided base IOS does not match the required version for this base!")
print(f"Building cIOS \"{args.cios_ver}\" from base IOS{target_base.get('ios')} v{base_version}...")
# We're ready to begin building the cIOS now. Find all the <content> tags that have <patch> tags, and then apply
# the patches listed in them to the content.
print("Patching existing commands...")
for content in target_base.findall("content"):
patches = content.findall("patch")
if patches:
cid = int(content.get("id"), 16)
dec_content = title.get_content_by_cid(cid)
content_index = title.content.get_index_from_cid(cid)
with io.BytesIO(dec_content) as content_data:
for patch in patches:
# Read patch info from the map. This requires some conversion since ciosmap files seem to use a
# comma-separated list of bytes.
offset = int(patch.get("offset"), 16)
original_data = b''
original_data_map = patch.get("originalbytes").split(",")
for byte in original_data_map:
original_data += bytes.fromhex(byte[2:])
new_data = b''
new_data_map = patch.get("newbytes").split(",")
for byte in new_data_map:
new_data += bytes.fromhex(byte[2:])
# Seek to the target offset and apply the patches. One last sanity check to ensure this
# original data exists.
if original_data in dec_content:
content_data.seek(offset)
content_data.write(new_data)
else:
raise Exception("An error occurred while patching! Please make sure your base IOS is valid.")
content_data.seek(0x0)
dec_content = content_data.read()
# Set the content in the title to the newly-patched content, and set the type to normal.
title.set_content(dec_content, content_index, content_type=libWiiPy.title.ContentType.NORMAL)
# Next phase of cIOS building is to add the required extra commands.
print("Adding required additional commands...")
for content in target_base.findall("content"):
target_module = content.get("module")
if target_module is not None:
target_index = int(content.get("tmdmoduleid"), 16)
# The cIOS map supplies a Content ID to use for each additional module.
cid = int(content.get("id"), 16)
target_path = modules_path.joinpath(target_module + ".app")
if not target_path.exists():
raise Exception(f"A required module \"{target_module}.app\" could not be found!")
# Check where this module belongs. If it's -1, add it to the end. If it's any other value, this module needs
# to go at the index specified.
new_module = target_path.read_bytes()
if target_index == -1:
title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL)
else:
existing_module = title.get_content_by_index(target_index)
existing_cid = title.content.content_records[target_index].content_id
existing_type = title.content.content_records[target_index].content_type
title.set_content(new_module, target_index, cid, libWiiPy.title.ContentType.NORMAL)
title.add_content(existing_module, existing_cid, existing_type)
# Last cIOS building step, we need to set the slot and version.
slot = args.slot
if 3 <= slot <= 255:
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
title.set_title_id(tid)
else:
raise ValueError(f"The provided slot \"{slot}\" is not valid!")
try:
title.set_title_version(args.version)
except ValueError:
raise ValueError(f"The provided version \"{args.version}\" is not valid!")
print(f"Set cIOS slot to \"{slot}\" and cIOS version to \"{args.version}\"!")
# If this is a vWii cIOS, then we need to re-encrypt it with the Wii Common key so that it's installable from
# within Wii mode.
title_key_dec = title.ticket.get_title_key()
title_key_common = libWiiPy.title.encrypt_title_key(title_key_dec, 0, title.tmd.title_id)
title.ticket.title_key_enc = title_key_common
title.ticket.common_key_index = 0
# Ensure the WAD is fakesigned.
title.fakesign()
# Write the new cIOS to the specified output path.
output_path.write_bytes(title.dump_wad())
print(f"Successfully built cIOS \"{args.cios_ver}\"!")

View File

@@ -0,0 +1,37 @@
# "commands/title/fakesign.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_fakesign(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if input_path.suffix.lower() == ".tmd":
tmd = libWiiPy.title.TMD()
tmd.load(open(input_path, "rb").read())
tmd.fakesign()
open(output_path, "wb").write(tmd.dump())
print("TMD fakesigned successfully!")
elif input_path.suffix.lower() == ".tik":
tik = libWiiPy.title.Ticket()
tik.load(open(input_path, "rb").read())
tik.fakesign()
open(output_path, "wb").write(tik.dump())
print("Ticket fakesigned successfully!")
elif input_path.suffix.lower() == ".wad":
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
title.fakesign()
open(output_path, "wb").write(title.dump_wad())
print("WAD fakesigned successfully!")
else:
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! Cannot fakesign.")

154
commands/title/info.py Normal file
View File

@@ -0,0 +1,154 @@
# "commands/title/info.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import binascii
import libWiiPy
def _print_tmd_info(tmd: libWiiPy.title.TMD):
# Get all important keys from the TMD and print them out nicely.
print("Title Info")
print(f" Title ID: {tmd.title_id}")
# This type of version number really only applies to the System Menu and IOS.
if tmd.title_id[:8] == "00000001":
print(f" Title Version: {tmd.title_version} ({tmd.title_version_converted})")
else:
print(f" Title Version: {tmd.title_version}")
print(f" TMD Version: {tmd.tmd_version}")
# IOSes just have an all-zero TID, so don't bothering showing that.
if tmd.ios_tid == "0000000000000000":
print(f" Required IOS: N/A")
else:
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
if tmd.signature_issuer.find("CP00000004") != -1:
print(f" Certificate: CP00000004 (Retail)")
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
elif tmd.signature_issuer.find("CP00000007") != -1:
print(f" Certificate: CP00000007 (Development)")
print(f" Certificate Issuer: Root-CA00000002 (Development)")
elif tmd.signature_issuer.find("CP10000000") != -1:
print(f" Certificate: CP10000000 (Arcade)")
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
else:
print(f" Certificate Info: {tmd.signature_issuer} (Unknown)")
if tmd.title_id == "0000000100000002":
match tmd.title_version_converted[-1:]:
case "U":
region = "USA"
case "E":
region = "EUR"
case "J":
region = "JPN"
case "K":
region = "KOR"
case _:
region = "None"
elif tmd.title_id[:8] == "00000001":
region = "None"
else:
region = tmd.get_title_region()
print(f" Region: {region}")
print(f" Title Type: {tmd.get_title_type()}")
print(f" vWii Title: {bool(tmd.vwii)}")
print(f" DVD Video Access: {tmd.get_access_right(tmd.AccessFlags.DVD_VIDEO)}")
print(f" AHB Access: {tmd.get_access_right(tmd.AccessFlags.AHB)}")
print(f" Fakesigned: {tmd.get_is_fakesigned()}")
# Iterate over the content and print their details.
print("\nContent Info")
print(f" Total Contents: {tmd.num_contents}")
print(f" Boot Content Index: {tmd.boot_index}")
print(" Content Records:")
for content in tmd.content_records:
print(f" Content Index: {content.index}")
print(f" Content ID: " + f"{content.content_id:08X}".lower())
print(f" Content Type: {tmd.get_content_type(content.index)}")
print(f" Content Size: {content.content_size} bytes")
print(f" Content Hash: {content.content_hash.decode()}")
def _print_ticket_info(ticket: libWiiPy.title.Ticket):
# Get all important keys from the TMD and print them out nicely.
print(f"Ticket Info")
print(f" Title ID: {ticket.title_id.decode()}")
# This type of version number really only applies to the System Menu and IOS.
if ticket.title_id.decode()[:8] == "00000001":
print(f" Title Version: {ticket.title_version} "
f"({libWiiPy.title.title_ver_dec_to_standard(ticket.title_version, ticket.title_id.decode())})")
else:
print(f" Title Version: {ticket.title_version}")
print(f" Ticket Version: {ticket.ticket_version}")
if ticket.signature_issuer.find("XS00000003") != -1:
print(f" Certificate: XS00000003 (Retail)")
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
elif ticket.signature_issuer.find("XS00000006") != -1:
print(f" Certificate: XS00000006 (Development)")
print(f" Certificate Issuer: Root-CA00000002 (Development)")
else:
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
match ticket.common_key_index:
case 0:
if ticket.is_dev:
key = "Common (Development)"
else:
key = "Common (Retail)"
case 1:
key = "Korean"
case 2:
key = "vWii"
case _:
key = "Unknown (Likely Common)"
print(f" Decryption Key: {key}")
print(f" Title Key (Encrypted): {binascii.hexlify(ticket.title_key_enc).decode()}")
print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}")
def _print_wad_info(title: libWiiPy.title.Title):
print(f"WAD Info")
match title.wad.wad_type:
case "Is":
print(f" WAD Type: Standard Installable")
case "ib":
print(f" WAD Type: boot2")
case _:
print(f" WAD Type: Unknown ({title.wad.wad_type})")
min_size_blocks = title.get_title_size_blocks()
max_size_blocks = title.get_title_size_blocks(absolute=True)
if min_size_blocks == max_size_blocks:
print(f" Installed Size: {min_size_blocks} blocks")
else:
print(f" Installed Size: {min_size_blocks}-{max_size_blocks} blocks")
min_size = round(title.get_title_size() / 1048576, 2)
max_size = round(title.get_title_size(absolute=True) / 1048576, 2)
if min_size == max_size:
print(f" Installed Size (MB): {min_size} MB")
else:
print(f" Installed Size (MB): {min_size}-{max_size} MB")
print(f" Has Meta/Footer: {bool(title.wad.wad_meta_size)}")
print(f" Has CRL: {bool(title.wad.wad_crl_size)}")
print("")
_print_ticket_info(title.ticket)
print("")
_print_tmd_info(title.tmd)
def handle_info(args):
input_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if input_path.suffix.lower() == ".tmd":
tmd = libWiiPy.title.TMD()
tmd.load(open(input_path, "rb").read())
_print_tmd_info(tmd)
elif input_path.suffix.lower() == ".tik":
tik = libWiiPy.title.Ticket()
tik.load(open(input_path, "rb").read())
_print_ticket_info(tik)
elif input_path.suffix.lower() == ".wad":
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
_print_wad_info(title)
else:
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! No info can be provided.")

View File

@@ -0,0 +1,129 @@
# "commands/title/iospatcher.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def _patch_fakesigning(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying fakesigning patch... ", end="", flush=True)
count = ios_patcher.patch_fakesigning()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_es_identify(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying ES_Identify access patch... ", end="", flush=True)
count = ios_patcher.patch_es_identify()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_nand_access(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying /dev/flash access patch... ", end="", flush=True)
count = ios_patcher.patch_nand_access()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_version_downgrading(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("Applying version downgrading patch... ", end="", flush=True)
count = ios_patcher.patch_version_downgrading()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def _patch_drive_inquiry(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
print("\n/!\\ WARNING! /!\\\n"
"This drive inquiry patch is experimental, and may introduce unexpected side effects on some consoles.\n")
print("Applying drive inquiry patch... ", end="", flush=True)
count = ios_patcher.patch_drive_inquiry()
if count == 1:
print(f"{count} patch applied")
else:
print(f"{count} patches applied")
return count
def handle_iospatch(args):
input_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
tid = title.tmd.title_id
if tid[:8] != "00000001" or tid[8:] == "00000001" or tid[8:] == "00000002":
raise ValueError("This WAD does not appear to contain an IOS! Patching cannot continue.")
patch_count = 0
if args.version is not None:
title.set_title_version(args.version)
print(f"Title version set to {args.version}!")
if args.slot is not None:
slot = args.slot
if 3 <= slot <= 255:
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
title.set_title_id(tid)
print(f"IOS slot set to {slot}!")
ios_patcher = libWiiPy.title.IOSPatcher()
ios_patcher.load(title)
if args.all is True:
patch_count += _patch_fakesigning(ios_patcher)
patch_count += _patch_es_identify(ios_patcher)
patch_count += _patch_nand_access(ios_patcher)
patch_count += _patch_version_downgrading(ios_patcher)
else:
if args.fakesigning is True:
patch_count += _patch_fakesigning(ios_patcher)
if args.es_identify is True:
patch_count += _patch_es_identify(ios_patcher)
if args.nand_access is True:
patch_count += _patch_nand_access(ios_patcher)
if args.version_downgrading is True:
patch_count += _patch_version_downgrading(ios_patcher)
if args.drive_inquiry is True:
patch_count += _patch_drive_inquiry(ios_patcher)
print(f"\nTotal patches applied: {patch_count}")
if patch_count == 0 and args.version is None and args.slot is None:
raise ValueError("No patches were applied! Please select patches to apply, and ensure that selected patches are"
" compatible with this IOS.")
if patch_count > 0 or args.version is not None or args.slot is not None:
# Set patched content to non-shared if that argument was passed.
if args.no_shared:
ios_patcher.title.content.content_records[ios_patcher.es_module_index].content_type = 1
if ios_patcher.dip_module_index != -1:
ios_patcher.title.content.content_records[ios_patcher.dip_module_index].content_type = 1
ios_patcher.title.fakesign() # Signature is broken anyway, so fakesign for maximum installation openings
if args.output is not None:
output_path = pathlib.Path(args.output)
output_file = open(output_path, "wb")
output_file.write(ios_patcher.title.dump_wad())
output_file.close()
else:
output_file = open(input_path, "wb")
output_file.write(ios_patcher.title.dump_wad())
output_file.close()
print("IOS successfully patched!")

234
commands/title/nus.py Normal file
View File

@@ -0,0 +1,234 @@
# "commands/title/nus.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import hashlib
import pathlib
import binascii
import libWiiPy
def handle_nus_title(args):
title_version = None
wad_file = None
output_dir = None
can_decrypt = False
tid = args.tid
if args.wii:
wiiu_nus_enabled = False
else:
wiiu_nus_enabled = True
# Check if --version was passed, because it'll be None if it wasn't.
if args.version is not None:
try:
title_version = int(args.version)
except ValueError:
print("Enter a valid integer for the Title Version.")
return
# If --wad was passed, check to make sure the path is okay.
if args.wad is not None:
wad_file = pathlib.Path(args.wad)
if wad_file.suffix != ".wad":
wad_file = wad_file.with_suffix(".wad")
# If --output was passed, make sure the directory either doesn't exist or is empty.
if args.output is not None:
output_dir = pathlib.Path(args.output)
if output_dir.exists():
if output_dir.is_file():
raise ValueError("A file already exists with the provided directory name!")
else:
output_dir.mkdir()
# Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can
# provide verbose output.
title = libWiiPy.title.Title()
# Announce the title being downloaded, and the version if applicable.
if title_version is not None:
print("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
else:
print("Downloading title " + tid + " vLatest, please wait...")
print(" - Downloading and parsing TMD...")
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
if title_version is not None:
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
else:
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
title_version = title.tmd.title_version
# Write out the TMD to a file.
if output_dir is not None:
output_dir.joinpath("tmd." + str(title_version)).write_bytes(title.tmd.dump())
# Download the ticket, if we can.
print(" - Downloading and parsing Ticket...")
try:
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
can_decrypt = True
if output_dir is not None:
output_dir.joinpath("tik").write_bytes(title.ticket.dump())
except ValueError:
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a
# ticket so that they aren't attempted later.
print(" - No Ticket is available!")
if wad_file is not None and output_dir is None:
print("--wad was passed, but this title cannot be packed into a WAD!")
return
# Load the content records from the TMD, and begin iterating over the records.
title.load_content_records()
content_list = []
for content in range(len(title.tmd.content_records)):
# Generate the content file name by converting the Content ID to hex and then removing the 0x.
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
while len(content_file_name) < 8:
content_file_name = "0" + content_file_name
print(" - Downloading content " + str(content + 1) + " of " +
str(len(title.tmd.content_records)) + " (Content ID: " +
str(title.tmd.content_records[content].content_id) + ", Size: " +
str(title.tmd.content_records[content].content_size) + " bytes)...")
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
wiiu_endpoint=wiiu_nus_enabled))
print(" - Done!")
# If we're supposed to be outputting to a folder, then write these files out.
if output_dir is not None:
output_dir.joinpath(content_file_name).write_bytes(content_list[content])
title.content.content_list = content_list
# Try to decrypt the contents for this title if a ticket was available.
if output_dir is not None:
if can_decrypt is True:
for content in range(len(title.tmd.content_records)):
print(" - Decrypting content " + str(content + 1) + " of " + str(len(title.tmd.content_records)) +
" (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...")
dec_content = title.get_content_by_index(content)
content_file_name = f"{title.tmd.content_records[content].content_id:08X}".lower() + ".app"
output_dir.joinpath(content_file_name).write_bytes(dec_content)
else:
print("Title has no Ticket, so content will not be decrypted!")
# If --wad was passed, pack a WAD and output that.
if wad_file is not None:
# Get the WAD certificate chain.
print(" - Building certificate...")
title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled))
# Ensure that the path ends in .wad, and add that if it doesn't.
print("Packing WAD...")
if wad_file.suffix != ".wad":
wad_file = wad_file.with_suffix(".wad")
# Have libWiiPy dump the WAD, and write that data out.
pathlib.Path(wad_file).write_bytes(title.dump_wad())
print("Downloaded title with Title ID \"" + args.tid + "\"!")
def handle_nus_content(args):
tid = args.tid
cid = args.cid
version = args.version
if args.decrypt:
decrypt_content = True
else:
decrypt_content = False
# Only accepting the 000000xx format because it's the one that would be most commonly known, rather than using the
# actual integer that the hex Content ID translates to.
try:
content_id = int.from_bytes(binascii.unhexlify(cid))
except binascii.Error:
raise ValueError("Invalid Content ID! Content ID must be in format \"000000xx\"!")
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
if args.output is None:
content_file_name = f"{content_id:08X}".lower()
output_path = pathlib.Path(content_file_name)
else:
output_path = pathlib.Path(args.output)
# Ensure that a version was supplied before downloading, because we need the matching TMD for decryption to work.
if decrypt_content is True and version is None:
print("You must specify the version that the requested content belongs to for decryption!")
return
# Try to download the content, and catch the ValueError libWiiPy will throw if it can't be found.
print("Downloading content with Content ID " + cid + "...")
try:
content_data = libWiiPy.title.download_content(tid, content_id)
except ValueError:
raise ValueError("The Title ID or Content ID you specified could not be found!")
if decrypt_content is True:
output_path = output_path.with_suffix(".app")
tmd = libWiiPy.title.TMD()
tmd.load(libWiiPy.title.download_tmd(tid, version))
# Try to get a Ticket for the title, if a common one is available.
try:
ticket = libWiiPy.title.Ticket()
ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True))
except ValueError:
print("No Ticket is available! Content cannot be decrypted!")
return
content_hash = 'gggggggggggggggggggggggggggggggggggggggg'
content_size = 0
content_index = 0
for record in tmd.content_records:
if record.content_id == content_id:
content_hash = record.content_hash.decode()
content_size = record.content_size
content_index = record.index
# If the default hash never changed, then a content record matching the downloaded content couldn't be found,
# which most likely means that the wrong version was specified.
if content_hash == 'gggggggggggggggggggggggggggggggggggggggg':
print("Content was not found in the TMD from the specified version! Content cannot be decrypted!")
return
# Manually decrypt the content and verify its hash, which is what libWiiPy's get_content() methods do. We just
# can't really use that here because that require setting up a lot more of the title than is necessary.
content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size)
content_dec_hash = hashlib.sha1(content_dec).hexdigest()
if content_hash != content_dec_hash:
raise ValueError("The decrypted content provided does not match the record at the provided index. \n"
"Expected hash is: {}\n".format(content_hash) +
"Actual hash is: {}".format(content_dec_hash))
output_path.write_bytes(content_dec)
else:
output_path.write_bytes(content_data)
print(f"Downloaded content with Content ID \"{cid}\"!")
def handle_nus_tmd(args):
tid = args.tid
# Check if --version was passed, because it'll be None if it wasn't.
if args.version is not None:
try:
version = int(args.version)
except ValueError:
raise ValueError("Enter a valid integer for the TMD Version.")
else:
version = None
# Use the supplied output path if one was specified, otherwise generate one using the Title ID. If a version has
# been specified, append the version to the end of the path as well.
if args.output is None:
if version is not None:
output_path = pathlib.Path(f"{tid}.tmd.{version}")
else:
output_path = pathlib.Path(f"{tid}.tmd")
else:
output_path = pathlib.Path(args.output)
# Try to download the TMD, and catch the ValueError libWiiPy will throw if it can't be found.
print(f"Downloading TMD for title {tid}...")
try:
tmd_data = libWiiPy.title.download_tmd(tid, version)
except ValueError:
raise ValueError("The Title ID or version you specified could not be found!")
output_path.write_bytes(tmd_data)
print(f"Downloaded TMD for title \"{tid}\"!")

47
commands/title/tmd.py Normal file
View File

@@ -0,0 +1,47 @@
# "commands/title/tmd.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_tmd_remove(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
tmd = libWiiPy.title.TMD()
tmd.load(input_path.read_bytes())
if args.index is not None:
# Make sure the target index exists, then remove it from the TMD.
if args.index >= len(tmd.content_records):
raise ValueError("The provided index could not be found in this TMD!")
tmd.content_records.pop(args.index)
tmd.num_contents -= 1
# Auto fakesign because we've edited the TMD.
tmd.fakesign()
output_path.write_bytes(tmd.dump())
print(f"Removed content record at index {args.index}!")
elif args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
# List Contents IDs in the title, and ensure that the target Content ID exists.
valid_ids = []
for record in tmd.content_records:
valid_ids.append(record.content_id)
if target_cid not in valid_ids:
raise ValueError("The provided Content ID could not be found in this TMD!")
tmd.content_records.pop(valid_ids.index(target_cid))
tmd.num_contents -= 1
# Auto fakesign because we've edited the TMD.
tmd.fakesign()
output_path.write_bytes(tmd.dump())
print(f"Removed content record with Content ID \"{target_cid:08X}\"!")

370
commands/title/wad.py Normal file
View File

@@ -0,0 +1,370 @@
# "commands/title/wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
from random import randint
import libWiiPy
def handle_wad_add(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if not content_path.exists():
raise FileNotFoundError(content_path)
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
content_data = content_path.read_bytes()
# Prepare the CID so it's ready when we go to add this content to the WAD.
# We need to both validate that this is a real CID, and also that it isn't already taken by another content.
if args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
for record in title.content.content_records:
if target_cid == record.content_id:
raise ValueError("The provided Content ID is already being used by this title!")
print(f"Using provided Content ID \"{target_cid:08X}\".")
# If we weren't given a CID, then we need to randomly assign one, and ensure it isn't being used.
else:
used_cids = []
for record in title.content.content_records:
used_cids.append(record.content_id)
target_cid = randint(0, 0x000000FF)
while target_cid in used_cids:
target_cid = randint(0, 0x000000FF)
print(f"Using randomly assigned Content ID \"{target_cid:08X}\" since none were provided.")
# Get the type of the new content.
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
raise ValueError("The provided content type is invalid!")
else:
target_type = libWiiPy.title.ContentType.NORMAL
# Call add_content to add our new content with the set parameters.
title.add_content(content_data, target_cid, target_type)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Successfully added new content with Content ID \"{target_cid:08X}\" and type \"{target_type.name}\"!")
def handle_wad_convert(args):
input_path = pathlib.Path(args.input)
if args.dev:
target = "development"
elif args.retail:
target = "retail"
elif args.vwii:
target = "vWii"
else:
raise ValueError("No valid target was provided!")
if args.output is None:
match target:
case "development":
output_path = pathlib.Path(input_path.stem + "_dev" + input_path.suffix)
case "retail":
output_path = pathlib.Path(input_path.stem + "_retail" + input_path.suffix)
case "vWii":
output_path = pathlib.Path(input_path.stem + "_vWii" + input_path.suffix)
case _:
raise ValueError("No valid target was provided!")
else:
output_path = pathlib.Path(args.output)
if not input_path.exists():
raise FileNotFoundError(input_path)
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
# First, verify that this WAD isn't already the type we're trying to convert to.
if title.ticket.is_dev and target == "development":
raise ValueError("This is already a development WAD!")
elif not title.ticket.is_dev and not title.tmd.vwii and target == "retail":
raise ValueError("This is already a retail WAD!")
elif not title.ticket.is_dev and title.tmd.vwii and target == "vWii":
raise ValueError("This is already a vWii WAD!")
# Get the current type to display later.
if title.ticket.is_dev:
source = "development"
elif title.tmd.vwii:
source = "vWii"
else:
source = "retail"
# Extract the Title Key so it can be re-encrypted with the correct key later.
title_key = title.ticket.get_title_key()
title_key_new = None
if target == "development":
# Set the development signature info.
title.ticket.signature_issuer = "Root-CA00000002-XS00000006" + title.ticket.signature_issuer[26:]
title.tmd.signature_issuer = "Root-CA00000002-CP00000007" + title.tmd.signature_issuer[26:]
# Re-encrypt the title key with the dev key, and set that in the Ticket.
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 0, title.ticket.title_id, True)
title.ticket.common_key_index = 0
else:
# Set the retail signature info.
title.ticket.signature_issuer = "Root-CA00000001-XS00000003" + title.ticket.signature_issuer[26:]
title.tmd.signature_issuer = "Root-CA00000001-CP00000004" + title.tmd.signature_issuer[26:]
if target == "retail":
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 0, title.ticket.title_id, False)
title.ticket.common_key_index = 0
elif target == "vWii":
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 2, title.ticket.title_id, False)
title.ticket.common_key_index = 2
title.ticket.title_key_enc = title_key_new
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Successfully converted {source} WAD to {target} WAD \"{output_path.name}\"!")
def handle_wad_pack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
# Make sure input path both exists and is a directory. Separate checks because this provides more relevant
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
if not input_path.exists():
raise FileNotFoundError(input_path)
if not input_path.is_dir():
raise NotADirectoryError(input_path)
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
# means we can't pack a WAD because we couldn't really tell which TMD is intended for this WAD.
tmd_list = list(input_path.glob('*.[tT][mM][dD]'))
if len(tmd_list) > 1:
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
elif len(tmd_list) == 0:
raise FileNotFoundError("No TMD file found! Cannot pack WAD.")
tmd_file = pathlib.Path(tmd_list[0])
# Repeat the same process as above for all .tik files.
ticket_list = list(input_path.glob('*.[tT][iI][kK]'))
if len(ticket_list) > 1:
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
elif len(ticket_list) == 0:
raise FileNotFoundError("No Ticket file found! Cannot pack WAD.")
ticket_file = pathlib.Path(ticket_list[0])
# And one more time for all .cert files.
cert_list = list(input_path.glob('*.[cC][eE][rR][tT]'))
if len(cert_list) > 1:
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
"WAD.")
elif len(cert_list) == 0:
raise FileNotFoundError("No certificate file found! Cannot pack WAD.")
cert_file = pathlib.Path(cert_list[0])
# Make sure that there's at least one content to pack.
content_files = list(input_path.glob("*.[aA][pP][pP]"))
if not content_files:
raise FileNotFoundError("No contents found! Cannot pack WAD.")
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
# object.
title = libWiiPy.title.Title()
title.load_tmd(tmd_file.read_bytes())
title.load_ticket(ticket_file.read_bytes())
title.wad.set_cert_data(cert_file.read_bytes())
# Footers are not super common and are not required, so we don't care about one existing until we get to
# the step where we'd pack it.
footer_file = pathlib.Path(list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0])
if footer_file.exists():
title.wad.set_meta_data(footer_file.read_bytes())
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
title.load_content_records()
# Sort the contents based on the records. May still be kinda hacky.
content_indices = []
for record in title.content.content_records:
content_indices.append(record.index)
content_files_ordered = []
for _ in content_files:
content_files_ordered.append(None)
for index in range(len(content_files)):
target_index = content_indices.index(int(content_files[index].stem, 16))
content_files_ordered[target_index] = content_files[index]
# Iterate over every file in the content_files list, and set them in the Title().
for index in range(title.content.num_contents):
dec_content = content_files_ordered[index].read_bytes()
title.set_content(dec_content, index)
# Fakesign the TMD and Ticket using the trucha bug, if enabled. This is built-in in libWiiPy v0.4.1+.
if args.fakesign:
title.fakesign()
output_path.write_bytes(title.dump_wad())
print("WAD file packed!")
def handle_wad_remove(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
# TODO: see if this implementation is problematic now
if args.index is not None:
# List indices in the title, and ensure that the target content index exists.
valid_indices = []
for record in title.content.content_records:
valid_indices.append(record.index)
if args.index not in valid_indices:
raise ValueError("The provided content index could not be found in this title!")
title.content.remove_content_by_index(args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Removed content at content index {args.index}!")
elif args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
# List Contents IDs in the title, and ensure that the target Content ID exists.
valid_ids = []
for record in title.content.content_records:
valid_ids.append(record.content_id)
if target_cid not in valid_ids:
raise ValueError("The provided Content ID could not be found in this title!")
title.content.remove_content_by_cid(target_cid)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Removed content with Content ID \"{target_cid:08X}\"!")
def handle_wad_set(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if not content_path.exists():
raise FileNotFoundError(content_path)
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
content_data = content_path.read_bytes()
# Get the new type of the content, if one was specified.
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
raise ValueError("The provided content type is invalid!")
else:
target_type = None
if args.index is not None:
# If we're replacing based on the index, then make sure the specified index exists.
existing_indices = []
for record in title.content.content_records:
existing_indices.append(record.index)
if args.index not in existing_indices:
raise ValueError("The provided index could not be found in this title!")
if target_type:
title.set_content(content_data, args.index, content_type=target_type)
else:
title.set_content(content_data, args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Replaced content at content index {args.index}!")
elif args.cid is not None:
# If we're replacing based on the CID, then make sure the specified CID is valid and exists.
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
existing_cids = []
for record in title.content.content_records:
existing_cids.append(record.content_id)
if target_cid not in existing_cids:
raise ValueError("The provided Content ID could not be found in this title!")
target_index = title.content.get_index_from_cid(target_cid)
if target_type:
title.set_content(content_data, target_index, content_type=target_type)
else:
title.set_content(content_data, target_index)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Replaced content with Content ID \"{target_cid:08X}\"!")
def handle_wad_unpack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
if not input_path.exists():
raise FileNotFoundError(input_path)
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
if output_path.exists():
if output_path.is_file():
raise ValueError("A file already exists with the provided directory name!")
else:
output_path.mkdir()
# Step through each component of a WAD and dump it to a file.
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
cert_name = title.tmd.title_id + ".cert"
output_path.joinpath(cert_name).write_bytes(title.wad.get_cert_data())
tmd_name = title.tmd.title_id + ".tmd"
output_path.joinpath(tmd_name).write_bytes(title.wad.get_tmd_data())
ticket_name = title.tmd.title_id + ".tik"
output_path.joinpath(ticket_name).write_bytes(title.wad.get_ticket_data())
meta_name = title.tmd.title_id + ".footer"
output_path.joinpath(meta_name).write_bytes(title.wad.get_meta_data())
# Skip validating hashes if -s/--skip-hash was passed.
if args.skip_hash:
skip_hash = True
else:
skip_hash = False
for content_file in range(0, title.tmd.num_contents):
content_index = title.content.content_records[content_file].index
content_file_name = f"{content_index:08X}".lower() + ".app"
output_path.joinpath(content_file_name).write_bytes(title.get_content_by_index(content_file, skip_hash))
print("WAD file unpacked!")