nitro-patcher 0.3.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,201 @@
1
+ declare const UnitCode: Readonly<{
2
+ NDS: 0;
3
+ NDS_DSi: 2;
4
+ DSi: 3;
5
+ }>;
6
+ declare class Header {
7
+ raw: Buffer;
8
+ trimmedRom: boolean;
9
+ doublePadding: boolean;
10
+ nitrocode: boolean;
11
+ decrypted: boolean;
12
+ headerCRC: boolean;
13
+ logoCRC: boolean;
14
+ secureCRC: boolean;
15
+ dlp_signature: Buffer;
16
+ ARM9romOffset: number;
17
+ ARM9entryAddress: number;
18
+ ARM9ramAddress: number;
19
+ ARM9size: number;
20
+ ARM7romOffset: number;
21
+ ARM7entryAddress: number;
22
+ ARM7ramAddress: number;
23
+ ARM7size: number;
24
+ FNToffset: number;
25
+ FNTsize: number;
26
+ FAToffset: number;
27
+ FATsize: number;
28
+ ARM9overlayOffset: number;
29
+ ARM9overlaySize: number;
30
+ ARM7overlayOffset: number;
31
+ ARM7overlaySize: number;
32
+ flagsRead: number;
33
+ flagsInit: number;
34
+ bannerOffset: number;
35
+ dsi9_rom_offset: number;
36
+ offset_0x1C4: number;
37
+ dsi9_ram_address: number;
38
+ dsi9_size: number;
39
+ dsi7_rom_offset: number;
40
+ offset_0x1D4: number;
41
+ dsi7_ram_address: number;
42
+ dsi7_size: number;
43
+ digest_ntr_start: number;
44
+ digest_ntr_size: number;
45
+ digest_twl_start: number;
46
+ digest_twl_size: number;
47
+ sector_hashtable_start: number;
48
+ sector_hashtable_size: number;
49
+ block_hashtable_start: number;
50
+ block_hashtable_size: number;
51
+ digest_sector_size: number;
52
+ digest_block_sectorcount: number;
53
+ banner_size: number;
54
+ offset_0x20C: number;
55
+ total_rom_size: number;
56
+ offset_0x214: number;
57
+ offset_0x218: number;
58
+ offset_0x21C: number;
59
+ modcrypt1_start: number;
60
+ modcrypt1_size: number;
61
+ modcrypt2_start: number;
62
+ modcrypt2_size: number;
63
+ tid_low: number;
64
+ tid_high: number;
65
+ public_sav_size: number;
66
+ private_sav_size: number;
67
+ unitCode: number;
68
+ encryptionSeed: number;
69
+ twlInternalFlags: number;
70
+ permitsFlags: number;
71
+ ROMversion: number;
72
+ internalFlags: number;
73
+ secureCRC16: number;
74
+ ROMtimeout: number;
75
+ ARM9autoload: number;
76
+ ARM7autoload: number;
77
+ ROMsize: number;
78
+ ntr_region_end: number;
79
+ twl_region_start: number;
80
+ headerSize: number;
81
+ logoCRC16: number;
82
+ headerCRC16: number;
83
+ debug_romOffset: number;
84
+ debug_size: number;
85
+ debug_ramAddress: number;
86
+ reserved3: number;
87
+ mbk9_wramcnt_setting: number;
88
+ region_flags: number;
89
+ access_control: number;
90
+ scfg_ext_mask: number;
91
+ gameTitle: string;
92
+ gameCode: string;
93
+ makerCode: string;
94
+ reserved: Buffer;
95
+ reserved2: Buffer;
96
+ logo: Buffer;
97
+ appflags: Buffer;
98
+ hmac_arm9: Buffer;
99
+ hmac_arm7: Buffer;
100
+ hmac_digest_master: Buffer;
101
+ hmac_icon_title: Buffer;
102
+ hmac_arm9i: Buffer;
103
+ hmac_arm7i: Buffer;
104
+ hmac_arm9_no_secure: Buffer;
105
+ rsa_signature: Buffer;
106
+ secureDisable: bigint;
107
+ constructor(input: Uint8Array);
108
+ get size(): number;
109
+ set size(value: number);
110
+ toBuffer(): Buffer<ArrayBuffer>;
111
+ toString(): string;
112
+ }
113
+
114
+ interface Arm9Header {
115
+ nitrocode?: boolean;
116
+ reserved2: Buffer;
117
+ ARM9ramAddress: number;
118
+ ARM9size: number;
119
+ }
120
+ declare const decompressBLZ: (input: Buffer, maxSize?: number) => Buffer;
121
+ declare const compressBLZ: (input: Buffer) => Buffer;
122
+ declare const decompressArm9: (data: Buffer, header: Arm9Header, maxSize?: number) => {
123
+ compressed: boolean;
124
+ data: Buffer;
125
+ };
126
+ declare const compressArm9: (data: Buffer, header: Arm9Header, postSize?: number) => Buffer;
127
+
128
+ interface DsiCheck {
129
+ status: "valid" | "invalid" | "not-applicable";
130
+ reason?: string;
131
+ }
132
+ interface DsiVerification {
133
+ arm9: DsiCheck;
134
+ arm7: DsiCheck;
135
+ arm9i: DsiCheck;
136
+ arm7i: DsiCheck;
137
+ arm9NoSecure: DsiCheck;
138
+ banner: DsiCheck;
139
+ sectors: DsiCheck;
140
+ blocks: DsiCheck;
141
+ master: DsiCheck;
142
+ signature: DsiCheck;
143
+ }
144
+ type ProgramName = "arm9" | "arm7" | "arm9i" | "arm7i";
145
+ interface Program {
146
+ name: ProgramName;
147
+ start: number;
148
+ length: number;
149
+ data: Buffer;
150
+ original: Buffer;
151
+ replaced: boolean;
152
+ }
153
+ interface CryptRegion {
154
+ id: 1 | 2;
155
+ program: ProgramName;
156
+ offset: number;
157
+ length: number;
158
+ kind: "whole" | "secure" | "fixed";
159
+ }
160
+ interface DsiSection {
161
+ label: string;
162
+ start: number;
163
+ originalSize: number;
164
+ data: Buffer;
165
+ place: (start: number) => void;
166
+ }
167
+ /** Complete cartridge processing; legacy TWL serialization stays independent. */
168
+ declare class CompleteTWL {
169
+ readonly header: Header;
170
+ readonly programs: Map<ProgramName, Program>;
171
+ readonly regions: CryptRegion[];
172
+ readonly hasDigests: boolean;
173
+ readonly Hashtable1Data: Buffer;
174
+ readonly Hashtable2Data: Buffer;
175
+ readonly Header2Data: Buffer[];
176
+ readonly Overlays9Sha1Hmac: Buffer[];
177
+ readonly twlEncrypted: boolean;
178
+ readonly firstDSiHashOffset: number;
179
+ readonly dsiHashSize: number;
180
+ private readonly original;
181
+ private readonly limit;
182
+ constructor(header: Header, overlays: readonly Buffer[], original: Buffer, limit: number);
183
+ private validateDigests;
184
+ private decryptPrograms;
185
+ get DSi9Data(): Buffer;
186
+ set DSi9Data(data: Buffer);
187
+ get DSi7Data(): Buffer;
188
+ set DSi7Data(data: Buffer);
189
+ getProgram(name: ProgramName): Buffer;
190
+ setProgram(name: ProgramName, data: Buffer): void;
191
+ importArm9iData(data: Buffer | string, offset?: number, size?: number): void;
192
+ importArm7iData(data: Buffer | string, offset?: number, size?: number): void;
193
+ unchanged(): boolean;
194
+ updateOverlays9Sha1Hmac(arm9: Buffer, h: Arm9Header, overlays: readonly Buffer[]): Buffer;
195
+ private view;
196
+ verify(data?: Buffer<ArrayBufferLike>, h?: Header): DsiVerification;
197
+ serialize(h: Header, sections: DsiSection[], forceLayout: boolean): Buffer;
198
+ private checkMovableTwl;
199
+ }
200
+
201
+ export { type Arm9Header as A, CompleteTWL as C, type DsiCheck as D, Header as H, UnitCode as U, type DsiVerification as a, compressBLZ as b, compressArm9 as c, decompressArm9 as d, decompressBLZ as e };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-patcher",
3
- "version": "0.3.0",
3
+ "version": "1.1.0",
4
4
  "description": "Native Node.js Nintendo DS ROM reader, repacker and ZIP/Xdelta patcher",
5
5
  "type": "module",
6
6
  "engines": {
@@ -49,6 +49,7 @@
49
49
  "@noble/ciphers": "2.4.0",
50
50
  "@noble/hashes": "2.4.0",
51
51
  "@types/node": "22.20.2",
52
+ "@types/node-forge": "1.3.14",
52
53
  "buffer": "6.0.3",
53
54
  "esbuild": "0.28.1",
54
55
  "eslint": "10.9.1",
@@ -56,10 +57,11 @@
56
57
  "eslint-plugin-import-x": "4.17.1",
57
58
  "eslint-plugin-prettier": "5.5.5",
58
59
  "eslint-plugin-unused-imports": "4.4.1",
59
- "fflate": "0.8.3",
60
60
  "globals": "16.5.0",
61
61
  "jiti": "2.7.0",
62
+ "playwright-core": "1.64.1",
62
63
  "prettier": "3.9.6",
64
+ "string_decoder": "1.3.0",
63
65
  "tsup": "8.5.1",
64
66
  "tsx": "4.23.13",
65
67
  "type-fest": "4.41.0",
@@ -73,7 +75,11 @@
73
75
  "#browser-aes": "@noble/ciphers/aes.js"
74
76
  },
75
77
  "dependencies": {
76
- "markdansi": "0.3.3"
78
+ "@limrun/xdelta3-wasm": "0.2.0",
79
+ "fflate": "0.8.3",
80
+ "iconv-lite": "0.7.3",
81
+ "markdansi": "0.3.3",
82
+ "node-forge": "1.4.0"
77
83
  },
