This commit is contained in:
2025-11-28 03:53:05 +01:00
commit 2bf084d710
21 changed files with 1725 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
build_bins/
__pycache__/
*.egg-info
*.pyc
+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()
+24
View File
@@ -0,0 +1,24 @@
.PHONY: loader9 loader7 clean
all: checklibtwl loader9 loader7 data/aplist.bin data/savelist.bin
PICO_PLATFORM ?= DSPICO
checklibtwl:
$(MAKE) -C libs/libtwl
loader9: checklibtwl
$(MAKE) -f Makefile.arm9 PLATFORM=$(PICO_PLATFORM)
loader7: checklibtwl
$(MAKE) -f Makefile.arm7
data/%.bin: data/%.csv
PicoLoaderConverter $* -i "$<" -o "$@"
clean:
$(MAKE) -f Makefile.arm7 clean
$(MAKE) -f Makefile.arm9 clean
$(RM) -rf build
+367
View File
@@ -0,0 +1,367 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
.idea/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Wavefront obj resources
!*/Resources/Models/*.obj
@@ -0,0 +1,165 @@
from dataclasses import dataclass
from enum import IntEnum
from math import log
import struct
__all__ = [ 'CardSaveType', 'DSProtectVersion', 'HexInt', 'BinInt'
, 'SaveListEntry', 'CsvSaveListEntry'
, 'ApListEntry', 'CsvApListEntry'
]
class CardSaveType_(IntEnum):
NONE = 0
EEPROM = 1
FLASH = 2
NAND = 3
def __str__(self):
return self.name.lower()
def CardSaveType(s):
if isinstance(s, str):
return CardSaveType_(['none', 'eeprom', 'flash', 'nand'].index(s))
return CardSaveType_(s)
class DSProtectVersion_(IntEnum):
V1_06 = 0
V1_05 = 1
V1_08 = 2
V1_10 = 3
V1_20 = 4
V1_22 = 5
V1_23 = 6
V1_23Z = 7
V1_25 = 8
V1_26 = 9
V1_27 = 10
V1_28 = 11
V2_00 = 12
V2_01 = 13
V2_03 = 14
V2_05 = 15
V2_00s = 16
V2_01s = 17
V2_03s = 18
V2_05s = 19
def __str__(self) -> str:
return self.name[1:].replace('_', '.')
def DSProtectVersion(s):
if isinstance(s, str):
v = DSProtectVersion_.__members__.get("V" + s.replace('.', '_'))
if v is None:
raise ValueError("Can't convert '%s' to DSProtectVersion!" % s)
return v
return DSProtectVersion_(s)
def serialize_u24(v: int) -> bytes:
return bytes((v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0x24))
def serialize_gamecode(gc: str) -> int:
enc = gc.encode()
if len(gc) != 4 or len(enc) != 4:
raise ValueError("gamecode '%s' must be an ASCII string of length 4"%gc)
return struct.unpack('<I', enc)[0]
def HexInt(x): return int(x, 0) # input CSV has 0x prefixes for hex numbers
def BinInt(x): return int(x, 2)
@dataclass
class SaveListEntry: # little endian
gameCode: int # u32
saveType: CardSaveType # u8
saveSize: int # u8
# padding: u16
def __init__(self, gameCode: int, saveType: CardSaveType, saveSize: int):
super().__init__()
self.gameCode = gameCode & 0xffff_ffff
self.saveType = saveType
if saveSize & (saveSize - 1):
raise ValueError("saveSize %d not a power of two!" % saveSize)
self.saveSize = 0 if saveSize == 0 else (saveSize.bit_length() - 1)
def serialize(self) -> bytes:
r = struct.pack('<IBBH', self.gameCode, int(self.saveType), self.saveSize, 0)
assert len(r) == 8, r
return r
@dataclass
class CsvSaveListEntry:
gameCode: str
saveType: str
saveSize: HexInt
def tobinary(self) -> SaveListEntry:
return SaveListEntry(serialize_gamecode(self.gameCode),
CardSaveType(self.saveType), self.saveSize)
@dataclass
class ApListEntry: # little endian
gameCode: int # u32
gameVersion: int # 5 bits
dsprotVersion: DSProtectVersion_ # 5 bits
dsprotFuncMask: int # 6 bits
regularOvlId: int # u16
sOvlId: int # u16
regularOffset: int # u24
sOffset: int # u24
OVERLAY_ID_STATIC_ARM9 = 0xfffe
OVERLAY_ID_INVALID = 0xffff
def __init__(self, gameCode: int, gameVersion: int, dsprotVersion: DSProtectVersion_,
dsprotFuncMask: int, regularOvlId: int, sOvlId: int,
regularOffset: int, sOffset: int):
super().__init__()
self.gameCode = gameCode & 0xffff_ffff
self.gameVersion = gameVersion & 0x1f
self.dsprotVersion = dsprotVersion
self.dsprotFuncMask = dsprotFuncMask & 0x3f
self.regularOvlId = regularOvlId & 0xffff
self.sOvlId = sOvlId & 0xffff
self.regularOffset = regularOffset & 0xff_ffff
self.sOffset = sOffset & 0xff_ffff
def serialize(self) -> bytes:
info = (self.gameVersion & 0x1f) \
| ((int(self.dsprotVersion) & 0x1f) << 5) \
| ((self.dsprotFuncMask & 0x3f) << 10)
r = struct.pack('<IHHH3B3B', self.gameCode, info,
self.regularOvlId, self.sOvlId,
*serialize_u24(self.regularOffset),
*serialize_u24(self.sOffset))
assert len(r) == 16, len(r)
return r
@dataclass
class CsvApListEntry:
gameCode: str
gameVersion: int
dsprotVersion: str
dsprotFuncMask: BinInt
regularOvlId: int
regularOffset: HexInt
sOvlId: int
sOffset: HexInt
def tobinary(self) -> ApListEntry:
return ApListEntry(serialize_gamecode(self.gameCode), self.gameVersion,
DSProtectVersion(self.dsprotVersion),
self.dsprotFuncMask, self.regularOvlId, self.sOvlId,
self.regularOffset, self.sOffset)
@@ -0,0 +1,71 @@
import argparse, csv
from PicoLoaderConverter.plctypes import *
def scsv_parse_struct_table(f, typ):
annot = typ.__annotations__
header = f.readline().strip().split(';')
for x in header:
assert x in annot, (x, annot)
for k in annot.keys():
assert k in header, (k, header)
csvr = csv.DictReader(f, fieldnames=header, delimiter=';')
for row in csvr:
parsed = dict((k, annot[k](v)) for (k, v) in row.items())
yield typ(**parsed)
def run_aplist(args) -> int|None:
intable = list(scsv_parse_struct_table(args.input, CsvApListEntry))
intable.sort(key=lambda e: e.gameVersion)
intable.sort(key=lambda e: e.gameCode) # alphabetic sort, ie can't compare these as integers!
for e in intable:
args.output.write(e.tobinary().serialize())
def run_savelist(args) -> int|None:
intable = list(scsv_parse_struct_table(args.input, CsvSaveListEntry))
intable.sort(key=lambda e: e.gameCode) # alphabetic sort, ie can't compare these as integers!
for e in intable:
args.output.write(e.tobinary().serialize())
def main() -> int|None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="verb", help="subcommand")
aplist = subparsers.add_parser("aplist", help="")
aplist.add_argument('-i', "--input", type=argparse.FileType('r'), help="Input .csv file", required=True)
aplist.add_argument('-o', "--output", type=argparse.FileType('wb'), help="Output .bin file", required=True)
savelist = subparsers.add_parser("savelist", help="")
savelist.add_argument('-i', "--input", type=argparse.FileType('r'), help="Input .csv file", required=True)
savelist.add_argument('-o', "--output", type=argparse.FileType('wb'), help="Output .bin file", required=True)
args = parser.parse_args()
verb = {
'aplist': run_aplist,
'savelist': run_savelist
}.get(args.verb)
if verb is None:
print("ERROR: invalid verb?!")
return 1
return verb(args)
if __name__ == '__main__':
exit(main() or 0)
+37
View File
@@ -0,0 +1,37 @@
[project]
name = "PicoLoaderConverter"
version = "0.1.0"
description = "Converts AP-list and savetype-list CSV files to binary format"
#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]
PicoLoaderConverter = "PicoLoaderConverter.program:main"
+40
View File
@@ -0,0 +1,40 @@
# DSPico buildscripts
Scripts to automatically build the entire DSPico stack. Also, replaces the C#
components of the build with Python.
## Usage
```
$ ls
DSRomEncryptor buildscript dspico-bootloader dspico-dldi dspico-firmware dspico-wrfuxxed output pico-launcher pico-loader
$ ./buildscript/build.py --help
usage: build.py [-h] --ntr-blowfish NTR_BLOWFISH --twl-blowfish TWL_BLOWFISH
[--twl-dev-blowfish TWL_DEV_BLOWFISH] [--wrfu-tester WRFU_TESTER]
[--wonderful-env WONDERFUL_ENV]
srcdir outdir
positional arguments:
srcdir Directory containing all cloned repositories
outdir Directory containing all build artefacts
options:
-h, --help show this help message and exit
--ntr-blowfish NTR_BLOWFISH
Source file for NTR Blowfish keys (table binary or BIOS image)
--twl-blowfish TWL_BLOWFISH
Source file for TWL Blowfish keys (table binary or BIOS/bootrom image)
--twl-dev-blowfish TWL_DEV_BLOWFISH
Source file for TWL development Blowfish keys (table binary or SRL) (optional)
--wrfu-tester WRFU_TESTER
Path to WRFU Tester SRL (for Wfruxxed, optional)
--wonderful-env WONDERFUL_ENV
Environment file to source for the Wonderful/BlocksDS toolchains
$ ./buildscript/build.py \
--ntr-blowfish path/to/biosnds7.rom \
--twl-blowfish path/to/biosdsi7.rom \
--wrfu-tester path/to/WRFUTester_v0.60_20080821.srl \
. output
```
Executable
+393
View File
@@ -0,0 +1,393 @@
#!/usr/bin/env python3
import argparse, codecs, hashlib, os, sys
import shutil, subprocess, traceback
import multiprocessing
from dataclasses import dataclass
from pathlib import Path
from typing import Tuple, Sequence, Dict
BLOWFISH_P_S_LENGTH = 0x1048
HASH_NTR_BLOWFISH_TABLE = codecs.decode(b"84E467F2485078E401A17A5F231E3FE6E9686648", 'hex')
HASH_NTR_BLOWFISH_BIOS = codecs.decode(b"24F67BDEA115A2C847C8813A262502EE1607B7DF", 'hex')
HASH_TWL_BLOWFISH_TABLE = codecs.decode(b"2DEA11191F28C6CC1956DADB8941AFFD4B2B5102", 'hex')
HASH_TWL_BLOWFISH_BIOS = codecs.decode(b"A3AA751EB6BDAAF8A827BA9E03576A6F1AB0F547", 'hex')
HASH_TWL_BLOWFISH_BROM = codecs.decode(b"C7C7570BFE51C3C7C5DA3B01331B94E7E7CB4F53", 'hex')
HASH_WFRU_TESTER_0_60 = codecs.decode(b"2d65fb7a0c62a4f08954b98c95f42b804fccfd26", 'hex')
HASH_TWL_DEV_BLOWFISH = codecs.decode(b"CFF62F24444F5494001F019D505F9C51D40FC8B3", 'hex')
TWL_DEV_BLOWFISH_BEGIN = b"\x69\x63\x52\x05"
NPROC = "-j%d" % multiprocessing.cpu_count()
@dataclass
class Tools:
sed: str
make: str
cmake: str
dlditool: str
@dataclass
class InputBlobs:
ntr_bf: bytes
twl_bf: bytes
twl_dev_bf: bytes|None = None
wrfu_tester: bytes|None = None
HAS_ERRORS = False
def error(err, *args, fatal=False, **kwargs):
global HAS_ERRORS
#traceback.print_stack(file=sys.stderr)
#print(file=sys.stderr)
print(err, file=sys.stderr, *args, **kwargs)
if fatal: exit(1)
else: HAS_ERRORS = True
def check_errors():
if HAS_ERRORS:
print(file=sys.stderr)
print("Errors occurred. Exiting.", file=sys.stderr)
exit(1)
def subproc_run(args, **kwargs):
cp = subprocess.run(args, **kwargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if cp.returncode != 0:
sys.stderr.buffer.write(cp.stdout)
error("Subcommand '%s' failed." % " ".join(str(x) for x in args), fatal=True)
class Check:
def repos(srcdir: Path):
for x in ('dspico-dldi', 'dspico-bootloader', 'dspico-firmware'):
if not (srcdir / x).is_dir():
error("'%s' repository not found in source directory '%s'" % (x, srcdir))
for x in ('dspico-bootloader', 'pico-loader', 'pico-launcher'):
if not (srcdir / x / 'libs' / 'libtwl' / 'Makefile').is_file():
error("'%s' repository: did you properly set up the submodules?" % x)
if not (srcdir / 'dspico-firmware' / 'pico-sdk' / 'CMakeLists.txt').is_file():
error("'dspico-firmware' repository: did you properly set up the submodules?")
if not (srcdir / 'dspico-firmware' / 'pico-sdk' / 'lib' / 'tinyusb' / 'LICENSE').is_file():
error("'dspico-firmware' repository: did you properly set up the Pico SDK submodules?")
def toolchain(wonderful_env: Path|None) -> Tools:
r = {}
for x in Tools.__annotations__.keys():
tool = os.getenv(x.upper()) or shutil.which(x)
if x == 'dlditool' and (tool is None or not Path(tool).is_file()):
bds = Path(os.getenv('BLOCKSDS') or '/opt/blocksds/core')
if bds.is_dir():
tool = bds / "tools" / "dlditool" / "dlditool"
if not tool.is_file() and wonderful_env is not None:
tool = subprocess.run(["/bin/sh", "-c", ". '%s' && which dlditool" % wonderful_env],
check=True, capture_output=True) \
.stdout.decode().strip()
if tool is None or not Path(tool).is_file():
error("No '%s' binary found. Please specify its location with the '%s' environment variable." % (x, x.upper()))
r[x] = tool
if wonderful_env is None:
blocksds = Path(os.getenv('BLOCKSDS') or '/opt/blocksds/core')
blocksdsext = Path(os.getenv('BLOCKSDSEXT') or '/opt/blocksds/ext')
wftoolcahin = Path(os.getenv('WONDERFUL_TOOLCHAIN') or '/opt/wonderful')
if not blocksds.is_dir():
error("Blocks DS not found. Please specify it via the 'BLOCKSDS' environment variable, or use --wonderful-env.")
if not blocksdsext.is_dir():
error("Blocks DS EXT not found. Please specify it via the 'BLOCKSDSEXT' environment variable, or use --wonderful-env.")
if not wftoolchain.is_dir():
error("Blocks DS not found. Please specify it via the 'BLOCKSDS' environment variable, or use --wonderful-env.")
return Tools(**r)
def prerequisites(blobs: InputBlobs, filesdir: Path):
def calc_sha1(b: bytes) -> bytes:
s = hashlib.sha1()
s.update(b)
return s.digest()
ntr_bf_sha1 = calc_sha1(blobs.ntr_bf)
if ntr_bf_sha1 == HASH_NTR_BLOWFISH_TABLE:
(filesdir / 'ntrBlowfish.bin').write_bytes(blobs.ntr_bf)
elif ntr_bf_sha1 == HASH_NTR_BLOWFISH_BIOS:
(filesdir / 'biosnds7.rom').write_bytes(blobs.ntr_bf)
else:
error("Invalid NTR blowfish file. Must be either a flat table binary, or the ARM7 BIOS image.")
twl_bf_sha1 = calc_sha1(blobs.twl_bf)
if twl_bf_sha1 == HASH_TWL_BLOWFISH_TABLE:
(filesdir / 'twlBlowfish.bin').write_bytes(blobs.twl_bf)
elif twl_bf_sha1 in (HASH_TWL_BLOWFISH_BIOS, HASH_TWL_BLOWFISH_BROM):
(filesdir / 'biosdsi7.rom').write_bytes(blobs.twl_bf)
else:
error("Invalid TWL blowfish file. Must be either a flat table binary, or the ARM7 BIOS or bootROM image.")
if blobs.twl_dev_bf is not None:
if calc_sha1(blobs.twl_dev_bf) == HASH_TWL_DEV_BLOWFISH:
(filesdir / 'twlDevBlowfish.bin').write_bytes(blobs.twl_dev_bf)
else:
off = 0
found = False
while not found:
off = blobs.twl_dev_bf.find(b"\x69\x63\x52\x05", off)
if off == -1:
break
ptable = blobs.twl_dev_bf[off:(off+0x48)]
sboxes = blobs.twl_dev_bf[(off+0x600):(off+0x1600)]
if len(ptable)+len(sboxes) == BLOWFISH_P_S_LENGTH and calc_sha1(ptable + sboxes) == HASH_TWL_DEV_BLOWFISH:
(filesdir / 'twlDevBlowfish.bin').write_bytes(ptable + sboxes)
found = True
break
if not found:
error("Invalid TWL dev blowfish file. Must be either a flat table binary, or an SRL containing the key.")
if blobs.wrfu_tester is not None:
if calc_sha1(blobs.wrfu_tester) == HASH_WFRU_TESTER_0_60:
(filesdir / 'dsimode.nds').write_bytes(blobs.wrfu_tester)
else:
error("Invalid WRFU Tester binary. Must be the v0.60 SRL.")
def setup_venv(exedir: Path, filesdir: Path) -> Path:
venvdir = filesdir / 'dspico-build-venv'
if venvdir.is_dir():
shutil.rmtree(venvdir)
import venv
venv.create(str(venvdir), symlinks=True, with_pip=True,
prompt="dspico-build-venv")
subproc_run([str(venvdir / 'bin' / 'pip'), 'install', '-e',
str(exedir / 'DSRomEncryptor')])
subproc_run([str(venvdir / 'bin' / 'pip'), 'install', '-e',
str(exedir / 'PicoLoaderConverter')])
return venvdir
def do_args():
parser = argparse.ArgumentParser()
parser.add_argument('--ntr-blowfish', type=argparse.FileType('rb'), required=True,
help="Source file for NTR Blowfish keys (table binary or BIOS image)")
parser.add_argument('--twl-blowfish', type=argparse.FileType('rb'), required=True,
help="Source file for TWL Blowfish keys (table binary or BIOS/bootrom image)")
parser.add_argument('--twl-dev-blowfish', type=argparse.FileType('rb'),
help="Source file for TWL development Blowfish keys (table binary or SRL) (optional)")
parser.add_argument('--wrfu-tester', type=argparse.FileType('rb'),
help="Path to WRFU Tester SRL (for Wfruxxed, optional)")
parser.add_argument('--wonderful-env', type=Path,
help="Environment file to source for the Wonderful/BlocksDS toolchains")
parser.add_argument('srcdir', type=Path, help="Directory containing all cloned repositories")
parser.add_argument('outdir', type=Path, help="Directory containing all build artefacts")
args = parser.parse_args()
if not args.srcdir.is_dir(): error("Source directory '%s' does not exist or is not a directory!"%args.srcdir, fatal=True)
if not args.outdir.is_dir(): error("Source directory '%s' does not exist or is not a directory!"%args.srcdir, fatal=True)
return (args, Path(sys.argv[0]).parent.absolute())
class Build:
def run_with_env(envfiles: Path|None|Sequence[Path], args, **kwargs):
if envfiles is not None:
sources = (". '%s'"%envfiles) if isinstance(envfiles, (str,Path)) \
else " && ".join(". '%s'"%s for s in envfiles)
args = ["/bin/sh", "-c", "%s && '%s'" % (sources, "' '".join(str(x) for x in args))]
subproc_run(args, **kwargs)
def dspico_dldi(envfile, tools: Tools, srcdir: Path, filesdir: Path) -> Path:
prjdir = srcdir / "dspico-dldi"
Build.run_with_env(envfile, [tools.make, '-C', str(prjdir), NPROC])
return Path(shutil.copy((prjdir / "DSpico.dldi"), filesdir))
def dspico_bootloader(envfile, tools: Tools, srcdir: Path, romenc: Path, filesdir: Path) -> Path:
prjdir = srcdir / "dspico-bootloader"
Build.run_with_env(envfile, [tools.make, '-C', str(prjdir), NPROC])
subproc_run([tools.dlditool, filesdir / "DSpico.dldi", prjdir / "BOOTLOADER.nds"])
subproc_run([romenc, prjdir / "BOOTLOADER.nds", filesdir / 'default.nds'], cwd=filesdir)
return filesdir / 'default.nds'
def dspico_wrfuxxed(envfile, tools: Tools, srcdir: Path, filesdir: Path) -> Path:
prjdir = srcdir / "dspico-wrfuxxed"
Build.run_with_env(envfile, [tools.make, '-C', str(prjdir), NPROC])
return Path(shutil.copy((prjdir / "uartBufv060.bin"), filesdir))
def dspico_firmware(tools: Tools, srcdir: Path, filesdir: Path, default_nds: Path, uartBufv060=None) -> (Path, Path):
prjdir = srcdir / "dspico-firmware"
buildname = "vanilla" if uartBufv060 is None else "wrfuxxed"
shutil.copy(default_nds, prjdir / 'roms')
if uartBufv060:
shutil.copy(filesdir / 'dsimode.nds', prjdir / 'roms')
shutil.copy(uartBufv060, prjdir / 'data')
subproc_run([tools.sed, '--posix', '-i',
's/^.*DSPICO_ENABLE_WRFUXXED.*$/ DSPICO_ENABLE_WRFUXXED/', prjdir / 'CMakeLists.txt'])
else:
subproc_run([tools.sed, '--posix', '-i',
's/^.*DSPICO_ENABLE_WRFUXXED.*$/ #DSPICO_ENABLE_WRFUXXED/', prjdir / 'CMakeLists.txt'])
cmake_build = prjdir / ('cmake-build-%s' % buildname)
if cmake_build.is_dir(): shutil.rmtree(cmake_build)
cmake_build.mkdir()
cmake_ver_str = subprocess.run([tools.cmake, "--version"], check=True, capture_output=True).stdout.decode().splitlines()[0]
if not cmake_ver_str.startswith("cmake version "):
error("Can't parse CMake version output: %s" % cmake_ver_str)
return None
cmake_ver = tuple(int(x) for x in cmake_ver_str[len("cmake version "):].split("."))
cmake_env = dict(os.environ)
cmake_env['PICO_SDK_PATH'] = "../pico-sdk"
cmake_args = ["-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo",
"-G", "Unix Makefiles", "-B", cmake_build, prjdir]
if cmake_ver >= (4,):
# we are running CMake version 4.x or newer, so we need to fix up some stuff
cmake_args.insert(0, "-DCMAKE_POLICY_VERSION_MINIMUM=3.5")
subproc_run([tools.sed, '--posix', '-i',
's/^cmake_minimum_required(VERSION 3\\..*)/cmake_minimum_required(VERSION 3.13)/',
prjdir / "pico-sdk" / "tools" / "pioasm" / "CMakeLists.txt"])
subproc_run([tools.cmake, *cmake_args], env=cmake_env)
subproc_run([tools.make, "-C", cmake_build, NPROC])
return shutil.copy(cmake_build / 'DSpico.uf2', filesdir / ("DSpico-%s.uf2" % buildname))
def dspico_loader(envfile, tools: Tools, srcdir: Path, filesdir: Path, exedir: Path, venvdir: Path) -> Tuple[Path, Path, Path, Path]:
prjdir = srcdir / "pico-loader"
Build.run_with_env((envfile, venvdir/'bin'/'activate'),
[tools.make, '-f', exedir / 'Makefile.loader',
'-C', prjdir, NPROC])
a = shutil.copy(prjdir / 'picoLoader7.bin', filesdir)
b = shutil.copy(prjdir / 'picoLoader9_DSPICO.bin', filesdir)
c = shutil.copy(prjdir / 'data' / 'aplist.bin', filesdir)
d = shutil.copy(prjdir / 'data' / 'savelist.bin', filesdir)
return (a, b, c, d)
def dspico_launcher(envfile, tools: Tools, srcdir: Path, filesdir: Path) -> Tuple[Path, Path]:
prjdir = srcdir / "pico-launcher"
Build.run_with_env(envfile, [tools.make, '-C', prjdir, NPROC])
return (shutil.copy(prjdir / 'LAUNCHER.nds', filesdir), (prjdir / '_pico'))
def install_to_destdir(filesdict: Dict[str, Path], outdir: Path):
if (outdir / '_pîco').is_dir(): shutil.rmtree(outdir / '_pico')
shutil.copytree(filesdict['launcher_dir'], outdir / '_pico')
shutil.copy(filesdict['launcher'], outdir / '_picoboot.nds')
shutil.copy(filesdict['loader_7'], outdir / 'picoLoader7.bin')
shutil.copy(filesdict['loader_9'], outdir / 'picoLoader9.bin')
shutil.copy(filesdict['aplist'], outdir / 'aplist.bin')
shutil.copy(filesdict['savelist'], outdir / 'savelist.bin')
def run(args, exedir: Path):
blobs = InputBlobs(args.ntr_blowfish.read(), args.twl_blowfish.read(),
None if args.twl_dev_blowfish is None else args.twl_dev_blowfish.read(),
None if args.wrfu_tester is None else args.wrfu_tester.read(),
)
srcdir = args.srcdir.absolute()
outdir = args.outdir.absolute()
filesdir = (exedir / 'build_bins').absolute()
filesdir.mkdir(exist_ok=True, parents=True)
print("[===] Checking inputs...")
Check.repos(args.srcdir)
tools = Check.toolchain(args.wonderful_env)
Check.prerequisites(blobs, filesdir)
check_errors()
print("[===] Setting up Python venv...")
venvdir = setup_venv(exedir, filesdir).absolute()
check_errors()
romenc = venvdir / "bin" / "DSRomEncryptor"
plconv = venvdir / "bin" / "PicoLoaderConverter"
print("[-->] ROM encryptor =", romenc, ", loader CSV conv =", plconv)
print("[===] Compiling DLDI driver...")
dspico_dldi = Build.dspico_dldi(args.wonderful_env, tools, srcdir, filesdir)
check_errors()
print("[-->] DLDI driver =", dspico_dldi)
print("[===] Compiling bootloader...")
default_nds = Build.dspico_bootloader(args.wonderful_env, tools, srcdir, romenc, filesdir)
check_errors()
print("[-->] bootloader =", default_nds)
do_wrfuxxed = (srcdir / 'dspico-wrfuxxed').is_dir() and blobs.wrfu_tester is not None
if do_wrfuxxed:
print("[===] Compiling Wrfuxxed exploit...")
uartBufv060 = Build.dspico_wrfuxxed(args.wonderful_env, tools, srcdir, filesdir)
check_errors()
print("[-->] Wrfuxxed =", uartBufv060)
print("[===] Compiling RP2040 firmware (vanilla)...")
fw_v = Build.dspico_firmware(tools, srcdir, filesdir, default_nds)
if do_wrfuxxed:
print("[===] Compiling RP2040 firmware (with Wrfuxxed)...")
fw_w = Build.dspico_firmware(tools, srcdir, filesdir, default_nds, uartBufv060)
check_errors()
print("[-->] firmware (vanilla) =", fw_v)
if do_wrfuxxed:
print("[-->] firmware (Wrfuxxed) =", fw_w)
print("[===] Compiling DSPico Loader...")
loader_bins = Build.dspico_loader(args.wonderful_env, tools, srcdir, filesdir, exedir, venvdir)
check_errors()
print("[-->] loader binaries =", loader_bins)
print("[===] Compiling DSPico Launcher...")
launcher_nds, launcher_dir = Build.dspico_launcher(args.wonderful_env, tools, srcdir, filesdir)
check_errors()
print("[-->] DSPico launcher =", launcher_nds, "launcher dir =", launcher_dir)
print("[===] Installing files to destdir...")
install_to_destdir({
'launcher_dir': launcher_dir,
'launcher': launcher_nds,
'loader_7': loader_bins[0],
'loader_9': loader_bins[1],
'aplist': loader_bins[2],
'savelist': loader_bins[3],
}, outdir)
check_errors()
print("[===] All done! Enjoy!")
print("[===] Don't forget to flash your firmware to the RP2040!\n\t(",
fw_v, "or", fw_w, ")")
if __name__ == '__main__':
args = do_args()
exit(run(*args) or 0)