nitro-patcher 0.2.0 → 0.3.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.
- package/dist/browser.d.ts +5 -2
- package/dist/browser.js +1367 -1418
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-GFF67HX4.js → chunk-GLK3K2HY.js} +221 -282
- package/dist/chunk-GLK3K2HY.js.map +1 -0
- package/dist/{chunk-ZY3OSZCA.js → chunk-IGURDH5L.js} +180 -186
- package/dist/chunk-IGURDH5L.js.map +1 -0
- package/dist/cli.js +99 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/nitro-helper.d.ts +78 -78
- package/dist/nitro-helper.js +1 -1
- package/dist/nitro-patch-helper.d.ts +5 -2
- package/dist/nitro-patch-helper.js +4 -2
- package/package.json +6 -2
- package/src/browser/buffer.ts +1 -1
- package/src/browser.ts +16 -12
- package/src/cli.ts +137 -60
- package/src/index.ts +2 -2
- package/src/nitro/banner.ts +28 -31
- package/src/nitro/binary.ts +6 -16
- package/src/nitro/blz.ts +13 -21
- package/src/nitro/crypto.ts +78 -84
- package/src/nitro/filesystem.ts +36 -39
- package/src/nitro/header.ts +58 -71
- package/src/nitro/nds-file.ts +44 -63
- package/src/nitro/twl.ts +59 -100
- package/src/nitro-helper.ts +14 -14
- package/src/nitro-patch-helper.ts +49 -57
- package/src/patch/xdelta.ts +43 -49
- package/src/patch/zip.ts +47 -56
- package/src/runtime/browser.ts +9 -11
- package/src/runtime/node.ts +14 -15
- package/dist/chunk-GFF67HX4.js.map +0 -1
- package/dist/chunk-ZY3OSZCA.js.map +0 -1
package/src/nitro/nds-file.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// Native TypeScript port of NitroHelper/NDSFile.cs, GPL-3.0.
|
|
2
|
-
import { readFile, writeFile } from
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
2
|
+
import { readFile, writeFile } from "#nitro-runtime";
|
|
3
|
+
|
|
4
|
+
import { Banner } from "./banner";
|
|
5
|
+
import { BinaryWriter, DEFAULT_MAX_SIZE, bytes, range } from "./binary";
|
|
6
|
+
import { crc16 } from "./crc";
|
|
7
|
+
import { encryptSecureArea } from "./crypto";
|
|
8
|
+
import { FileAllocationTable, FileNameTable, OverlayTable } from "./filesystem";
|
|
9
|
+
import type { NitroFile, NitroFolder } from "./filesystem";
|
|
10
|
+
import { Header } from "./header";
|
|
11
|
+
import { TWL } from "./twl";
|
|
11
12
|
|
|
12
13
|
export interface RomOptions {
|
|
13
14
|
/** Maximum generated ROM size in bytes. Defaults to 1 GiB. */
|
|
@@ -22,7 +23,7 @@ export interface RomFileInfo {
|
|
|
22
23
|
size: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const key = (path: string): string => path.replaceAll(
|
|
26
|
+
const key = (path: string): string => path.replaceAll("\\", "/").toLowerCase();
|
|
26
27
|
|
|
27
28
|
/** Parsed Nintendo DS ROM. Replacements do not change the input buffer. */
|
|
28
29
|
export class NDSFile {
|
|
@@ -40,31 +41,21 @@ export class NDSFile {
|
|
|
40
41
|
private readonly maxOutputSize: number;
|
|
41
42
|
|
|
42
43
|
constructor(input: Uint8Array, options: RomOptions = {}) {
|
|
43
|
-
this.original = bytes(input,
|
|
44
|
+
this.original = bytes(input, "ROM");
|
|
44
45
|
this.maxOutputSize = options.maxOutputSize ?? DEFAULT_MAX_SIZE;
|
|
45
46
|
this.header = new Header(this.original);
|
|
46
47
|
const h = this.header;
|
|
47
|
-
range(this.original, h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0),
|
|
48
|
-
range(this.original, h.ARM7romOffset, h.ARM7size,
|
|
48
|
+
range(this.original, h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0), "ARM9");
|
|
49
|
+
range(this.original, h.ARM7romOffset, h.ARM7size, "ARM7");
|
|
49
50
|
this.banner = new Banner(this.original, h.bannerOffset, h.banner_size);
|
|
50
51
|
this.fatTable = new FileAllocationTable(this.original, h.FAToffset, h.FATsize);
|
|
51
52
|
this.fntTable = new FileNameTable(this.fatTable, this.original, h.FNToffset, h.FNTsize);
|
|
52
53
|
this.root = this.fntTable.root;
|
|
53
|
-
this.overlay9Table = new OverlayTable(
|
|
54
|
-
|
|
55
|
-
h.ARM9overlayOffset,
|
|
56
|
-
h.ARM9overlaySize,
|
|
57
|
-
true,
|
|
58
|
-
);
|
|
59
|
-
this.overlay7Table = new OverlayTable(
|
|
60
|
-
this.original,
|
|
61
|
-
h.ARM7overlayOffset,
|
|
62
|
-
h.ARM7overlaySize,
|
|
63
|
-
false,
|
|
64
|
-
);
|
|
54
|
+
this.overlay9Table = new OverlayTable(this.original, h.ARM9overlayOffset, h.ARM9overlaySize, true);
|
|
55
|
+
this.overlay7Table = new OverlayTable(this.original, h.ARM7overlayOffset, h.ARM7overlaySize, false);
|
|
65
56
|
const arm9Overlays = this.overlay9Table.readBasicOverlays(this.fatTable);
|
|
66
57
|
this.root.folders.push({
|
|
67
|
-
name:
|
|
58
|
+
name: "overlay",
|
|
68
59
|
id: 0xffff,
|
|
69
60
|
files: [...arm9Overlays, ...this.overlay7Table.readBasicOverlays(this.fatTable)],
|
|
70
61
|
folders: [],
|
|
@@ -73,23 +64,23 @@ export class NDSFile {
|
|
|
73
64
|
range(this.original, offset, size, name);
|
|
74
65
|
this.root.files.push({ name, id: 0xffff, offset, size });
|
|
75
66
|
};
|
|
76
|
-
system(
|
|
77
|
-
system(
|
|
78
|
-
system(
|
|
79
|
-
system(
|
|
80
|
-
system(
|
|
81
|
-
system(
|
|
82
|
-
if (h.ARM9overlaySize) system(
|
|
83
|
-
if (h.ARM7overlaySize) system(
|
|
67
|
+
system("header.bin", 0, h.headerSize);
|
|
68
|
+
system("banner.bin", h.bannerOffset, this.banner.raw.length);
|
|
69
|
+
system("fnt.bin", h.FNToffset, h.FNTsize);
|
|
70
|
+
system("fat.bin", h.FAToffset, h.FATsize);
|
|
71
|
+
system("arm9.bin", h.ARM9romOffset, h.ARM9size + (h.nitrocode ? 12 : 0));
|
|
72
|
+
system("arm7.bin", h.ARM7romOffset, h.ARM7size);
|
|
73
|
+
if (h.ARM9overlaySize) system("overarm9.bin", h.ARM9overlayOffset, h.ARM9overlaySize);
|
|
74
|
+
if (h.ARM7overlaySize) system("overarm7.bin", h.ARM7overlayOffset, h.ARM7overlaySize);
|
|
84
75
|
const visit = (folder: NitroFolder, prefix: string): void => {
|
|
85
76
|
for (const file of folder.files) {
|
|
86
77
|
const path = prefix + file.name;
|
|
87
78
|
if (this.paths.has(key(path))) throw new Error(`Duplicate ROM path: ${path}`);
|
|
88
79
|
this.paths.set(key(path), { path, file });
|
|
89
80
|
}
|
|
90
|
-
for (const child of folder.folders) visit(child, prefix + child.name
|
|
81
|
+
for (const child of folder.folders) visit(child, `${prefix + child.name}/`);
|
|
91
82
|
};
|
|
92
|
-
visit(this.root,
|
|
83
|
+
visit(this.root, "");
|
|
93
84
|
if (h.unitCode & 2 && h.twlInternalFlags & 1 && h.tid_high && h.tid_high !== 0xffffffff) {
|
|
94
85
|
// Upstream silently discarded malformed TWL regions. Preserve valid DSi data,
|
|
95
86
|
// and reject malformed data instead of emitting a ROM with missing sections.
|
|
@@ -135,20 +126,13 @@ export class NDSFile {
|
|
|
135
126
|
|
|
136
127
|
/** Rebuild offsets, allocation table, CRCs and ROM padding. Safe to call repeatedly. */
|
|
137
128
|
toBuffer(): Buffer {
|
|
138
|
-
const fntReplacement = this.root.files.find((file) => file.name ===
|
|
129
|
+
const fntReplacement = this.root.files.find((file) => file.name === "fnt.bin")?.replacement;
|
|
139
130
|
if (fntReplacement) {
|
|
140
|
-
const replacementTree = new FileNameTable(
|
|
141
|
-
this.fatTable,
|
|
142
|
-
fntReplacement,
|
|
143
|
-
0,
|
|
144
|
-
fntReplacement.length,
|
|
145
|
-
);
|
|
131
|
+
const replacementTree = new FileNameTable(this.fatTable, fntReplacement, 0, fntReplacement.length);
|
|
146
132
|
const collect = (folder: NitroFolder): number[] =>
|
|
147
|
-
[...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort(
|
|
148
|
-
|
|
149
|
-
);
|
|
150
|
-
if (collect(replacementTree.root).join(',') !== collect(this.data).join(','))
|
|
151
|
-
throw new Error('Replacement FNT must preserve existing file IDs');
|
|
133
|
+
[...folder.files.map((file) => file.id), ...folder.folders.flatMap(collect)].sort((a, b) => a - b);
|
|
134
|
+
if (collect(replacementTree.root).join(",") !== collect(this.data).join(","))
|
|
135
|
+
throw new Error("Replacement FNT must preserve existing file IDs");
|
|
152
136
|
}
|
|
153
137
|
const h = new Header(this.header.toBuffer());
|
|
154
138
|
h.nitrocode = this.header.nitrocode;
|
|
@@ -161,7 +145,7 @@ export class NDSFile {
|
|
|
161
145
|
if (!file) throw new Error(`Missing ROM system file: ${name}`);
|
|
162
146
|
return file;
|
|
163
147
|
};
|
|
164
|
-
const replacedHeader = system(
|
|
148
|
+
const replacedHeader = system("header.bin").replacement;
|
|
165
149
|
if (replacedHeader) {
|
|
166
150
|
const replacement = new Header(replacedHeader);
|
|
167
151
|
h.gameTitle = replacement.gameTitle;
|
|
@@ -177,13 +161,13 @@ export class NDSFile {
|
|
|
177
161
|
const overlayIds = new Set<number>();
|
|
178
162
|
const writeOverlays = (arm9: boolean): void => {
|
|
179
163
|
const originalTable = arm9 ? this.overlay9Table : this.overlay7Table;
|
|
180
|
-
const name = arm9 ?
|
|
164
|
+
const name = arm9 ? "overarm9.bin" : "overarm7.bin";
|
|
181
165
|
const entry = this.root.files.find((file) => file.name === name);
|
|
182
166
|
if (!entry) return;
|
|
183
167
|
const raw = entry.replacement ?? originalTable.toBuffer();
|
|
184
168
|
const table = new OverlayTable(raw, 0, raw.length, arm9);
|
|
185
169
|
const files = this.overlay.files
|
|
186
|
-
.filter((file) => file.name.startsWith(arm9 ?
|
|
170
|
+
.filter((file) => file.name.startsWith(arm9 ? "overlay_" : "overlay7_"))
|
|
187
171
|
.sort((a, b) => a.id - b.id);
|
|
188
172
|
for (const file of files) {
|
|
189
173
|
const item = table.entries.find((item) => item.fileId === file.id);
|
|
@@ -193,8 +177,7 @@ export class NDSFile {
|
|
|
193
177
|
item.reserved = ((item.reserved & 0xff000000) | (size & 0xffffff)) >>> 0;
|
|
194
178
|
overlayIds.add(file.id);
|
|
195
179
|
}
|
|
196
|
-
if (table.entries.length !== files.length)
|
|
197
|
-
throw new Error('Changing overlay file count is unsupported');
|
|
180
|
+
if (table.entries.length !== files.length) throw new Error("Changing overlay file count is unsupported");
|
|
198
181
|
const encoded = table.toBuffer();
|
|
199
182
|
if (arm9) {
|
|
200
183
|
h.ARM9overlayOffset = writer.position;
|
|
@@ -209,30 +192,29 @@ export class NDSFile {
|
|
|
209
192
|
};
|
|
210
193
|
writer.position = h.headerSize;
|
|
211
194
|
h.ARM9romOffset = writer.position;
|
|
212
|
-
const arm9 = system(
|
|
195
|
+
const arm9 = system("arm9.bin");
|
|
213
196
|
const arm9Bytes = this.fileBytes(arm9);
|
|
214
197
|
if (arm9.replacement) {
|
|
215
|
-
h.nitrocode =
|
|
216
|
-
arm9Bytes.length >= 12 && arm9Bytes.readUInt32LE(arm9Bytes.length - 12) === 0xdec00621;
|
|
198
|
+
h.nitrocode = arm9Bytes.length >= 12 && arm9Bytes.readUInt32LE(arm9Bytes.length - 12) === 0xdec00621;
|
|
217
199
|
}
|
|
218
200
|
h.ARM9size = arm9Bytes.length - (h.nitrocode ? 12 : 0);
|
|
219
201
|
write(arm9);
|
|
220
202
|
h.ARM9overlayOffset = 0;
|
|
221
203
|
writeOverlays(true);
|
|
222
204
|
h.ARM7romOffset = writer.position;
|
|
223
|
-
h.ARM7size = this.fileBytes(system(
|
|
224
|
-
write(system(
|
|
205
|
+
h.ARM7size = this.fileBytes(system("arm7.bin")).length;
|
|
206
|
+
write(system("arm7.bin"));
|
|
225
207
|
h.ARM7overlayOffset = 0;
|
|
226
208
|
writeOverlays(false);
|
|
227
209
|
h.FNToffset = writer.position;
|
|
228
|
-
h.FNTsize = this.fileBytes(system(
|
|
229
|
-
write(system(
|
|
210
|
+
h.FNTsize = this.fileBytes(system("fnt.bin")).length;
|
|
211
|
+
write(system("fnt.bin"));
|
|
230
212
|
h.FAToffset = writer.position;
|
|
231
213
|
h.FATsize = this.fatTable.entries.length * 8;
|
|
232
214
|
writer.write(Buffer.alloc(h.FATsize));
|
|
233
215
|
writer.pad(0x200);
|
|
234
216
|
h.bannerOffset = writer.position;
|
|
235
|
-
const bannerFile = system(
|
|
217
|
+
const bannerFile = system("banner.bin");
|
|
236
218
|
const banner = bannerFile.replacement
|
|
237
219
|
? new Banner(bannerFile.replacement, 0, bannerFile.replacement.length)
|
|
238
220
|
: this.banner;
|
|
@@ -265,8 +247,7 @@ export class NDSFile {
|
|
|
265
247
|
writer.position = logicalEnd;
|
|
266
248
|
// An ARM9 replacement can add or remove the decrypted secure-area marker.
|
|
267
249
|
// Both TWL hashing and the header CRC must use the bytes being serialized.
|
|
268
|
-
h.decrypted =
|
|
269
|
-
writer.length >= 0x4008 && writer.buffer.readBigUInt64LE(0x4000) === 0xe7ffdeffe7ffdeffn;
|
|
250
|
+
h.decrypted = writer.length >= 0x4008 && writer.buffer.readBigUInt64LE(0x4000) === 0xe7ffdeffe7ffdeffn;
|
|
270
251
|
if (this.twl) this.twl.writeTo(writer, h);
|
|
271
252
|
h.ROMsize = writer.position;
|
|
272
253
|
h.size = 2 ** Math.ceil(Math.log2(Math.max(h.ROMsize + h.dlp_signature.length, 0x20000)));
|
package/src/nitro/twl.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { hashDigest, hmacSha1,
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
import type { Arm9Header } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { hashDigest, hmacSha1, readFileSync, verifyRsaSha1 } from "#nitro-runtime";
|
|
2
|
+
|
|
3
|
+
import type { BinaryWriter } from "./binary";
|
|
4
|
+
import type { Arm9Header } from "./blz";
|
|
5
|
+
import { compressArm9, decompressArm9 } from "./blz";
|
|
6
|
+
import type { ModcryptHeader } from "./crypto";
|
|
7
|
+
import { aes128CtrCrypt, encryptSecureArea, modcryptKey } from "./crypto";
|
|
7
8
|
|
|
8
9
|
export interface TwlHeader extends ModcryptHeader {
|
|
9
10
|
digest_sector_size: number;
|
|
@@ -43,8 +44,8 @@ export interface OverlayRange {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export const HMAC_SHA1_KEY = Buffer.from(
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
"2106c0deba98ce3fa692e39d46f2ed0176e3cc08562363facad4ecdf9a6278348f6d633cfe22ca9220889723d2cfaec232678dfeca836498acfd3e3787465824",
|
|
48
|
+
"hex",
|
|
48
49
|
);
|
|
49
50
|
const hash = (data: Buffer): Buffer => hmacSha1(HMAC_SHA1_KEY, data);
|
|
50
51
|
function slice(data: Buffer, start: number, size: number, label: string): Buffer {
|
|
@@ -60,46 +61,44 @@ function slice(data: Buffer, start: number, size: number, label: string): Buffer
|
|
|
60
61
|
}
|
|
61
62
|
function validate(h: TwlHeader): void {
|
|
62
63
|
for (const field of [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
"digest_ntr_start",
|
|
65
|
+
"digest_ntr_size",
|
|
66
|
+
"digest_twl_start",
|
|
67
|
+
"digest_twl_size",
|
|
68
|
+
"sector_hashtable_start",
|
|
69
|
+
"sector_hashtable_size",
|
|
70
|
+
"block_hashtable_start",
|
|
71
|
+
"block_hashtable_size",
|
|
72
|
+
"dsi9_rom_offset",
|
|
73
|
+
"dsi9_size",
|
|
74
|
+
"dsi7_rom_offset",
|
|
75
|
+
"dsi7_size",
|
|
76
|
+
"modcrypt1_start",
|
|
77
|
+
"modcrypt1_size",
|
|
78
|
+
"modcrypt2_start",
|
|
79
|
+
"modcrypt2_size",
|
|
80
|
+
"total_rom_size",
|
|
80
81
|
] as const) {
|
|
81
82
|
if (!Number.isSafeInteger(h[field]) || h[field] < 0 || h[field] > 0xffffffff) {
|
|
82
83
|
throw new Error(`Invalid TWL ${field}`);
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
const s = h.digest_sector_size;
|
|
86
|
-
if (!Number.isSafeInteger(s) || s <= 0 || s > 0x4000 || 0x4000 % s)
|
|
87
|
-
throw new Error('Invalid TWL digest sector size');
|
|
87
|
+
if (!Number.isSafeInteger(s) || s <= 0 || s > 0x4000 || 0x4000 % s) throw new Error("Invalid TWL digest sector size");
|
|
88
88
|
if (
|
|
89
89
|
!Number.isSafeInteger(h.digest_block_sectorcount) ||
|
|
90
90
|
h.digest_block_sectorcount <= 0 ||
|
|
91
91
|
h.digest_block_sectorcount > 0x100000
|
|
92
92
|
)
|
|
93
|
-
throw new Error(
|
|
93
|
+
throw new Error("Invalid TWL digest block sector count");
|
|
94
94
|
if (h.digest_ntr_size < 0x4000 || h.digest_ntr_size % s || h.digest_twl_size % s)
|
|
95
|
-
throw new Error(
|
|
95
|
+
throw new Error("Invalid TWL digest region size");
|
|
96
96
|
const hashes = (h.digest_ntr_size + h.digest_twl_size) / s;
|
|
97
97
|
if (
|
|
98
98
|
hashes * 20 > h.sector_hashtable_size ||
|
|
99
|
-
Math.ceil(h.sector_hashtable_size / (h.digest_block_sectorcount * 20)) * 20 >
|
|
100
|
-
h.block_hashtable_size
|
|
99
|
+
Math.ceil(h.sector_hashtable_size / (h.digest_block_sectorcount * 20)) * 20 > h.block_hashtable_size
|
|
101
100
|
)
|
|
102
|
-
throw new Error(
|
|
101
|
+
throw new Error("TWL hash tables are too small");
|
|
103
102
|
}
|
|
104
103
|
export class TWL {
|
|
105
104
|
readonly firstDSiHashOffset: number;
|
|
@@ -117,51 +116,31 @@ export class TWL {
|
|
|
117
116
|
this.firstDSiHashOffset = (h.digest_ntr_size / h.digest_sector_size) * 20;
|
|
118
117
|
this.dsiHashSize = (h.digest_twl_size / h.digest_sector_size) * 20;
|
|
119
118
|
this.Hashtable1Data = Buffer.from(
|
|
120
|
-
slice(original, h.sector_hashtable_start, h.sector_hashtable_size,
|
|
119
|
+
slice(original, h.sector_hashtable_start, h.sector_hashtable_size, "Sector hash table"),
|
|
121
120
|
);
|
|
122
121
|
this.Hashtable2Data = Buffer.from(
|
|
123
|
-
slice(original, h.block_hashtable_start, h.block_hashtable_size,
|
|
124
|
-
);
|
|
125
|
-
this.DSi9Data = Buffer.from(
|
|
126
|
-
slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), 'ARM9i'),
|
|
127
|
-
);
|
|
128
|
-
this.DSi7Data = Buffer.from(
|
|
129
|
-
slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), 'ARM7i'),
|
|
122
|
+
slice(original, h.block_hashtable_start, h.block_hashtable_size, "Block hash table"),
|
|
130
123
|
);
|
|
124
|
+
this.DSi9Data = Buffer.from(slice(original, h.dsi9_rom_offset, Math.max(h.modcrypt1_size, h.dsi9_size), "ARM9i"));
|
|
125
|
+
this.DSi7Data = Buffer.from(slice(original, h.dsi7_rom_offset, Math.max(h.modcrypt2_size, h.dsi7_size), "ARM7i"));
|
|
131
126
|
this.Header2Data = h.trimmedRom
|
|
132
127
|
? null
|
|
133
128
|
: Array.from({ length: 3 }, (_, i) =>
|
|
134
|
-
Buffer.from(
|
|
135
|
-
slice(original, h.digest_twl_start - 0x3000 + 0x1000 * i, 0x1000, 'Secondary header'),
|
|
136
|
-
),
|
|
129
|
+
Buffer.from(slice(original, h.digest_twl_start - 0x3000 + 0x1000 * i, 0x1000, "Secondary header")),
|
|
137
130
|
);
|
|
138
|
-
this.Overlays9Sha1Hmac = overlays.map((o) =>
|
|
139
|
-
hash(slice(original, o.offset, o.size, 'ARM9 overlay')),
|
|
140
|
-
);
|
|
131
|
+
this.Overlays9Sha1Hmac = overlays.map((o) => hash(slice(original, o.offset, o.size, "ARM9 overlay")));
|
|
141
132
|
let encrypted = false;
|
|
142
133
|
const modcryptStart = [
|
|
143
134
|
{ start: h.modcrypt1_start, size: h.modcrypt1_size },
|
|
144
135
|
{ start: h.modcrypt2_start, size: h.modcrypt2_size },
|
|
145
|
-
].find(
|
|
146
|
-
({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 0xffffffff,
|
|
147
|
-
)?.start;
|
|
136
|
+
].find(({ start, size }) => size > 0 && start >= h.digest_twl_start && start < 0xffffffff)?.start;
|
|
148
137
|
if (modcryptStart !== undefined) {
|
|
149
138
|
const index = Math.floor((modcryptStart - h.digest_twl_start) / h.digest_sector_size);
|
|
150
139
|
const actual = hash(
|
|
151
|
-
slice(
|
|
152
|
-
original,
|
|
153
|
-
h.digest_twl_start + index * h.digest_sector_size,
|
|
154
|
-
h.digest_sector_size,
|
|
155
|
-
'Modcrypt sector',
|
|
156
|
-
),
|
|
140
|
+
slice(original, h.digest_twl_start + index * h.digest_sector_size, h.digest_sector_size, "Modcrypt sector"),
|
|
157
141
|
);
|
|
158
142
|
encrypted = !actual.equals(
|
|
159
|
-
slice(
|
|
160
|
-
this.Hashtable1Data,
|
|
161
|
-
this.firstDSiHashOffset + index * 20,
|
|
162
|
-
20,
|
|
163
|
-
'Modcrypt sector hash',
|
|
164
|
-
),
|
|
143
|
+
slice(this.Hashtable1Data, this.firstDSiHashOffset + index * 20, 20, "Modcrypt sector hash"),
|
|
165
144
|
);
|
|
166
145
|
}
|
|
167
146
|
if (encrypted) this.cryptSections(h);
|
|
@@ -178,8 +157,8 @@ export class TWL {
|
|
|
178
157
|
const off = h[`modcrypt${id}_start`] - start;
|
|
179
158
|
const result = aes128CtrCrypt(
|
|
180
159
|
key,
|
|
181
|
-
slice(counter, 0, 16,
|
|
182
|
-
slice(data, off, size,
|
|
160
|
+
slice(counter, 0, 16, "Modcrypt counter"),
|
|
161
|
+
slice(data, off, size, "Modcrypt payload"),
|
|
183
162
|
);
|
|
184
163
|
if (writer) {
|
|
185
164
|
writer.position = start + off;
|
|
@@ -194,20 +173,16 @@ export class TWL {
|
|
|
194
173
|
this.DSi7Data = this.importData(data, offset, size);
|
|
195
174
|
}
|
|
196
175
|
private importData(data: Buffer | string, offset: number, size?: number): Buffer {
|
|
197
|
-
if (typeof data ===
|
|
176
|
+
if (typeof data === "string") data = readFileSync(data);
|
|
198
177
|
size ??= data.length - offset;
|
|
199
|
-
const src = slice(data, offset, size,
|
|
178
|
+
const src = slice(data, offset, size, "DSi import");
|
|
200
179
|
const out = Buffer.alloc(Math.ceil(size / 16) * 16, 255);
|
|
201
180
|
src.copy(out);
|
|
202
181
|
return out;
|
|
203
182
|
}
|
|
204
|
-
updateOverlays9Sha1Hmac(
|
|
205
|
-
arm9Data: Buffer,
|
|
206
|
-
header: Arm9Header,
|
|
207
|
-
overlays: readonly Buffer[],
|
|
208
|
-
): Buffer {
|
|
183
|
+
updateOverlays9Sha1Hmac(arm9Data: Buffer, header: Arm9Header, overlays: readonly Buffer[]): Buffer {
|
|
209
184
|
if (overlays.length !== this.Overlays9Sha1Hmac.length)
|
|
210
|
-
throw new Error(
|
|
185
|
+
throw new Error("Changing ARM9 overlay count is unsupported");
|
|
211
186
|
const hashes = overlays.map(hash);
|
|
212
187
|
if (hashes.every((h, i) => h.equals(this.Overlays9Sha1Hmac[i]))) return Buffer.from(arm9Data);
|
|
213
188
|
const init = header.reserved2.readUInt32LE(0) & 0x3fff;
|
|
@@ -215,30 +190,23 @@ export class TWL {
|
|
|
215
190
|
const unpacked = decompressArm9(arm9Data, header);
|
|
216
191
|
const end = unpacked.data.readUInt32LE(init + 8) - header.ARM9ramAddress;
|
|
217
192
|
let found = -1;
|
|
218
|
-
for (
|
|
219
|
-
let p = Math.min(end - hashes.length * 20, unpacked.data.length - hashes.length * 20);
|
|
220
|
-
p >= 0;
|
|
221
|
-
p--
|
|
222
|
-
) {
|
|
193
|
+
for (let p = Math.min(end - hashes.length * 20, unpacked.data.length - hashes.length * 20); p >= 0; p--) {
|
|
223
194
|
if (unpacked.data.subarray(p, p + 20).equals(this.Overlays9Sha1Hmac[0])) {
|
|
224
195
|
found = p;
|
|
225
196
|
break;
|
|
226
197
|
}
|
|
227
198
|
}
|
|
228
|
-
if (found < 0)
|
|
229
|
-
throw new Error('ARM9 overlay hashes changed but original hash table was not found');
|
|
199
|
+
if (found < 0) throw new Error("ARM9 overlay hashes changed but original hash table was not found");
|
|
230
200
|
hashes.forEach((h, i) => h.copy(unpacked.data, found + i * 20));
|
|
231
201
|
// Original C# inverted this check and tried to compress uncompressed data.
|
|
232
202
|
// Preserve the original storage format: recompress only a compressed ARM9.
|
|
233
|
-
return unpacked.compressed
|
|
234
|
-
? compressArm9(unpacked.data, header, arm9Data.length - hdrptr)
|
|
235
|
-
: unpacked.data;
|
|
203
|
+
return unpacked.compressed ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr) : unpacked.data;
|
|
236
204
|
}
|
|
237
205
|
writeTo(writer: BinaryWriter, h: TwlHeader): Buffer {
|
|
238
206
|
validate(h);
|
|
239
207
|
// Fixed DSi regions cannot be shifted safely without rewriting the header.
|
|
240
208
|
if (writer.position > h.sector_hashtable_start) {
|
|
241
|
-
throw new Error(
|
|
209
|
+
throw new Error("NTR output overlaps the fixed DSi sector hash table");
|
|
242
210
|
}
|
|
243
211
|
if (
|
|
244
212
|
h.sector_hashtable_start + h.sector_hashtable_size > h.block_hashtable_start ||
|
|
@@ -246,7 +214,7 @@ export class TWL {
|
|
|
246
214
|
h.dsi9_rom_offset + this.DSi9Data.length > h.dsi7_rom_offset ||
|
|
247
215
|
h.dsi7_rom_offset + this.DSi7Data.length > h.total_rom_size
|
|
248
216
|
) {
|
|
249
|
-
throw new Error(
|
|
217
|
+
throw new Error("DSi sections overlap or exceed the ROM size");
|
|
250
218
|
}
|
|
251
219
|
const fill = (end: number): void => {
|
|
252
220
|
if (writer.position < end) writer.fillTo(end, 255);
|
|
@@ -268,7 +236,7 @@ export class TWL {
|
|
|
268
236
|
fill(h.total_rom_size);
|
|
269
237
|
const finalPosition = writer.position;
|
|
270
238
|
const read = (p: number, n: number): Buffer =>
|
|
271
|
-
slice(writer.buffer.subarray(0, writer.length), p, n,
|
|
239
|
+
slice(writer.buffer.subarray(0, writer.length), p, n, "TWL output digest");
|
|
272
240
|
let secure: Buffer = Buffer.from(read(h.digest_ntr_start, 0x4000));
|
|
273
241
|
if (h.decrypted) secure = encryptSecureArea(h.gameCode, secure);
|
|
274
242
|
let index = 0;
|
|
@@ -276,19 +244,10 @@ export class TWL {
|
|
|
276
244
|
writer.position = h.sector_hashtable_start + index++ * 20;
|
|
277
245
|
writer.write(hash(b));
|
|
278
246
|
};
|
|
279
|
-
for (let p = 0; p < 0x4000; p += h.digest_sector_size)
|
|
280
|
-
|
|
281
|
-
for (
|
|
282
|
-
let p = h.digest_ntr_start + 0x4000;
|
|
283
|
-
p < h.digest_ntr_start + h.digest_ntr_size;
|
|
284
|
-
p += h.digest_sector_size
|
|
285
|
-
)
|
|
247
|
+
for (let p = 0; p < 0x4000; p += h.digest_sector_size) put(secure.subarray(p, p + h.digest_sector_size));
|
|
248
|
+
for (let p = h.digest_ntr_start + 0x4000; p < h.digest_ntr_start + h.digest_ntr_size; p += h.digest_sector_size)
|
|
286
249
|
put(read(p, h.digest_sector_size));
|
|
287
|
-
for (
|
|
288
|
-
let p = h.digest_twl_start;
|
|
289
|
-
p < h.digest_twl_start + h.digest_twl_size;
|
|
290
|
-
p += h.digest_sector_size
|
|
291
|
-
)
|
|
250
|
+
for (let p = h.digest_twl_start; p < h.digest_twl_start + h.digest_twl_size; p += h.digest_sector_size)
|
|
292
251
|
put(read(p, h.digest_sector_size));
|
|
293
252
|
index = 0;
|
|
294
253
|
for (
|
|
@@ -314,9 +273,9 @@ export const updateHeaderSignatures = (
|
|
|
314
273
|
originalHeader: Buffer,
|
|
315
274
|
keepOriginal: boolean,
|
|
316
275
|
): void => {
|
|
317
|
-
const signedData = Buffer.from(slice(originalHeader, 0, 0xe00,
|
|
318
|
-
const digest = slice(header.hmac_digest_master, 0, 20,
|
|
319
|
-
const signature = slice(header.rsa_signature, 0, 128,
|
|
276
|
+
const signedData = Buffer.from(slice(originalHeader, 0, 0xe00, "Signed header"));
|
|
277
|
+
const digest = slice(header.hmac_digest_master, 0, 20, "Master digest");
|
|
278
|
+
const signature = slice(header.rsa_signature, 0, 128, "RSA signature");
|
|
320
279
|
digest.copy(signedData, 0x328);
|
|
321
280
|
const valid = verifyRsaSha1(signedData, signature);
|
|
322
281
|
const position = writer.position;
|
|
@@ -328,7 +287,7 @@ export const updateHeaderSignatures = (
|
|
|
328
287
|
mask[0] = 0;
|
|
329
288
|
mask[1] = 1;
|
|
330
289
|
mask[107] = 0;
|
|
331
|
-
hashDigest(
|
|
290
|
+
hashDigest("sha1", signedData).copy(mask, 108);
|
|
332
291
|
header.rsa_signature = mask;
|
|
333
292
|
}
|
|
334
293
|
writer.position = 0xf80;
|
package/src/nitro-helper.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export { NDSFile } from
|
|
2
|
-
export type {
|
|
3
|
-
export { Header, UnitCode } from
|
|
4
|
-
export { Banner } from
|
|
5
|
-
export { FileAllocationTable, FileNameTable, OverlayTable } from
|
|
6
|
-
export type { NitroFile, NitroFolder,
|
|
7
|
-
export { BinaryWriter } from
|
|
8
|
-
export { crc16, crc32 } from
|
|
9
|
-
export {
|
|
10
|
-
export type { ModcryptHeader } from
|
|
11
|
-
export {
|
|
12
|
-
export type { Arm9Header } from
|
|
13
|
-
export { TWL, updateHeaderSignatures } from
|
|
14
|
-
export type {
|
|
1
|
+
export { NDSFile } from "./nitro/nds-file";
|
|
2
|
+
export type { RomFileInfo, RomOptions } from "./nitro/nds-file";
|
|
3
|
+
export { Header, UnitCode } from "./nitro/header";
|
|
4
|
+
export { Banner } from "./nitro/banner";
|
|
5
|
+
export { FileAllocationTable, FileNameTable, OverlayTable } from "./nitro/filesystem";
|
|
6
|
+
export type { FatEntry, NitroFile, NitroFolder, OverlayItem } from "./nitro/filesystem";
|
|
7
|
+
export { BinaryWriter } from "./nitro/binary";
|
|
8
|
+
export { crc16, crc32 } from "./nitro/crc";
|
|
9
|
+
export { aes128CtrCrypt, encryptSecureArea, modcryptKey } from "./nitro/crypto";
|
|
10
|
+
export type { ModcryptHeader } from "./nitro/crypto";
|
|
11
|
+
export { compressArm9, compressBLZ, decompressArm9, decompressBLZ } from "./nitro/blz";
|
|
12
|
+
export type { Arm9Header } from "./nitro/blz";
|
|
13
|
+
export { TWL, updateHeaderSignatures } from "./nitro/twl";
|
|
14
|
+
export type { OverlayRange, SignedTwlHeader, TwlHeader } from "./nitro/twl";
|