78
84
  "scripts": {
79
85
  "clean": "tsx scripts/clean.ts",
@@ -81,6 +87,7 @@
81
87
  "typecheck": "tsc -p tsconfig.test.json",
82
88
  "test": "tsx --test test/*.test.ts",
83
89
  "test:reference": "tsx scripts/verify-reference.ts",
90
+ "test:dsi": "tsx scripts/verify-dsi.ts",
84
91
  "check": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm test && pnpm run build && pnpm run test:browser",
85
92
  "test:package": "tsx scripts/verify-package.ts",
86
93
  "lint": "eslint . --max-warnings 0",
@@ -1 +1,2 @@
1
1
  export { Buffer } from "buffer";
2
+ export const window = globalThis;
package/src/browser.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NDSFile } from "./nitro/nds-file";
2
2
  import {
3
+ createPatch as create,
3
4
  extractZipEntry as extractEntry,
4
5
  patchBuffer as patch,
5
6
  readPatchInfo as readInfo,
@@ -8,6 +9,7 @@ import {
8
9
  } from "./nitro-patch-helper";
9
10
 
10
11
  export interface PatchMetadata {
12
+ formatVersion?: number;
11
13
  id?: string;
12
14
  author?: string;
13
15
  name?: string;
@@ -30,11 +32,43 @@ export interface BrowserPatchOptions {
30
32
  maxUncompressedSize?: number;
31
33
  maxEntries?: number;
32
34
  }
35
+ export interface CreatePatchOptions extends BrowserPatchOptions {
36
+ metadata?: Omit<PatchMetadata, "formatVersion">;
37
+ readme?: string;
38
+ }
39
+ export interface CreatePatchResult {
40
+ buffer: Uint8Array<ArrayBuffer>;
41
+ changedFiles: number;
42
+ warnings: string[];
43
+ }
44
+ /** Create an in-memory XZP with diagnostic warnings. Call in a Web Worker. */
45
+ export const createPatch = (
46
+ original: Uint8Array,
47
+ modified: Uint8Array,
48
+ options: CreatePatchOptions = {},
49
+ ): Promise<CreatePatchResult> => create(original, modified, options);
50
+ export interface DsiCheck {
51
+ status: "valid" | "invalid" | "not-applicable";
52
+ reason?: string;
53
+ }
54
+ export interface DsiVerification {
55
+ arm9: DsiCheck;
56
+ arm7: DsiCheck;
57
+ arm9i: DsiCheck;
58
+ arm7i: DsiCheck;
59
+ arm9NoSecure: DsiCheck;
60
+ banner: DsiCheck;
61
+ sectors: DsiCheck;
62
+ blocks: DsiCheck;
63
+ master: DsiCheck;
64
+ signature: DsiCheck;
65
+ }
33
66
  export interface BrowserPatchResult {
34
67
  buffer: Uint8Array<ArrayBuffer>;
35
68
  returnValue: "SUCCESS" | "MD5_MISMATCH";
36
69
  inputMd5: string;
37
70
  outputMd5: string;
71
+ dsi?: DsiVerification;
38
72
  }
39
73
  export interface RomInfo {
40
74
  gameTitle: string;
@@ -42,10 +76,11 @@ export interface RomInfo {
42
76
  size: number;
43
77
  fileCount: number;
44
78
  checksums: { header: boolean; logo: boolean; secureArea: boolean; banner: boolean };
79
+ dsi?: DsiVerification;
45
80
  }
46
81
  /** Parse ROM metadata entirely in memory. No filesystem or Node types required. */
47
- export const inspectRom = (bytes: Uint8Array): RomInfo => {
48
- const rom = new NDSFile(bytes);
82
+ export const inspectRom = (bytes: Uint8Array, options: { dsiMode?: "legacy" | "complete" } = {}): RomInfo => {
83
+ const rom = new NDSFile(bytes, options);
49
84
  return {
50
85
  gameTitle: rom.header.gameTitle,
51
86
  gameCode: rom.header.gameCode,
@@ -57,6 +92,7 @@ export const inspectRom = (bytes: Uint8Array): RomInfo => {
57
92
  secureArea: rom.header.secureCRC,
58
93
  banner: rom.banner.bannerCRC,
59
94
  },
95
+ ...(options.dsiMode === "complete" && { dsi: rom.verifyDsi() }),
60
96
  };
61
97
  };
62
98
  /** Synchronous CPU work; call in a Web Worker for responsive browser UIs. */
@@ -77,6 +113,7 @@ export const patchBuffer = (
77
113
  returnValue: result.returnValue,
78
114
  inputMd5: result.inputMd5,
79
115
  outputMd5: result.outputMd5,
116
+ ...(result.dsi && { dsi: result.dsi }),
80
117
  };
81
118
  };
82
119
  /** Read and validate metadata.json from a patch package entirely in memory. */
@@ -96,6 +133,7 @@ export const extractZipEntry = (
96
133
  ): Uint8Array<ArrayBuffer> => new Uint8Array(extractEntry(archive, entryName, options));
97
134
 
98
135
  export const PatchHelper = Object.freeze({
136
+ createPatch,
99
137
  extractZipEntry,
100
138
  patchBuffer,
101
139
  readPatchInfo,
package/src/cli.ts CHANGED
@@ -17,6 +17,7 @@ const help = `nitro-patcher — NDS ROM 补丁工具
17
17
 
18
18
  --json 结果输出为 JSON 格式
19
19
  --dry-run 完整执行补丁流程,但不写入输出 ROM
20
+ --dsi-mode extract/inspect 的 DSi 处理模式:legacy 或 complete
20
21
  -i, --input 输入 ROM 路径
21
22
  -p, --patch 补丁 ZIP 路径
22
23
  -o, --output 输出 ROM 或提取目录;试运行时可省略
@@ -29,6 +30,7 @@ interface CommandOptions {
29
30
  input: string | undefined;
30
31
  patch: string | undefined;
31
32
  output: string | undefined;
33
+ dsiMode: "legacy" | "complete" | undefined;
32
34
  }
33
35
 
34
36
  const sameFile = async (input: string, output: string): Promise<boolean> => {
@@ -79,7 +81,7 @@ const formatMetadata = (metadata: PatchMetadata): string => {
79
81
  const extractRom = async (options: CommandOptions): Promise<void> => {
80
82
  if (!options.input || !options.output || options.patch !== undefined || options.dryRun) throw new Error(help);
81
83
  const destination = join(await realpath(dirname(resolve(options.output))), basename(resolve(options.output)));
82
- const rom = new NDSFile(await readFile(options.input));
84
+ const rom = new NDSFile(await readFile(options.input), { dsiMode: options.dsiMode ?? "legacy" });
83
85
  const files = rom.listFiles();
84
86
  await mkdir(destination, { recursive: true });
85
87
  for (const { path } of files) {
@@ -106,7 +108,7 @@ const inspectRom = async (options: CommandOptions): Promise<void> => {
106
108
  if (!options.input || options.patch !== undefined || options.output !== undefined || options.dryRun)
107
109
  throw new Error(help);
108
110
  const input = await readFile(options.input);
109
- const rom = new NDSFile(input);
111
+ const rom = new NDSFile(input, { dsiMode: options.dsiMode ?? "legacy" });
110
112
  const info = {
111
113
  gameTitle: rom.header.gameTitle.replace(/\0+$/, ""),
112
114
  gameCode: rom.header.gameCode,
@@ -118,6 +120,7 @@ const inspectRom = async (options: CommandOptions): Promise<void> => {
118
120
  secureArea: rom.header.secureCRC,
119
121
  banner: rom.banner.bannerCRC,
120
122
  },
123
+ ...(options.dsiMode === "complete" && { dsi: rom.verifyDsi() }),
121
124
  };
122
125
  if (options.json) {
123
126
  console.log(JSON.stringify(info));
@@ -134,6 +137,9 @@ const inspectRom = async (options: CommandOptions): Promise<void> => {
134
137
  - Logo:${info.checksums.logo ? "通过" : "失败"}
135
138
  - Secure Area:${info.checksums.secureArea ? "通过" : "失败"}
136
139
  - Banner:${info.checksums.banner ? "通过" : "失败"}`);
140
+ if (info.dsi)
141
+ for (const [name, result] of Object.entries(info.dsi))
142
+ console.log(`- DSi ${name}:${result.status}${result.reason ? ` (${result.reason})` : ""}`);
137
143
  };
138
144
 
139
145
  const main = async (): Promise<void> => {
@@ -144,6 +150,7 @@ const main = async (): Promise<void> => {
144
150
  version: { type: "boolean", short: "v" },
145
151
  json: { type: "boolean" },
146
152
  "dry-run": { type: "boolean" },
153
+ "dsi-mode": { type: "string" },
147
154
  input: { type: "string", short: "i" },
148
155
  patch: { type: "string", short: "p" },
149
156
  output: { type: "string", short: "o" },
@@ -160,12 +167,16 @@ const main = async (): Promise<void> => {
160
167
  console.log(metadata.version);
161
168
  return;
162
169
  }
170
+ const dsiMode = values["dsi-mode"];
171
+ if (dsiMode !== undefined && dsiMode !== "legacy" && dsiMode !== "complete")
172
+ throw new Error("Invalid --dsi-mode; expected legacy or complete");
163
173
  const options: CommandOptions = {
164
174
  json: values.json ?? false,
165
175
  dryRun: values["dry-run"] ?? false,
166
176
  input: values.input,
167
177
  patch: values.patch,
168
178
  output: values.output,
179
+ dsiMode,
169
180
  };
170
181
  const command = positionals.length === 1 ? positionals[0] : undefined;
171
182
  if (positionals.length !== 0 && positionals.length !== 1 && positionals.length !== 3) throw new Error(help);
@@ -181,6 +192,7 @@ const main = async (): Promise<void> => {
181
192
  }
182
193
 
183
194
  const dryRun = options.dryRun;
195
+ if (options.dsiMode !== undefined) throw new Error("patch selects DSi processing from metadata.json formatVersion");
184
196
  const usesLegacyPaths = positionals.length === 3;
185
197
  if (usesLegacyPaths && (values.input !== undefined || values.patch !== undefined || values.output !== undefined))
186
198
  throw new Error(help);
@@ -216,6 +228,7 @@ const main = async (): Promise<void> => {
216
228
  outputMd5,
217
229
  ...info,
218
230
  ...(dryRun && { dryRun: true }),
231
+ ...(result.dsi && { dsi: result.dsi }),
219
232
  }),
220
233
  );
221
234
  return;
package/src/nitro/blz.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export interface Arm9Header {
2
+ nitrocode?: boolean;
2
3
  reserved2: Buffer;
3
4
  ARM9ramAddress: number;
4
5
  ARM9size: number;
@@ -130,22 +131,30 @@ export const compressBLZ = (input: Buffer): Buffer => {
130
131
  return out;
131
132
  };
132
133
 
133
- export const decompressArm9 = (data: Buffer, header: Arm9Header): { compressed: boolean; data: Buffer } => {
134
+ export const decompressArm9 = (
135
+ data: Buffer,
136
+ header: Arm9Header,
137
+ maxSize = 0x10000000,
138
+ ): { compressed: boolean; data: Buffer } => {
134
139
  if (data.length < 0x18) throw new Error("ARM9 data too short");
135
- const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
140
+ if (data.length > maxSize) throw new RangeError("ARM9 exceeds output size limit");
141
+ const nitro = (header.nitrocode ?? data.readUInt32LE(0xc) === 0xdec00621) ? 12 : 0;
136
142
  const init = header.reserved2.readUInt32LE(0) & 0x3fff;
137
143
  const ptr = init ? data.readUInt32LE(init + 0x14) : header.ARM9ramAddress + header.ARM9size;
138
144
  const compressed = ptr > header.ARM9ramAddress && ptr + nitro >= header.ARM9ramAddress + data.length;
139
145
  return {
140
146
  compressed,
141
147
  data: compressed
142
- ? Buffer.concat([decompressBLZ(data.subarray(0, data.length - nitro)), data.subarray(data.length - nitro)])
148
+ ? Buffer.concat([
149
+ decompressBLZ(data.subarray(0, data.length - nitro), maxSize - nitro),
150
+ data.subarray(data.length - nitro),
151
+ ])
143
152
  : Buffer.from(data),
144
153
  };
145
154
  };
146
155
  export const compressArm9 = (data: Buffer, header: Arm9Header, postSize = 0): Buffer => {
147
156
  if (data.length < 0x4000) throw new Error("ARM9 secure area is truncated");
148
- const nitro = data.readUInt32LE(0xc) === 0xdec00621 ? 12 : 0;
157
+ const nitro = (header.nitrocode ?? data.readUInt32LE(0xc) === 0xdec00621) ? 12 : 0;
149
158
  const packed = compressBLZ(data.subarray(0x4000, data.length - nitro));
150
159
  if (!packed.length) throw new Error("ARM9 BLZ compression produced no smaller representation");
151
160
  const out = Buffer.concat([data.subarray(0, 0x4000), packed, data.subarray(data.length - nitro)]);
@@ -1,5 +1,7 @@
1
1
  import { createAesBlockCipher } from "#nitro-runtime";
2
2
 
3
+ import { crc16 } from "./crc";
4
+
3
5
  export interface ModcryptHeader {
4
6
  gameCode: string;
5
7
  gameTitle: string;
@@ -117,32 +119,37 @@ export const modcryptKey = (header: ModcryptHeader): Buffer => {
117
119
  return out;
118
120
  };
119
121
 
120
- export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
122
+ export const encryptSecureArea = (gameCode: string, input: Buffer, refreshCrc = false): Buffer =>
123
+ secureAreaCrypt(gameCode, input, false, refreshCrc);
124
+ export const decryptSecureArea = (gameCode: string, input: Buffer): Buffer => secureAreaCrypt(gameCode, input, true);
125
+
126
+ const secureAreaCrypt = (gameCode: string, input: Buffer, decrypting: boolean, refreshCrc = false): Buffer => {
121
127
  if (typeof gameCode !== "string" || gameCode.length !== 4) throw new Error("Expected four-character game code");
122
128
  if (input.length < 0x800) throw new RangeError("Secure area must contain at least 0x800 bytes");
123
129
  const output = Buffer.from(input);
130
+ if (refreshCrc) output.writeUInt16LE(crc16(output.subarray(0x10, 0x800)), 0xe);
124
131
  let magic = new Uint32Array(0x412);
125
132
  let args: [number, number, number] = [0, 0, 0];
126
- function encrypt(a: number, b: number): [number, number] {
133
+ function crypt(a: number, b: number, reverse = false): [number, number] {
127
134
  for (let i = 0; i < 16; i++) {
128
- const c = (magic[i] ^ a) >>> 0;
135
+ const c = (magic[reverse ? 17 - i : i] ^ a) >>> 0;
129
136
  const sum = (magic[18 + (c >>> 24)] + magic[274 + ((c >>> 16) & 255)]) >>> 0;
130
137
  const f = (magic[786 + (c & 255)] + (magic[530 + ((c >>> 8) & 255)] ^ sum)) >>> 0;
131
138
  a = (b ^ f) >>> 0;
132
139
  b = c;
133
140
  }
134
- return [(b ^ magic[17]) >>> 0, (a ^ magic[16]) >>> 0];
141
+ return [(b ^ magic[reverse ? 0 : 17]) >>> 0, (a ^ magic[reverse ? 1 : 16]) >>> 0];
135
142
  }
136
143
  function init2() {
137
- [args[2], args[1]] = encrypt(args[2], args[1]);
138
- [args[1], args[0]] = encrypt(args[1], args[0]);
144
+ [args[2], args[1]] = crypt(args[2], args[1]);
145
+ [args[1], args[0]] = crypt(args[1], args[0]);
139
146
  const key = Buffer.alloc(12);
140
147
  args.forEach((v, i) => key.writeUInt32LE(v, i * 4));
141
148
  for (let j = 0; j < 18; j++) magic[j] ^= key.readUInt32BE((j * 4) & 7);
142
149
  let a = 0,
143
150
  b = 0;
144
151
  for (let i = 0; i < 0x412; i += 2) {
145
- [a, b] = encrypt(a, b);
152
+ [a, b] = crypt(a, b);
146
153
  magic[i] = a;
147
154
  magic[i + 1] = b;
148
155
  }
@@ -155,17 +162,28 @@ export const encryptSecureArea = (gameCode: string, input: Buffer): Buffer => {
155
162
  init2();
156
163
  }
157
164
  init1();
165
+ if (decrypting) {
166
+ const [b, a] = crypt(output.readUInt32LE(4), output.readUInt32LE(0), true);
167
+ output.writeUInt32LE(a, 0);
168
+ output.writeUInt32LE(b, 4);
169
+ }
158
170
  args[1] = (args[1] << 1) >>> 0;
159
171
  args[2] >>>= 1;
160
172
  init2();
161
- for (let p = 8; p < 0x800; p += 8) {
162
- const [b, a] = encrypt(output.readUInt32LE(p + 4), output.readUInt32LE(p));
173
+ for (let p = decrypting ? 0 : 8; p < 0x800; p += 8) {
174
+ const [b, a] = crypt(output.readUInt32LE(p + 4), output.readUInt32LE(p), decrypting);
163
175
  output.writeUInt32LE(a, p);
164
176
  output.writeUInt32LE(b, p + 4);
165
177
  }
166
- let [b, a] = encrypt(0x6a624f79, 0x72636e65);
178
+ if (decrypting) {
179
+ if (output.readUInt32LE(0) !== 0x72636e65 || output.readUInt32LE(4) !== 0x6a624f79)
180
+ throw new Error("Invalid KEY1 secure-area marker");
181
+ output.writeBigUInt64LE(0xe7ffdeffe7ffdeffn);
182
+ return output;
183
+ }
184
+ let [b, a] = crypt(0x6a624f79, 0x72636e65);
167
185
  init1();
168
- [b, a] = encrypt(b, a);
186
+ [b, a] = crypt(b, a);
169
187
  output.writeUInt32LE(a, 0);
170
188
  output.writeUInt32LE(b, 4);
171
189
  return output;