ntool/lib/ctr_tik.py
2023-03-26 23:48:17 +08:00

219 lines
8.4 KiB
Python

from .common import *
from .keys import *
class tikData(BigEndianStructure):
_pack_ = 1
_fields_ = [
('issuer', c_char * 0x40),
('ecc_pubkey', c_uint8 * 0x3C),
('format_ver', c_uint8),
('ca_crl_ver', c_uint8),
('signer_crl_ver', c_uint8),
('enc_titlekey', c_uint8 * 16),
('reserved1', c_uint8),
('ticketID', c_uint64),
('consoleID', c_uint32),
('titleID', c_uint8 * 8),
('reserved2', c_uint16),
('title_ver', c_uint16),
('reserved3', c_uint64),
('license_type', c_uint8),
('common_key_index', c_uint8),
('reserved4', c_uint8 * 0x2A),
('eshop_acc_id', c_uint8 * 4),
('reserved5', c_uint8),
('audit', c_uint8),
('reserved6', c_uint8 * 0x42),
('limits', c_uint8 * 0x40),
]
def __new__(cls, buf):
return cls.from_buffer_copy(buf)
def __init__(self, data):
pass
signature_types = { # Each tuple is (signature size, size of padding after signature)
# RSA_4096 SHA1 (unused on 3DS)
0x00010000: (0x200, 0x3C),
# RSA_2048 SHA1 (unused on 3DS)
0x00010001: (0x100, 0x3C),
# Elliptic Curve with SHA1 (unused on 3DS)
0x00010002: (0x3C, 0x40),
# RSA_4096 SHA256
0x00010003: (0x200, 0x3C),
# RSA_2048 SHA256
0x00010004: (0x100, 0x3C),
# ECDSA with SHA256
0x00010005: (0x3C, 0x40),
}
class tikReader:
def __init__(self, file, dev=0):
self.file = file
self.dev = dev
with open(file, 'rb') as f:
sig_type = readbe(f.read(4))
self.sig = f.read(signature_types[sig_type][0])
padding = f.read(signature_types[sig_type][1])
self.data = tikData(f.read(0x164))
self.content_index_hdr = f.read(0x28)
self.content_index_offset = f.read(4)
self.content_index = f.read(0x80)
# Decrypt TitleKey
normal_key = CTR.key_scrambler(CTR.KeyX0x3D[dev], CTR.KeyY0x3D[self.data.common_key_index][dev])
cipher = AES.new(normal_key, AES.MODE_CBC, iv=bytes(self.data.titleID)+(b'\0'*8))
self.titlekey = cipher.decrypt(bytes(self.data.enc_titlekey))
def verify(self, no_print=0): # 'no_print' parameter to facilitate CIAReader.verify()
sig_check = []
sig_check.append(('Ticket', Crypto.verify_rsa_sha256(CTR.tik_mod[self.dev], bytes(self.data) + self.content_index_hdr + self.content_index_offset + self.content_index, self.sig)))
if no_print == 0:
print('Signatures:')
for i in sig_check:
print(' > {0:15} {1:4}'.format(i[0] + ':', 'GOOD' if i[1] else 'FAIL'))
return sig_check
def __str__(self):
enabled_content_idxs = []
for i in range(0, 0x80 * 8):
if self.content_index[i // 8] & (1 << (i % 8)):
enabled_content_idxs.append(hex(i)[2:].zfill(4))
contents = ''
for i in enabled_content_idxs:
contents += f' > {i}\n'
contents = contents[:-1] # Remove last '\n'
if self.content_index == b'\xff' * 0x80: # If all content indexes are enabled, make printout shorter
contents = f' > 0000 \n ...\n > 03ff'
return (
f'TitleKey: {hex(readbe(self.data.enc_titlekey))[2:].zfill(32)} (decrypted: {hex(readbe(self.titlekey))[2:].zfill(32)})\n'
f'TicketID: {hex(self.data.ticketID)[2:].zfill(16)}\n'
f'ConsoleID: {hex(self.data.consoleID)[2:].zfill(8)}\n'
f'TitleID: {hex(readbe(bytes(self.data.titleID)))[2:].zfill(16)}\n'
f'Title version: {self.data.title_ver}\n'
f'Common KeyY index: {self.data.common_key_index}\n'
f'eShop account ID: {hex(readle(bytes(self.data.eshop_acc_id)))[2:].zfill(8)}\n' # ctrtool shows this as LE
f'Enabled contents:\n'
f'{contents}'
)
class tikBuilder:
def __init__(self, tik='', titleID='', title_ver=-1, ticketID='', consoleID='', eshop_acc_id='', titlekey='', common_key_index=-1, regen_sig='', out='tik_new'):
'''
tik: path to ticket (if available)
Following parameters are required if no ticket is provided; if both ticket and parameter is supplied, the parameter overrides the ticket
- titleID: titleID in hex, e.g. '000400000FF3FF00'
- title_ver: title version in decimal
- ticketID, consoleID, eshop_acc_id: in hex
- titlekey: decrypted title key in hex (if not provided, use titlekey generation algorithm)
- common_key_index: 0 or 1 or 2 or 3 or 4 or 5
regen_sig: '' or 'retail' (test keys) or 'dev'
out: path to output file
'''
# Checks
if titleID != '':
if not all([i in string.hexdigits for i in titleID]) or len(titleID) != 16:
raise Exception('Invalid TitleID')
if titlekey != '':
if not all([i in string.hexdigits for i in titlekey]) or len(titlekey) != 32:
raise Exception('Invalid TitleKey')
# Defaults
if tik == '':
if regen_sig == '':
regen_sig = 'retail'
if ticketID == '':
ticketID = '0'
if consoleID == '':
consoleID = '0'
if eshop_acc_id == '':
eshop_acc_id = '0'
if common_key_index == -1:
common_key_index = 0
# Create (or modify) ticket data
if tik == '':
data = tikData(b'\x00' * 0x164)
data.format_ver = 1
data.audit = 1
if titlekey == '':
titlekey = CTR.titlekey_gen(titleID, 'mypass')
else:
with open(tik, 'rb') as f:
sig_type = readbe(f.read(4))
sig = f.read(signature_types[sig_type][0])
padding = f.read(signature_types[sig_type][1])
data = tikData(f.read(0x164))
content_index_hdr = f.read(0x28)
content_index_offset = f.read(4)
content_index = f.read(0x80)
if tik == '' or regen_sig != '':
data.issuer = b'Root-CA00000003-XS0000000c'
if regen_sig == 'dev':
data.issuer = b'Root-CA00000004-XS00000009'
if ticketID != '':
data.ticketID = int(ticketID, 16)
if consoleID != '':
data.consoleID = int(consoleID, 16)
if titleID != '':
titleID_bytes = int.to_bytes((int(titleID, 16)), 8, 'big')
data.titleID = (c_uint8 * sizeof(data.titleID))(*titleID_bytes)
if title_ver != -1:
data.title_ver = title_ver
if common_key_index != -1:
data.common_key_index = common_key_index
if eshop_acc_id != '':
eshop_acc_id_bytes = int32tobytes(int(eshop_acc_id, 16))
data.eshop_acc_id = (c_uint8 * sizeof(data.eshop_acc_id))(*eshop_acc_id_bytes)
if titlekey != '': # Encrypt TitleKey
if regen_sig == 'dev':
dev = 1
else:
dev = 0
normal_key = CTR.key_scrambler(CTR.KeyX0x3D[dev], CTR.KeyY0x3D[data.common_key_index][dev])
cipher = AES.new(normal_key, AES.MODE_CBC, iv=bytes(data.titleID)+(b'\0'*8))
enc_titlekey = cipher.encrypt(hextobytes(titlekey))
data.enc_titlekey = (c_uint8 * sizeof(data.enc_titlekey))(*enc_titlekey)
# Create content index
if tik == '':
content_index_hdr = hextobytes('00010014 000000AC 00000014 00010014 00000000 00000028 00000001 00000084 00000084 00030000'.strip())
content_index_offset = b'\x00' * 4
content_index = b'\xff' * 0x80 # Enable all content indexes
# Write ticket
if regen_sig == 'retail':
sig = Crypto.sign_rsa_sha256(CTR.test_mod, CTR.test_priv, bytes(data) + content_index_hdr + content_index_offset + content_index)
elif regen_sig == 'dev':
sig = Crypto.sign_rsa_sha256(CTR.tik_mod[1], CTR.tik_priv[1], bytes(data) + content_index_hdr + content_index_offset + content_index)
with open(f'{out}', 'wb') as f:
f.write(int.to_bytes(0x00010004, 4, 'big'))
f.write(sig)
f.write(b'\x00' * 0x3C)
f.write(bytes(data))
f.write(content_index_hdr)
f.write(content_index_offset)
f.write(content_index)
print(f'Wrote to {out}')