Skip to content

Commit 71c0726

Browse files
committed
Allow for custom endpoints for NUS downloads
1 parent 1410dd6 commit 71c0726

File tree

4 files changed

+100
-88
lines changed

4 files changed

+100
-88
lines changed

commands/title/info.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD, signing_cert=None):
4343
elif tmd.signature_issuer.find("CP00000007") != -1:
4444
print(f" Certificate: CP00000007 (Development)")
4545
print(f" Certificate Issuer: Root-CA00000002 (Development)")
46+
elif tmd.signature_issuer.find("CP00000005") != -1:
47+
print(f" Certificate: CP00000005 (Development/Unknown)")
48+
print(f" Certificate Issuer: Root-CA00000002 (Development)")
4649
elif tmd.signature_issuer.find("CP10000000") != -1:
4750
print(f" Certificate: CP10000000 (Arcade)")
4851
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
@@ -128,6 +131,9 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket, signing_cert=None):
128131
elif ticket.signature_issuer.find("XS00000006") != -1:
129132
print(f" Certificate: XS00000006 (Development)")
130133
print(f" Certificate Issuer: Root-CA00000002 (Development)")
134+
elif ticket.signature_issuer.find("XS00000004") != -1:
135+
print(f" Certificate: XS00000004 (Development/Unknown)")
136+
print(f" Certificate Issuer: Root-CA00000002 (Development)")
131137
else:
132138
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
133139
match ticket.common_key_index:

commands/title/nus.py

Lines changed: 89 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,91 @@
88
from modules.core import fatal_error
99

1010

11+
def handle_nus_content(args):
12+
tid = args.tid
13+
cid = args.cid
14+
version = args.version
15+
if args.decrypt:
16+
decrypt_content = True
17+
else:
18+
decrypt_content = False
19+
20+
# Only accepting the 000000xx format because it's the one that would be most commonly known, rather than using the
21+
# actual integer that the hex Content ID translates to.
22+
content_id = None
23+
try:
24+
content_id = int.from_bytes(binascii.unhexlify(cid))
25+
except binascii.Error:
26+
fatal_error("The provided Content ID is invalid! The Content ID must be in the format \"000000xx\"!")
27+
28+
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
29+
if args.output is None:
30+
content_file_name = f"{content_id:08X}".lower()
31+
output_path = pathlib.Path(content_file_name)
32+
else:
33+
output_path = pathlib.Path(args.output)
34+
35+
# Ensure that a version was supplied before downloading, because we need the matching TMD for decryption to work.
36+
if decrypt_content is True and version is None:
37+
fatal_error("You must specify the version that the requested content belongs to for decryption!")
38+
39+
# Try to download the content, and catch the ValueError libWiiPy will throw if it can't be found.
40+
print(f"Downloading content with Content ID {cid}...")
41+
content_data = None
42+
try:
43+
content_data = libWiiPy.title.download_content(tid, content_id)
44+
except ValueError:
45+
fatal_error("The specified Title ID or Content ID could not be found!")
46+
47+
if decrypt_content is True:
48+
output_path = output_path.with_suffix(".app")
49+
tmd = libWiiPy.title.TMD()
50+
tmd.load(libWiiPy.title.download_tmd(tid, version))
51+
# Try to get a Ticket for the title, if a common one is available.
52+
ticket = None
53+
try:
54+
ticket = libWiiPy.title.Ticket()
55+
ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True))
56+
except ValueError:
57+
fatal_error("No Ticket is available! Content cannot be decrypted.")
58+
59+
content_hash = 'gggggggggggggggggggggggggggggggggggggggg'
60+
content_size = 0
61+
content_index = 0
62+
for record in tmd.content_records:
63+
if record.content_id == content_id:
64+
content_hash = record.content_hash.decode()
65+
content_size = record.content_size
66+
content_index = record.index
67+
68+
# If the default hash never changed, then a content record matching the downloaded content couldn't be found,
69+
# which most likely means that the wrong version was specified.
70+
if content_hash == 'gggggggggggggggggggggggggggggggggggggggg':
71+
fatal_error("Content was not found in the TMD for the specified version! Content cannot be decrypted.")
72+
73+
# Manually decrypt the content and verify its hash, which is what libWiiPy's get_content() methods do. We just
74+
# can't really use that here because that require setting up a lot more of the title than is necessary.
75+
content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size)
76+
content_dec_hash = hashlib.sha1(content_dec).hexdigest()
77+
if content_hash != content_dec_hash:
78+
fatal_error("The decrypted content provided does not match the record at the provided index. \n"
79+
"Expected hash is: {}\n".format(content_hash) +
80+
"Actual hash is: {}".format(content_dec_hash))
81+
output_path.write_bytes(content_dec)
82+
else:
83+
output_path.write_bytes(content_data)
84+
85+
print(f"Downloaded content with Content ID \"{cid}\"!")
86+
87+
1188
def handle_nus_title(args):
1289
title_version = None
1390
wad_file = None
1491
output_dir = None
1592
can_decrypt = False
1693
tid = args.tid
17-
if args.wii:
18-
wiiu_nus_enabled = False
19-
else:
20-
wiiu_nus_enabled = True
94+
wiiu_nus_enabled = False if args.wii else True
95+
endpoint_override = args.endpoint if args.endpoint else None
2196

2297
# Check if --version was passed, because it'll be None if it wasn't.
2398
if args.version is not None:
@@ -53,9 +128,11 @@ def handle_nus_title(args):
53128
print(" - Downloading and parsing TMD...")
54129
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
55130
if title_version is not None:
56-
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
131+
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled,
132+
endpoint_override=endpoint_override))
57133
else:
58-
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
134+
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled,
135+
endpoint_override=endpoint_override))
59136
title_version = title.tmd.title_version
60137
# Write out the TMD to a file.
61138
if output_dir is not None:
@@ -64,7 +141,8 @@ def handle_nus_title(args):
64141
# Download the ticket, if we can.
65142
print(" - Downloading and parsing Ticket...")
66143
try:
67-
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
144+
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled,
145+
endpoint_override=endpoint_override))
68146
can_decrypt = True
69147
if output_dir is not None:
70148
output_dir.joinpath("tik").write_bytes(title.ticket.dump())
@@ -87,7 +165,8 @@ def handle_nus_title(args):
87165
f"(Content ID: {title.tmd.content_records[content].content_id}, "
88166
f"Size: {title.tmd.content_records[content].content_size} bytes)...")
89167
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
90-
wiiu_endpoint=wiiu_nus_enabled))
168+
wiiu_endpoint=wiiu_nus_enabled,
169+
endpoint_override=endpoint_override))
91170
print(" - Done!")
92171
# If we're supposed to be outputting to a folder, then write these files out.
93172
if output_dir is not None:
@@ -110,7 +189,8 @@ def handle_nus_title(args):
110189
if wad_file is not None:
111190
# Get the WAD certificate chain.
112191
print(" - Building certificate...")
113-
title.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled))
192+
title.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled,
193+
endpoint_override=endpoint_override))
114194
# Ensure that the path ends in .wad, and add that if it doesn't.
115195
print("Packing WAD...")
116196
if wad_file.suffix != ".wad":
@@ -121,83 +201,6 @@ def handle_nus_title(args):
121201
print(f"Downloaded title with Title ID \"{args.tid}\"!")
122202

123203

