Skip to content

Instantly share code, notes, and snippets.

@rvtr
Forked from NWPlayer123/decrypt_tad.py
Last active December 14, 2023 04:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rvtr/f1069530129b7a57967e3fc4b30866b4 to your computer and use it in GitHub Desktop.
Save rvtr/f1069530129b7a57967e3fc4b30866b4 to your computer and use it in GitHub Desktop.
Tool for decrypting DSi TADs (not the ones listed on dsibrew, that's wrong, the installable ones that are closer to WADs), py2/3
from io import BytesIO
from struct import unpack
from binascii import hexlify, unhexlify
from Crypto.Cipher import AES #pip install pycryptodome
import sys
dsi_common_key = unhexlify(b"%032X" % 0xAF1BF516A807D21AEA45984F04742861) # DSi common key
debugger_common_key = unhexlify(b"%032X" % 0xA2FDDDF2E423574AE7ED8657B5AB19D3) # DSi debugger common key (used for `maketad.updater` TADs)
wii_debug_key = unhexlify(b"%032X" % 0xA1604A6A7123B529AE8BEC32C816FCAA) # Wii debug key
def align(val): #Tads have 64-byte alignment between sections
return val + (64 - (val % 64))
with open(sys.argv[1], "rb") as f:
header = unpack(">I2sH6I", f.read(0x20))
if header[1] != b"Is": #Installable? same-ish as Wii WADs
raise BaseException("Invalid TAD file")
if header[0] != 0x20: #header size
raise BaseException("unknown header size %d" % header[0])
if header[2] != 0: #WAD version according to wiibrew
raise BaseException("unknown TAD version %d" % header[2])
print("Input Tad file: %s" % sys.argv[1])
print("Header: %08X cert %08X ticket %08X tmd %08X content %08X footer" %\
(header[3], header[5], header[6], header[7], header[8]))
with open("title.cert", "wb") as o:
o.write(f.read(header[3]))
f.seek(align(f.tell()))
ticket = BytesIO(f.read(header[5])) #store to parse a bit
f.seek(align(f.tell()))
with open("title.tik", "wb") as o:
o.write(ticket.read())
ticket.seek(0)
tmd = BytesIO(f.read(header[6])) #store to parse a bit
f.seek(align(f.tell()))
with open("title.tmd", "wb") as o:
o.write(tmd.read())
tmd.seek(0)
tmd.seek(0x18C)
title_id = tmd.read(8)
tmd.seek(0x1DE)
content_count, boot_index = unpack(">2H", tmd.read(4))
print("Title ID: %016X %s" % (unpack(">Q", title_id)[0], title_id[4:].decode("UTF-8")))
ticket.seek(0x1BF)
enc_title_key = ticket.read(0x10)
print("Encrypted Title Key: %032X" % (int(hexlify(enc_title_key), 16)))
if content_count > 1:
raise BaseException("Multiple contents not supported, very easy to add")
#if you encounter multi-content files, handle tmd table here, and then assume
#content is in a row in the content blob in the TAD
content = BytesIO(f.read(header[7]))
f.seek(align(f.tell()))
with open("%08X" % boot_index, "wb") as o: #store the encrypted content
o.write(content.read())
content.seek(0)
if header[8] != 0: #there is a footer
with open("footer.bin", "wb") as o:
o.write(f.read(header[8]))
f.seek(align(f.tell()))
title_id += b"\x00" * 8 #pad to 16 bytes for IV
obj = AES.new(dsi_common_key, AES.MODE_CBC, title_id)
dsi_dec_title_key = obj.decrypt(enc_title_key)
obj = AES.new(debugger_common_key, AES.MODE_CBC, title_id)
debugger_dec_title_key = obj.decrypt(enc_title_key)
obj = AES.new(wii_debug_key, AES.MODE_CBC, title_id)
wii_dec_title_key = obj.decrypt(enc_title_key)
#try both keys and check the srl's "reserved" bytes, we decrypt the entire file
#because otherwise the CBC blocks get off and the start of the srl gets garbled
obj = AES.new(dsi_dec_title_key, AES.MODE_CBC, b"\x00" * 16)
decrypted_content = BytesIO(obj.decrypt(content.read()))
content.seek(0)
decrypted_content.seek(0x15) #reserved bytes
if decrypted_content.read(7) == b"\x00" * 7: #this is an srl
decrypted_content.seek(0xC)
print("DSi Common Key Used")
game_code = decrypted_content.read(6).decode("UTF-8") #for export filename
print("Game Code: %s" % game_code)
decrypted_content.seek(0)
print("Output file name: %s.srl" % game_code)
with open("%s.srl" % game_code, "wb") as o:
o.write(decrypted_content.read())
sys.exit(1)
obj = AES.new(debugger_dec_title_key, AES.MODE_CBC, b"\x00" * 16)
decrypted_content = BytesIO(obj.decrypt(content.read()))
content.seek(0)
decrypted_content.seek(0x15) #reserved bytes
if decrypted_content.read(7) == b"\x00" * 7: #this is an srl
decrypted_content.seek(0xC)
print("Debugger Common Key Used")
game_code = decrypted_content.read(6).decode("UTF-8") #for export filename
print("Game Code: %s" % game_code)
decrypted_content.seek(0)
print("Output file name: %s.srl" % game_code)
with open("%s.srl" % game_code, "wb") as o:
o.write(decrypted_content.read())
sys.exit(1)
obj = AES.new(wii_dec_title_key, AES.MODE_CBC, b"\x00" * 16)
decrypted_content = BytesIO(obj.decrypt(content.read()))
content.seek(0)
decrypted_content.seek(0x15) #reserved bytes
if decrypted_content.read(7) == b"\x00" * 7: #this is an srl
decrypted_content.seek(0xC)
print("Wii Debug Key Used")
game_code = decrypted_content.read(6).decode("UTF-8") #for export filename
print("Game Code: %s" % game_code)
decrypted_content.seek(0)
print("Output file name: %s.srl" % game_code)
with open("%s.srl" % game_code, "wb") as o:
o.write(decrypted_content.read())
sys.exit(1)
raise BaseException("Was not able to decrypt the content, oops")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment