mirror of
https://github.com/xprism1/ntool.git
synced 2025-06-18 16:05:33 -04:00
ntr_twl_srl: preliminary support
This commit is contained in:
parent
2cc05ae420
commit
2417b6831d
23
README.md
23
README.md
@ -6,9 +6,18 @@
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- **For the example commands, those in brackets are optional**
|
- For the example commands, those **in brackets are optional**
|
||||||
- Use **python** instead of **python3** if using Windows
|
- Use **python** instead of **python3** if using Windows
|
||||||
- "CCI" is equivalent to a .3ds file
|
|
||||||
|
### Notes
|
||||||
|
- "SRL" is equivalent to a .nds/.dsi file; "CCI" is equivalent to a .3ds file
|
||||||
|
- A ROM resigned for dev needs to match your devkit's region for it to load successfully
|
||||||
|
- IS-NITRO/TWL does not emulate Slot-1 save memory. Most games will show an error message if the save memory is not present. To work around this, you will need to insert a Slot-1 card with a matching save memory chip before loading the ROM image.
|
||||||
|
|
||||||
|
### Re-sign and re-encrypt NTR/TWL-enhanced/TWL-exclusive SRL for dev:
|
||||||
|
```py
|
||||||
|
python3 ntool.py srl_retail2dev <path_to_srl> (--out <path_to_output_file>)
|
||||||
|
```
|
||||||
|
|
||||||
### Re-sign and re-encrypt CIA/CCI for retail/dev:
|
### Re-sign and re-encrypt CIA/CCI for retail/dev:
|
||||||
```py
|
```py
|
||||||
@ -34,7 +43,17 @@ python3 ntool.py cci2cia <path_to_cci> (--out <path_to_output_file>) (--cci_dev)
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Convert CDN contents to CIA
|
### Convert CDN contents to CIA
|
||||||
|
Example folder structure required:
|
||||||
|
```
|
||||||
|
cdn_folder/
|
||||||
|
|__ 00000001
|
||||||
|
|__ 00000002
|
||||||
|
|__ 00000003
|
||||||
|
|__ tmd.0
|
||||||
|
|__ tmd.16
|
||||||
|
```
|
||||||
- If `--title-ver` is not provided and there are multiple TMD versions in the CDN folder, the latest TMD will be used
|
- If `--title-ver` is not provided and there are multiple TMD versions in the CDN folder, the latest TMD will be used
|
||||||
|
- If a `cetk` exists in the CDN folder, it will be used as the ticket; if not, a ticket with a fake signature will be generated and used
|
||||||
- Pass `--cdn-dev` if the CDN contents are dev-crypted/signed, pass `--cia-dev` if you want to build a dev-signed CIA
|
- Pass `--cdn-dev` if the CDN contents are dev-crypted/signed, pass `--cia-dev` if you want to build a dev-signed CIA
|
||||||
```py
|
```py
|
||||||
python3 ntool.py cdn2cia <path_to_cdn_folder> (--out <path_to_output_file>) (--title-ver <ver>) (--cdn-dev) (--cia-dev)
|
python3 ntool.py cdn2cia <path_to_cdn_folder> (--out <path_to_output_file>) (--title-ver <ver>) (--cdn-dev) (--cia-dev)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import os, sys, platform, struct, shutil, subprocess, string, warnings, hashlib, secrets, math
|
import os, sys, platform, struct, shutil, subprocess, string, warnings, hashlib, hmac, secrets, math
|
||||||
|
|
||||||
from ctypes import *
|
from ctypes import *
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
@ -51,7 +51,17 @@ def roundup(size, alignment):
|
|||||||
else:
|
else:
|
||||||
return size
|
return size
|
||||||
|
|
||||||
|
def rol(val, r_bits, max_bits):
|
||||||
|
return (val << r_bits % max_bits) & (2 ** max_bits - 1) | ((val & (2 ** max_bits - 1)) >> (max_bits - (r_bits % max_bits)))
|
||||||
|
|
||||||
class Crypto:
|
class Crypto:
|
||||||
|
def sha1(f, size, chunk_size=0x10000):
|
||||||
|
h = hashlib.sha1()
|
||||||
|
for _ in range(size // chunk_size):
|
||||||
|
h.update(f.read(chunk_size))
|
||||||
|
h.update(f.read(size % chunk_size))
|
||||||
|
return h.digest()
|
||||||
|
|
||||||
def sha256(f, size, chunk_size=0x10000):
|
def sha256(f, size, chunk_size=0x10000):
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
for _ in range(size // chunk_size):
|
for _ in range(size // chunk_size):
|
||||||
@ -73,3 +83,42 @@ class Crypto:
|
|||||||
return True
|
return True
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
crc16tab = [0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
|
||||||
|
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
|
||||||
|
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
|
||||||
|
0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
|
||||||
|
0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
|
||||||
|
0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
|
||||||
|
0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
|
||||||
|
0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
|
||||||
|
0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
|
||||||
|
0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
|
||||||
|
0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
|
||||||
|
0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
|
||||||
|
0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
|
||||||
|
0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
|
||||||
|
0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
|
||||||
|
0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
|
||||||
|
0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
|
||||||
|
0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
|
||||||
|
0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
|
||||||
|
0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
|
||||||
|
0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
|
||||||
|
0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
|
||||||
|
0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
|
||||||
|
0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
|
||||||
|
0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
|
||||||
|
0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
|
||||||
|
0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
|
||||||
|
0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
|
||||||
|
0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
|
||||||
|
0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
|
||||||
|
0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
|
||||||
|
0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040]
|
||||||
|
|
||||||
|
def crc16(data): # polynomial: x**16 + x**15 + x**2 + 1, 0xA001
|
||||||
|
crc = 0xFFFF
|
||||||
|
for i in range(0, len(data)):
|
||||||
|
crc = (crc >> 8) ^ crc16tab[(crc ^ data[i]) & 0xFF]
|
||||||
|
return crc
|
62
lib/keys.py
62
lib/keys.py
File diff suppressed because one or more lines are too long
833
lib/ntr_twl_srl.py
Normal file
833
lib/ntr_twl_srl.py
Normal file
@ -0,0 +1,833 @@
|
|||||||
|
from .common import *
|
||||||
|
from .keys import *
|
||||||
|
|
||||||
|
magic30 = 0x72636E65 # 'encr'
|
||||||
|
magic34 = 0x6A624F79 # 'yObj'
|
||||||
|
decrypted_id = 0xE7FFDEFF
|
||||||
|
block_size = 512
|
||||||
|
|
||||||
|
def mod_add(a, b):
|
||||||
|
return (a + b) % (2 ** 32)
|
||||||
|
|
||||||
|
def blowfish_encrypt(key, xl, xr): # xl and xr are u32
|
||||||
|
a = xl
|
||||||
|
b = xr
|
||||||
|
for i in range(0, 16):
|
||||||
|
c = key[i] ^ a
|
||||||
|
a = b ^ f(key, c)
|
||||||
|
b = c
|
||||||
|
|
||||||
|
xr = a ^ key[16]
|
||||||
|
xl = b ^ key[17]
|
||||||
|
return xl, xr
|
||||||
|
|
||||||
|
def blowfish_decrypt(key, xl, xr):
|
||||||
|
a = xl
|
||||||
|
b = xr
|
||||||
|
for i in range(17, 1, -1):
|
||||||
|
c = key[i] ^ a
|
||||||
|
a = b ^ f(key, c)
|
||||||
|
b = c
|
||||||
|
|
||||||
|
xl = b ^ key[0]
|
||||||
|
xr = a ^ key[1]
|
||||||
|
return xl, xr
|
||||||
|
|
||||||
|
def f(key, v):
|
||||||
|
a = key[18 + 0 + ((v >> 24) & 0xFF)]
|
||||||
|
b = key[18 + 256 + ((v >> 16) & 0xFF)]
|
||||||
|
c = key[18 + 512 + ((v >> 8) & 0xFF)]
|
||||||
|
d = key[18 + 768 + ((v >> 0) & 0xFF)]
|
||||||
|
|
||||||
|
return mod_add((mod_add(a, b) ^ c), d)
|
||||||
|
|
||||||
|
def apply_keycode(key, mod, keycode): # keycode is an array of size 3
|
||||||
|
mod //= 4
|
||||||
|
|
||||||
|
keycode[2], keycode[1] = blowfish_encrypt(key, keycode[2], keycode[1])
|
||||||
|
keycode[1], keycode[0] = blowfish_encrypt(key, keycode[1], keycode[0])
|
||||||
|
|
||||||
|
tmp1 = tmp2 = 0
|
||||||
|
for i in range(0, 18):
|
||||||
|
key[i] ^= byteswap32(keycode[i % mod])
|
||||||
|
for i in range(0, 18 + 1024, 2):
|
||||||
|
tmp1, tmp2 = blowfish_encrypt(key, tmp1, tmp2)
|
||||||
|
key[i + 0] = tmp1
|
||||||
|
key[i + 1] = tmp2
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def init_keycode(key, gamecode, level, mod):
|
||||||
|
key = [byteswap32(i) for i in key] # Original table is raw bytes which is big endian
|
||||||
|
keycode = [gamecode, gamecode // 2, gamecode * 2]
|
||||||
|
|
||||||
|
if level >= 1:
|
||||||
|
key = apply_keycode(key, mod, keycode)
|
||||||
|
if level >= 2:
|
||||||
|
key = apply_keycode(key, mod, keycode)
|
||||||
|
keycode[1] *= 2
|
||||||
|
keycode[2] //= 2
|
||||||
|
if level >= 3:
|
||||||
|
key = apply_keycode(key, mod, keycode)
|
||||||
|
|
||||||
|
return key
|
||||||
|
|
||||||
|
def get_rsa_key_idx(hdr, hdr_ext): # The RSA key to be used depends on which bits in the titleID are set
|
||||||
|
if hdr.unit_code == 0 and readbe(hdr_ext.sig) != 0:
|
||||||
|
return 3
|
||||||
|
elif hdr.unit_code == 2 or hdr.unit_code == 3:
|
||||||
|
if (hdr_ext.titleID_hi >> 1) & 1:
|
||||||
|
return 0
|
||||||
|
elif (hdr_ext.titleID_hi >> 4) & 1:
|
||||||
|
return 2
|
||||||
|
elif hdr_ext.titleID_hi & 1:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 3
|
||||||
|
|
||||||
|
class NTRBaseHdr(Structure): # For all games, 0x0 - 0x17F
|
||||||
|
_pack_ = 1
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('game_title', c_char * 12),
|
||||||
|
('game_code', c_char * 4),
|
||||||
|
('maker_code', c_char * 2),
|
||||||
|
('unit_code', c_uint8),
|
||||||
|
('encryption_seed_select', c_uint8),
|
||||||
|
('device_capacity', c_uint8),
|
||||||
|
('reserved1', c_uint8 * 7),
|
||||||
|
('data1', c_uint8), # DS: reserved, DSi enhanced/exclusive: crypto flags
|
||||||
|
('data2', c_uint8), # DS: region, DSi enhanced/exclusive: permit jump
|
||||||
|
('rom_ver', c_uint8),
|
||||||
|
('autostart', c_uint8),
|
||||||
|
('arm9_rom_offset', c_uint32),
|
||||||
|
('arm9_entry_addr', c_uint32),
|
||||||
|
('arm9_ram_addr', c_uint32),
|
||||||
|
('arm9_size', c_uint32),
|
||||||
|
('arm7_rom_offset', c_uint32),
|
||||||
|
('arm7_entry_addr', c_uint32),
|
||||||
|
('arm7_ram_addr', c_uint32),
|
||||||
|
('arm7_size', c_uint32),
|
||||||
|
('fnt_offset', c_uint32),
|
||||||
|
('fnt_size', c_uint32),
|
||||||
|
('fat_offset', c_uint32),
|
||||||
|
('fat_size', c_uint32),
|
||||||
|
('arm9_overlay_offset', c_uint32),
|
||||||
|
('arm9_overlay_size', c_uint32),
|
||||||
|
('arm7_overlay_offset', c_uint32),
|
||||||
|
('arm7_overlay_size', c_uint32),
|
||||||
|
('rom_control_normal', c_uint32),
|
||||||
|
('rom_control_key1', c_uint32),
|
||||||
|
('banner_offset', c_uint32),
|
||||||
|
('secure_area_crc', c_uint16),
|
||||||
|
('secure_area_delay', c_uint16),
|
||||||
|
('arm9_autoload_ram_addr', c_uint32),
|
||||||
|
('arm7_autoload_ram_addr', c_uint32),
|
||||||
|
('secure_area_disable', c_uint64),
|
||||||
|
('ntr_rom_size', c_uint32),
|
||||||
|
('hdr_size', c_uint32),
|
||||||
|
('data3', c_uint32), # DS: unknown, DS games after DSi / DSi enhanced/exclusive: ARM9 parameters table offset
|
||||||
|
('data4', c_uint32), # DS: reserved, DS games after DSi / DSi enhanced/exclusive: ARM7 parameters table offset
|
||||||
|
('data5', c_uint16), # DS: reserved, DSi enhanced/exclusive: NTR ROM region end
|
||||||
|
('data6', c_uint16), # DS: reserved, DSi enhanced/exclusive: TWL ROM region start
|
||||||
|
('nand_rom_end', c_uint16),
|
||||||
|
('nand_rw_start', c_uint16),
|
||||||
|
('reserved2', c_uint8 * 0x28),
|
||||||
|
('logo', c_uint8 * 0x9C),
|
||||||
|
('logo_crc', c_uint16),
|
||||||
|
('hdr_crc', c_uint16),
|
||||||
|
('debug_rom_offset', c_uint32),
|
||||||
|
('debug_size', c_uint32),
|
||||||
|
('debug_ram_addr', c_uint32),
|
||||||
|
('reserved3', c_uint8 * 0x14)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __new__(cls, buf):
|
||||||
|
return cls.from_buffer_copy(buf)
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NTRExtendedHdr(Structure): # For DS games released after the DSi, 0x1BF - 0x1000
|
||||||
|
_pack_ = 1
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('flags', c_uint8),
|
||||||
|
('reserved1', c_uint8 * 0x17C),
|
||||||
|
('banner_hmac', c_uint8 * 20),
|
||||||
|
('reserved2', c_uint8 * 0x28),
|
||||||
|
('hdr_arm9_arm7_hmac', c_uint8 * 20),
|
||||||
|
('arm9overlay_fat_hmac', c_uint8 * 20),
|
||||||
|
('reserved3', c_uint8 * 0xBE0),
|
||||||
|
('sig', c_uint8 * 0x80)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __new__(cls, buf):
|
||||||
|
return cls.from_buffer_copy(buf)
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TWLExtendedHdr(Structure): # For DSi enhanced or exclusive games, 0x180 - 0x1000
|
||||||
|
_pack_ = 1
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('global_mbk1_5_settings', c_uint8 * 20),
|
||||||
|
('local_mbk6_8_settings_wram_arm9', c_uint8 * 12),
|
||||||
|
('local_mbk6_8_settings_wram_arm7', c_uint8 * 12),
|
||||||
|
('global_mbk9_wram_write_protect', c_uint8 * 3),
|
||||||
|
('global_wramcnt', c_uint8),
|
||||||
|
('region', c_uint32),
|
||||||
|
('access_control', c_uint32),
|
||||||
|
('arm7_scfg_ext7', c_uint32),
|
||||||
|
('reserved1', c_uint8 * 3),
|
||||||
|
('flags', c_uint8),
|
||||||
|
('arm9i_rom_offset', c_uint32),
|
||||||
|
('reserved2', c_uint32),
|
||||||
|
('arm9i_ram_addr', c_uint32),
|
||||||
|
('arm9i_size', c_uint32),
|
||||||
|
('arm7i_rom_offset', c_uint32),
|
||||||
|
('arm7i_ram_addr_sd', c_uint32),
|
||||||
|
('arm7i_ram_addr', c_uint32),
|
||||||
|
('arm7i_size', c_uint32),
|
||||||
|
('ntr_digest_region_offset', c_uint32),
|
||||||
|
('ntr_digest_region_size', c_uint32),
|
||||||
|
('twl_digest_region_offset', c_uint32),
|
||||||
|
('twl_digest_region_size', c_uint32),
|
||||||
|
('digest1_table_offset', c_uint32),
|
||||||
|
('digest1_table_size', c_uint32),
|
||||||
|
('digest2_table_offset', c_uint32),
|
||||||
|
('digest2_table_size', c_uint32),
|
||||||
|
('digest1_block_size', c_uint32),
|
||||||
|
('digest2_digest1_count', c_uint32),
|
||||||
|
('banner_size', c_uint32),
|
||||||
|
('shared2_0000_size', c_uint8),
|
||||||
|
('shared2_0001_size', c_uint8),
|
||||||
|
('eula_ver', c_uint8),
|
||||||
|
('use_ratings', c_uint8),
|
||||||
|
('total_rom_size', c_uint32),
|
||||||
|
('shared2_0002_size', c_uint8),
|
||||||
|
('shared2_0003_size', c_uint8),
|
||||||
|
('shared2_0004_size', c_uint8),
|
||||||
|
('shared2_0005_size', c_uint8),
|
||||||
|
('arm9i_params_table_offset', c_uint32),
|
||||||
|
('arm7i_params_table_offset', c_uint32),
|
||||||
|
('modcrypt_area_1_offset', c_uint32),
|
||||||
|
('modcrypt_area_1_size', c_uint32),
|
||||||
|
('modcrypt_area_2_offset', c_uint32),
|
||||||
|
('modcrypt_area_2_size', c_uint32),
|
||||||
|
('titleID_lo', c_uint32),
|
||||||
|
('titleID_hi', c_uint32),
|
||||||
|
('pub_save_data_size', c_uint32),
|
||||||
|
('priv_save_data_size', c_uint32),
|
||||||
|
('reserved3', c_uint8 * 0xB0),
|
||||||
|
('parental_control', c_uint8 * 16),
|
||||||
|
('arm9_hmac', c_uint8 * 20),
|
||||||
|
('arm7_hmac', c_uint8 * 20),
|
||||||
|
('digest2_hmac', c_uint8 * 20),
|
||||||
|
('banner_hmac', c_uint8 * 20),
|
||||||
|
('arm9i_hmac', c_uint8 * 20),
|
||||||
|
('arm7i_hmac', c_uint8 * 20),
|
||||||
|
('reserved4', c_uint8 * 40),
|
||||||
|
('arm9_no_secure_area_hmac', c_uint8 * 20),
|
||||||
|
('reserved5', c_uint8 * 0xA4C),
|
||||||
|
('debug_args', c_uint8 * 0x180),
|
||||||
|
('sig', c_uint8 * 0x80)
|
||||||
|
]
|
||||||
|
|
||||||
|
def __new__(cls, buf):
|
||||||
|
return cls.from_buffer_copy(buf)
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class KeyTable(Structure): # In ROM dumps, the NTR KeyTable is all '00', and the TWL KeyTable contains mirrors of the data in 0x8000 - 0x8FFF
|
||||||
|
_pack_ = 1
|
||||||
|
|
||||||
|
_fields_ = [
|
||||||
|
('reserved_1', c_uint8 * 0x600),
|
||||||
|
('p_array', c_uint8 * 0x48),
|
||||||
|
('reserved_2', c_uint8 * 0x5B8),
|
||||||
|
('s_boxes', c_uint8 * 0x1000),
|
||||||
|
('reserved_3', c_uint8 * 0x400),
|
||||||
|
('test_pattern', c_uint8 * 0x1000) # Only in NTR KeyTable
|
||||||
|
]
|
||||||
|
|
||||||
|
def __new__(cls, buf):
|
||||||
|
return cls.from_buffer_copy(buf)
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SRLReader:
|
||||||
|
def __init__(self, file, dev=0):
|
||||||
|
self.file = file
|
||||||
|
self.dev = dev
|
||||||
|
self.media = 'Game card'
|
||||||
|
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
self.hdr = NTRBaseHdr(f.read(0x180))
|
||||||
|
if self.hdr.unit_code == 0:
|
||||||
|
f.seek(0x1BF)
|
||||||
|
self.hdr_ext = NTRExtendedHdr(f.read(0xE41))
|
||||||
|
elif self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
self.hdr_ext = TWLExtendedHdr(f.read(0xE80))
|
||||||
|
if (self.hdr_ext.titleID_hi >> 2) & 1:
|
||||||
|
self.media = 'NAND'
|
||||||
|
|
||||||
|
if self.media == 'Game card':
|
||||||
|
f.seek(0x1000)
|
||||||
|
self.keytable = KeyTable(f.read(0x3000))
|
||||||
|
if self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
f.seek(self.hdr.data6 * 0x80000)
|
||||||
|
self.keytable_2 = KeyTable(f.read(0x3000))
|
||||||
|
|
||||||
|
# Check NTR secure area
|
||||||
|
if self.hdr.arm9_rom_offset != 0x4000:
|
||||||
|
self.secure_area_status = 'not present'
|
||||||
|
else:
|
||||||
|
f.seek(0x4000)
|
||||||
|
tmp1 = readle(f.read(4))
|
||||||
|
tmp2 = readle(f.read(4))
|
||||||
|
if tmp1 == 0 and tmp2 == 0:
|
||||||
|
self.secure_area_status = 'empty'
|
||||||
|
elif (tmp1, tmp2) in [
|
||||||
|
# properly decrypted standard value
|
||||||
|
(decrypted_id, decrypted_id),
|
||||||
|
# properly decrypted non-standard value
|
||||||
|
(0xD0D48B67, 0x39392F23), # Dragon Quest 5 (EU)
|
||||||
|
(0x014A191A, 0xA5C470B9), # Dragon Quest 5 (USA)
|
||||||
|
(0x7829BC8D, 0x9968EF44), # Dragon Quest 5 (JP)
|
||||||
|
(0xC4A15AB8, 0xD2E667C8), # Prince of Persia (EU)
|
||||||
|
(0xD5E97D20, 0x21B2A159), # Prince of Persia (USA)
|
||||||
|
# properly decrypted prototype value
|
||||||
|
(0xBA35F813, 0xB691AAE8),
|
||||||
|
# improperly decrypted empty secure area (decrypt empty with woodsec)
|
||||||
|
(0xE386C397, 0x82775B7E),
|
||||||
|
(0xF98415B8, 0x698068FC),
|
||||||
|
(0xA71329EE, 0x2A1D4C38),
|
||||||
|
(0xC44DCC48, 0x38B6F8CB),
|
||||||
|
(0x3A9323B5, 0xC0387241),
|
||||||
|
]:
|
||||||
|
self.secure_area_status = 'decrypted'
|
||||||
|
else:
|
||||||
|
self.secure_area_status = 'encrypted'
|
||||||
|
|
||||||
|
files = {}
|
||||||
|
|
||||||
|
files['header.bin'] = {
|
||||||
|
'name': 'Header',
|
||||||
|
'offset': 0,
|
||||||
|
'size': 0x1000
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.media == 'Game card' and bytes(self.keytable) != b'\x00' * 0x3000: # Only exists for game card SRLs
|
||||||
|
files['keytable.bin'] = {
|
||||||
|
'name': 'KeyTable',
|
||||||
|
'offset': 0x1000,
|
||||||
|
'size': 0x3000
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr.arm9_rom_offset:
|
||||||
|
size = self.hdr.arm9_size
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
f.seek(self.hdr.arm9_rom_offset + self.hdr.arm9_size)
|
||||||
|
if readle(f.read(4)) == 0xDEC00621:
|
||||||
|
size += 0xC
|
||||||
|
files['arm9.bin'] = {
|
||||||
|
'name': 'ARM9',
|
||||||
|
'offset': self.hdr.arm9_rom_offset,
|
||||||
|
'size': size
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr.arm9_overlay_offset:
|
||||||
|
files['arm9overlay.bin'] = {
|
||||||
|
'name': 'ARM9 overlay',
|
||||||
|
'offset': self.hdr.arm9_overlay_offset,
|
||||||
|
'size': self.hdr.arm9_overlay_size
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr.arm7_rom_offset:
|
||||||
|
files['arm7.bin'] = {
|
||||||
|
'name': 'ARM7',
|
||||||
|
'offset': self.hdr.arm7_rom_offset,
|
||||||
|
'size': self.hdr.arm7_size
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr.arm7_overlay_offset:
|
||||||
|
files['arm7overlay.bin'] = {
|
||||||
|
'name': 'ARM7 overlay',
|
||||||
|
'offset': self.hdr.arm7_overlay_offset,
|
||||||
|
'size': self.hdr.arm7_overlay_size
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr.banner_offset:
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
f.seek(self.hdr.banner_offset)
|
||||||
|
ver = readle(f.read(2))
|
||||||
|
banner_sizes = { 0x0001: 0x0840,
|
||||||
|
0x0002: 0x0940,
|
||||||
|
0x0003: 0x1240,
|
||||||
|
0x0103: 0x23C0 }
|
||||||
|
if self.hdr.unit_code == 0:
|
||||||
|
size = banner_sizes[ver]
|
||||||
|
elif self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
size = self.hdr_ext.banner_size
|
||||||
|
files['banner.bin'] = {
|
||||||
|
'name': 'Banner',
|
||||||
|
'offset': self.hdr.banner_offset,
|
||||||
|
'size': size
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.media == 'Game card' and (self.hdr.unit_code == 2 or self.hdr.unit_code == 3): # Only exists for game card SRLs
|
||||||
|
if bytes(self.keytable_2) != b'\x00' * 0x3000:
|
||||||
|
files['keytable2.bin'] = {
|
||||||
|
'name': 'KeyTable2',
|
||||||
|
'offset': self.hdr.data6 * 0x80000,
|
||||||
|
'size': 0x3000
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
if self.hdr_ext.arm9i_rom_offset:
|
||||||
|
files['arm9i.bin'] = {
|
||||||
|
'name': 'ARM9i',
|
||||||
|
'offset': self.hdr_ext.arm9i_rom_offset,
|
||||||
|
'size': self.hdr_ext.arm9i_size
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.hdr_ext.arm7i_rom_offset:
|
||||||
|
files['arm7i.bin'] = {
|
||||||
|
'name': 'ARM7i',
|
||||||
|
'offset': self.hdr_ext.arm7i_rom_offset,
|
||||||
|
'size': self.hdr_ext.arm7i_size
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: parse FNT/FAT
|
||||||
|
self.files = files
|
||||||
|
|
||||||
|
# Generate modcrypt keys (if present)
|
||||||
|
if self.hdr.unit_code == 0:
|
||||||
|
self.modcrypted = False
|
||||||
|
elif self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
self.modcrypted = True
|
||||||
|
if self.hdr.data1 >> 1 == 0:
|
||||||
|
self.modcrypted = False
|
||||||
|
|
||||||
|
modcrypt = []
|
||||||
|
if self.modcrypted:
|
||||||
|
if (self.hdr.data1 >> 2 & 1) or ((self.hdr_ext.flags >> 7) & 1): # ModcryptKeyDebug or DeveloperApp
|
||||||
|
self.normal_key = bytes(self.hdr)[:16]
|
||||||
|
else:
|
||||||
|
keyX = b'Nintendo' + bytes(self.hdr.game_code) + bytes(self.hdr.game_code)[::-1]
|
||||||
|
keyY = bytes(self.hdr_ext.arm9i_hmac)[:16]
|
||||||
|
self.normal_key = TWL.key_scrambler(readle(keyX), readle(keyY))[::-1]
|
||||||
|
self.normal_key = self.normal_key[::-1] # Key and IV needs to be reversed before use
|
||||||
|
|
||||||
|
if self.hdr_ext.modcrypt_area_1_offset:
|
||||||
|
modcrypt.append({
|
||||||
|
'name': 'modcrypt area 1',
|
||||||
|
'offset': self.hdr_ext.modcrypt_area_1_offset,
|
||||||
|
'size': self.hdr_ext.modcrypt_area_1_size,
|
||||||
|
'key': self.normal_key,
|
||||||
|
'counter': bytes(self.hdr_ext.arm9_hmac)[:16][::-1]
|
||||||
|
})
|
||||||
|
if self.hdr_ext.modcrypt_area_2_offset:
|
||||||
|
modcrypt.append({
|
||||||
|
'name': 'modcrypt area 2',
|
||||||
|
'offset': self.hdr_ext.modcrypt_area_2_offset,
|
||||||
|
'size': self.hdr_ext.modcrypt_area_2_size,
|
||||||
|
'key': self.normal_key,
|
||||||
|
'counter': bytes(self.hdr_ext.arm7_hmac)[:16][::-1]
|
||||||
|
})
|
||||||
|
self.modcrypt = modcrypt
|
||||||
|
|
||||||
|
def decrypt_secure_area(self, secure_area, key):
|
||||||
|
# Checks
|
||||||
|
if self.secure_area_status != 'encrypted':
|
||||||
|
raise Exception(f'Secure area is {self.secure_area_status}, cannot be decrypted')
|
||||||
|
|
||||||
|
# Initialize with level 2, modulo 8 and decrypt first 8 bytes of secure area
|
||||||
|
key_lvl2 = init_keycode(key, readle(self.hdr.game_code), 2, 8)
|
||||||
|
p1, p0 = blowfish_decrypt(key_lvl2, readle(secure_area[4:8]), readle(secure_area[:4]))
|
||||||
|
secure_area = int32tobytes(p0) + int32tobytes(p1) + secure_area[8:]
|
||||||
|
|
||||||
|
# Initialize again with level 3, modulo 8 and decrypt first 2KB of secure area
|
||||||
|
key_lvl3 = init_keycode(key, readle(self.hdr.game_code), 3, 8)
|
||||||
|
for i in range(0, 0x800, 8):
|
||||||
|
p1, p0 = blowfish_decrypt(key_lvl3, readle(secure_area[i + 4:i + 8]), readle(secure_area[i:i + 4]))
|
||||||
|
secure_area = secure_area[:i] + int32tobytes(p0) + int32tobytes(p1) + secure_area[i + 8:]
|
||||||
|
|
||||||
|
if readle(secure_area[:4]) == magic30 and readle(secure_area[4:8]) == magic34:
|
||||||
|
secure_area = int32tobytes(decrypted_id) + int32tobytes(decrypted_id) + secure_area[8:]
|
||||||
|
else:
|
||||||
|
raise Exception('Secure area ID decryption failed')
|
||||||
|
|
||||||
|
secure_area_crc = readle(secure_area[0xE:0x10])
|
||||||
|
crc_calculated = crc16(list(secure_area[0x10:0x800]))
|
||||||
|
if secure_area_crc != crc_calculated:
|
||||||
|
raise Exception('Secure area CRC invalid')
|
||||||
|
|
||||||
|
return secure_area
|
||||||
|
|
||||||
|
def encrypt_secure_area(self, secure_area, key):
|
||||||
|
# Checks
|
||||||
|
if self.secure_area_status != 'decrypted':
|
||||||
|
raise Exception(f'Secure area is {self.secure_area_status}, cannot be encrypted')
|
||||||
|
|
||||||
|
# Set the secure area ID, which was overwritten with decrypted_id
|
||||||
|
secure_area = int32tobytes(magic30) + int32tobytes(magic34) + secure_area[8:]
|
||||||
|
|
||||||
|
# Initialize with level 3, modulo 8 and encrypt first 2KB of secure area
|
||||||
|
key_lvl3 = init_keycode(key, readle(self.hdr.game_code), 3, 8)
|
||||||
|
for i in range(0, 0x800, 8):
|
||||||
|
p1, p0 = blowfish_encrypt(key_lvl3, readle(secure_area[i + 4:i + 8]), readle(secure_area[i:i + 4]))
|
||||||
|
secure_area = secure_area[:i] + int32tobytes(p0) + int32tobytes(p1) + secure_area[i + 8:]
|
||||||
|
|
||||||
|
# Initialize with level 2, modulo 8 and encrypt first 8 bytes of secure area
|
||||||
|
key_lvl2 = init_keycode(key, readle(self.hdr.game_code), 2, 8)
|
||||||
|
p1, p0 = blowfish_encrypt(key_lvl2, readle(secure_area[4:8]), readle(secure_area[:4]))
|
||||||
|
secure_area = int32tobytes(p0) + int32tobytes(p1) + secure_area[8:]
|
||||||
|
|
||||||
|
return secure_area
|
||||||
|
|
||||||
|
def regen_undumpable(self):
|
||||||
|
# Checks
|
||||||
|
if self.media == 'NAND':
|
||||||
|
raise Exception('No undumpable region to re-generate for NAND SRL')
|
||||||
|
|
||||||
|
with open(os.path.join(resources_dir, 'test_pattern.bin'), 'rb') as f:
|
||||||
|
test_pattern = f.read()
|
||||||
|
with open(os.path.join(resources_dir, 'keytable2_dev.bin'), 'rb') as f:
|
||||||
|
keytable2_dev = f.read()
|
||||||
|
|
||||||
|
shutil.copyfile(self.file, 'new.nds')
|
||||||
|
with open('new.nds', 'r+b') as f:
|
||||||
|
# KeyTable (NTR)
|
||||||
|
key = init_keycode(NTR.blowfish_key, readle(self.hdr.game_code), 2, 8)
|
||||||
|
|
||||||
|
f.seek(0x1600)
|
||||||
|
for i in range(0, 18):
|
||||||
|
f.write(int32tobytes(key[i]))
|
||||||
|
|
||||||
|
f.seek(0x1C00)
|
||||||
|
for i in range(18, 18 + 1024):
|
||||||
|
f.write(int32tobytes(key[i]))
|
||||||
|
|
||||||
|
f.seek(0x3000)
|
||||||
|
f.write(test_pattern)
|
||||||
|
|
||||||
|
# KeyTable2 (TWL)
|
||||||
|
if self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
if self.dev:
|
||||||
|
f.seek(self.hdr.data6 * 0x80000)
|
||||||
|
f.write(keytable2_dev)
|
||||||
|
else:
|
||||||
|
key = init_keycode(TWL.blowfish_key[0], readle(self.hdr.game_code), 1, 8)
|
||||||
|
|
||||||
|
f.seek(self.hdr.data6 * 0x80000 + 0x600)
|
||||||
|
for i in range(0, 18):
|
||||||
|
f.write(int32tobytes(key[i]))
|
||||||
|
|
||||||
|
f.seek(self.hdr.data6 * 0x80000 + 0xC00)
|
||||||
|
for i in range(18, 18 + 1024):
|
||||||
|
f.write(int32tobytes(key[i]))
|
||||||
|
|
||||||
|
print('Wrote to new.nds')
|
||||||
|
|
||||||
|
def decrypt_modcrypt(self):
|
||||||
|
if self.modcrypted:
|
||||||
|
shutil.copyfile(self.file, 'decrypted.nds')
|
||||||
|
f = open(self.file, 'rb')
|
||||||
|
for i in self.modcrypt:
|
||||||
|
g = open('decrypted.nds', 'r+b')
|
||||||
|
f.seek(i['offset'])
|
||||||
|
g.seek(i['offset'])
|
||||||
|
|
||||||
|
counter = bytearray(i['counter'])
|
||||||
|
for data in read_chunks(f, i['size']):
|
||||||
|
for j in range(len(data) // 16):
|
||||||
|
output, counter = TWL.aes_ctr_block(i['key'], counter, data[j * 16:(j + 1) * 16])
|
||||||
|
g.write(output)
|
||||||
|
|
||||||
|
print(f'Decrypted {i["name"]}')
|
||||||
|
g.close()
|
||||||
|
f.close()
|
||||||
|
print(f'Wrote to decrypted.nds')
|
||||||
|
else:
|
||||||
|
raise Exception('Not modcrypted')
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
# NOTE: Some checks (those that involve the ARM9) will report FAIL if (NTR) secure area is decrypted; since the HMACs are calculated over the ARM9 with encrypted secure area
|
||||||
|
|
||||||
|
# Decrypt modcrypt first (if present) since HMACs and digests are calculated with modcrypt decrypted
|
||||||
|
file = self.file
|
||||||
|
if self.hdr.data1 >> 1 != 0:
|
||||||
|
sys.stdout = open(os.devnull, 'w') # Block print statements
|
||||||
|
self.decrypt_modcrypt()
|
||||||
|
file = 'decrypted.nds'
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
|
||||||
|
crc_check = []
|
||||||
|
with open(file, 'rb') as f:
|
||||||
|
f.seek(self.hdr.arm9_rom_offset)
|
||||||
|
data = f.read(0x4000)
|
||||||
|
crc_check.append(('Secure area', crc16(list(data)) == self.hdr.secure_area_crc))
|
||||||
|
|
||||||
|
f.seek(0xC0)
|
||||||
|
data = f.read(0x9C)
|
||||||
|
crc_check.append(('Nintendo logo', crc16(list(data)) == self.hdr.logo_crc))
|
||||||
|
|
||||||
|
f.seek(0)
|
||||||
|
data = f.read(0x15E)
|
||||||
|
crc_check.append(('Header', crc16(list(data)) == self.hdr.hdr_crc))
|
||||||
|
|
||||||
|
hmac_check = []
|
||||||
|
if self.hdr.unit_code == 0 and readbe(self.hdr_ext.sig) != 0:
|
||||||
|
f = open(file, 'rb')
|
||||||
|
|
||||||
|
if (self.hdr_ext.flags >> 5) & 1 and bytes(self.hdr_ext.banner_hmac) != b'\x00' * 20:
|
||||||
|
f.seek(self.files['banner.bin']['offset'])
|
||||||
|
hmac_digest = hmac.new(key=TWL.hmac_key_whitelist34, digestmod=hashlib.sha1)
|
||||||
|
for data in read_chunks(f, self.files['banner.bin']['size']):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
hmac_check.append(('Banner', hmac_digest.digest() == bytes(self.hdr_ext.banner_hmac)))
|
||||||
|
|
||||||
|
if (self.hdr_ext.flags >> 6) & 1 and bytes(self.hdr_ext.hdr_arm9_arm7_hmac) != b'\x00' * 20:
|
||||||
|
hmac_digest = hmac.new(key=TWL.hmac_key_whitelist12, digestmod=hashlib.sha1)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
f.seek(0)
|
||||||
|
hmac_digest.update(f.read(0x160))
|
||||||
|
|
||||||
|
# ARM9
|
||||||
|
f.seek(self.files['arm9.bin']['offset'])
|
||||||
|
for data in read_chunks(f, self.hdr.arm9_size):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
|
||||||
|
# ARM7
|
||||||
|
f.seek(self.files['arm7.bin']['offset'])
|
||||||
|
for data in read_chunks(f, self.files['arm7.bin']['size']):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
|
||||||
|
hmac_check.append(('Hdr,ARM9,ARM7', hmac_digest.digest() == bytes(self.hdr_ext.hdr_arm9_arm7_hmac)))
|
||||||
|
|
||||||
|
if (self.hdr_ext.flags >> 6) & 1 and bytes(self.hdr_ext.arm9overlay_fat_hmac) != b'\x00' * 20:
|
||||||
|
hmac_digest = hmac.new(key=TWL.hmac_key_whitelist12, digestmod=hashlib.sha1)
|
||||||
|
|
||||||
|
# ARM9 overlay
|
||||||
|
f.seek(self.files['arm9overlay.bin']['offset'])
|
||||||
|
for data in read_chunks(f, self.files['arm9overlay.bin']['size']):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
|
||||||
|
# FAT entries for ARM9 overlay
|
||||||
|
num_overlays = self.files['arm9overlay.bin']['size'] // 0x20
|
||||||
|
f.seek(self.hdr.fat_offset)
|
||||||
|
for data in read_chunks(f, num_overlays * 8):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
|
||||||
|
# Partial content of overlays
|
||||||
|
blocks_read = 0
|
||||||
|
for i in range(num_overlays):
|
||||||
|
f.seek(self.hdr.fat_offset + (i * 8))
|
||||||
|
overlay_off = readle(f.read(4))
|
||||||
|
overlay_size = roundup(readle(f.read(4)) - overlay_off, block_size)
|
||||||
|
|
||||||
|
remaining_overlays = num_overlays - i
|
||||||
|
max_size = ((1 << 0xA) - blocks_read) // remaining_overlays * block_size
|
||||||
|
if overlay_size > max_size:
|
||||||
|
hash_size = max_size
|
||||||
|
else:
|
||||||
|
hash_size = overlay_size
|
||||||
|
|
||||||
|
f.seek(overlay_off)
|
||||||
|
for data in read_chunks(f, hash_size):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
blocks_read += hash_size // block_size
|
||||||
|
|
||||||
|
hmac_check.append(('ARM9overlayFAT', hmac_digest.digest() == bytes(self.hdr_ext.arm9overlay_fat_hmac)))
|
||||||
|
|
||||||
|
f.close()
|
||||||
|
elif self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
hmac_info = [('arm9.bin', bytes(self.hdr_ext.arm9_hmac)),
|
||||||
|
('arm7.bin', bytes(self.hdr_ext.arm7_hmac)),
|
||||||
|
('banner.bin', bytes(self.hdr_ext.banner_hmac)),
|
||||||
|
('arm9i.bin', bytes(self.hdr_ext.arm9i_hmac)),
|
||||||
|
('arm7i.bin', bytes(self.hdr_ext.arm7i_hmac))]
|
||||||
|
f = open(file, 'rb')
|
||||||
|
for fname, expected_digest in hmac_info:
|
||||||
|
if fname in self.files.keys() and expected_digest != b'\x00' * 20:
|
||||||
|
info = self.files[fname]
|
||||||
|
f.seek(info['offset'])
|
||||||
|
hmac_digest = hmac.new(key=TWL.hmac_key, digestmod=hashlib.sha1)
|
||||||
|
for data in read_chunks(f, info['size']):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
hmac_check.append((info['name'], hmac_digest.digest() == expected_digest))
|
||||||
|
|
||||||
|
expected_digest = bytes(self.hdr_ext.arm9_no_secure_area_hmac)
|
||||||
|
if expected_digest != b'\x00' * 20:
|
||||||
|
f.seek(self.files['arm9.bin']['offset'] + 0x4000)
|
||||||
|
hmac_digest = hmac.new(key=TWL.hmac_key, digestmod=hashlib.sha1)
|
||||||
|
for data in read_chunks(f, self.files['arm9.bin']['size'] - 0x4000):
|
||||||
|
hmac_digest.update(data)
|
||||||
|
hmac_check.append(('ARM9 wosecarea', hmac_digest.digest() == expected_digest))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# Digests
|
||||||
|
f = open(file, 'rb')
|
||||||
|
data = []
|
||||||
|
info = [(self.hdr_ext.ntr_digest_region_offset, self.hdr_ext.ntr_digest_region_size),
|
||||||
|
(self.hdr_ext.twl_digest_region_offset, self.hdr_ext.twl_digest_region_size)]
|
||||||
|
for off, size in info:
|
||||||
|
f.seek(off)
|
||||||
|
data += [f.read(self.hdr_ext.digest1_block_size) for _ in range(size // self.hdr_ext.digest1_block_size)]
|
||||||
|
digest1 = b''.join([hmac.new(key=TWL.hmac_key, msg=i, digestmod=hashlib.sha1).digest() for i in data])
|
||||||
|
f.seek(self.hdr_ext.digest1_table_offset)
|
||||||
|
expected_digest = f.read(self.hdr_ext.digest1_table_size)
|
||||||
|
hmac_check.append(('Digest1 table', expected_digest[:len(digest1)] == digest1))
|
||||||
|
|
||||||
|
block_len = self.hdr_ext.digest2_digest1_count * 20
|
||||||
|
data = [digest1[i * block_len:(i + 1) * block_len].ljust(block_len, b'\x00') for i in range(self.hdr_ext.digest2_table_size // 20)]
|
||||||
|
digest2 = b''.join([hmac.new(key=TWL.hmac_key, msg=i, digestmod=hashlib.sha1).digest() for i in data])
|
||||||
|
f.seek(self.hdr_ext.digest2_table_offset)
|
||||||
|
expected_digest = f.read(self.hdr_ext.digest2_table_size)
|
||||||
|
hmac_check.append(('Digest2 table', expected_digest == digest2))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
hmac_digest = hmac.new(key=TWL.hmac_key, msg=digest2, digestmod=hashlib.sha1)
|
||||||
|
hmac_check.append(('Digest2', bytes(self.hdr_ext.digest2_hmac) == hmac_digest.digest()))
|
||||||
|
|
||||||
|
sig_check = []
|
||||||
|
# Header signature is the raw SHA1 hash (with padding); easier to manually decrypt and remove the padding
|
||||||
|
if not (self.hdr.unit_code == 0 and readbe(self.hdr_ext.sig) == 0): # Signature exists
|
||||||
|
idx = get_rsa_key_idx(self.hdr, self.hdr_ext)
|
||||||
|
n = readbe(TWL.rsa_key_mod[idx][self.dev])
|
||||||
|
e = 0x10001
|
||||||
|
dec = pow(readbe(bytes(self.hdr_ext.sig)), e, n).to_bytes(0x80, 'big')
|
||||||
|
|
||||||
|
f = open(self.file, 'rb')
|
||||||
|
sha1_calculated = Crypto.sha1(f, 0xE00)
|
||||||
|
f.close()
|
||||||
|
sig_check.append(('Header', dec[-20:] == sha1_calculated))
|
||||||
|
|
||||||
|
print("CRCs:")
|
||||||
|
for i in crc_check:
|
||||||
|
print(' > {0:15} {1:4}'.format(i[0] + ':', 'GOOD' if i[1] else 'FAIL'))
|
||||||
|
if hmac_check != []:
|
||||||
|
print("HMACs:")
|
||||||
|
for i in hmac_check:
|
||||||
|
print(' > {0:15} {1:4}'.format(i[0] + ':', 'GOOD' if i[1] else 'FAIL'))
|
||||||
|
if sig_check != []:
|
||||||
|
print("Signatures:")
|
||||||
|
for i in sig_check:
|
||||||
|
print(' > {0:15} {1:4}'.format(i[0] + ':', 'GOOD' if i[1] else 'FAIL'))
|
||||||
|
|
||||||
|
if os.path.isfile('decrypted.nds'):
|
||||||
|
os.remove('decrypted.nds')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
unit_code = {
|
||||||
|
0: 'DS',
|
||||||
|
2: 'DSi Enhanced',
|
||||||
|
3: 'DSi Exclusive',
|
||||||
|
}
|
||||||
|
|
||||||
|
ntr = (
|
||||||
|
f'Game title: {self.hdr.game_title.decode("ascii")}\n'
|
||||||
|
f'Game code: {self.hdr.game_code.decode("ascii")}\n'
|
||||||
|
f'Maker code: {self.hdr.maker_code.decode("ascii")}\n'
|
||||||
|
f'Unit code: {unit_code[self.hdr.unit_code]}\n'
|
||||||
|
f'Encryption seed: {hex(self.hdr.encryption_seed_select)[2:].zfill(2)}\n'
|
||||||
|
f'Chip size (KB): {128 << self.hdr.device_capacity}\n'
|
||||||
|
f'ROM version: {self.hdr.rom_ver}\n'
|
||||||
|
f'Autostart: {"Yes" if (self.hdr.autostart >> 2) & 1 else "No"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.hdr.unit_code == 2 or self.hdr.unit_code == 3:
|
||||||
|
reg = ''
|
||||||
|
if self.hdr_ext.region == 0xFFFFFFFF:
|
||||||
|
reg = 'Region free'
|
||||||
|
else:
|
||||||
|
if self.hdr_ext.region & 1: reg += 'Japan, '
|
||||||
|
if (self.hdr_ext.region >> 1) & 1: reg += 'USA, '
|
||||||
|
if (self.hdr_ext.region >> 2) & 1: reg += 'Europe, '
|
||||||
|
if (self.hdr_ext.region >> 3) & 1: reg += 'Australia, '
|
||||||
|
if (self.hdr_ext.region >> 4) & 1: reg += 'China, '
|
||||||
|
if (self.hdr_ext.region >> 5) & 1: reg += 'Korea, '
|
||||||
|
reg = reg[:-2]
|
||||||
|
|
||||||
|
def split_parental_control(b):
|
||||||
|
parental_ctrl = str(b & 0b00001111) # age
|
||||||
|
if (b >> 6) & 1:
|
||||||
|
parental_ctrl += ', prohibited in country'
|
||||||
|
elif (b >> 7) & 1:
|
||||||
|
parental_ctrl += ', rating valid'
|
||||||
|
return parental_ctrl
|
||||||
|
|
||||||
|
twl = (
|
||||||
|
f'\nCrypto flags:\n'
|
||||||
|
f' > Has DSi excl region:{"Yes" if self.hdr.data1 & 1 else "No"}\n'
|
||||||
|
f' > Modcrypted: {"Yes" if (self.hdr.data1 >> 1) & 1 else "No"}\n'
|
||||||
|
f' > Modcrypt key: {"Debug" if (self.hdr.data1 >> 2) & 1 else "Retail"}\n'
|
||||||
|
f' > Disable debug: {"Yes" if (self.hdr.data1 >> 3) & 1 else "No"}\n'
|
||||||
|
f'Permit jump:\n'
|
||||||
|
f' Normal jump: {self.hdr.data2 & 1}\n'
|
||||||
|
f' Temporary jump: {(self.hdr.data2 >> 1) & 1}\n'
|
||||||
|
f'Region: {reg}\n'
|
||||||
|
f'Access control:\n'
|
||||||
|
f' Common client key: {self.hdr_ext.access_control & 1}\n'
|
||||||
|
f' AES slot B: {(self.hdr_ext.access_control >> 1) & 1}\n'
|
||||||
|
f' AES slot C: {(self.hdr_ext.access_control >> 2) & 1}\n'
|
||||||
|
f' SD card: {(self.hdr_ext.access_control >> 3) & 1}\n'
|
||||||
|
f' NAND access: {(self.hdr_ext.access_control >> 4) & 1}\n'
|
||||||
|
f' Game card power on: {(self.hdr_ext.access_control >> 5) & 1}\n'
|
||||||
|
f' Shared2 file: {(self.hdr_ext.access_control >> 6) & 1}\n'
|
||||||
|
f' SignJPEGforlauncher: {(self.hdr_ext.access_control >> 7) & 1}\n'
|
||||||
|
f' Game card NTR mode: {(self.hdr_ext.access_control >> 8) & 1}\n'
|
||||||
|
f' SSL client cert: {(self.hdr_ext.access_control >> 9) & 1}\n'
|
||||||
|
f'Flags:\n'
|
||||||
|
f' > TSC mode: {"DSi" if self.hdr_ext.flags & 1 else "DS"}\n'
|
||||||
|
f' > EULA required: {"Yes" if (self.hdr_ext.flags >> 1) & 1 else "No"}\n'
|
||||||
|
f' > Banner: {"Use banner.sav" if (self.hdr_ext.flags >> 2) & 1 else "From ROM"}\n'
|
||||||
|
f' > ShowWiFiconnicon: {"Yes" if (self.hdr_ext.flags >> 3) & 1 else "No"}\n'
|
||||||
|
f' > ShowDSwirelessicon: {"Yes" if (self.hdr_ext.flags >> 4) & 1 else "No"}\n'
|
||||||
|
f' > DScartwithiconSHA1: {"Yes" if (self.hdr_ext.flags >> 5) & 1 else "No"}\n'
|
||||||
|
f' > DScartwithheaderRSA:{"Yes" if (self.hdr_ext.flags >> 6) & 1 else "No"}\n'
|
||||||
|
f' > Developer app: {"Yes" if (self.hdr_ext.flags >> 7) & 1 else "No"}\n'
|
||||||
|
f'EULA ver: {self.hdr_ext.eula_ver}\n'
|
||||||
|
f'TitleID: {hex(self.hdr_ext.titleID_hi)[2:].zfill(8) + hex(self.hdr_ext.titleID_lo)[2:].zfill(8)}\n'
|
||||||
|
f' Media: {"NAND" if (self.hdr_ext.titleID_hi >> 2) & 1 else "Game card"}\n'
|
||||||
|
f'Parental control:\n'
|
||||||
|
f' CERO (Japan): {split_parental_control(self.hdr_ext.parental_control[0])}\n'
|
||||||
|
f' ESRB (USA/Canada): {split_parental_control(self.hdr_ext.parental_control[1])}\n'
|
||||||
|
f' USK (Germany): {split_parental_control(self.hdr_ext.parental_control[3])}\n'
|
||||||
|
f' PEGI (Pan-Europe): {split_parental_control(self.hdr_ext.parental_control[4])}\n'
|
||||||
|
f' PEGI (Portugal): {split_parental_control(self.hdr_ext.parental_control[6])}\n'
|
||||||
|
f' PEGI and BBFC (UK): {split_parental_control(self.hdr_ext.parental_control[7])}\n'
|
||||||
|
f' AGCB (Australia): {split_parental_control(self.hdr_ext.parental_control[8])}\n'
|
||||||
|
f' GRB (South Korea): {split_parental_control(self.hdr_ext.parental_control[9])}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return ntr + twl
|
||||||
|
else:
|
||||||
|
if self.hdr.data2 == 0:
|
||||||
|
reg = 'Normal'
|
||||||
|
elif self.hdr.data2 == 0x40:
|
||||||
|
reg = 'Korea'
|
||||||
|
elif self.hdr.data2 == 0x80:
|
||||||
|
reg = 'China'
|
||||||
|
ntr += f'\nRegion: {reg}'
|
||||||
|
|
||||||
|
if self.hdr_ext.flags != 0:
|
||||||
|
ntr += (
|
||||||
|
f'\nFlags:\n'
|
||||||
|
f' > TSC mode: {"DSi" if self.hdr_ext.flags & 1 else "DS"}\n'
|
||||||
|
f' > EULA required: {"Yes" if (self.hdr_ext.flags >> 1) & 1 else "No"}\n'
|
||||||
|
f' > Banner: {"Use banner.sav" if (self.hdr_ext.flags >> 2) & 1 else "From cartridge"}\n'
|
||||||
|
f' > ShowWiFiconnicon: {"Yes" if (self.hdr_ext.flags >> 3) & 1 else "No"}\n'
|
||||||
|
f' > ShowDSwirelessicon: {"Yes" if (self.hdr_ext.flags >> 4) & 1 else "No"}\n'
|
||||||
|
f' > DScartwithiconSHA1: {"Yes" if (self.hdr_ext.flags >> 5) & 1 else "No"}\n'
|
||||||
|
f' > DScartwithheaderRSA:{"Yes" if (self.hdr_ext.flags >> 6) & 1 else "No"}\n'
|
||||||
|
f' > Developer app: {"Yes" if (self.hdr_ext.flags >> 7) & 1 else "No"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return ntr
|
BIN
lib/resources/keytable2_dev.bin
Normal file
BIN
lib/resources/keytable2_dev.bin
Normal file
Binary file not shown.
2
ntool.py
2
ntool.py
@ -2,7 +2,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from utils import *
|
from utils import *
|
||||||
|
|
||||||
if sys.argv[1] in ['cia_dev2retail', 'cia_retail2dev', 'cci_dev2retail', 'cci_retail2dev', 'csu2retailcias']:
|
if sys.argv[1] in ['srl_retail2dev', 'cia_dev2retail', 'cia_retail2dev', 'cci_dev2retail', 'cci_retail2dev', 'csu2retailcias']:
|
||||||
path = sys.argv[2]
|
path = sys.argv[2]
|
||||||
out = ''
|
out = ''
|
||||||
for i in range(2, len(sys.argv)):
|
for i in range(2, len(sys.argv)):
|
||||||
|
65
utils.py
65
utils.py
@ -1,4 +1,5 @@
|
|||||||
from lib.common import *
|
from lib.common import *
|
||||||
|
from lib.keys import NTR, TWL
|
||||||
from lib.ctr_cia import CIAReader, CIABuilder
|
from lib.ctr_cia import CIAReader, CIABuilder
|
||||||
from lib.ctr_cci import CCIReader, CCIBuilder
|
from lib.ctr_cci import CCIReader, CCIBuilder
|
||||||
from lib.ctr_ncch import NCCHReader, NCCHBuilder
|
from lib.ctr_ncch import NCCHReader, NCCHBuilder
|
||||||
@ -9,6 +10,70 @@ from lib.ctr_tmd import TMDReader, TMDBuilder
|
|||||||
from lib.ctr_tik import tikReader, tikBuilder
|
from lib.ctr_tik import tikReader, tikBuilder
|
||||||
from lib.ctr_cdn import CDNReader, CDNBuilder
|
from lib.ctr_cdn import CDNReader, CDNBuilder
|
||||||
from lib.ctr_cnt import cntReader
|
from lib.ctr_cnt import cntReader
|
||||||
|
from lib.ntr_twl_srl import SRLReader, get_rsa_key_idx
|
||||||
|
|
||||||
|
def srl_retail2dev(path, out=''):
|
||||||
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
if out == '':
|
||||||
|
out = f'{name}_dev.srl'
|
||||||
|
|
||||||
|
srl = SRLReader(path, dev=0)
|
||||||
|
shutil.copyfile(path, 'tmp.nds')
|
||||||
|
|
||||||
|
if srl.media == 'Game card' and srl.secure_area_status == 'decrypted': # Encrypt NTR secure area for decrypted game card SRLs
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
f.seek(0x4000)
|
||||||
|
secure_area = f.read(2048)
|
||||||
|
secure_area_enc = srl.encrypt_secure_area(secure_area, NTR.blowfish_key)
|
||||||
|
with open('tmp.nds', 'r+b') as f:
|
||||||
|
f.seek(0x4000)
|
||||||
|
f.write(secure_area_enc)
|
||||||
|
|
||||||
|
if srl.modcrypted: # Decrypt modcrypt regions and re-encrypt with dev key
|
||||||
|
srl.decrypt_modcrypt()
|
||||||
|
f = open('decrypted.nds', 'rb')
|
||||||
|
key = bytes(srl.hdr)[:16][::-1]
|
||||||
|
for i in srl.modcrypt:
|
||||||
|
g = open('tmp.nds', 'r+b')
|
||||||
|
f.seek(i['offset'])
|
||||||
|
g.seek(i['offset'])
|
||||||
|
|
||||||
|
counter = bytearray(i['counter'])
|
||||||
|
for data in read_chunks(f, i['size']):
|
||||||
|
for j in range(len(data) // 16):
|
||||||
|
output, counter = TWL.aes_ctr_block(key, counter, data[j * 16:(j + 1) * 16])
|
||||||
|
g.write(output)
|
||||||
|
g.close()
|
||||||
|
f.close()
|
||||||
|
os.remove('decrypted.nds')
|
||||||
|
|
||||||
|
if srl.hdr.unit_code == 2 or srl.hdr.unit_code == 3 or (srl.hdr.unit_code == 0 and srl.hdr_ext.flags != 0): # Set DeveloperApp flag
|
||||||
|
srl.hdr_ext.flags |= (1 << 7)
|
||||||
|
with open('tmp.nds', 'r+b') as f:
|
||||||
|
f.seek(0x1BF)
|
||||||
|
f.write(int8tobytes(srl.hdr_ext.flags))
|
||||||
|
|
||||||
|
if not (srl.hdr.unit_code == 0 and readbe(srl.hdr_ext.sig) == 0): # Re-generate header signature
|
||||||
|
idx = get_rsa_key_idx(srl.hdr, srl.hdr_ext)
|
||||||
|
n = TWL.rsa_key_mod[idx]
|
||||||
|
d = TWL.rsa_key_priv[idx]
|
||||||
|
|
||||||
|
f = open('tmp.nds', 'rb')
|
||||||
|
sha1_calculated = Crypto.sha1(f, 0xE00)
|
||||||
|
f.close()
|
||||||
|
sha1_padded = b'\x00\x01' + b'\xff' * 105 + b'\x00' + sha1_calculated
|
||||||
|
enc = pow(readbe(sha1_padded), readbe(d[1]), readbe(n[1])).to_bytes(0x80, 'big')
|
||||||
|
with open('tmp.nds', 'r+b') as f:
|
||||||
|
f.seek(0xF80)
|
||||||
|
f.write(enc)
|
||||||
|
|
||||||
|
if srl.media == 'Game card': # Re-generate undumpable area i.e. KeyTables for game card SRLs
|
||||||
|
srl = SRLReader('tmp.nds', dev=1)
|
||||||
|
srl.regen_undumpable()
|
||||||
|
os.remove('tmp.nds')
|
||||||
|
shutil.move('new.nds', out)
|
||||||
|
else:
|
||||||
|
shutil.move('tmp.nds', out)
|
||||||
|
|
||||||
def cia_dev2retail(path, out=''):
|
def cia_dev2retail(path, out=''):
|
||||||
name = os.path.splitext(os.path.basename(path))[0]
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
Loading…
Reference in New Issue
Block a user