mirror of
https://github.com/xprism1/ntool.git
synced 2025-06-18 22:05:39 -04:00
219 lines
8.4 KiB
Python
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}')
|