124-
def handle_nus_content(args):
125-
tid = args.tid
126-
cid = args.cid
127-
version = args.version
128-
if args.decrypt:
129-
decrypt_content = True
130-
else:
131-
decrypt_content = False
132-
133-
# Only accepting the 000000xx format because it's the one that would be most commonly known, rather than using the
134-
# actual integer that the hex Content ID translates to.
135-
content_id = None
136-
try:
137-
content_id = int.from_bytes(binascii.unhexlify(cid))
138-
except binascii.Error:
139-
fatal_error("The provided Content ID is invalid! The Content ID must be in the format \"000000xx\"!")
140-
141-
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
142-
if args.output is None:
143-
content_file_name = f"{content_id:08X}".lower()
144-
output_path = pathlib.Path(content_file_name)
145-
else:
146-
output_path = pathlib.Path(args.output)
147-
148-
# Ensure that a version was supplied before downloading, because we need the matching TMD for decryption to work.
149-
if decrypt_content is True and version is None:
150-
fatal_error("You must specify the version that the requested content belongs to for decryption!")
151-
152-
# Try to download the content, and catch the ValueError libWiiPy will throw if it can't be found.
153-
print(f"Downloading content with Content ID {cid}...")
154-
content_data = None
155-
try:
156-
content_data = libWiiPy.title.download_content(tid, content_id)
157-
except ValueError:
158-
fatal_error("The specified Title ID or Content ID could not be found!")
159-
160-
if decrypt_content is True:
161-
output_path = output_path.with_suffix(".app")
162-
tmd = libWiiPy.title.TMD()
163-
tmd.load(libWiiPy.title.download_tmd(tid, version))
164-
# Try to get a Ticket for the title, if a common one is available.
165-
ticket = None
166-
try:
167-
ticket = libWiiPy.title.Ticket()
168-
ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True))
169-
except ValueError:
170-
fatal_error("No Ticket is available! Content cannot be decrypted.")
171-
172-
content_hash = 'gggggggggggggggggggggggggggggggggggggggg'
173-
content_size = 0
174-
content_index = 0
175-
for record in tmd.content_records:
176-
if record.content_id == content_id:
177-
content_hash = record.content_hash.decode()
178-
content_size = record.content_size
179-
content_index = record.index
180-
181-
# If the default hash never changed, then a content record matching the downloaded content couldn't be found,
182-
# which most likely means that the wrong version was specified.
183-
if content_hash == 'gggggggggggggggggggggggggggggggggggggggg':
184-
fatal_error("Content was not found in the TMD for the specified version! Content cannot be decrypted.")
185-
186-
# Manually decrypt the content and verify its hash, which is what libWiiPy's get_content() methods do. We just
187-
# can't really use that here because that require setting up a lot more of the title than is necessary.
188-
content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size)
189-
content_dec_hash = hashlib.sha1(content_dec).hexdigest()
190-
if content_hash != content_dec_hash:
191-
fatal_error("The decrypted content provided does not match the record at the provided index. \n"
192-
"Expected hash is: {}\n".format(content_hash) +
193-
"Actual hash is: {}".format(content_dec_hash))
194-
output_path.write_bytes(content_dec)
195-
else:
196-
output_path.write_bytes(content_data)
197-
198-
print(f"Downloaded content with Content ID \"{cid}\"!")
199-
200-
201204
def handle_nus_tmd(args):
202205
tid = args.tid
203206

commands/title/wad.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ def handle_wad_convert(args):
8181
else:
8282
fatal_error("No valid encryption target was specified!")
8383

84-
output_path = pathlib.Path(args.output)
8584
if args.output is None:
8685
match target:
8786
case "development":
@@ -92,6 +91,8 @@ def handle_wad_convert(args):
9291
output_path = pathlib.Path(input_path.stem + "_vWii" + input_path.suffix)
9392
case _:
9493
fatal_error("No valid encryption target was specified!")
94+
else:
95+
output_path = pathlib.Path(args.output)
9596

9697
if not input_path.exists():
9798
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")

wiipy.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,10 @@
186186
help="download the title to a folder")
187187
nus_title_out_group.add_argument("-w", "--wad", metavar="WAD", type=str,
188188
help="pack a wad with the provided name")
189-
nus_title_parser.add_argument("--wii", help="use original Wii NUS instead of the Wii U servers",
189+
nus_title_parser.add_argument("--wii", help="use the original Wii NUS endpoint instead of the Wii U endpoint",
190190
action="store_true")
191+
nus_title_parser.add_argument("-e", "--endpoint", metavar="ENDPOINT", type=str,
192+
help="use the specified NUS endpoint instead of the official one")
191193
# Content NUS subcommand.
192194
nus_content_parser = nus_subparsers.add_parser("content", help="download a specific content from the NUS",
193195
description="download a specific content from the NUS")

0 commit comments

Comments
 (0)