initial
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
|
||||
import DSRomEncryptor.blowfish_locator as blowfish_locator
|
||||
import DSRomEncryptor.keytransform as keytransform
|
||||
import DSRomEncryptor.blowfish as blowfish
|
||||
import DSRomEncryptor.crc16 as crc16
|
||||
import DSRomEncryptor.util as util
|
||||
|
||||
crc16 = crc16.crc16
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
import struct
|
||||
|
||||
from DSRomEncryptor.util import *
|
||||
|
||||
|
||||
__all__ = [ 'Blowfish' ]
|
||||
|
||||
|
||||
P_TABLE_ENTRY_COUNT = 18
|
||||
S_BOX_COUNT = 4
|
||||
S_BOX_ENTRY_COUNT = 256
|
||||
KEY_TABLE_P_TABLE_LENGTH = P_TABLE_ENTRY_COUNT * 4
|
||||
KEY_TABLE_S_BOXES_LENGTH = S_BOX_COUNT * S_BOX_ENTRY_COUNT * 4
|
||||
KEY_TABLE_LENGTH = KEY_TABLE_P_TABLE_LENGTH + KEY_TABLE_S_BOXES_LENGTH
|
||||
BLOCK_LENGTH = 8
|
||||
|
||||
|
||||
class Blowfish:
|
||||
def __init__(self, keyTable: bytes):
|
||||
if len(keyTable) < KEY_TABLE_LENGTH:
|
||||
raise ValueError("Key table length %d is invalid" % len(keyTable))
|
||||
|
||||
#print("\tblowfish init")
|
||||
self._pTable = bytes_read_u32le(keyTable, 0, P_TABLE_ENTRY_COUNT)
|
||||
self._sBoxes = (
|
||||
bytes_read_u32le(keyTable, 0x048, S_BOX_ENTRY_COUNT),
|
||||
bytes_read_u32le(keyTable, 0x448, S_BOX_ENTRY_COUNT),
|
||||
bytes_read_u32le(keyTable, 0x848, S_BOX_ENTRY_COUNT),
|
||||
bytes_read_u32le(keyTable, 0xc48, S_BOX_ENTRY_COUNT),
|
||||
)
|
||||
#print("\tblowfish init end")
|
||||
|
||||
|
||||
def enc_block(self, blk: int) -> int:
|
||||
y = blk & 0xffff_ffff
|
||||
x = (blk >> 32) & 0xffff_ffff
|
||||
|
||||
for i in range(16):
|
||||
z = self._pTable[i] ^ x
|
||||
a = self._sBoxes[0][(z >> 24) & 0xff]
|
||||
b = self._sBoxes[1][(z >> 16) & 0xff]
|
||||
c = self._sBoxes[2][(z >> 8) & 0xff]
|
||||
d = self._sBoxes[3][(z ) & 0xff]
|
||||
|
||||
ab = (a + b) & 0xffff_ffff
|
||||
dcab = (d + (c ^ ab)) & 0xffff_ffff
|
||||
x = dcab ^ y
|
||||
y = z
|
||||
|
||||
return (x ^ self._pTable[16]) | ((y ^ self._pTable[17]) << 32)
|
||||
|
||||
def dec_block(self, blk: int) -> int:
|
||||
y = blk & ((1<<32)-1)
|
||||
x = (blk >> 32) & ((1<<32)-1)
|
||||
|
||||
for i in range(17, 1, -1):
|
||||
z = self._pTable[i] ^ x
|
||||
a = self._sBoxes[0][(z >> 24) & 0xff]
|
||||
b = self._sBoxes[1][(z >> 16) & 0xff]
|
||||
c = self._sBoxes[2][(z >> 8) & 0xff]
|
||||
d = self._sBoxes[3][(z >> 0) & 0xff]
|
||||
|
||||
a = (a + b) & ((1<<32)-1)
|
||||
d = (d + (c ^ a)) & ((1<<32)-1)
|
||||
x = d ^ y
|
||||
y = z
|
||||
|
||||
return (x ^ self._pTable[1]) | ((y ^ self._pTable[0]) << 32)
|
||||
|
||||
|
||||
def encrypt(self, data: bytes, mut: bool=False) -> bytes|None:
|
||||
if len(data) & 7:
|
||||
raise ValueError("Input length must be a multiple of 8, but is %d" % len(data))
|
||||
|
||||
mut = mut and isinstance(data, bytearray)
|
||||
r = data if mut else bytearray(len(data))
|
||||
|
||||
for i in range(0, len(data), BLOCK_LENGTH):
|
||||
#print("\tblowfish block")
|
||||
v = self.enc_block(bytes_read_u64le(data, i))
|
||||
bytearr_write_u64le(r, i, v)
|
||||
|
||||
assert len(r) == len(data)
|
||||
return None if mut else bytes(r)
|
||||
|
||||
def decrypt(self, data: bytes, mut: bool=False) -> bytes|None:
|
||||
if len(data) & 7:
|
||||
raise ValueError("Input length must be a multiple of 8, but is %d" % len(data))
|
||||
|
||||
mut = mut and isinstance(data, bytearray)
|
||||
r = data if mut else bytearray(len(data))
|
||||
|
||||
for i in range(0, len(data), BLOCK_LENGTH):
|
||||
v = self.dec_block(bytes_read_u64le(data, i))
|
||||
bytearr_write_u64le(r, i, v)
|
||||
|
||||
return None if mut else bytes(r)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
|
||||
|
||||
import os, sys, traceback
|
||||
from pathlib import Path
|
||||
|
||||
from DSRomEncryptor.blowfish import KEY_TABLE_LENGTH
|
||||
|
||||
|
||||
__all__ = [ 'try_get_ntr_blowfish', 'try_get_twl_blowfish', 'try_get_twl_dev_blowfish' ]
|
||||
|
||||
|
||||
NTR_BLOWFISH_NAME = "ntrBlowfish.bin"
|
||||
TWL_BLOWFISH_NAME = "twlBlowfish.bin"
|
||||
TWL_DEV_BLOWFISH_NAME = "twlDevBlowfish.bin"
|
||||
BIOS_NDS_7_NAME = "biosnds7.rom"
|
||||
BIOS_DSI_7_NAME = "biosdsi7.rom"
|
||||
BIOS_NDS_7_LENGTH = 0x4000
|
||||
BIOS_NDS_7_BLOWFISH_OFFSET = 0x30
|
||||
BIOS_DSI_7_LENGTH = 0x10000
|
||||
BIOS_DSI_7_BLOWFISH_OFFSET = 0xc6d0
|
||||
|
||||
|
||||
def _try_load_from_cwd_dir(name: str) -> bytes|None:
|
||||
appdir = Path(os.getcwd()).absolute()
|
||||
|
||||
try:
|
||||
return (appdir / name).read_bytes()
|
||||
except IOError:
|
||||
return None
|
||||
|
||||
|
||||
def _try_load_from_app_dir(name: str) -> bytes|None:
|
||||
appdir = Path(sys.argv[0]).absolute().parent
|
||||
|
||||
try:
|
||||
return (appdir / name).read_bytes()
|
||||
except IOError:
|
||||
return _try_load_from_cwd_dir(name)
|
||||
|
||||
|
||||
def try_get_ntr_blowfish() -> bytes|None:
|
||||
r = _try_load_from_app_dir(NTR_BLOWFISH_NAME)
|
||||
if r: return r
|
||||
|
||||
r = _try_load_from_app_dir(BIOS_NDS_7_NAME)
|
||||
if not r or len(r) != BIOS_NDS_7_LENGTH:
|
||||
return None
|
||||
|
||||
return r[BIOS_NDS_7_BLOWFISH_OFFSET:][:KEY_TABLE_LENGTH]
|
||||
|
||||
def try_get_twl_blowfish() -> bytes|None:
|
||||
r = _try_load_from_app_dir(TWL_BLOWFISH_NAME)
|
||||
if r: return r
|
||||
|
||||
r = _try_load_from_app_dir(BIOS_DSI_7_NAME)
|
||||
if not r or len(r) != BIOS_DSI_7_LENGTH:
|
||||
return None
|
||||
|
||||
return r[BIOS_DSI_7_BLOWFISH_OFFSET:][:KEY_TABLE_LENGTH]
|
||||
|
||||
def try_get_twl_dev_blowfish() -> bytes|None:
|
||||
return _try_load_from_app_dir(TWL_DEV_BLOWFISH_NAME)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
__all__ = [ 'parse_args' ]
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="== DSRomEncryptor by Gericom (Python 3 port by PoroCYon) ==")
|
||||
|
||||
parser.add_argument('--dsidev', default=False, action='store_true', help="Use DSi dev blowfish")
|
||||
parser.add_argument('input_path', type=argparse.FileType('rb'), help="The path of the input .nds file")
|
||||
parser.add_argument('output_path', type=argparse.FileType('wb'), help="The path of the output .nds file")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
|
||||
__all__ = [ 'crc16' ]
|
||||
|
||||
|
||||
_TABLE = [
|
||||
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: bytes) -> int:
|
||||
r = 0xffff
|
||||
|
||||
for d in data:
|
||||
r = (r >> 8) ^ _TABLE[(r ^ d) & 0xff]
|
||||
r = r & 0xffff
|
||||
|
||||
#print("crc16: len", len(data), hex(len(data)), "->", hex(r))
|
||||
return r
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
|
||||
import struct, codecs
|
||||
|
||||
from DSRomEncryptor.blowfish import Blowfish, KEY_TABLE_LENGTH, P_TABLE_ENTRY_COUNT, BLOCK_LENGTH
|
||||
from DSRomEncryptor.util import *
|
||||
|
||||
|
||||
__all__ = [ 'transform_table' ]
|
||||
|
||||
|
||||
def apply_keycode(keycode: bytearray, modulo: int, keyTable: bytearray):
|
||||
bf = Blowfish(keyTable)
|
||||
|
||||
#print(" keycode", codecs.encode(keycode, 'hex'))
|
||||
keycode[4:12] = bf.encrypt(keycode[4:12])
|
||||
#print(" keycode", codecs.encode(keycode, 'hex'))
|
||||
keycode[0: 8] = bf.encrypt(keycode[0: 8])
|
||||
#print(" keycode", codecs.encode(keycode, 'hex'))
|
||||
|
||||
assert len(keycode) == 12
|
||||
|
||||
#print(" keycode shuffle")
|
||||
for i in range(P_TABLE_ENTRY_COUNT):
|
||||
a = bytes_read_u32le(keyTable, i*4)
|
||||
b = bytes_read_u32be(keycode , (i*4) % modulo)
|
||||
bytearr_write_u32le(keyTable, i*4, a ^ b)
|
||||
|
||||
scratch = bytearray(8)
|
||||
|
||||
for i in range(0, KEY_TABLE_LENGTH, BLOCK_LENGTH):
|
||||
#print(" keycode block", i)
|
||||
bf = Blowfish(keyTable)
|
||||
bf.encrypt(scratch, mut=True)
|
||||
#print(" scratch", codecs.encode(scratch, 'hex'))
|
||||
|
||||
copy_bytes(scratch[4:8], keyTable, i )
|
||||
copy_bytes(scratch[0:4], keyTable, i+4)
|
||||
|
||||
|
||||
def transform_table(gamecode: int, level: int, modulo: int, keyTable: bytearray) -> bytes:
|
||||
assert len(keyTable) == KEY_TABLE_LENGTH, len(keyTable)
|
||||
newtab = bytearray(keyTable[:KEY_TABLE_LENGTH])
|
||||
|
||||
keycode = bytearray(12)
|
||||
#print("keyxform init level", level, "gamecode", hex(gamecode))
|
||||
bytearr_write_u32le(keycode, 0, gamecode )
|
||||
bytearr_write_u32le(keycode, 4, gamecode >> 1)
|
||||
bytearr_write_u32le(keycode, 8, (gamecode << 1) & ((1<<32)-1))
|
||||
|
||||
if level >= 1:
|
||||
#print("keycode lv1 modulo", modulo)
|
||||
apply_keycode(keycode, modulo, newtab)
|
||||
if level >= 2:
|
||||
#print("keycode lv2 modulo", modulo)
|
||||
apply_keycode(keycode, modulo, newtab)
|
||||
|
||||
#print("keyxform second level", level, "gamecode", hex(gamecode))
|
||||
bytearr_write_u32le(keycode, 4, (bytes_read_u32le(keycode, 4) << 1)&((1<<32)-1))
|
||||
bytearr_write_u32le(keycode, 8, bytes_read_u32le(keycode, 8) >> 1)
|
||||
|
||||
if level >= 3:
|
||||
#print("keycode lv3 modulo", modulo)
|
||||
apply_keycode(keycode, modulo, newtab)
|
||||
|
||||
assert len(keycode) == 12
|
||||
assert len(newtab) == len(keyTable[:KEY_TABLE_LENGTH])
|
||||
|
||||
return newtab
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
|
||||
import struct, sys, traceback
|
||||
|
||||
import DSRomEncryptor.blowfish_locator as blowfish_locator
|
||||
import DSRomEncryptor.cmdline as cmdline
|
||||
import DSRomEncryptor.keytransform as keytransform
|
||||
from DSRomEncryptor.blowfish import Blowfish
|
||||
from DSRomEncryptor.blowfish import KEY_TABLE_P_TABLE_LENGTH, KEY_TABLE_S_BOXES_LENGTH
|
||||
from DSRomEncryptor.crc16 import crc16
|
||||
from DSRomEncryptor.util import *
|
||||
|
||||
|
||||
__all__ = [ 'main' ]
|
||||
|
||||
|
||||
SECURE_AREA_ID = struct.unpack('<Q', b"encryObj")[0]
|
||||
|
||||
ROM_HEADER_GAME_CODE_OFFSET = 0xC
|
||||
ROM_HEADER_UNIT_CODE_OFFSET = 0x12
|
||||
ROM_HEADER_TWL_FLAGS_OFFSET = 0x1C
|
||||
ROM_HEADER_ARM9_OFFSET_OFFSET = 0x20
|
||||
ROM_HEADER_SECURE_AREA_CRC_OFFSET = 0x6C
|
||||
ROM_HEADER_NTR_AREA_END_OFFSET = 0x90
|
||||
ROM_HEADER_TWL_AREA_START_OFFSET = 0x92
|
||||
ROM_HEADER_CRC_OFFSET = 0x15E
|
||||
ROM_NTR_BLOWFISH_P_TABLE_OFFSET = 0x1600
|
||||
ROM_NTR_BLOWFISH_S_BOXES_OFFSET = 0x1C00
|
||||
ROM_SECURE_AREA_START_OFFSET = 0x4000
|
||||
ROM_ENCRYPTED_SECURE_AREA_END_OFFSET = 0x4800
|
||||
ROM_SECURE_AREA_END_OFFSET = 0x8000
|
||||
ROM_TWL_BLOWFISH_P_TABLE_OFFSET = 0x600
|
||||
ROM_TWL_BLOWFISH_S_BOXES_OFFSET = 0xC00
|
||||
TWL_CHUNK_SHIFT = 19
|
||||
TWL_CHUNK_SIZE = 0x80000
|
||||
UNIT_CODE_TWL_FLAG = 2
|
||||
TWL_FLAGS_HAS_TWL_EXCLUSIVE_AREA_FLAG = 1
|
||||
|
||||
|
||||
TEST_PATTERN = b"\xff\x00\xff\x00\xaa\x55\xaa\x55"
|
||||
|
||||
|
||||
def should_rom_have_twl_area(romData: bytes) -> bool:
|
||||
return (romData[ROM_HEADER_UNIT_CODE_OFFSET] & UNIT_CODE_TWL_FLAG) != 0 \
|
||||
and (romData[ROM_HEADER_TWL_FLAGS_OFFSET] & TWL_FLAGS_HAS_TWL_EXCLUSIVE_AREA_FLAG) != 0
|
||||
|
||||
|
||||
def insert_test_patterns(romData: bytearray):
|
||||
copy_bytes(TEST_PATTERN, romData, 0x3000)
|
||||
|
||||
for i in range(8, 0x200):
|
||||
romData[0x3000 + i] = i & 0xff
|
||||
for i in range(0, 0x200):
|
||||
romData[0x3200 + i] = 0xff - (i & 0xff)
|
||||
|
||||
copy_bytes(b"\x00" * 0x200, romData, 0x3400)
|
||||
copy_bytes(b"\xff" * 0x200, romData, 0x3600)
|
||||
copy_bytes(b"\x0f" * 0x200, romData, 0x3800)
|
||||
copy_bytes(b"\xf0" * 0x200, romData, 0x3a00)
|
||||
copy_bytes(b"\x55" * 0x200, romData, 0x3c00)
|
||||
copy_bytes(b"\xaa" * 0x1ff, romData, 0x3e00)
|
||||
|
||||
romData[0x3fff] = 0
|
||||
|
||||
|
||||
def run_options(args) -> int|None:
|
||||
assert args is not None
|
||||
|
||||
ntr_bf = blowfish_locator.try_get_ntr_blowfish()
|
||||
if ntr_bf is None:
|
||||
print("Error: Could not load NTR Blowfish key.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
romData = bytearray(args.input_path.read())
|
||||
|
||||
gameCode = bytes_read_u32le(romData, ROM_HEADER_GAME_CODE_OFFSET)
|
||||
#print("gamecode", hex(gameCode), struct.pack('<I', gameCode))
|
||||
ntrTable = keytransform.transform_table(gameCode, 2, 8, ntr_bf)
|
||||
|
||||
copy_bytes(ntrTable[:KEY_TABLE_P_TABLE_LENGTH],
|
||||
romData, ROM_NTR_BLOWFISH_P_TABLE_OFFSET)
|
||||
copy_bytes(ntrTable[KEY_TABLE_P_TABLE_LENGTH:][:KEY_TABLE_S_BOXES_LENGTH],
|
||||
romData, ROM_NTR_BLOWFISH_S_BOXES_OFFSET)
|
||||
|
||||
insert_test_patterns(romData)
|
||||
|
||||
arm9off = bytes_read_u32le(romData, ROM_HEADER_ARM9_OFFSET_OFFSET)
|
||||
if ROM_SECURE_AREA_END_OFFSET - arm9off > 0: #arm9off < ROM_SECURE_AREA_END_OFFSET:
|
||||
sec_crc = crc16(romData[arm9off:ROM_SECURE_AREA_END_OFFSET])
|
||||
if sec_crc != bytes_read_u16le(romData, ROM_HEADER_SECURE_AREA_CRC_OFFSET):
|
||||
sectbl = keytransform.transform_table(gameCode, 3, 8, ntr_bf)
|
||||
bf = Blowfish(sectbl)
|
||||
|
||||
enc_secareaID = bf.enc_block(SECURE_AREA_ID)
|
||||
enc_secareaID = Blowfish(ntrTable).enc_block(enc_secareaID)
|
||||
bytearr_write_u64le(romData, ROM_SECURE_AREA_START_OFFSET, enc_secareaID)
|
||||
|
||||
secarea_pt = romData[(ROM_SECURE_AREA_START_OFFSET + 8):ROM_ENCRYPTED_SECURE_AREA_END_OFFSET]
|
||||
secarea_ct = bf.encrypt(secarea_pt)
|
||||
romData[(ROM_SECURE_AREA_START_OFFSET + 8):ROM_ENCRYPTED_SECURE_AREA_END_OFFSET] = secarea_ct
|
||||
|
||||
sec_crc = crc16(romData[arm9off:ROM_SECURE_AREA_END_OFFSET])
|
||||
bytearr_write_u16le(romData, ROM_HEADER_SECURE_AREA_CRC_OFFSET, sec_crc)
|
||||
|
||||
try: # twl
|
||||
if should_rom_have_twl_area(romData):
|
||||
twl_area_start = bytes_read_u16le(romData, ROM_HEADER_TWL_AREA_START_OFFSET) * TWL_CHUNK_SIZE
|
||||
|
||||
if twl_area_start == 0:
|
||||
tas_value = (len(romData) + (TWL_CHUNK_SIZE - 1)) >> TWL_CHUNK_SHIFT
|
||||
twl_area_start = tas_value * TWL_CHUNK_SIZE
|
||||
|
||||
bytearr_write_u16le(romData, ROM_HEADER_NTR_AREA_END_OFFSET , tas_value)
|
||||
bytearr_write_u16le(romData, ROM_HEADER_TWL_AREA_START_OFFSET, tas_value)
|
||||
|
||||
romData.extend(b"\x00"*(twl_area_start - len(romData) + 0x8000))
|
||||
|
||||
twltab = blowfish_locator.try_get_twl_dev_blowfish() if args.dsidev \
|
||||
else blowfish_locator.try_get_twl_blowfish()
|
||||
if twltab is None:
|
||||
print("Error: could not load TWL %sBlowfish key."%("dev " if args.dsidev else ""), file=sys.stderr)
|
||||
return 1
|
||||
if not args.dsidev:
|
||||
twltab = keytransform.transform_table(gameCode, 1, 8, twltab)
|
||||
|
||||
copy_bytes(twltab[:KEY_TABLE_P_TABLE_LENGTH],
|
||||
romData, twl_area_start + ROM_TWL_BLOWFISH_P_TABLE_OFFSET)
|
||||
copy_bytes(twltab[KEY_TABLE_P_TABLE_LENGTH:][:KEY_TABLE_S_BOXES_LENGTH],
|
||||
romData, twl_area_start + ROM_TWL_BLOWFISH_S_BOXES_OFFSET)
|
||||
except:
|
||||
print("Warning: couldn't insert TWL Blowfish.", file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
||||
# fix header CRC
|
||||
bytearr_write_u16le(romData, ROM_HEADER_CRC_OFFSET, crc16(romData[:ROM_HEADER_CRC_OFFSET]))
|
||||
|
||||
args.output_path.write(romData)
|
||||
|
||||
|
||||
def main():
|
||||
args = cmdline.parse_args()
|
||||
exit(run_options(args) or 0)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
|
||||
import struct
|
||||
from typing import Sequence, Tuple
|
||||
|
||||
|
||||
__all__ = [ 'copy_bytes'
|
||||
, 'bytes_read_u16le', 'bytes_read_u32le', 'bytes_read_u64le'
|
||||
, 'bytes_read_u32be'
|
||||
, 'bytearr_write_u16le', 'bytearr_write_u32le', 'bytearr_write_u64le'
|
||||
]
|
||||
|
||||
|
||||
def copy_bytes(src: bytes, dest: bytearray, destoff: int=0, srclen: int=None):
|
||||
srclen = len(src) if srclen is None else srclen
|
||||
assert srclen+destoff <= len(dest), (srclen, destoff, len(dest))
|
||||
dest[destoff:(destoff+srclen)] = src
|
||||
|
||||
|
||||
def bytes_read_u16le(bs: bytes, off: int, n: int = 1) -> int|Tuple[int]:
|
||||
assert n >= 1, n
|
||||
data = bs[off:(off+n*2)]
|
||||
r = struct.unpack('<%dH'%n, data)
|
||||
#print("\t\tbytes_read_u16le", hex(off), n, "->", "snip" if len(r) > 20 else [hex(x) for x in r])
|
||||
return r[0] if n == 1 else r
|
||||
|
||||
def bytes_read_u32le(bs: bytes, off: int, n: int = 1) -> int|Tuple[int]:
|
||||
assert n >= 1, n
|
||||
data = bs[off:(off+n*4)]
|
||||
r = struct.unpack('<%dI'%n, data)
|
||||
#print("\t\tbytes_read_u32le", hex(off), n, "->", "snip" if len(r) > 20 else [hex(x) for x in r])
|
||||
return r[0] if n == 1 else r
|
||||
|
||||
def bytes_read_u32be(bs: bytes, off: int, n: int = 1) -> int|Tuple[int]:
|
||||
assert n >= 1, n
|
||||
data = bs[off:(off+n*4)]
|
||||
r = struct.unpack('>%dI'%n, data)
|
||||
#print("\t\tbytes_read_u32be", hex(off), n, "->", "snip" if len(r) > 20 else [hex(x) for x in r])
|
||||
return r[0] if n == 1 else r
|
||||
|
||||
def bytes_read_u64le(bs: bytes, off: int, n: int = 1) -> int|Tuple[int]:
|
||||
assert n >= 1, n
|
||||
data = bs[off:(off+n*8)]
|
||||
r = struct.unpack('<%dQ'%n, data)
|
||||
#print("\t\tbytes_read_u64le", hex(off), n, "->", "snip" if len(r) > 20 else [hex(x) for x in r])
|
||||
return r[0] if n == 1 else r
|
||||
|
||||
|
||||
def bytearr_write_u16le(ba: bytearray, off: int, v: int|Sequence[int]):
|
||||
if isinstance(v, int): v = (v,)
|
||||
#print("\t\tbytearr_write_u16le", hex(off), len(v), "snip" if len(v) > 20 else [hex(x) for x in v])
|
||||
copy_bytes(struct.pack('<%dH'%len(v), *v), ba, off)
|
||||
|
||||
def bytearr_write_u32le(ba: bytearray, off: int, v: int|Sequence[int]):
|
||||
if isinstance(v, int): v = (v,)
|
||||
#print("\t\tbytearr_write_u32le", hex(off), len(v), "snip" if len(v) > 20 else [hex(x) for x in v])
|
||||
copy_bytes(struct.pack('<%dI'%len(v), *v), ba, off)
|
||||
|
||||
def bytearr_write_u64le(ba: bytearray, off: int, v: int|Sequence[int]):
|
||||
if isinstance(v, int): v = (v,)
|
||||
#print("\t\tbytearr_write_u64le", hex(off), len(v), "snip" if len(v) > 20 else [hex(x) for x in v])
|
||||
copy_bytes(struct.pack('<%dQ'%len(v), *v), ba, off)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Gericom
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,25 @@
|
||||
# DSRomEncryptor
|
||||
Tool to prepare DS and DSi roms for use on a cartridge. The tool inserts the necessary NTR and TWL blowfish key blocks and encrypts the NTR secure area (0x4000-0x4800).
|
||||
|
||||
## Usage
|
||||
`DSRomEncryptor [--dsidev] input.nds output.nds`
|
||||
|
||||
### Arguments
|
||||
- `--dsidev` - (optional) insert dev twl blowfish instead of retail, for use with twl dev units;
|
||||
- `input.nds` - path to the input nds file;
|
||||
- `output.nds` - path to the output nds file.
|
||||
|
||||
## Blowfish tables
|
||||
The blowfish tables to be used by DSRomEncryptor are not included and need to be supplied by the user. The files described below go in the executable directory of DSRomEncryptor. Twl blowfish is only necessary when processing twl hybrid or exclusive roms, and the dev variant is only used when using the `--dsidev` flag.
|
||||
|
||||
- Ntr blowfish
|
||||
- Either `ntrBlowfish.bin` containing the ntr blowfish table (SHA1: `84E467F2485078E401A17A5F231E3FE6E9686648`)
|
||||
- Or a ds arm7 bios dump named `biosnds7.rom` (SHA1: `24F67BDEA115A2C847C8813A262502EE1607B7DF`)
|
||||
- Twl blowfish (retail)
|
||||
- Either `twlBlowfish.bin` containing the twl blowfish table (SHA1: `2DEA11191F28C6CC1956DADB8941AFFD4B2B5102`)
|
||||
- Or a dsi arm7i bios dump named `biosdsi7.rom` (SHA1: `A3AA751EB6BDAAF8A827BA9E03576A6F1AB0F547` for an incomplete dump or `C7C7570BFE51C3C7C5DA3B01331B94E7E7CB4F53` for a complete dump)
|
||||
- Twl blowfish (dev)
|
||||
- `twlDevBlowfish.bin` containing the dev twl blowfish table (SHA1: `CFF62F24444F5494001F019D505F9C51D40FC8B3`)
|
||||
|
||||
## License
|
||||
DSRomEncryptor is licensed under the MIT License, see [LICENSE](./LICENSE.txt) for more information.
|
||||
@@ -0,0 +1,37 @@
|
||||
[project]
|
||||
name = "DSRomEncryptor"
|
||||
version = "0.1.0"
|
||||
description = "Tool to prepare DS and DSi roms for use on a cartridge. The tool inserts the necessary NTR and TWL blowfish key blocks and encrypts the NTR secure area (0x4000-0x4800)."
|
||||
readme = "README.md"
|
||||
license = 'MIT'
|
||||
keywords = []
|
||||
|
||||
authors = [
|
||||
{name = "Gericom", email = "fnouwt2@gmail.com"},
|
||||
{name = "PoroCYon", email = "p@pcy.be"},
|
||||
]
|
||||
maintainers = [
|
||||
{name = "PoroCYon", email = "p@pcy.be"},
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Environment :: Console",
|
||||
"Typing :: Typed"
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
requires-python = ">= 3.9"
|
||||
|
||||
[project.scripts]
|
||||
DSRomEncryptor = "DSRomEncryptor.program:main"
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import codecs, hashlib
|
||||
|
||||
from DSRomEncryptor import *
|
||||
from DSRomEncryptor.util import *
|
||||
|
||||
|
||||
def sha1(bs: bytes):
|
||||
s1 = hashlib.sha1()
|
||||
s1.update(bs)
|
||||
return s1.digest()
|
||||
|
||||
|
||||
def test():
|
||||
a = bytearray(8)
|
||||
bytearr_write_u32le(a, 4, 0x45434D41)
|
||||
assert a == b"\0\0\0\0\x41\x4d\x43\x45"
|
||||
assert bytes_read_u32le(a, 4) == 0x45434D41, a
|
||||
assert bytes_read_u32le(a, 0, 2) == (0,0x45434D41), a
|
||||
copy_bytes(a[4:], a, 0)
|
||||
assert bytes_read_u32le(a, 0, 2) == (0x45434D41,0x45434D41), a
|
||||
assert bytes_read_u32be(a, 0) == 0x414D4345, a
|
||||
|
||||
ntr_bf = blowfish_locator.try_get_ntr_blowfish()
|
||||
assert ntr_bf is not None, "Could not find NTR Blowfish table."
|
||||
assert sha1(ntr_bf) == codecs.decode(b"84E467F2485078E401A17A5F231E3FE6E9686648", 'hex')
|
||||
|
||||
twl_bf = blowfish_locator.try_get_twl_blowfish()
|
||||
assert twl_bf is not None, "Could not find twl Blowfish table."
|
||||
assert sha1(twl_bf) == codecs.decode(b"2DEA11191F28C6CC1956DADB8941AFFD4B2B5102", 'hex')
|
||||
|
||||
table = keytransform.transform_table(0x45434D41, 2, 8, ntr_bf)
|
||||
|
||||
digest = sha1(table)
|
||||
SHA1_EXPECT = b"d7215dede5a9bf97fc75338b037a681f158c3bbc"
|
||||
|
||||
assert digest == codecs.decode(SHA1_EXPECT, 'hex'), (SHA1_EXPECT, codecs.encode(digest, 'hex'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
||||
Reference in New Issue
Block a user