Created
April 12, 2026 13:20
-
-
Save Sergey004/941e99c1735bc2d49750c674d3f1839b to your computer and use it in GitHub Desktop.
DSRomEncryptor port to Python
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| DSRomEncryptor - Python port of Gericom's DSRomEncryptor | |
| Encrypts Nintendo DS ROM secure areas using Blowfish encryption. | |
| Original: https://github.com/Gericom/DSRomEncryptor | |
| """ | |
| import argparse | |
| import struct | |
| import sys | |
| import os | |
| from pathlib import Path | |
| # --------------------------------------------------------------------------- | |
| # Constants | |
| # --------------------------------------------------------------------------- | |
| HEADING_TEXT = "== DSRomEncryptor by Gericom (Python port) ==" | |
| SECURE_AREA_ID = 0x6A624F7972636E65 # "encryObj" | |
| 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 | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| # CRC-16 | |
| # --------------------------------------------------------------------------- | |
| 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 calculate_crc16(data: bytes) -> int: | |
| """Calculate CRC-16 of the given data.""" | |
| result = 0xFFFF | |
| for b in data: | |
| result = (result >> 8) ^ CRC16_TABLE[(result ^ b) & 0xFF] | |
| return result & 0xFFFF | |
| # --------------------------------------------------------------------------- | |
| # Blowfish | |
| # --------------------------------------------------------------------------- | |
| class Blowfish: | |
| """Blowfish cipher implementation for NDS ROM encryption.""" | |
| P_TABLE_ENTRY_COUNT = 18 | |
| S_BOX_COUNT = 4 | |
| S_BOX_ENTRY_COUNT = 256 | |
| BLOCK_LENGTH = 8 | |
| KEY_TABLE_P_TABLE_LENGTH = P_TABLE_ENTRY_COUNT * 4 # 72 bytes | |
| KEY_TABLE_S_BOXES_LENGTH = S_BOX_COUNT * S_BOX_ENTRY_COUNT * 4 # 4096 bytes | |
| KEY_TABLE_LENGTH = KEY_TABLE_P_TABLE_LENGTH + KEY_TABLE_S_BOXES_LENGTH # 4168 bytes | |
| def __init__(self, key_table: bytes | bytearray | memoryview): | |
| if len(key_table) < self.KEY_TABLE_LENGTH: | |
| raise ValueError("Key table length is invalid.") | |
| data = bytes(key_table) | |
| # Read P-table: 18 uint32 LE values starting at offset 0 | |
| self._p_table = list(struct.unpack_from('<18I', data, 0)) | |
| # Read S-boxes: 4 x 256 uint32 LE values | |
| self._s_boxes = [] | |
| self._s_boxes.append(list(struct.unpack_from('<256I', data, 0x48))) | |
| self._s_boxes.append(list(struct.unpack_from('<256I', data, 0x448))) | |
| self._s_boxes.append(list(struct.unpack_from('<256I', data, 0x848))) | |
| self._s_boxes.append(list(struct.unpack_from('<256I', data, 0xC48))) | |
| def _encrypt_value(self, val: int) -> int: | |
| """Encrypt a single 64-bit value.""" | |
| y = val & 0xFFFFFFFF | |
| x = (val >> 32) & 0xFFFFFFFF | |
| for i in range(16): | |
| z = self._p_table[i] ^ x | |
| a = self._s_boxes[0][(z >> 24) & 0xFF] | |
| b = self._s_boxes[1][(z >> 16) & 0xFF] | |
| c = self._s_boxes[2][(z >> 8) & 0xFF] | |
| d = self._s_boxes[3][z & 0xFF] | |
| x = (d + ((c ^ b) + a)) ^ y | |
| x &= 0xFFFFFFFF | |
| y = z | |
| result_x = (x ^ self._p_table[16]) & 0xFFFFFFFF | |
| result_y = (y ^ self._p_table[17]) & 0xFFFFFFFF | |
| return result_x | (result_y << 32) | |
| def encrypt_value(self, val: int) -> int: | |
| """Encrypt a single 64-bit value.""" | |
| return self._encrypt_value(val) | |
| def _decrypt_value(self, val: int) -> int: | |
| """Decrypt a single 64-bit value.""" | |
| y = val & 0xFFFFFFFF | |
| x = (val >> 32) & 0xFFFFFFFF | |
| for i in range(17, 1, -1): | |
| z = self._p_table[i] ^ x | |
| a = self._s_boxes[0][(z >> 24) & 0xFF] | |
| b = self._s_boxes[1][(z >> 16) & 0xFF] | |
| c = self._s_boxes[2][(z >> 8) & 0xFF] | |
| d = self._s_boxes[3][z & 0xFF] | |
| x = (d + ((c ^ b) + a)) ^ y | |
| x &= 0xFFFFFFFF | |
| y = z | |
| result_x = (x ^ self._p_table[1]) & 0xFFFFFFFF | |
| result_y = (y ^ self._p_table[0]) & 0xFFFFFFFF | |
| return result_x | (result_y << 32) | |
| def decrypt_value(self, val: int) -> int: | |
| """Decrypt a single 64-bit value.""" | |
| return self._decrypt_value(val) | |
| def encrypt(self, data: bytearray) -> None: | |
| """Encrypt data in place. Length must be a multiple of 8.""" | |
| if len(data) % 8 != 0: | |
| raise ValueError("Data length must be a multiple of 8.") | |
| for i in range(0, len(data), self.BLOCK_LENGTH): | |
| val = struct.unpack_from('<Q', data, i)[0] | |
| enc = self._encrypt_value(val) | |
| struct.pack_into('<Q', data, i, enc) | |
| def decrypt(self, data: bytearray) -> None: | |
| """Decrypt data in place. Length must be a multiple of 8.""" | |
| if len(data) % 8 != 0: | |
| raise ValueError("Data length must be a multiple of 8.") | |
| for i in range(0, len(data), self.BLOCK_LENGTH): | |
| val = struct.unpack_from('<Q', data, i)[0] | |
| dec = self._decrypt_value(val) | |
| struct.pack_into('<Q', data, i, dec) | |
| # --------------------------------------------------------------------------- | |
| # Key Transform | |
| # --------------------------------------------------------------------------- | |
| def transform_table(game_code: int, level: int, modulo: int, key_table: bytes) -> bytearray: | |
| """ | |
| Transform the given blowfish key table based on game code, level and modulo. | |
| Returns a new transformed table as bytearray. | |
| """ | |
| new_table = bytearray(key_table[:Blowfish.KEY_TABLE_LENGTH]) | |
| # Build key code: 12 bytes (3 x uint32 LE) | |
| key_code = bytearray(12) | |
| struct.pack_into('<I', key_code, 0, game_code & 0xFFFFFFFF) | |
| struct.pack_into('<I', key_code, 4, (game_code >> 1) & 0xFFFFFFFF) | |
| struct.pack_into('<I', key_code, 8, (game_code << 1) & 0xFFFFFFFF) | |
| if level >= 1: | |
| _apply_key_code(key_code, modulo, new_table) | |
| if level >= 2: | |
| _apply_key_code(key_code, modulo, new_table) | |
| # Modify key code bytes 4..8 and 8..12 | |
| val4 = struct.unpack_from('<I', key_code, 4)[0] | |
| val8 = struct.unpack_from('<I', key_code, 8)[0] | |
| struct.pack_into('<I', key_code, 4, (val4 << 1) & 0xFFFFFFFF) | |
| struct.pack_into('<I', key_code, 8, (val8 >> 1) & 0xFFFFFFFF) | |
| if level >= 3: | |
| _apply_key_code(key_code, modulo, new_table) | |
| return new_table | |
| def _apply_key_code(key_code: bytearray, modulo: int, key_table: bytearray) -> None: | |
| """Apply key code transformation to the key table in place.""" | |
| bf = Blowfish(key_table) | |
| # Encrypt key_code[4:12] in place (8 bytes) | |
| chunk = bytearray(key_code[4:12]) | |
| bf.encrypt(chunk) | |
| key_code[4:12] = chunk | |
| # Encrypt key_code[0:8] in place (8 bytes) | |
| chunk = bytearray(key_code[0:8]) | |
| bf.encrypt(chunk) | |
| key_code[0:8] = chunk | |
| # XOR P-table entries with key code (big-endian read from key code) | |
| for i in range(Blowfish.P_TABLE_ENTRY_COUNT): | |
| offset_in_keycode = (i * 4) % modulo | |
| # Read 4 bytes from key_code as big-endian uint32 | |
| if offset_in_keycode + 4 <= len(key_code): | |
| kc_val = struct.unpack_from('>I', key_code, offset_in_keycode)[0] | |
| else: | |
| # Handle wrap-around case | |
| kc_val = 0 | |
| for j in range(4): | |
| idx = (offset_in_keycode + j) % len(key_code) | |
| kc_val = (kc_val << 8) | key_code[idx] | |
| table_offset = i * 4 | |
| existing = struct.unpack_from('<I', key_table, table_offset)[0] | |
| struct.pack_into('<I', key_table, table_offset, existing ^ kc_val) | |
| # Update table using scratch buffer | |
| scratch = bytearray(8) | |
| for i in range(0, Blowfish.KEY_TABLE_LENGTH, Blowfish.BLOCK_LENGTH): | |
| bf = Blowfish(key_table) | |
| bf.encrypt(scratch) | |
| key_table[i:i + 4] = scratch[4:8] | |
| key_table[i + 4:i + 8] = scratch[0:4] | |
| # --------------------------------------------------------------------------- | |
| # Blowfish Locator | |
| # --------------------------------------------------------------------------- | |
| def _find_script_dir() -> Path: | |
| """Return the directory where this script is located.""" | |
| return Path(__file__).resolve().parent | |
| def try_get_ntr_blowfish(script_dir: Path | None = None) -> bytes | None: | |
| """Try to load the NTR (DS) blowfish key.""" | |
| if script_dir is None: | |
| script_dir = _find_script_dir() | |
| # Try direct file | |
| path = script_dir / NTR_BLOWFISH_NAME | |
| if path.is_file(): | |
| data = path.read_bytes() | |
| if len(data) == Blowfish.KEY_TABLE_LENGTH: | |
| return data | |
| # Try from DS arm7 BIOS dump | |
| path = script_dir / BIOS_NDS_7_NAME | |
| if path.is_file(): | |
| data = path.read_bytes() | |
| if len(data) == BIOS_NDS_7_LENGTH: | |
| return data[BIOS_NDS_7_BLOWFISH_OFFSET:BIOS_NDS_7_BLOWFISH_OFFSET + Blowfish.KEY_TABLE_LENGTH] | |
| return None | |
| def try_get_twl_blowfish(script_dir: Path | None = None) -> bytes | None: | |
| """Try to load the TWL (DSi) blowfish key.""" | |
| if script_dir is None: | |
| script_dir = _find_script_dir() | |
| # Try direct file | |
| path = script_dir / TWL_BLOWFISH_NAME | |
| if path.is_file(): | |
| data = path.read_bytes() | |
| if len(data) == Blowfish.KEY_TABLE_LENGTH: | |
| return data | |
| # Try from DSi arm7 BIOS dump | |
| path = script_dir / BIOS_DSI_7_NAME | |
| if path.is_file(): | |
| data = path.read_bytes() | |
| if len(data) == BIOS_DSI_7_LENGTH: | |
| return data[BIOS_DSI_7_BLOWFISH_OFFSET:BIOS_DSI_7_BLOWFISH_OFFSET + Blowfish.KEY_TABLE_LENGTH] | |
| return None | |
| def try_get_twl_dev_blowfish(script_dir: Path | None = None) -> bytes | None: | |
| """Try to load the TWL dev (DSi development) blowfish key.""" | |
| if script_dir is None: | |
| script_dir = _find_script_dir() | |
| path = script_dir / TWL_DEV_BLOWFISH_NAME | |
| if path.is_file(): | |
| return path.read_bytes() | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Helper functions | |
| # --------------------------------------------------------------------------- | |
| def _insert_test_patterns(rom_data: bytearray) -> None: | |
| """Insert standard test patterns into the ROM data.""" | |
| # Pattern at 0x3000 | |
| rom_data[0x3000:0x3008] = bytes([0xFF, 0x00, 0xFF, 0x00, 0xAA, 0x55, 0xAA, 0x55]) | |
| # Ascending pattern at 0x3008 | |
| for i in range(8, 0x200): | |
| rom_data[0x3000 + i] = i & 0xFF | |
| # Descending pattern at 0x3200 | |
| for i in range(0x200): | |
| rom_data[0x3200 + i] = 0xFF - (i & 0xFF) | |
| # Fill patterns | |
| rom_data[0x3400:0x3600] = bytes(0x200) # 0x00 | |
| rom_data[0x3600:0x3800] = bytes([0xFF] * 0x200) | |
| rom_data[0x3800:0x3A00] = bytes([0x0F] * 0x200) | |
| rom_data[0x3A00:0x3C00] = bytes([0xF0] * 0x200) | |
| rom_data[0x3C00:0x3E00] = bytes([0x55] * 0x200) | |
| rom_data[0x3E00:0x3FFF] = bytes([0xAA] * 0x1FF) | |
| rom_data[0x3FFF] = 0 | |
| def _should_rom_have_twl_area(rom_data: bytearray) -> bool: | |
| """Check if a ROM should have a TWL (DSi) area.""" | |
| return ( | |
| (rom_data[ROM_HEADER_UNIT_CODE_OFFSET] & UNIT_CODE_TWL_FLAG) != 0 | |
| and (rom_data[ROM_HEADER_TWL_FLAGS_OFFSET] & TWL_FLAGS_HAS_TWL_EXCLUSIVE_AREA_FLAG) != 0 | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Main logic | |
| # --------------------------------------------------------------------------- | |
| def encrypt_rom(input_path: str, output_path: str, use_dsi_dev_blowfish: bool = False) -> None: | |
| """ | |
| Encrypt a Nintendo DS ROM. | |
| Args: | |
| input_path: Path to the input .nds file. | |
| output_path: Path for the output .nds file. | |
| use_dsi_dev_blowfish: Whether to use the DSi development blowfish key. | |
| """ | |
| # Determine search directory for key files (same dir as the input ROM or script dir) | |
| search_dirs = [Path(input_path).resolve().parent, _find_script_dir()] | |
| # Try to load NTR blowfish key | |
| ntr_blowfish = None | |
| for d in search_dirs: | |
| ntr_blowfish = try_get_ntr_blowfish(d) | |
| if ntr_blowfish is not None: | |
| break | |
| if ntr_blowfish is None: | |
| print("Error: Could not load ntr blowfish key.") | |
| print(f" Place '{NTR_BLOWFISH_NAME}' or '{BIOS_NDS_7_NAME}' in the same directory as the ROM or this script.") | |
| sys.exit(1) | |
| # Read ROM | |
| rom_data = bytearray(Path(input_path).read_bytes()) | |
| # Read game code | |
| game_code = struct.unpack_from('<I', rom_data, ROM_HEADER_GAME_CODE_OFFSET)[0] | |
| # Transform and write NTR blowfish tables | |
| ntr_table = transform_table(game_code, 2, 8, ntr_blowfish) | |
| rom_data[ROM_NTR_BLOWFISH_P_TABLE_OFFSET:ROM_NTR_BLOWFISH_P_TABLE_OFFSET + Blowfish.KEY_TABLE_P_TABLE_LENGTH] = \ | |
| ntr_table[:Blowfish.KEY_TABLE_P_TABLE_LENGTH] | |
| rom_data[ROM_NTR_BLOWFISH_S_BOXES_OFFSET:ROM_NTR_BLOWFISH_S_BOXES_OFFSET + Blowfish.KEY_TABLE_S_BOXES_LENGTH] = \ | |
| ntr_table[Blowfish.KEY_TABLE_P_TABLE_LENGTH:Blowfish.KEY_TABLE_P_TABLE_LENGTH + Blowfish.KEY_TABLE_S_BOXES_LENGTH] | |
| # Insert test patterns | |
| _insert_test_patterns(rom_data) | |
| # Encrypt secure area | |
| arm9_offset = struct.unpack_from('<I', rom_data, ROM_HEADER_ARM9_OFFSET_OFFSET)[0] | |
| if ROM_SECURE_AREA_END_OFFSET - arm9_offset > 0: | |
| secure_crc = calculate_crc16(rom_data[arm9_offset:ROM_SECURE_AREA_END_OFFSET]) | |
| existing_crc = struct.unpack_from('<H', rom_data, ROM_HEADER_SECURE_AREA_CRC_OFFSET)[0] | |
| if existing_crc != secure_crc: | |
| secure_table = transform_table(game_code, 3, 8, ntr_blowfish) | |
| secure_bf = Blowfish(secure_table) | |
| ntr_bf = Blowfish(ntr_table) | |
| # Encrypt secure area ID | |
| encrypted_id = secure_bf.encrypt_value(SECURE_AREA_ID) | |
| encrypted_id = ntr_bf.encrypt_value(encrypted_id) | |
| struct.pack_into('<Q', rom_data, ROM_SECURE_AREA_START_OFFSET, encrypted_id) | |
| # Encrypt secure area data | |
| secure_area = rom_data[ROM_SECURE_AREA_START_OFFSET + 8:ROM_ENCRYPTED_SECURE_AREA_END_OFFSET] | |
| secure_bf.encrypt(secure_area) | |
| # Update secure area CRC | |
| secure_crc = calculate_crc16(rom_data[arm9_offset:ROM_SECURE_AREA_END_OFFSET]) | |
| struct.pack_into('<H', rom_data, ROM_HEADER_SECURE_AREA_CRC_OFFSET, secure_crc) | |
| # TWL (DSi) area | |
| try: | |
| if _should_rom_have_twl_area(rom_data): | |
| twl_area_start = struct.unpack_from('<H', rom_data, ROM_HEADER_TWL_AREA_START_OFFSET)[0] * TWL_CHUNK_SIZE | |
| if twl_area_start == 0: | |
| # Add TWL area to ROM | |
| twl_area_start_value = (len(rom_data) + (TWL_CHUNK_SIZE - 1)) >> TWL_CHUNK_SHIFT | |
| twl_area_start = twl_area_start_value * TWL_CHUNK_SIZE | |
| struct.pack_into('<H', rom_data, ROM_HEADER_NTR_AREA_END_OFFSET, twl_area_start_value) | |
| struct.pack_into('<H', rom_data, ROM_HEADER_TWL_AREA_START_OFFSET, twl_area_start_value) | |
| rom_data.extend(b'\x00' * (twl_area_start + 0x8000 - len(rom_data))) | |
| twl_table = None | |
| if use_dsi_dev_blowfish: | |
| twl_table_bytes = None | |
| for d in search_dirs: | |
| twl_table_bytes = try_get_twl_dev_blowfish(d) | |
| if twl_table_bytes is not None: | |
| break | |
| if twl_table_bytes is None: | |
| print("Error: Could not load twl dev blowfish key.") | |
| sys.exit(1) | |
| twl_table = twl_table_bytes | |
| else: | |
| twl_blowfish_bytes = None | |
| for d in search_dirs: | |
| twl_blowfish_bytes = try_get_twl_blowfish(d) | |
| if twl_blowfish_bytes is not None: | |
| break | |
| if twl_blowfish_bytes is None: | |
| print("Error: Could not load twl blowfish key.") | |
| sys.exit(1) | |
| twl_table = transform_table(game_code, 1, 8, twl_blowfish_bytes) | |
| twl_offset_p = twl_area_start + ROM_TWL_BLOWFISH_P_TABLE_OFFSET | |
| twl_offset_s = twl_area_start + ROM_TWL_BLOWFISH_S_BOXES_OFFSET | |
| rom_data[twl_offset_p:twl_offset_p + Blowfish.KEY_TABLE_P_TABLE_LENGTH] = \ | |
| twl_table[:Blowfish.KEY_TABLE_P_TABLE_LENGTH] | |
| rom_data[twl_offset_s:twl_offset_s + Blowfish.KEY_TABLE_S_BOXES_LENGTH] = \ | |
| twl_table[Blowfish.KEY_TABLE_P_TABLE_LENGTH:Blowfish.KEY_TABLE_P_TABLE_LENGTH + Blowfish.KEY_TABLE_S_BOXES_LENGTH] | |
| except Exception: | |
| print("Warning: Couldn't insert twl blowfish.") | |
| # Fix header CRC | |
| header_crc = calculate_crc16(rom_data[:ROM_HEADER_CRC_OFFSET]) | |
| struct.pack_into('<H', rom_data, ROM_HEADER_CRC_OFFSET, header_crc) | |
| # Write output | |
| Path(output_path).write_bytes(rom_data) | |
| print(f"Done! Encrypted ROM written to: {output_path}") | |
| # --------------------------------------------------------------------------- | |
| # CLI | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description=HEADING_TEXT, | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| parser.add_argument('input_path', help='The path of the input nds file.') | |
| parser.add_argument('output_path', help='The path of the output nds file.') | |
| parser.add_argument('--dsidev', action='store_true', default=False, | |
| help='Use DSi dev blowfish.') | |
| args = parser.parse_args() | |
| if not os.path.isfile(args.input_path): | |
| print(f"Error: Input file not found: {args.input_path}") | |
| sys.exit(1) | |
| encrypt_rom(args.input_path, args.output_path, args.dsidev) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment