This commit is contained in:
2025-11-28 03:53:05 +01:00
commit 2bf084d710
21 changed files with 1725 additions and 0 deletions
+10
View File
@@ -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
+99
View File
@@ -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)
+16
View File
@@ -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()
+39
View File
@@ -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
+142
View File
@@ -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)
+62
View File
@@ -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)
+21
View File
@@ -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.
+25
View File
@@ -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.
+37
View File
@@ -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"
+41
View File
@@ -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()