nitro-patcher 0.3.0 → 1.0.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 +25 -2
- package/dist/browser.js +10507 -78
- package/dist/browser.js.LEGAL.txt +460 -0
- package/dist/browser.js.map +4 -4
- package/dist/{chunk-IGURDH5L.js → chunk-B7NTPBDX.js} +70 -8
- package/dist/chunk-B7NTPBDX.js.map +1 -0
- package/dist/chunk-LK3JXA24.js +2067 -0
- package/dist/chunk-LK3JXA24.js.map +1 -0
- package/dist/cli.js +19 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -2
- package/dist/nitro-helper.d.ts +43 -134
- package/dist/nitro-helper.js +3 -1
- package/dist/nitro-patch-helper.d.ts +4 -0
- package/dist/nitro-patch-helper.js +2 -2
- package/dist/twl-complete-B2is30H0.d.ts +201 -0
- package/package.json +8 -2
- package/src/browser/buffer.ts +1 -0
- package/src/browser.ts +23 -2
- package/src/cli.ts +15 -2
- package/src/nitro/blz.ts +13 -4
- package/src/nitro/crypto.ts +29 -11
- package/src/nitro/filesystem.ts +88 -5
- package/src/nitro/header.ts +4 -0
- package/src/nitro/nds-file.ts +378 -30
- package/src/nitro/twl-complete.ts +665 -0
- package/src/nitro/twl.ts +53 -19
- package/src/nitro-helper.ts +2 -1
- package/src/nitro-patch-helper.ts +64 -3
- package/src/patch/zip.ts +14 -4
- package/src/runtime/browser.ts +2 -2
- package/src/runtime/node.ts +2 -15
- package/src/runtime/rsa.ts +14 -0
- package/dist/chunk-GLK3K2HY.js +0 -1120
- package/dist/chunk-GLK3K2HY.js.map +0 -1
- package/dist/chunk-IGURDH5L.js.map +0 -1
package/src/nitro/twl.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hmacSha1, readFileSync, verifyRsaSha1 } from "#nitro-runtime";
|
|
2
2
|
|
|
3
3
|
import type { BinaryWriter } from "./binary";
|
|
4
|
+
import { DEFAULT_MAX_SIZE } from "./binary";
|
|
4
5
|
import type { Arm9Header } from "./blz";
|
|
5
6
|
import { compressArm9, decompressArm9 } from "./blz";
|
|
6
7
|
import type { ModcryptHeader } from "./crypto";
|
|
7
8
|
import { aes128CtrCrypt, encryptSecureArea, modcryptKey } from "./crypto";
|
|
9
|
+
import { Header } from "./header";
|
|
10
|
+
import { CompleteTWL } from "./twl-complete";
|
|
8
11
|
|
|
9
12
|
export interface TwlHeader extends ModcryptHeader {
|
|
10
13
|
digest_sector_size: number;
|
|
@@ -108,9 +111,46 @@ export class TWL {
|
|
|
108
111
|
readonly Header2Data: Buffer[] | null;
|
|
109
112
|
readonly Overlays9Sha1Hmac: Buffer[];
|
|
110
113
|
readonly twlEncrypted: boolean;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
readonly complete: CompleteTWL | undefined;
|
|
115
|
+
private dsi9Data: Buffer = Buffer.alloc(0);
|
|
116
|
+
private dsi7Data: Buffer = Buffer.alloc(0);
|
|
117
|
+
get DSi9Data(): Buffer {
|
|
118
|
+
return this.complete?.DSi9Data ?? this.dsi9Data;
|
|
119
|
+
}
|
|
120
|
+
set DSi9Data(data: Buffer) {
|
|
121
|
+
if (this.complete) this.complete.DSi9Data = data;
|
|
122
|
+
else this.dsi9Data = data;
|
|
123
|
+
}
|
|
124
|
+
get DSi7Data(): Buffer {
|
|
125
|
+
return this.complete?.DSi7Data ?? this.dsi7Data;
|
|
126
|
+
}
|
|
127
|
+
set DSi7Data(data: Buffer) {
|
|
128
|
+
if (this.complete) this.complete.DSi7Data = data;
|
|
129
|
+
else this.dsi7Data = data;
|
|
130
|
+
}
|
|
131
|
+
constructor(
|
|
132
|
+
header: TwlHeader,
|
|
133
|
+
overlays: readonly OverlayRange[],
|
|
134
|
+
original: Buffer,
|
|
135
|
+
options: { dsiMode?: "legacy" | "complete"; maxOutputSize?: number } = {},
|
|
136
|
+
) {
|
|
137
|
+
if (options.dsiMode === "complete") {
|
|
138
|
+
if (!(header instanceof Header)) throw new TypeError("Complete DSi handling requires a parsed Header");
|
|
139
|
+
this.complete = new CompleteTWL(
|
|
140
|
+
header,
|
|
141
|
+
overlays.map((overlay) => slice(original, overlay.offset, overlay.size, "Signed ARM9 overlay")),
|
|
142
|
+
original,
|
|
143
|
+
options.maxOutputSize ?? DEFAULT_MAX_SIZE,
|
|
144
|
+
);
|
|
145
|
+
this.firstDSiHashOffset = this.complete.firstDSiHashOffset;
|
|
146
|
+
this.dsiHashSize = this.complete.dsiHashSize;
|
|
147
|
+
this.Hashtable1Data = this.complete.Hashtable1Data;
|
|
148
|
+
this.Hashtable2Data = this.complete.Hashtable2Data;
|
|
149
|
+
this.Header2Data = this.complete.Header2Data;
|
|
150
|
+
this.Overlays9Sha1Hmac = this.complete.Overlays9Sha1Hmac;
|
|
151
|
+
this.twlEncrypted = this.complete.twlEncrypted;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
114
154
|
validate(header);
|
|
115
155
|
const h = header;
|
|
116
156
|
this.firstDSiHashOffset = (h.digest_ntr_size / h.digest_sector_size) * 20;
|
|
@@ -167,9 +207,11 @@ export class TWL {
|
|
|
167
207
|
}
|
|
168
208
|
}
|
|
169
209
|
importArm9iData(data: Buffer | string, offset = 0, size?: number): void {
|
|
210
|
+
if (this.complete) return this.complete.importArm9iData(data, offset, size);
|
|
170
211
|
this.DSi9Data = this.importData(data, offset, size);
|
|
171
212
|
}
|
|
172
213
|
importArm7iData(data: Buffer | string, offset = 0, size?: number): void {
|
|
214
|
+
if (this.complete) return this.complete.importArm7iData(data, offset, size);
|
|
173
215
|
this.DSi7Data = this.importData(data, offset, size);
|
|
174
216
|
}
|
|
175
217
|
private importData(data: Buffer | string, offset: number, size?: number): Buffer {
|
|
@@ -181,6 +223,7 @@ export class TWL {
|
|
|
181
223
|
return out;
|
|
182
224
|
}
|
|
183
225
|
updateOverlays9Sha1Hmac(arm9Data: Buffer, header: Arm9Header, overlays: readonly Buffer[]): Buffer {
|
|
226
|
+
if (this.complete) return this.complete.updateOverlays9Sha1Hmac(arm9Data, header, overlays);
|
|
184
227
|
if (overlays.length !== this.Overlays9Sha1Hmac.length)
|
|
185
228
|
throw new Error("Changing ARM9 overlay count is unsupported");
|
|
186
229
|
const hashes = overlays.map(hash);
|
|
@@ -203,6 +246,7 @@ export class TWL {
|
|
|
203
246
|
return unpacked.compressed ? compressArm9(unpacked.data, header, arm9Data.length - hdrptr) : unpacked.data;
|
|
204
247
|
}
|
|
205
248
|
writeTo(writer: BinaryWriter, h: TwlHeader): Buffer {
|
|
249
|
+
if (this.complete) throw new Error("Complete DSi serialization requires NDSFile.toBuffer()");
|
|
206
250
|
validate(h);
|
|
207
251
|
// Fixed DSi regions cannot be shifted safely without rewriting the header.
|
|
208
252
|
if (writer.position > h.sector_hashtable_start) {
|
|
@@ -266,32 +310,22 @@ export class TWL {
|
|
|
266
310
|
}
|
|
267
311
|
}
|
|
268
312
|
|
|
269
|
-
/** Refresh the digest
|
|
313
|
+
/** Refresh the master digest while preserving the cartridge signature. */
|
|
270
314
|
export const updateHeaderSignatures = (
|
|
271
315
|
writer: BinaryWriter,
|
|
272
316
|
header: SignedTwlHeader,
|
|
273
317
|
originalHeader: Buffer,
|
|
274
|
-
|
|
318
|
+
_keepOriginal = true,
|
|
275
319
|
): void => {
|
|
276
320
|
const signedData = Buffer.from(slice(originalHeader, 0, 0xe00, "Signed header"));
|
|
277
321
|
const digest = slice(header.hmac_digest_master, 0, 20, "Master digest");
|
|
278
322
|
const signature = slice(header.rsa_signature, 0, 128, "RSA signature");
|
|
279
323
|
digest.copy(signedData, 0x328);
|
|
280
|
-
|
|
324
|
+
verifyRsaSha1(signedData, signature);
|
|
281
325
|
const position = writer.position;
|
|
282
326
|
writer.position = 0x328;
|
|
283
327
|
writer.write(digest);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const mask = Buffer.alloc(128, 0xff);
|
|
287
|
-
mask[0] = 0;
|
|
288
|
-
mask[1] = 1;
|
|
289
|
-
mask[107] = 0;
|
|
290
|
-
hashDigest("sha1", signedData).copy(mask, 108);
|
|
291
|
-
header.rsa_signature = mask;
|
|
292
|
-
}
|
|
293
|
-
writer.position = 0xf80;
|
|
294
|
-
writer.write(header.rsa_signature);
|
|
295
|
-
}
|
|
328
|
+
writer.position = 0xf80;
|
|
329
|
+
writer.write(signature);
|
|
296
330
|
writer.position = position;
|
|
297
331
|
};
|
package/src/nitro-helper.ts
CHANGED
|
@@ -6,9 +6,10 @@ export { FileAllocationTable, FileNameTable, OverlayTable } from "./nitro/filesy
|
|
|
6
6
|
export type { FatEntry, NitroFile, NitroFolder, OverlayItem } from "./nitro/filesystem";
|
|
7
7
|
export { BinaryWriter } from "./nitro/binary";
|
|
8
8
|
export { crc16, crc32 } from "./nitro/crc";
|
|
9
|
-
export { aes128CtrCrypt, encryptSecureArea, modcryptKey } from "./nitro/crypto";
|
|
9
|
+
export { aes128CtrCrypt, decryptSecureArea, encryptSecureArea, modcryptKey } from "./nitro/crypto";
|
|
10
10
|
export type { ModcryptHeader } from "./nitro/crypto";
|
|
11
11
|
export { compressArm9, compressBLZ, decompressArm9, decompressBLZ } from "./nitro/blz";
|
|
12
12
|
export type { Arm9Header } from "./nitro/blz";
|
|
13
13
|
export { TWL, updateHeaderSignatures } from "./nitro/twl";
|
|
14
14
|
export type { OverlayRange, SignedTwlHeader, TwlHeader } from "./nitro/twl";
|
|
15
|
+
export type { DsiCheck, DsiVerification } from "./nitro/twl-complete";
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { hashDigest, readFile } from "#nitro-runtime";
|
|
3
3
|
|
|
4
4
|
import { bytes } from "./nitro/binary";
|
|
5
|
+
import { OverlayTable } from "./nitro/filesystem";
|
|
5
6
|
import { NDSFile } from "./nitro/nds-file";
|
|
7
|
+
import type { DsiVerification } from "./nitro/twl-complete";
|
|
6
8
|
import { decodeXdelta } from "./patch/xdelta";
|
|
7
|
-
import { extractZipEntry as extractEntry, readZip } from "./patch/zip";
|
|
9
|
+
import { extractZipEntry as extractEntry, readZip, readZipEntries } from "./patch/zip";
|
|
8
10
|
|
|
9
11
|
export const PatchReturnValue = Object.freeze({
|
|
10
12
|
SUCCESS: "SUCCESS",
|
|
@@ -19,6 +21,7 @@ export interface PatchResult {
|
|
|
19
21
|
/** MD5 of the original input, before preprocessing. */
|
|
20
22
|
inputMd5: string;
|
|
21
23
|
outputMd5: string;
|
|
24
|
+
dsi?: DsiVerification;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export interface PatchOptions {
|
|
@@ -33,6 +36,7 @@ export interface PatchOptions {
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
export interface PatchMetadata {
|
|
39
|
+
formatVersion?: number;
|
|
36
40
|
id?: string;
|
|
37
41
|
author?: string;
|
|
38
42
|
name?: string;
|
|
@@ -77,6 +81,13 @@ const readMetadata = (files: Map<string, Buffer>): PatchMetadata | null => {
|
|
|
77
81
|
|
|
78
82
|
const value = metadata as Record<string, unknown>;
|
|
79
83
|
const result: PatchMetadata = {};
|
|
84
|
+
if (value.formatVersion !== undefined) {
|
|
85
|
+
const version = value.formatVersion;
|
|
86
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1)
|
|
87
|
+
throw new Error("metadata.json formatVersion must be a positive safe integer");
|
|
88
|
+
if (version !== 1 && version !== 2) throw new Error(`Unsupported XZP format version: ${version}`);
|
|
89
|
+
result.formatVersion = version;
|
|
90
|
+
}
|
|
80
91
|
for (const field of ["id", "author", "name", "homepage", "version"] as const) {
|
|
81
92
|
const fieldValue = value[field];
|
|
82
93
|
if (fieldValue === undefined) continue;
|
|
@@ -146,10 +157,18 @@ export const readPatchInfoFile = async (patchPath: string, options: PatchOptions
|
|
|
146
157
|
/** Apply an existing NitroPatcher ZIP package entirely in memory. */
|
|
147
158
|
export const patchBuffer = (original: Uint8Array, patch: Uint8Array, options: PatchOptions = {}): PatchResult => {
|
|
148
159
|
const input = bytes(original, "original ROM");
|
|
149
|
-
const
|
|
160
|
+
const entries = readZipEntries(patch, options);
|
|
161
|
+
const patches = new Map([...entries].map(([name, entry]) => [name, entry.data]));
|
|
162
|
+
const formatVersion = readMetadata(patches)?.formatVersion ?? 1;
|
|
150
163
|
const inputMd5 = md5(input);
|
|
151
164
|
const preprocessing = patches.get(`preprocessing/${inputMd5}.xdelta`);
|
|
152
|
-
const
|
|
165
|
+
const romOptions = { ...options, dsiMode: formatVersion === 2 ? ("complete" as const) : ("legacy" as const) };
|
|
166
|
+
const rom = new NDSFile(preprocessing ? decodeXdelta(input, preprocessing, options) : input, romOptions);
|
|
167
|
+
if (formatVersion === 2)
|
|
168
|
+
for (const name of ["arm9i.bin", "arm7i.bin"]) {
|
|
169
|
+
if ((patches.has(name) || patches.has(`xdelta/${name}`)) && !rom.hasFile(name))
|
|
170
|
+
throw new Error(`${name} requires a complete DSi-enhanced cartridge`);
|
|
171
|
+
}
|
|
153
172
|
let matched = true;
|
|
154
173
|
const md5File = patches.get("md5.txt");
|
|
155
174
|
if (md5File) {
|
|
@@ -171,12 +190,54 @@ export const patchBuffer = (original: Uint8Array, patch: Uint8Array, options: Pa
|
|
|
171
190
|
const replacement = delta ? decodeXdelta(rom.getFile(path), delta, options) : patches.get(normalized);
|
|
172
191
|
if (replacement) rom.replaceFile(path, replacement);
|
|
173
192
|
}
|
|
193
|
+
if (formatVersion === 2) {
|
|
194
|
+
for (const name of ["overarm9.bin", "overarm7.bin"])
|
|
195
|
+
if (!rom.hasFile(name)) {
|
|
196
|
+
const delta = patches.get(`xdelta/${name}`);
|
|
197
|
+
const replacement = delta ? decodeXdelta(Buffer.alloc(0), delta, options) : patches.get(name);
|
|
198
|
+
if (replacement) rom.replaceFile(name, replacement);
|
|
199
|
+
}
|
|
200
|
+
const candidates = new Map<string, { path: string; data: Buffer; delta: boolean }>();
|
|
201
|
+
for (const [name, entry] of entries) {
|
|
202
|
+
const delta = name.startsWith("xdelta/");
|
|
203
|
+
const target = delta ? name.slice(7) : name;
|
|
204
|
+
if ((!target.startsWith("data/") && !target.startsWith("overlay/")) || rom.hasFile(target)) continue;
|
|
205
|
+
if (!delta && entries.has(`xdelta/${target}`)) continue;
|
|
206
|
+
candidates.set(target, { path: delta ? entry.path.slice(7) : entry.path, data: entry.data, delta });
|
|
207
|
+
}
|
|
208
|
+
if (candidates.size && (patches.has("fnt.bin") || patches.has("xdelta/fnt.bin")))
|
|
209
|
+
throw new Error("Adding files conflicts with an fnt.bin replacement");
|
|
210
|
+
const overlayTables = new Map<string, OverlayTable>();
|
|
211
|
+
for (const name of ["overarm9.bin", "overarm7.bin"])
|
|
212
|
+
if (rom.hasFile(name)) {
|
|
213
|
+
const raw = rom.getFile(name);
|
|
214
|
+
overlayTables.set(name, new OverlayTable(raw, 0, raw.length, name === "overarm9.bin"));
|
|
215
|
+
}
|
|
216
|
+
const additions = [...candidates.values()].sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
|
|
217
|
+
for (const entry of additions) {
|
|
218
|
+
const content = entry.delta ? decodeXdelta(Buffer.alloc(0), entry.data, options) : entry.data;
|
|
219
|
+
if (entry.path.toLowerCase().startsWith("overlay/")) {
|
|
220
|
+
const match = /^overlay\/overlay(7)?_(\d{4,})\.bin$/i.exec(entry.path);
|
|
221
|
+
const overlayId = match?.[2];
|
|
222
|
+
const arm9 = !match?.[1];
|
|
223
|
+
if (!overlayId || String(Number(overlayId)).padStart(4, "0") !== overlayId)
|
|
224
|
+
throw new Error(`Invalid overlay path: ${entry.path}`);
|
|
225
|
+
const name = arm9 ? "overarm9.bin" : "overarm7.bin";
|
|
226
|
+
const table = overlayTables.get(name);
|
|
227
|
+
if (!table) throw new Error(`New overlay requires ${name}: ${entry.path}`);
|
|
228
|
+
const metadata = table.entries.find((item) => item.overlayId === Number(overlayId));
|
|
229
|
+
if (!metadata) throw new Error(`New overlay is missing its table entry: ${entry.path}`);
|
|
230
|
+
rom.addOverlay(arm9, metadata, content);
|
|
231
|
+
} else rom.addFile(entry.path, content);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
174
234
|
const buffer = rom.toBuffer();
|
|
175
235
|
return {
|
|
176
236
|
buffer,
|
|
177
237
|
returnValue: matched ? PatchReturnValue.SUCCESS : PatchReturnValue.MD5_MISMATCH,
|
|
178
238
|
inputMd5,
|
|
179
239
|
outputMd5: md5(buffer),
|
|
240
|
+
...(formatVersion === 2 && rom.header.unitCode === 2 && { dsi: new NDSFile(buffer, romOptions).verifyDsi() }),
|
|
180
241
|
};
|
|
181
242
|
};
|
|
182
243
|
|
package/src/patch/zip.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface ZipOptions {
|
|
|
8
8
|
maxEntries?: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface ZipEntry {
|
|
12
|
+
path: string;
|
|
13
|
+
data: Buffer;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const fail = (message: string): never => {
|
|
12
17
|
throw new Error(`ZIP: ${message}`);
|
|
13
18
|
};
|
|
@@ -64,7 +69,7 @@ function filename(raw: Buffer, flags: number, fields: Map<number, Buffer>): stri
|
|
|
64
69
|
* ZIP64, encryption, split archives and other compression methods fail explicitly.
|
|
65
70
|
* Format reference: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
|
66
71
|
*/
|
|
67
|
-
export const
|
|
72
|
+
export const readZipEntries = (input: Uint8Array, options: ZipOptions = {}): Map<string, ZipEntry> => {
|
|
68
73
|
if (!(input instanceof Uint8Array)) throw new TypeError("ZIP input must be a Buffer or Uint8Array");
|
|
69
74
|
const bytes = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
70
75
|
const maxSize = boundOption(options.maxUncompressedSize, 1024 * 1024 * 1024, "maxUncompressedSize");
|
|
@@ -96,7 +101,7 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
|
|
|
96
101
|
checkRange(bytes, centralOffset, centralSize, eocd);
|
|
97
102
|
if (centralOffset + centralSize !== eocd) fail("central directory size/offset mismatch");
|
|
98
103
|
if (count > Math.floor(centralSize / 46)) fail("entry count exceeds central directory size");
|
|
99
|
-
const result = new Map<string,
|
|
104
|
+
const result = new Map<string, ZipEntry>();
|
|
100
105
|
const ranges: { start: number; end: number }[] = [];
|
|
101
106
|
let offset = centralOffset,
|
|
102
107
|
totalSize = 0;
|
|
@@ -124,7 +129,8 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
|
|
|
124
129
|
checkRange(bytes, offset + 46, nameLength + extraLength + commentLength, eocd);
|
|
125
130
|
const rawName = bytes.subarray(offset + 46, offset + 46 + nameLength);
|
|
126
131
|
const fields = extras(bytes.subarray(offset + 46 + nameLength, offset + 46 + nameLength + extraLength));
|
|
127
|
-
const
|
|
132
|
+
const path = filename(rawName, flags, fields).replaceAll("\\", "/");
|
|
133
|
+
const name = path.toLowerCase();
|
|
128
134
|
offset += 46 + nameLength + extraLength + commentLength;
|
|
129
135
|
|
|
130
136
|
checkRange(bytes, localOffset, 30, centralOffset);
|
|
@@ -177,7 +183,7 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
|
|
|
177
183
|
}
|
|
178
184
|
if (data.length !== size) fail(`uncompressed size mismatch for ${JSON.stringify(name)}`);
|
|
179
185
|
if (crc32(data) !== checksum) fail(`CRC32 mismatch for ${JSON.stringify(name)}`);
|
|
180
|
-
if (name && !name.endsWith("/")) result.set(name, data);
|
|
186
|
+
if (name && !name.endsWith("/")) result.set(name, { path, data });
|
|
181
187
|
}
|
|
182
188
|
if (offset !== eocd) fail("central directory entry count/length mismatch");
|
|
183
189
|
ranges.sort((a, b) => a.start - b.start);
|
|
@@ -186,6 +192,10 @@ export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string
|
|
|
186
192
|
return result;
|
|
187
193
|
};
|
|
188
194
|
|
|
195
|
+
/** Read file contents using normalized paths, with the final duplicate winning. */
|
|
196
|
+
export const readZip = (input: Uint8Array, options: ZipOptions = {}): Map<string, Buffer> =>
|
|
197
|
+
new Map([...readZipEntries(input, options)].map(([name, entry]) => [name, entry.data]));
|
|
198
|
+
|
|
189
199
|
/** Extract one file using the same normalized, case-insensitive paths as readZip. */
|
|
190
200
|
export const extractZipEntry = (input: Uint8Array, entryName: string, options: ZipOptions = {}): Buffer => {
|
|
191
201
|
if (typeof entryName !== "string" || !entryName) throw new TypeError("ZIP entry name must be a non-empty string");
|
package/src/runtime/browser.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ecb } from "#browser-aes";
|
|
|
2
2
|
import { md5, sha1 } from "#browser-hashes";
|
|
3
3
|
import { hmac } from "#browser-hmac";
|
|
4
4
|
import { Inflate } from "fflate";
|
|
5
|
+
export { verifyDsiSignature as verifyRsaSha1 } from "./rsa";
|
|
5
6
|
|
|
6
7
|
export const hashDigest = (algorithm: "md5" | "sha1", data: Uint8Array): Buffer =>
|
|
7
8
|
Buffer.from((algorithm === "md5" ? md5 : sha1)(data));
|
|
@@ -30,9 +31,8 @@ export const inflateRawSync = (data: Uint8Array, options: { maxOutputLength: num
|
|
|
30
31
|
// These Node-only methods are not exposed by the browser public API. Keep a
|
|
31
32
|
// clear error if a future internal call accidentally crosses that boundary.
|
|
32
33
|
const nodeOnly = (): never => {
|
|
33
|
-
throw new Error("Filesystem
|
|
34
|
+
throw new Error("Filesystem APIs are only available in the Node entry");
|
|
34
35
|
};
|
|
35
36
|
export const readFile = nodeOnly;
|
|
36
37
|
export const writeFile = nodeOnly;
|
|
37
38
|
export const readFileSync = nodeOnly;
|
|
38
|
-
export const verifyRsaSha1 = nodeOnly;
|
package/src/runtime/node.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createCipheriv, createHash, createHmac
|
|
1
|
+
import { createCipheriv, createHash, createHmac } from "node:crypto";
|
|
2
|
+
export { verifyDsiSignature as verifyRsaSha1 } from "./rsa";
|
|
2
3
|
export { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
export { readFileSync } from "node:fs";
|
|
4
5
|
export { inflateRawSync } from "node:zlib";
|
|
@@ -11,17 +12,3 @@ export const createAesBlockCipher = (key: Uint8Array): ((block: Uint8Array) => B
|
|
|
11
12
|
cipher.setAutoPadding(false);
|
|
12
13
|
return (block) => cipher.update(block);
|
|
13
14
|
};
|
|
14
|
-
export const verifyRsaSha1 = (data: Uint8Array, signature: Uint8Array): boolean => {
|
|
15
|
-
const key = createPublicKey({
|
|
16
|
-
key: {
|
|
17
|
-
kty: "RSA",
|
|
18
|
-
n: Buffer.from(
|
|
19
|
-
"956F790DF08BB85A76AAEFA27FE874758BED9EDF9E9A670CD818BEB9B2885203B3FA11AEAA186513B5D6BB85A384D0D0EFB366CBC6051AAA86827AB74311F59C9BFC6C7079D5F17BD0819F522056738C721F40CF2361932590A3C5DC94CFD17A8CBC954A918AA858F4D804BAF7D3C1C4D7B8F077012FA170260B2C049056F3A5",
|
|
20
|
-
"hex",
|
|
21
|
-
).toString("base64url"),
|
|
22
|
-
e: "AQAB",
|
|
23
|
-
},
|
|
24
|
-
format: "jwk",
|
|
25
|
-
});
|
|
26
|
-
return verify("RSA-SHA1", data, key, signature);
|
|
27
|
-
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import forge from "node-forge";
|
|
2
|
+
|
|
3
|
+
import { hashDigest } from "#nitro-runtime";
|
|
4
|
+
|
|
5
|
+
const publicKey = forge.pki.rsa.setPublicKey(
|
|
6
|
+
new forge.jsbn.BigInteger(
|
|
7
|
+
"956F790DF08BB85A76AAEFA27FE874758BED9EDF9E9A670CD818BEB9B2885203B3FA11AEAA186513B5D6BB85A384D0D0EFB366CBC6051AAA86827AB74311F59C9BFC6C7079D5F17BD0819F522056738C721F40CF2361932590A3C5DC94CFD17A8CBC954A918AA858F4D804BAF7D3C1C4D7B8F077012FA170260B2C049056F3A5",
|
|
8
|
+
16,
|
|
9
|
+
),
|
|
10
|
+
new forge.jsbn.BigInteger("10001", 16),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const verifyDsiSignature = (data: Uint8Array, signature: Uint8Array): boolean =>
|
|
14
|
+
publicKey.verify(hashDigest("sha1", data).toString("latin1"), Buffer.from(signature).toString("latin1"), "NONE");
|