mirror of
https://github.com/xprism1/ntool.git
synced 2025-06-18 16:05:33 -04:00
695 lines
29 KiB
Python
695 lines
29 KiB
Python
from .common import *
|
|
from .keys import *
|
|
from .ctr_exefs import ExeFSFileHdr
|
|
from .ctr_romfs import RomFSReader
|
|
|
|
class NCCHHdr(Structure):
|
|
_fields_ = [
|
|
('sig', c_uint8 * 0x100),
|
|
('magic', c_char * 4),
|
|
('ncch_size', c_uint32),
|
|
('titleID', c_uint8 * 8),
|
|
('maker_code', c_char * 2),
|
|
('format_ver', c_uint16),
|
|
('seed_hash', c_uint8 * 4),
|
|
('programID', c_uint8 * 8),
|
|
('reserved1', c_uint8 * 16),
|
|
('logo_hash', c_uint8 * 32),
|
|
('product_code', c_char * 16),
|
|
('exh_hash', c_uint8 * 32),
|
|
('exh_size', c_uint32),
|
|
('reserved2', c_uint32),
|
|
('flags', c_uint8 * 8),
|
|
('plain_offset', c_uint32),
|
|
('plain_size', c_uint32),
|
|
('logo_offset', c_uint32),
|
|
('logo_size', c_uint32),
|
|
('exefs_offset', c_uint32),
|
|
('exefs_size', c_uint32),
|
|
('exefs_hash_size', c_uint32),
|
|
('reserved4', c_uint32),
|
|
('romfs_offset', c_uint32),
|
|
('romfs_size', c_uint32),
|
|
('romfs_hash_size', c_uint32),
|
|
('reserved5', c_uint32),
|
|
('exefs_hash', c_uint8 * 32),
|
|
('romfs_hash', c_uint8 * 32),
|
|
]
|
|
|
|
def __new__(cls, buf):
|
|
return cls.from_buffer_copy(buf)
|
|
|
|
def __init__(self, data):
|
|
pass
|
|
|
|
# Returns the initial value for the AES-CTR counter
|
|
def get_ncch_counter(hdr, component):
|
|
counter = bytearray(b'\0' * 16)
|
|
|
|
if hdr.format_ver == 0 or hdr.format_ver == 2:
|
|
section = { 'exheader.bin': 0x01,
|
|
'exefs.bin': 0x02,
|
|
'romfs.bin': 0x03 }
|
|
counter[:8] = bytearray(hdr.titleID[::-1])
|
|
counter[8:9] = int8tobytes(section[component])
|
|
elif hdr.format_ver == 1:
|
|
if component == 'exheader.bin':
|
|
x = 0x200
|
|
elif component == 'exefs.bin':
|
|
x = hdr.exefs_offset
|
|
elif component == 'romfs.bin':
|
|
x = hdr.romfs_offset
|
|
counter[:8] = bytearray(hdr.titleID)
|
|
for i in range(4):
|
|
counter[12 + i:12 + i + 1] = int8tobytes(x >> (3 - i) * 8 & 255)
|
|
|
|
return bytes(counter)
|
|
|
|
def get_seed(titleID: bytes):
|
|
with open(os.path.join(resources_dir, 'seeddb.bin'), 'rb') as f:
|
|
seed_count = readle(f.read(4))
|
|
f.seek(0x10)
|
|
seed = -1
|
|
for _ in range(seed_count):
|
|
entry = f.read(0x20)
|
|
if entry[:8] == titleID:
|
|
seed = entry[8:24]
|
|
if seed == -1:
|
|
raise Exception('Could not find TitleID in SEEDDB')
|
|
|
|
return seed
|
|
|
|
class NCCHReader:
|
|
def __init__(self, file, dev=0, build=0): # 'build' parameter is to facilitate NCCHBuilder class
|
|
self.file = file
|
|
self.dev = dev
|
|
|
|
with open(file, 'rb') as f:
|
|
self.hdr = NCCHHdr(f.read(0x200))
|
|
|
|
if self.hdr.format_ver == 0 or self.hdr.format_ver == 2:
|
|
media_unit = 0x200
|
|
elif self.hdr.format_ver == 1:
|
|
media_unit = 1
|
|
|
|
# Parse flags
|
|
self.keyX_2 = { 0x00: CTR.KeyX0x2C,
|
|
0x01: CTR.KeyX0x25,
|
|
0x0A: CTR.KeyX0x18,
|
|
0x0B: CTR.KeyX0x1B }[self.hdr.flags[3]]
|
|
self.fixed_key = self.hdr.flags[7] & 0x1
|
|
self.no_romfs = self.hdr.flags[7] & 0x2
|
|
self.is_decrypted = self.hdr.flags[7] & 0x4
|
|
self.uses_seed = self.hdr.flags[7] & 0x20
|
|
|
|
# Generate keys
|
|
if self.fixed_key:
|
|
if readle(bytes(self.hdr.titleID)) & (0x10 << 32): # System category bit set in TitleID
|
|
self.normal_key = [hextobytes(hex(CTR.fixed_system)[2:]) for _ in range(2)]
|
|
else:
|
|
self.normal_key = [b'\0' * 16 for _ in range(2)]
|
|
else:
|
|
self.keyY = [bytes(self.hdr.sig)[:0x10], bytes(self.hdr.sig)[:0x10]]
|
|
self.keyX = [CTR.KeyX0x2C[dev], self.keyX_2[dev]]
|
|
|
|
if self.uses_seed: # This will result in keyY_2 being different
|
|
seed = get_seed(bytes(self.hdr.programID))
|
|
|
|
# Verify seed in SEEDDB
|
|
if hashlib.sha256(seed + self.hdr.programID).digest()[:4] != bytes(self.hdr.seed_hash):
|
|
raise Exception('Seed in SEEDDB failed verification')
|
|
|
|
self.keyY[1] = hashlib.sha256(self.keyY[0] + seed).digest()[:16]
|
|
|
|
self.normal_key = [CTR.key_scrambler(self.keyX[i], readbe(self.keyY[i])) for i in range(2)]
|
|
|
|
# Get component offset, size, AES-CTR key and initial value for counter, hash and size of component to calculate hash over
|
|
# Exheader, ExeFS and RomFS are encrypted
|
|
files = {}
|
|
files['ncch_header.bin'] = {
|
|
'name': 'NCCH Header',
|
|
'size': 0x200,
|
|
'offset': 0,
|
|
'crypt': 'none'
|
|
}
|
|
|
|
if self.hdr.exh_size:
|
|
files['exheader.bin'] = {
|
|
'name': 'Exheader',
|
|
'size': 0x800,
|
|
'offset': 0x200,
|
|
'crypt': 'normal',
|
|
'key': self.normal_key[0],
|
|
'counter': get_ncch_counter(self.hdr, 'exheader.bin'),
|
|
'hashes': (bytes(self.hdr.exh_hash), 0x400)
|
|
}
|
|
|
|
if self.hdr.logo_offset:
|
|
files['logo.bin'] = {
|
|
'name': 'Logo',
|
|
'size': self.hdr.logo_size * media_unit,
|
|
'offset': self.hdr.logo_offset * media_unit,
|
|
'crypt': 'none',
|
|
'hashes': (bytes(self.hdr.logo_hash), self.hdr.logo_size * media_unit)
|
|
}
|
|
|
|
if self.hdr.plain_size:
|
|
files['plain.bin'] = {
|
|
'name': 'Plain',
|
|
'size': self.hdr.plain_size * media_unit,
|
|
'offset': self.hdr.plain_offset * media_unit,
|
|
'crypt': 'none',
|
|
}
|
|
|
|
if self.hdr.exefs_offset:
|
|
files['exefs.bin'] = {
|
|
'name': 'ExeFS',
|
|
'size': self.hdr.exefs_size * media_unit,
|
|
'offset': self.hdr.exefs_offset * media_unit,
|
|
'crypt': 'exefs',
|
|
'key': self.normal_key,
|
|
'counter': get_ncch_counter(self.hdr, 'exefs.bin'),
|
|
'hashes': (bytes(self.hdr.exefs_hash), self.hdr.exefs_hash_size * media_unit)
|
|
}
|
|
|
|
# ExeFS header, 'icon' and 'banner' use normal_key[0], all other files in ExeFS use normal_key[1]
|
|
counter = Counter.new(128, initial_value=readbe(files['exefs.bin']['counter']))
|
|
cipher = AES.new(self.normal_key[0], AES.MODE_CTR, counter=counter)
|
|
with open(file, 'rb') as f:
|
|
f.seek(self.hdr.exefs_offset * media_unit)
|
|
if self.is_decrypted or build:
|
|
exefs_file_hdr = f.read(0xA0)
|
|
else:
|
|
exefs_file_hdr = cipher.decrypt(f.read(0xA0))
|
|
|
|
exefs_files = [(0, 0x200, 0, 'header')] # Each tuple is (offset in ExeFS, size, normal_key index, name)
|
|
for i in range(10):
|
|
file_hdr = ExeFSFileHdr(exefs_file_hdr[i * 16:(i + 1) * 16])
|
|
if file_hdr.size:
|
|
name = file_hdr.name.decode('utf-8').strip('\0')
|
|
if name in ('icon', 'banner'):
|
|
exefs_files.append((0x200 + file_hdr.offset, file_hdr.size, 0, name))
|
|
else:
|
|
exefs_files.append((0x200 + file_hdr.offset, file_hdr.size, 1, name))
|
|
curr = 0x200 + file_hdr.offset + file_hdr.size
|
|
if align(curr, 0x200): # Padding between ExeFS files uses normal_key[0]
|
|
exefs_files.append((curr, align(curr, 0x200), 0, 'padding'))
|
|
files['exefs.bin']['files'] = exefs_files
|
|
|
|
if not self.no_romfs:
|
|
if self.hdr.romfs_offset:
|
|
files['romfs.bin'] = {
|
|
'name': 'RomFS',
|
|
'size': self.hdr.romfs_size * media_unit,
|
|
'offset': self.hdr.romfs_offset * media_unit,
|
|
'crypt': 'normal',
|
|
'key': self.normal_key[1],
|
|
'counter': get_ncch_counter(self.hdr, 'romfs.bin'),
|
|
'hashes': (bytes(self.hdr.romfs_hash), self.hdr.romfs_hash_size * media_unit)
|
|
}
|
|
|
|
self.files = files
|
|
|
|
def extract(self):
|
|
f = open(self.file, 'rb')
|
|
for name, info in self.files.items():
|
|
f.seek(info['offset'])
|
|
g = open(name, 'wb')
|
|
|
|
if self.is_decrypted or info['crypt'] == 'none':
|
|
for data in read_chunks(f, info['size']):
|
|
g.write(data)
|
|
elif info['crypt'] == 'normal':
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']))
|
|
cipher = AES.new(info['key'], AES.MODE_CTR, counter=counter)
|
|
for data in read_chunks(f, info['size']):
|
|
g.write(cipher.decrypt(data))
|
|
elif info['crypt'] == 'exefs':
|
|
for off, size, key, _ in info['files']:
|
|
f.seek(info['offset'] + off)
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']) + (off // 16)) # We have to set the counter manually (initial value increments by 1 per AES block i.e. 16 bytes) since we are decrypting an arbitrary portion (and not from beginning)
|
|
cipher = AES.new(info['key'][key], AES.MODE_CTR, counter=counter)
|
|
cipher.decrypt(b'\0' * (off % 16)) # Cipher has to be advanced manually also
|
|
for data in read_chunks(f, size):
|
|
g.write(cipher.decrypt(data))
|
|
|
|
print(f'Extracted {name}')
|
|
g.close()
|
|
f.close()
|
|
|
|
def decrypt(self):
|
|
f = open(self.file, 'rb')
|
|
g = open('decrypted.ncch', 'wb')
|
|
curr = 0
|
|
for name, info in self.files.items():
|
|
if curr < info['offset']: # Padding between NCCH components
|
|
pad_size = info['offset'] - curr
|
|
g.write(b'\x00' * pad_size)
|
|
curr += pad_size
|
|
f.seek(info['offset'])
|
|
|
|
if name == 'ncch_header.bin':
|
|
hdr_dec = self.hdr
|
|
hdr_dec.flags[3] = 0 # Set keyX_2 to Key 0x2C
|
|
hdr_dec.flags[7] |= 4 # Set NoCrypto flag
|
|
hdr_dec.flags[7] &= ~1 # Unset FixedCryptoKey flag
|
|
hdr_dec.flags[7] &= ~0x20 # Unset UseSeedCrypto flag
|
|
g.write(bytes(hdr_dec))
|
|
elif self.is_decrypted or info['crypt'] == 'none':
|
|
for data in read_chunks(f, info['size']):
|
|
g.write(data)
|
|
elif info['crypt'] == 'normal':
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']))
|
|
cipher = AES.new(info['key'], AES.MODE_CTR, counter=counter)
|
|
for data in read_chunks(f, info['size']):
|
|
g.write(cipher.decrypt(data))
|
|
elif info['crypt'] == 'exefs':
|
|
for off, size, key, _ in info['files']:
|
|
f.seek(info['offset'] + off)
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']) + (off // 16))
|
|
cipher = AES.new(info['key'][key], AES.MODE_CTR, counter=counter)
|
|
cipher.decrypt(b'\0' * (off % 16))
|
|
for data in read_chunks(f, size):
|
|
g.write(cipher.decrypt(data))
|
|
curr += info['size']
|
|
|
|
f.close()
|
|
g.close()
|
|
print(f'Decrypted to decrypted.ncch')
|
|
|
|
def verify(self):
|
|
f = open(self.file, 'rb')
|
|
|
|
# Hash checks
|
|
hash_check = []
|
|
for name, info in self.files.items():
|
|
if 'hashes' in info.keys():
|
|
hashes = info['hashes']
|
|
f.seek(info['offset'])
|
|
if self.is_decrypted or info['crypt'] == 'none':
|
|
hash_check.append((info['name'], Crypto.sha256(f, hashes[1]) == hashes[0]))
|
|
else:
|
|
h = hashlib.sha256()
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']))
|
|
if info['crypt'] == 'normal':
|
|
cipher = AES.new(info['key'], AES.MODE_CTR, counter=counter)
|
|
elif info['crypt'] == 'exefs': # ExeFS hash is only over the ExeFS header (size 0x200), so we don't need to change the counter or the key
|
|
cipher = AES.new(info['key'][0], AES.MODE_CTR, counter=counter)
|
|
for data in read_chunks(f, hashes[1]):
|
|
h.update(cipher.decrypt(data))
|
|
hash_check.append((info['name'], h.digest() == hashes[0]))
|
|
|
|
# Signature checks
|
|
sig_check = []
|
|
if self.hdr.flags[5] & 0x2: # CXI
|
|
# Modulus for NCCH header signature is in accessdesc of exheader
|
|
f.seek(0x700)
|
|
if self.is_decrypted:
|
|
ncch_mod = f.read(0x100)
|
|
else:
|
|
info = self.files['exheader.bin']
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']) + 0x500 // 16)
|
|
cipher = AES.new(info['key'], AES.MODE_CTR, counter=counter)
|
|
ncch_mod = cipher.decrypt(f.read(0x100))
|
|
sig_check.append(('NCCH Header', Crypto.verify_rsa_sha256(ncch_mod, bytes(self.hdr)[0x100:], bytes(self.hdr.sig))))
|
|
|
|
f.seek(0x600)
|
|
if self.is_decrypted:
|
|
data = f.read(0x400)
|
|
else:
|
|
info = self.files['exheader.bin']
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']) + 0x400 // 16)
|
|
cipher = AES.new(info['key'], AES.MODE_CTR, counter=counter)
|
|
data = cipher.decrypt(f.read(0x400))
|
|
sig_check.append(('Exheader', Crypto.verify_rsa_sha256(CTR.accessdesc_mod[self.dev], data[0x100:], data[:0x100])))
|
|
elif self.hdr.flags[5] & 0x1: # CFA
|
|
sig_check.append(('NCCH Header', Crypto.verify_rsa_sha256(CTR.cfa_mod[self.dev], bytes(self.hdr)[0x100:], bytes(self.hdr.sig))))
|
|
|
|
f.close()
|
|
print("Hashes:")
|
|
for i in hash_check:
|
|
print(' > {0:15} {1:4}'.format(i[0] + ':', 'GOOD' if i[1] else 'FAIL'))
|
|
print("Signatures:")
|
|
for i in sig_check:
|
|
print(' > {0:15} {1:4}'.format(i[0] + ':', 'GOOD' if i[1] else 'FAIL'))
|
|
|
|
def __str__(self):
|
|
keyX_2 = { 0x00: 'Secure1 (Key 0x2C)',
|
|
0x01: 'Secure2 (Key 0x25)',
|
|
0x0A: 'Secure3 (Key 0x18)',
|
|
0x0B: 'Secure4 (Key 0x1B)' }
|
|
if self.is_decrypted:
|
|
crypto = 'None (Decrypted)'
|
|
elif self.fixed_key:
|
|
if readle(bytes(self.hdr.titleID)) & (0x10 << 32):
|
|
crypto = 'Fixed key (System)'
|
|
else:
|
|
crypto = 'Fixed key (Zero key)'
|
|
else:
|
|
crypto = keyX_2[self.hdr.flags[3]]
|
|
if self.uses_seed:
|
|
crypto += ' (KeyY seeded)'
|
|
|
|
platform = {
|
|
1: 'CTR',
|
|
2: 'SNAKE'
|
|
}
|
|
form_type = {
|
|
1: 'CFA',
|
|
2: 'CXI without RomFS',
|
|
3: 'CXI'
|
|
}
|
|
content_type = {
|
|
0: 'Application',
|
|
1: 'CTR System Update',
|
|
2: 'Manual',
|
|
3: 'Child',
|
|
4: 'Trial',
|
|
5: 'SNAKE System Update'
|
|
}
|
|
|
|
return (
|
|
f'TitleID: {hex(readle(self.hdr.titleID))[2:].zfill(16)}\n'
|
|
f'Maker code: {self.hdr.maker_code.decode("ascii")}\n'
|
|
f'Format version: {self.hdr.format_ver}\n'
|
|
f'Product code: {self.hdr.product_code.decode("ascii")}\n'
|
|
f'Flags:\n'
|
|
f' > Crypto method: {crypto}\n'
|
|
f' > Platform: {platform[self.hdr.flags[4]]}\n'
|
|
f' > Form type: {form_type[self.hdr.flags[5] & 0b11]}\n' # Lower 2 bits
|
|
f' > Content type: {content_type[self.hdr.flags[5] >> 2]}' # Bits 2-7
|
|
)
|
|
|
|
class NCCHBuilder:
|
|
def __init__(self, ncch_header='', exheader='', logo='', plain='', exefs='', romfs='', platform='', ncch_type='', maker_code='', product_code='', titleID='', programID='', crypto='', seed=0, regen_sig='', replace_tid=0, dev=0, out='new.ncch'):
|
|
'''
|
|
ncch_header, exheader, logo, plain, exefs, romfs: path to respective component (if available)
|
|
Following parameters are required if no NCCH header is provided; if both header and parameter is supplied, the parameter overrides the header(s)
|
|
- platform: 'CTR' or 'SNAKE'
|
|
- ncch_type: 'CXI' or 'CTRSystemUpdate' or 'SNAKESystemUpdate' or 'Manual' or 'Child' or 'Trial'
|
|
- maker_code: maker code, e.g. '00'
|
|
- product_code: product code, e.g. 'CTR-P-CTAP'
|
|
- titleID: titleID in hex (if not provided, take from exheader), e.g. '000400000FF3FF00'
|
|
- programID: programID in hex (if not provided, use the titleID)
|
|
- crypto: 'none' / 'fixed' / 'Secure1' / 'Secure2' / 'Secure3' / 'Secure4'
|
|
- seed: 0 or 1
|
|
regen_sig: '' or 'retail' (test keys; CXI header signature only) or 'dev' (NCCH header and exheader signature)
|
|
replace_tid: 0 or 1 (replaces TitleID in NCCH header and exheader)
|
|
dev: 0 or 1
|
|
out: path to output file
|
|
'''
|
|
|
|
# Checks
|
|
if ncch_type != '' and ncch_type != 'CXI' and exheader != '':
|
|
warnings.warn('Ignoring exheader since NCCH type is CFA')
|
|
exheader = ''
|
|
|
|
if maker_code != '':
|
|
if not len(maker_code) == 2:
|
|
raise Exception('Maker code length must be 2')
|
|
|
|
if product_code != '':
|
|
if not all([i == '-' or i.isdigit() or i.isupper() for i in product_code]) or len(product_code) < 10 or len(product_code) > 16 or product_code[:3] not in ['CTR', 'KTR']:
|
|
raise Exception('Invalid product code')
|
|
|
|
if titleID != '':
|
|
if not all([i in string.hexdigits for i in titleID]) or len(titleID) != 16:
|
|
raise Exception('Invalid TitleID')
|
|
|
|
if programID != '':
|
|
if not all([i in string.hexdigits for i in programID]) or len(programID) != 16:
|
|
raise Exception('Invalid programID')
|
|
|
|
if seed == 1 and (not crypto.startswith('Secure')):
|
|
raise Exception('Seed crypto can only be used with Secure crypto')
|
|
|
|
# Defaults
|
|
if ncch_header == '' and regen_sig == '':
|
|
regen_sig = 'retail'
|
|
|
|
if regen_sig == 'dev':
|
|
dev = 1
|
|
|
|
# Create (or modify) NCCH header
|
|
if exheader != '' and (regen_sig != '' or replace_tid == 1):
|
|
shutil.copyfile(exheader, 'exheader_mod')
|
|
|
|
if ncch_header == '':
|
|
hdr = NCCHHdr(b'\x00' * 0x200)
|
|
hdr.magic = b'NCCH'
|
|
if romfs == '':
|
|
hdr.flags[7] |= 2
|
|
if titleID == '' and exheader != '':
|
|
with open(exheader, 'rb') as f:
|
|
f.seek(0x200)
|
|
titleID = hex(readle(f.read(8)))[2:].zfill(16)
|
|
else:
|
|
with open(ncch_header, 'rb') as f:
|
|
hdr = NCCHHdr(f.read())
|
|
|
|
if ncch_header == '' and programID == '': # Defaults
|
|
programID = titleID
|
|
|
|
if titleID != '':
|
|
titleID_bytes = int64tobytes(int(titleID, 16))
|
|
hdr.titleID = (c_uint8 * sizeof(hdr.titleID))(*titleID_bytes)
|
|
|
|
if replace_tid == 1: # Replace TitleID in exheader
|
|
if exheader != '':
|
|
with open('exheader_mod', 'r+b') as f:
|
|
offs = [0x1C8, 0x200, 0x600]
|
|
for off in offs:
|
|
f.seek(off)
|
|
f.write(hextobytes(titleID))
|
|
|
|
if programID != '':
|
|
programID_bytes = int64tobytes(int(programID, 16))
|
|
hdr.programID = (c_uint8 * sizeof(hdr.programID))(*programID_bytes)
|
|
|
|
if maker_code != '':
|
|
hdr.maker_code = maker_code.encode('ascii')
|
|
|
|
if ncch_type != '':
|
|
if ncch_type == 'CXI':
|
|
hdr.format_ver = 2
|
|
else:
|
|
hdr.format_ver = 0
|
|
|
|
if ncch_header != '': # Reset content type flag first if existing value already exists
|
|
hdr.flags[5] = 0
|
|
if ncch_type == 'CXI' and romfs == '':
|
|
hdr.flags[5] |= 2
|
|
elif ncch_type == 'CXI' and romfs != '':
|
|
hdr.flags[5] |= 3
|
|
else:
|
|
hdr.flags[5] |= 1
|
|
if ncch_type == 'CTRSystemUpdate':
|
|
hdr.flags[5] |= 4
|
|
elif ncch_type == 'Manual':
|
|
hdr.flags[5] |= 8
|
|
elif ncch_type == 'Child':
|
|
hdr.flags[5] |= 0xC
|
|
elif ncch_type == 'Trial':
|
|
hdr.flags[5] |= 0x10
|
|
elif ncch_type == 'SNAKESystemUpdate':
|
|
hdr.flags[5] |= 0x14
|
|
|
|
if product_code != '':
|
|
hdr.product_code = product_code.encode('ascii')
|
|
|
|
if platform != '':
|
|
if platform == 'CTR':
|
|
hdr.flags[4] = 1
|
|
elif platform == 'SNAKE':
|
|
hdr.flags[4] = 2
|
|
|
|
if crypto != '':
|
|
# Reset crypto flags
|
|
hdr.flags[7] &= ~1
|
|
hdr.flags[7] &= ~4
|
|
hdr.flags[7] &= ~0x20
|
|
|
|
if crypto == 'none':
|
|
hdr.flags[7] |= 4
|
|
elif crypto == 'fixed':
|
|
hdr.flags[7] |= 1
|
|
else:
|
|
hdr.flags[3] = { 'Secure1': 0x00,
|
|
'Secure2': 0x01,
|
|
'Secure3': 0x0A,
|
|
'Secure4': 0x0B }[crypto]
|
|
if seed == 1:
|
|
hdr.flags[7] |= 0x20
|
|
seed = get_seed(bytes(hdr.programID))
|
|
seed_hash = hashlib.sha256(seed + hdr.programID).digest()[:4]
|
|
hdr.seed_hash = (c_uint8 * sizeof(hdr.seed_hash))(*seed_hash)
|
|
|
|
# Modify exheader (if necessary)
|
|
if regen_sig == 'retail':
|
|
if exheader != '':
|
|
with open('exheader_mod', 'r+b') as f: # Replace NCCH header mod
|
|
f.seek(0x500)
|
|
f.write(CTR.test_mod)
|
|
elif regen_sig == 'dev':
|
|
if exheader != '':
|
|
with open('exheader_mod', 'r+b') as f:
|
|
# NCCH header mod
|
|
f.seek(0x500)
|
|
f.write(CTR.test_mod)
|
|
|
|
# Exheader signature
|
|
f.seek(0x500)
|
|
sig = Crypto.sign_rsa_sha256(CTR.accessdesc_mod[1], CTR.accessdesc_priv[1], f.read(0x300))
|
|
f.seek(0x400)
|
|
f.write(sig)
|
|
|
|
if hdr.format_ver == 0 or hdr.format_ver == 2:
|
|
media_unit = 0x200
|
|
elif hdr.format_ver == 1:
|
|
media_unit = 1
|
|
|
|
curr = 0x200
|
|
files = {}
|
|
size_check = []
|
|
if exheader != '':
|
|
if regen_sig != '' or replace_tid == 1:
|
|
exheader = 'exheader_mod'
|
|
hdr.exh_size = 0x400
|
|
|
|
f = open(exheader, 'rb')
|
|
h = Crypto.sha256(f, 0x400)
|
|
hdr.exh_hash = (c_uint8 * sizeof(hdr.exh_hash))(*h)
|
|
f.close()
|
|
|
|
curr += os.path.getsize(exheader)
|
|
files['exheader.bin'] = {
|
|
'path': exheader
|
|
}
|
|
if logo != '':
|
|
curr += align(curr, 0x200)
|
|
size_check.append(hdr.logo_size == os.path.getsize(logo) // media_unit)
|
|
|
|
hdr.logo_offset = curr // media_unit
|
|
hdr.logo_size = os.path.getsize(logo) // media_unit
|
|
|
|
f = open(logo, 'rb')
|
|
h = Crypto.sha256(f, os.path.getsize(logo))
|
|
hdr.logo_hash = (c_uint8 * sizeof(hdr.logo_hash))(*h)
|
|
f.close()
|
|
|
|
curr += os.path.getsize(logo)
|
|
files['logo.bin'] = {
|
|
'path': logo
|
|
}
|
|
if plain != '':
|
|
curr += align(curr, 0x200)
|
|
size_check.append(hdr.plain_size == os.path.getsize(plain) // media_unit)
|
|
|
|
hdr.plain_offset = curr // media_unit
|
|
hdr.plain_size = os.path.getsize(plain) // media_unit
|
|
|
|
curr += os.path.getsize(plain)
|
|
files['plain.bin'] = {
|
|
'path': plain
|
|
}
|
|
if exefs != '':
|
|
curr += align(curr, 0x200)
|
|
size_check.append(hdr.exefs_size == os.path.getsize(exefs) // media_unit)
|
|
|
|
hdr.exefs_offset = curr // media_unit
|
|
hdr.exefs_size = os.path.getsize(exefs) // media_unit
|
|
|
|
f = open(exefs, 'rb')
|
|
h = Crypto.sha256(f, 0x200)
|
|
hdr.exefs_hash = (c_uint8 * sizeof(hdr.exefs_hash))(*h)
|
|
f.close()
|
|
hdr.exefs_hash_size = 0x200 // media_unit
|
|
|
|
curr += os.path.getsize(exefs)
|
|
files['exefs.bin'] = {
|
|
'path': exefs
|
|
}
|
|
if romfs != '':
|
|
r = RomFSReader(romfs)
|
|
romfs_hash_size = roundup(0x60 + r.hdr.master_hash_size, media_unit) # RomFS hash in NCCH is over RomFS header + master hash
|
|
|
|
size_check.append(hdr.romfs_size == os.path.getsize(romfs) // media_unit)
|
|
if all(size_check): # RomFS offset may be 0x200 aligned ("SDK 2.x and prior" according to makerom). In order for rebuilt NCCH to match original, we don't overwrite the existing RomFS offset if all sizes in provided NCCH header match provided files
|
|
curr = hdr.romfs_offset * media_unit
|
|
else:
|
|
curr += align(curr, 0x1000)
|
|
|
|
hdr.romfs_offset = curr // media_unit
|
|
hdr.romfs_size = os.path.getsize(romfs) // media_unit
|
|
|
|
f = open(romfs, 'rb')
|
|
h = Crypto.sha256(f, romfs_hash_size)
|
|
hdr.romfs_hash = (c_uint8 * sizeof(hdr.romfs_hash))(*h)
|
|
f.close()
|
|
hdr.romfs_hash_size = romfs_hash_size // media_unit
|
|
|
|
curr += os.path.getsize(romfs)
|
|
files['romfs.bin'] = {
|
|
'path': romfs
|
|
}
|
|
hdr.ncch_size = curr // media_unit
|
|
|
|
# Generate header signature (if necessary)
|
|
if regen_sig == 'retail':
|
|
if hdr.flags[5] & 0x2: # CXI
|
|
sig = Crypto.sign_rsa_sha256(CTR.test_mod, CTR.test_priv, bytes(hdr)[0x100:])
|
|
hdr.sig = (c_uint8 * sizeof(hdr.sig))(*sig)
|
|
elif regen_sig == 'dev':
|
|
if hdr.flags[5] & 0x2: # CXI
|
|
sig = Crypto.sign_rsa_sha256(CTR.test_mod, CTR.test_priv, bytes(hdr)[0x100:])
|
|
hdr.sig = (c_uint8 * sizeof(hdr.sig))(*sig)
|
|
else: # CFA
|
|
sig = Crypto.sign_rsa_sha256(CTR.cfa_mod[1], CTR.cfa_priv[1], bytes(hdr)[0x100:])
|
|
hdr.sig = (c_uint8 * sizeof(hdr.sig))(*sig)
|
|
|
|
# Generate keys by using NCCHReader on dummy NCCH with only NCCH header and ExeFS header
|
|
with open('tmp', 'wb') as f:
|
|
f.write(bytes(hdr))
|
|
if hdr.exefs_offset:
|
|
f.seek(hdr.exefs_offset * media_unit)
|
|
with open(files['exefs.bin']['path'], 'rb') as g:
|
|
exefs_file_hdr = g.read(0xA0)
|
|
f.write(exefs_file_hdr)
|
|
ncch = NCCHReader('tmp', dev=dev, build=1)
|
|
|
|
# Write NCCH
|
|
f = open(f'{out}', 'wb')
|
|
f.write(bytes(hdr))
|
|
curr = 0x200
|
|
for name, info in files.items():
|
|
info.update(ncch.files[name])
|
|
g = open(info['path'], 'rb')
|
|
if curr < info['offset']:
|
|
pad_size = info['offset'] - curr
|
|
f.write(b'\x00' * pad_size)
|
|
curr += pad_size
|
|
|
|
if ncch.is_decrypted or info['crypt'] == 'none':
|
|
for data in read_chunks(g, info['size']):
|
|
f.write(data)
|
|
elif info['crypt'] == 'normal':
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']))
|
|
cipher = AES.new(info['key'], AES.MODE_CTR, counter=counter)
|
|
for data in read_chunks(g, info['size']):
|
|
f.write(cipher.encrypt(data))
|
|
elif info['crypt'] == 'exefs':
|
|
for off, size, key, _ in info['files']:
|
|
g.seek(off)
|
|
counter = Counter.new(128, initial_value=readbe(info['counter']) + (off // 16))
|
|
cipher = AES.new(info['key'][key], AES.MODE_CTR, counter=counter)
|
|
cipher.encrypt(b'\0' * (off % 16))
|
|
for data in read_chunks(g, size):
|
|
f.write(cipher.encrypt(data))
|
|
curr += info['size']
|
|
g.close()
|
|
f.close()
|
|
os.remove('tmp')
|
|
if os.path.isfile('exheader_mod'):
|
|
os.remove('exheader_mod')
|
|
print(f'Wrote to {out}')
|