jsilnet 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +98 -3
- package/REFERENCE.md +191 -0
- package/dist/browser/index.js +345 -0
- package/dist/browser/worker.js +42480 -0
- package/example/README.md +22 -0
- package/example/Sample.cs +10 -0
- package/example/Sample.csproj +7 -0
- package/example/edit-constant.js +48 -0
- package/example/inspect.js +37 -0
- package/package.json +61 -6
- package/src/assembly/instruction-editing.js +165 -0
- package/src/assembly/method-removal.js +789 -0
- package/src/assembly/pointer-table-editing.js +506 -0
- package/src/assembly/portable-pdb-editing.js +441 -0
- package/src/binary/errors.js +17 -0
- package/src/binary/reader.js +206 -0
- package/src/browser/index.d.ts +123 -0
- package/src/browser/index.js +373 -0
- package/src/browser/worker-runtime.js +287 -0
- package/src/browser/worker.js +47 -0
- package/src/il/body.js +165 -0
- package/src/il/instructions.js +217 -0
- package/src/il/opcodes.js +102 -0
- package/src/il/rewrite.js +331 -0
- package/src/index.d.ts +267 -0
- package/src/index.js +13307 -0
- package/src/metadata/classic-pdb.js +2530 -0
- package/src/metadata/custom-attributes.js +387 -0
- package/src/metadata/portable-pdb.js +3347 -0
- package/src/metadata/resources.js +37 -0
- package/src/metadata/root-writer.js +54 -0
- package/src/metadata/signatures.js +339 -0
- package/src/metadata/streams.js +285 -0
- package/src/metadata/strong-name.js +230 -0
- package/src/metadata/tables.js +390 -0
- package/src/metadata/writer.js +191 -0
- package/src/pe/headers.js +491 -0
- package/src/platform/bytes.js +21 -0
- package/src/platform/compression.js +24 -0
- package/src/platform/crypto.js +17 -0
- package/src/resolution/module.js +64 -0
- package/src/types/assembly/instruction-editing.d.ts +33 -0
- package/src/types/assembly/method-removal.d.ts +3 -0
- package/src/types/assembly/pointer-table-editing.d.ts +3 -0
- package/src/types/assembly/portable-pdb-editing.d.ts +23 -0
- package/src/types/binary/errors.d.ts +7 -0
- package/src/types/binary/reader.d.ts +30 -0
- package/src/types/il/body.d.ts +51 -0
- package/src/types/il/instructions.d.ts +33 -0
- package/src/types/il/opcodes.d.ts +2 -0
- package/src/types/il/rewrite.d.ts +38 -0
- package/src/types/index.d.ts +864 -0
- package/src/types/metadata/classic-pdb.d.ts +688 -0
- package/src/types/metadata/custom-attributes.d.ts +28 -0
- package/src/types/metadata/portable-pdb.d.ts +296 -0
- package/src/types/metadata/resources.d.ts +1 -0
- package/src/types/metadata/root-writer.d.ts +1 -0
- package/src/types/metadata/signatures.d.ts +5 -0
- package/src/types/metadata/streams.d.ts +26 -0
- package/src/types/metadata/strong-name.d.ts +11 -0
- package/src/types/metadata/tables.d.ts +25 -0
- package/src/types/metadata/writer.d.ts +16 -0
- package/src/types/pe/headers.d.ts +160 -0
- package/src/types/platform/bytes.d.ts +3 -0
- package/src/types/platform/compression.d.ts +2 -0
- package/src/types/platform/crypto.d.ts +4 -0
- package/src/types/resolution/module.d.ts +12 -0
- package/src/types/writer/patch.d.ts +4 -0
- package/src/types/writer/pe.d.ts +9 -0
- package/src/types/writer/structural.d.ts +4 -0
- package/src/writer/patch.js +42 -0
- package/src/writer/pe.js +196 -0
- package/src/writer/structural.js +241 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { BinaryReader, readCompressedUInt } from "../binary/reader.js";
|
|
2
|
+
import { FormatError } from "../binary/errors.js";
|
|
3
|
+
|
|
4
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
5
|
+
|
|
6
|
+
function validateStringsHeap(bytes) {
|
|
7
|
+
let start = 1;
|
|
8
|
+
while (start < bytes.length) {
|
|
9
|
+
const end = bytes.indexOf(0, start);
|
|
10
|
+
if (end < 0) {
|
|
11
|
+
throw new FormatError("Unterminated string heap entry", { context: "#Strings" });
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
utf8Decoder.decode(bytes.subarray(start, end));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new FormatError(`Invalid UTF-8 string: ${error.message}`, { context: "#Strings" });
|
|
17
|
+
}
|
|
18
|
+
start = end + 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateLengthPrefixedHeap(
|
|
23
|
+
bytes,
|
|
24
|
+
name,
|
|
25
|
+
validatePayload = () => {},
|
|
26
|
+
allowTrailingZeroPadding = false,
|
|
27
|
+
entryOffsets = new Set([0])
|
|
28
|
+
) {
|
|
29
|
+
const reader = new BinaryReader(bytes, { start: 1, baseOffset: 1, context: name });
|
|
30
|
+
while (reader.remaining > 0) {
|
|
31
|
+
if (allowTrailingZeroPadding &&
|
|
32
|
+
bytes.subarray(reader.absoluteOffset).every((byte) => byte === 0)) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const entryOffset = reader.absoluteOffset;
|
|
36
|
+
entryOffsets.add(entryOffset);
|
|
37
|
+
const prefixOffset = reader.position;
|
|
38
|
+
const size = readCompressedUInt(reader);
|
|
39
|
+
const prefixSize = reader.position - prefixOffset;
|
|
40
|
+
const expectedPrefixSize = size <= 0x7f ? 1 : size <= 0x3fff ? 2 : 4;
|
|
41
|
+
if (prefixSize !== expectedPrefixSize) {
|
|
42
|
+
throw new FormatError(`Noncanonical length prefix in ${name}`, {
|
|
43
|
+
offset: entryOffset,
|
|
44
|
+
context: name
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
validatePayload(reader.take(size), entryOffset);
|
|
48
|
+
}
|
|
49
|
+
return reader.absoluteOffset;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseMetadataRoot(input, offset, size) {
|
|
53
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
54
|
+
if (!Number.isInteger(offset) || !Number.isInteger(size) || offset < 0 || size < 0 || offset + size > bytes.length) {
|
|
55
|
+
throw new FormatError("Metadata directory is outside the file", { offset, context: "metadata root" });
|
|
56
|
+
}
|
|
57
|
+
const reader = new BinaryReader(bytes, { start: offset, length: size, baseOffset: offset, context: "metadata root" });
|
|
58
|
+
if (reader.u4() !== 0x424a5342) throw new FormatError("Invalid BSJB metadata signature", { offset });
|
|
59
|
+
const majorVersion = reader.u2();
|
|
60
|
+
const minorVersion = reader.u2();
|
|
61
|
+
const reserved = reader.u4();
|
|
62
|
+
if (reserved !== 0) {
|
|
63
|
+
throw new FormatError("Metadata root reserved field must be zero", {
|
|
64
|
+
offset: offset + 8,
|
|
65
|
+
context: "metadata root"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const versionLength = reader.u4();
|
|
69
|
+
const versionBytes = reader.take(versionLength);
|
|
70
|
+
const nul = versionBytes.indexOf(0);
|
|
71
|
+
if (nul < 0 || versionBytes.subarray(nul).some((byte) => byte !== 0)) {
|
|
72
|
+
throw new FormatError("Metadata version string is not null-terminated", {
|
|
73
|
+
offset: offset + 16,
|
|
74
|
+
context: "metadata root"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
let version;
|
|
78
|
+
try {
|
|
79
|
+
version = utf8Decoder.decode(versionBytes.subarray(0, nul));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new FormatError(`Invalid UTF-8 metadata version: ${error.message}`, {
|
|
82
|
+
offset: offset + 16,
|
|
83
|
+
context: "metadata root"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
reader.align(4);
|
|
87
|
+
const flags = reader.u2();
|
|
88
|
+
if (flags !== 0) {
|
|
89
|
+
throw new FormatError("Metadata root flags must be zero", {
|
|
90
|
+
offset: reader.absoluteOffset - 2,
|
|
91
|
+
context: "metadata root"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const streamCount = reader.u2();
|
|
95
|
+
const streams = new Map();
|
|
96
|
+
for (let i = 0; i < streamCount; i++) {
|
|
97
|
+
const streamOffset = reader.u4();
|
|
98
|
+
const streamSize = reader.u4();
|
|
99
|
+
const nameBytes = [];
|
|
100
|
+
for (let j = 0; j < 32; j++) {
|
|
101
|
+
const value = reader.u1();
|
|
102
|
+
if (value === 0) break;
|
|
103
|
+
if (value > 0x7f) {
|
|
104
|
+
throw new FormatError("Metadata stream name contains a non-ASCII byte", {
|
|
105
|
+
offset: reader.absoluteOffset - 1,
|
|
106
|
+
context: "metadata root"
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
nameBytes.push(value);
|
|
110
|
+
}
|
|
111
|
+
if (reader.bytes[reader.position - 1] !== 0) {
|
|
112
|
+
throw new FormatError("Metadata stream name is not null-terminated within 32 bytes", {
|
|
113
|
+
offset: reader.absoluteOffset,
|
|
114
|
+
context: "metadata root"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const nameEnd = reader.position;
|
|
118
|
+
reader.align(4);
|
|
119
|
+
if (reader.bytes.subarray(nameEnd, reader.position).some((byte) => byte !== 0)) {
|
|
120
|
+
throw new FormatError("Metadata stream name padding must be zero", {
|
|
121
|
+
offset: offset + nameEnd,
|
|
122
|
+
context: "metadata root"
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (nameBytes.length === 0) {
|
|
126
|
+
throw new FormatError("Metadata stream name must not be empty", {
|
|
127
|
+
offset: reader.absoluteOffset,
|
|
128
|
+
context: "metadata root"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const name = String.fromCharCode(...nameBytes);
|
|
132
|
+
if (streams.has(name)) throw new FormatError(`Duplicate metadata stream ${name}`, { offset: reader.absoluteOffset });
|
|
133
|
+
if ((streamOffset & 3) !== 0) {
|
|
134
|
+
throw new FormatError(`Metadata stream ${name} is not four-byte aligned`, {
|
|
135
|
+
offset: offset + streamOffset,
|
|
136
|
+
context: "metadata root"
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (streamOffset + streamSize > size) {
|
|
140
|
+
throw new FormatError(`Metadata stream ${name} exceeds the metadata directory`, {
|
|
141
|
+
offset: offset + streamOffset,
|
|
142
|
+
context: "metadata root"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
streams.set(name, {
|
|
146
|
+
name,
|
|
147
|
+
offset: offset + streamOffset,
|
|
148
|
+
relativeOffset: streamOffset,
|
|
149
|
+
size: streamSize,
|
|
150
|
+
bytes: bytes.slice(offset + streamOffset, offset + streamOffset + streamSize)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const streamHeaderEnd = reader.position;
|
|
154
|
+
const ranges = [...streams.values()]
|
|
155
|
+
.sort((left, right) => left.relativeOffset - right.relativeOffset);
|
|
156
|
+
for (let index = 0; index < ranges.length; index++) {
|
|
157
|
+
const stream = ranges[index];
|
|
158
|
+
if (stream.relativeOffset < streamHeaderEnd) {
|
|
159
|
+
throw new FormatError(`Metadata stream ${stream.name} overlaps the stream directory`, {
|
|
160
|
+
offset: offset + stream.relativeOffset,
|
|
161
|
+
context: "metadata root"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const next = ranges[index + 1];
|
|
165
|
+
if (stream.size > 0 && next && stream.relativeOffset + stream.size > next.relativeOffset) {
|
|
166
|
+
throw new FormatError(`Metadata streams ${stream.name} and ${next.name} overlap`, {
|
|
167
|
+
offset: offset + next.relativeOffset,
|
|
168
|
+
context: "metadata root"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { offset, size, majorVersion, minorVersion, version, versionLength, versionBytes, flags, streams };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class MetadataHeaps {
|
|
176
|
+
constructor(streams) {
|
|
177
|
+
this.strings = streams.get("#Strings")?.bytes.slice();
|
|
178
|
+
this.userStrings = streams.get("#US")?.bytes.slice();
|
|
179
|
+
this.guids = streams.get("#GUID")?.bytes.slice();
|
|
180
|
+
this.blobs = streams.get("#Blob")?.bytes.slice();
|
|
181
|
+
this.userStringDataEnd = this.userStrings?.length ?? 0;
|
|
182
|
+
this.blobEntryOffsets = null;
|
|
183
|
+
this.userStringEntryOffsets = null;
|
|
184
|
+
for (const [name, bytes] of [
|
|
185
|
+
["#Strings", this.strings],
|
|
186
|
+
["#US", this.userStrings],
|
|
187
|
+
["#Blob", this.blobs]
|
|
188
|
+
]) {
|
|
189
|
+
if (bytes && (bytes.length === 0 || bytes[0] !== 0)) {
|
|
190
|
+
throw new FormatError(`${name} heap is missing its zero entry`, { context: name });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (this.guids && this.guids.length % 16 !== 0) {
|
|
194
|
+
throw new FormatError("#GUID heap size is not a multiple of 16 bytes", { context: "#GUID" });
|
|
195
|
+
}
|
|
196
|
+
if (this.strings) validateStringsHeap(this.strings);
|
|
197
|
+
if (this.blobs) {
|
|
198
|
+
this.blobEntryOffsets = new Set([0]);
|
|
199
|
+
validateLengthPrefixedHeap(this.blobs, "#Blob", () => {}, false, this.blobEntryOffsets);
|
|
200
|
+
}
|
|
201
|
+
if (this.userStrings) {
|
|
202
|
+
this.userStringEntryOffsets = new Set([0]);
|
|
203
|
+
this.userStringDataEnd = validateLengthPrefixedHeap(this.userStrings, "#US", (payload, offset) => {
|
|
204
|
+
const textLength = payload.length - 1;
|
|
205
|
+
if (payload.length < 1 || (textLength & 1) !== 0 || payload[textLength] > 1) {
|
|
206
|
+
throw new FormatError("Invalid UTF-16LE user-string encoding", {
|
|
207
|
+
offset,
|
|
208
|
+
context: "#US"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}, true, this.userStringEntryOffsets);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#heap(bytes, name, index) {
|
|
216
|
+
if (!bytes) throw new FormatError(`Metadata heap ${name} is missing`);
|
|
217
|
+
if (!Number.isInteger(index) || index < 0 || index >= bytes.length) {
|
|
218
|
+
throw new FormatError(`Index ${index} is outside ${name}`, { context: "metadata heap" });
|
|
219
|
+
}
|
|
220
|
+
return bytes;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getString(index) {
|
|
224
|
+
if (index === 0) return "";
|
|
225
|
+
const bytes = this.#heap(this.strings, "#Strings", index);
|
|
226
|
+
let end = index;
|
|
227
|
+
while (end < bytes.length && bytes[end] !== 0) end++;
|
|
228
|
+
if (end === bytes.length) throw new FormatError("Unterminated string heap entry", { context: "#Strings" });
|
|
229
|
+
try {
|
|
230
|
+
return utf8Decoder.decode(bytes.subarray(index, end));
|
|
231
|
+
} catch (error) {
|
|
232
|
+
throw new FormatError(`Invalid UTF-8 string: ${error.message}`, { context: "#Strings" });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getBlob(index) {
|
|
237
|
+
if (index === 0) return new Uint8Array();
|
|
238
|
+
const bytes = this.#heap(this.blobs, "#Blob", index);
|
|
239
|
+
if (!this.blobEntryOffsets?.has(index)) {
|
|
240
|
+
throw new FormatError(`Index ${index} is not a #Blob entry boundary`, {
|
|
241
|
+
context: "#Blob"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const reader = new BinaryReader(bytes, { start: index, baseOffset: index, context: "#Blob" });
|
|
245
|
+
const size = readCompressedUInt(reader);
|
|
246
|
+
return reader.take(size).slice();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getGuid(index) {
|
|
250
|
+
if (index === 0) return null;
|
|
251
|
+
const bytes = this.#heap(this.guids, "#GUID", (index - 1) * 16);
|
|
252
|
+
const offset = (index - 1) * 16;
|
|
253
|
+
if (offset + 16 > bytes.length) throw new FormatError("GUID index exceeds #GUID heap", { context: "#GUID" });
|
|
254
|
+
return bytes.slice(offset, offset + 16);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getUserString(tokenOrOffset) {
|
|
258
|
+
const offset = tokenOrOffset > 0xffffff ? tokenOrOffset & 0x00ffffff : tokenOrOffset;
|
|
259
|
+
if (offset === 0) return "";
|
|
260
|
+
const bytes = this.#heap(this.userStrings, "#US", offset);
|
|
261
|
+
if (!this.userStringEntryOffsets?.has(offset) || offset >= this.userStringDataEnd) {
|
|
262
|
+
throw new FormatError(`Index ${offset} is not a #US entry boundary`, {
|
|
263
|
+
context: "#US"
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const reader = new BinaryReader(bytes, { start: offset, baseOffset: offset, context: "#US" });
|
|
267
|
+
const size = readCompressedUInt(reader);
|
|
268
|
+
if (size < 1) throw new FormatError("#US entry is missing its special-character byte", { context: "#US" });
|
|
269
|
+
const payload = reader.take(size);
|
|
270
|
+
const textLength = payload.length - 1;
|
|
271
|
+
if ((textLength & 1) !== 0 || payload[textLength] > 1) {
|
|
272
|
+
throw new FormatError("Invalid UTF-16LE user-string encoding", { context: "#US" });
|
|
273
|
+
}
|
|
274
|
+
let result = "";
|
|
275
|
+
for (let index = 0; index < textLength; index += 0x8000) {
|
|
276
|
+
const end = Math.min(textLength, index + 0x8000);
|
|
277
|
+
const units = [];
|
|
278
|
+
for (let offset = index; offset < end; offset += 2) {
|
|
279
|
+
units.push(payload[offset] | (payload[offset + 1] << 8));
|
|
280
|
+
}
|
|
281
|
+
result += String.fromCharCode(...units);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import forge from "node-forge";
|
|
2
|
+
import { FormatError, UnsupportedFormatError } from "../binary/errors.js";
|
|
3
|
+
import { binaryStringToBytes, bytesToBinaryString } from "../platform/bytes.js";
|
|
4
|
+
|
|
5
|
+
const rsaSignAlgorithm = 0x00002400;
|
|
6
|
+
const sha1Algorithm = 0x00008004;
|
|
7
|
+
const hashNames = new Map([
|
|
8
|
+
[sha1Algorithm, "sha1"],
|
|
9
|
+
[0x0000800c, "sha256"],
|
|
10
|
+
[0x0000800d, "sha384"],
|
|
11
|
+
[0x0000800e, "sha512"]
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function getStrongNameHashName(algorithm) {
|
|
15
|
+
const resolvedAlgorithm = algorithm || sha1Algorithm;
|
|
16
|
+
const name = hashNames.get(resolvedAlgorithm);
|
|
17
|
+
if (!name) {
|
|
18
|
+
throw new UnsupportedFormatError(
|
|
19
|
+
`Strong-name hash algorithm 0x${resolvedAlgorithm.toString(16)} is unsupported`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return name;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function binaryIntegerBytes(value, length) {
|
|
26
|
+
let hex = value.toString(16);
|
|
27
|
+
if ((hex.length & 1) !== 0) hex = `0${hex}`;
|
|
28
|
+
const encoded = binaryStringToBytes(forge.util.hexToBytes(hex));
|
|
29
|
+
if (encoded.length > length) throw new FormatError("RSA integer exceeds its declared size");
|
|
30
|
+
const output = new Uint8Array(length);
|
|
31
|
+
output.set(encoded, length - encoded.length);
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getRsaModulusBits(key) {
|
|
36
|
+
return key.n.bitLength();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function importStrongNamePrivateKey(input) {
|
|
40
|
+
if (input && typeof input === "object" && input.n && input.e && input.d) return input;
|
|
41
|
+
let pem = input;
|
|
42
|
+
if (input && typeof input === "object" && input.type === "private" &&
|
|
43
|
+
typeof input.export === "function") {
|
|
44
|
+
if (input.asymmetricKeyType !== "rsa") {
|
|
45
|
+
throw new UnsupportedFormatError("Strong-name signing requires an RSA private key");
|
|
46
|
+
}
|
|
47
|
+
pem = input.export({ format: "pem", type: "pkcs8" });
|
|
48
|
+
} else if (input instanceof Uint8Array) {
|
|
49
|
+
pem = new TextDecoder("ascii", { fatal: true }).decode(input);
|
|
50
|
+
}
|
|
51
|
+
if (typeof pem !== "string") {
|
|
52
|
+
throw new UnsupportedFormatError("Strong-name signing requires an RSA PEM or private key");
|
|
53
|
+
}
|
|
54
|
+
const key = forge.pki.privateKeyFromPem(pem);
|
|
55
|
+
if (!key.n || !key.e || !key.d) {
|
|
56
|
+
throw new UnsupportedFormatError("Strong-name signing requires an RSA private key");
|
|
57
|
+
}
|
|
58
|
+
return key;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function encodeStrongNamePublicKey(keyInput, hashAlgorithm) {
|
|
62
|
+
const hashName = getStrongNameHashName(hashAlgorithm);
|
|
63
|
+
const key = importStrongNamePrivateKey(keyInput);
|
|
64
|
+
const modulusBits = getRsaModulusBits(key);
|
|
65
|
+
const modulusLength = Math.ceil(modulusBits / 8);
|
|
66
|
+
const modulus = binaryIntegerBytes(key.n, modulusLength);
|
|
67
|
+
const exponent = Number(key.e.toString(10));
|
|
68
|
+
if (modulusBits % 8 !== 0) {
|
|
69
|
+
throw new UnsupportedFormatError("Strong-name RSA modulus length must be byte-aligned");
|
|
70
|
+
}
|
|
71
|
+
if (exponent < 3 || exponent % 2 === 0) {
|
|
72
|
+
throw new UnsupportedFormatError("Strong-name RSA public exponent is invalid");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const capiKey = new Uint8Array(20 + modulus.length);
|
|
76
|
+
const capiView = new DataView(capiKey.buffer);
|
|
77
|
+
capiKey[0] = 0x06;
|
|
78
|
+
capiKey[1] = 0x02;
|
|
79
|
+
capiView.setUint32(4, rsaSignAlgorithm, true);
|
|
80
|
+
capiView.setUint32(8, 0x31415352, true);
|
|
81
|
+
capiView.setUint32(12, modulusBits, true);
|
|
82
|
+
capiView.setUint32(16, exponent, true);
|
|
83
|
+
for (let index = 0; index < modulus.length; index++) {
|
|
84
|
+
capiKey[20 + index] = modulus[modulus.length - 1 - index];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const hashAlgorithmId = [...hashNames].find(([, name]) => name === hashName)?.[0];
|
|
88
|
+
const publicKey = new Uint8Array(12 + capiKey.length);
|
|
89
|
+
const publicKeyView = new DataView(publicKey.buffer);
|
|
90
|
+
publicKeyView.setUint32(0, rsaSignAlgorithm, true);
|
|
91
|
+
publicKeyView.setUint32(4, hashAlgorithmId, true);
|
|
92
|
+
publicKeyView.setUint32(8, capiKey.length, true);
|
|
93
|
+
publicKey.set(capiKey, 12);
|
|
94
|
+
return typeof Buffer === "undefined" ? publicKey : Buffer.from(publicKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function decodeStrongNamePublicKey(bytes) {
|
|
98
|
+
if (!(bytes instanceof Uint8Array) || bytes.length < 32) {
|
|
99
|
+
throw new FormatError("Strong-name public key is truncated");
|
|
100
|
+
}
|
|
101
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
102
|
+
const signatureAlgorithm = view.getUint32(0, true);
|
|
103
|
+
const hashAlgorithm = view.getUint32(4, true);
|
|
104
|
+
const publicKeySize = view.getUint32(8, true);
|
|
105
|
+
if (signatureAlgorithm !== rsaSignAlgorithm || publicKeySize !== bytes.length - 12) {
|
|
106
|
+
throw new UnsupportedFormatError("Strong-name public key uses an unsupported algorithm or size");
|
|
107
|
+
}
|
|
108
|
+
getStrongNameHashName(hashAlgorithm);
|
|
109
|
+
if (bytes[12] !== 0x06 || bytes[13] !== 0x02 ||
|
|
110
|
+
view.getUint16(14, true) !== 0 ||
|
|
111
|
+
view.getUint32(16, true) !== rsaSignAlgorithm ||
|
|
112
|
+
view.getUint32(20, true) !== 0x31415352) {
|
|
113
|
+
throw new UnsupportedFormatError("Strong-name public key does not contain a supported CAPI RSA key");
|
|
114
|
+
}
|
|
115
|
+
const modulusBits = view.getUint32(24, true);
|
|
116
|
+
const exponent = view.getUint32(28, true);
|
|
117
|
+
if (modulusBits === 0 || modulusBits % 8 !== 0 ||
|
|
118
|
+
publicKeySize !== 20 + modulusBits / 8 || exponent < 3 || exponent % 2 === 0) {
|
|
119
|
+
throw new FormatError("Strong-name CAPI RSA key has inconsistent dimensions");
|
|
120
|
+
}
|
|
121
|
+
const modulus = Uint8Array.from(bytes.subarray(32)).reverse();
|
|
122
|
+
if ((modulus[0] & 0x80) === 0) {
|
|
123
|
+
throw new FormatError("Strong-name RSA modulus does not match its declared bit length");
|
|
124
|
+
}
|
|
125
|
+
const modulusHex = forge.util.bytesToHex(bytesToBinaryString(modulus));
|
|
126
|
+
const publicKey = forge.pki.rsa.setPublicKey(
|
|
127
|
+
new forge.jsbn.BigInteger(modulusHex, 16),
|
|
128
|
+
new forge.jsbn.BigInteger(String(exponent), 10)
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
publicKey,
|
|
132
|
+
hashAlgorithm,
|
|
133
|
+
signatureSize: modulusBits / 8
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function assertStrongNamePEProfile(pe) {
|
|
138
|
+
const fixedOptionalHeaderSize = pe.optionalHeaderMagic === 0x10b ? 96 : 112;
|
|
139
|
+
if (pe.optionalHeaderSize !== fixedOptionalHeaderSize + 16 * 8 ||
|
|
140
|
+
pe.directories.length < 15 || pe.directories.length > 16) {
|
|
141
|
+
throw new UnsupportedFormatError(
|
|
142
|
+
"Strong-name signing requires the standard PE optional header with 15 or 16 declared data directories"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return fixedOptionalHeaderSize;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getStrongNameRanges(bytes, pe, cli) {
|
|
149
|
+
const fixedOptionalHeaderSize = assertStrongNamePEProfile(pe);
|
|
150
|
+
const signature = cli.strongNameSignature;
|
|
151
|
+
if (signature.rva === 0 || signature.size === 0) {
|
|
152
|
+
throw new UnsupportedFormatError("Strong-name signing requires a reserved signature directory");
|
|
153
|
+
}
|
|
154
|
+
const signatureOffset = pe.rvaToOffset(signature.rva, signature.size);
|
|
155
|
+
const signatureEnd = signatureOffset + signature.size;
|
|
156
|
+
const containingSection = pe.sections.find((section) =>
|
|
157
|
+
signatureOffset >= section.rawPointer &&
|
|
158
|
+
signatureEnd <= section.rawPointer + section.rawSize
|
|
159
|
+
);
|
|
160
|
+
if (!containingSection) {
|
|
161
|
+
throw new FormatError("Strong-name signature slot is not contained in section data");
|
|
162
|
+
}
|
|
163
|
+
return { signatureOffset, signatureEnd, fixedOptionalHeaderSize };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function updateStrongNameData(signer, bytes, pe, ranges) {
|
|
167
|
+
const update = (part) => signer.update(bytesToBinaryString(part));
|
|
168
|
+
update(bytes.subarray(0, pe.peOffset));
|
|
169
|
+
update(bytes.subarray(pe.peOffset, pe.peOffset + 24));
|
|
170
|
+
|
|
171
|
+
const optionalHeader = Uint8Array.from(bytes.subarray(
|
|
172
|
+
pe.optionalHeaderOffset,
|
|
173
|
+
pe.optionalHeaderOffset + ranges.fixedOptionalHeaderSize
|
|
174
|
+
));
|
|
175
|
+
optionalHeader.fill(0, 64, 68);
|
|
176
|
+
update(optionalHeader);
|
|
177
|
+
|
|
178
|
+
const directoriesOffset = pe.optionalHeaderOffset + ranges.fixedOptionalHeaderSize;
|
|
179
|
+
const directories = Uint8Array.from(bytes.subarray(directoriesOffset, directoriesOffset + 16 * 8));
|
|
180
|
+
directories.fill(0, 4 * 8, 5 * 8);
|
|
181
|
+
update(directories);
|
|
182
|
+
update(bytes.subarray(pe.sectionOffset, pe.sectionOffset + pe.sectionCount * 40));
|
|
183
|
+
|
|
184
|
+
for (const section of pe.sections) {
|
|
185
|
+
const start = section.rawPointer;
|
|
186
|
+
const end = start + section.rawSize;
|
|
187
|
+
const beforeEnd = Math.min(end, ranges.signatureOffset);
|
|
188
|
+
if (start < beforeEnd) update(bytes.subarray(start, beforeEnd));
|
|
189
|
+
const afterStart = Math.max(start, ranges.signatureEnd);
|
|
190
|
+
if (afterStart < end) update(bytes.subarray(afterStart, end));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function signStrongNameImage(bytes, pe, cli, privateKey, hashAlgorithm) {
|
|
195
|
+
const hashName = getStrongNameHashName(hashAlgorithm);
|
|
196
|
+
const output = Uint8Array.from(bytes);
|
|
197
|
+
const ranges = getStrongNameRanges(output, pe, cli);
|
|
198
|
+
const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
|
|
199
|
+
view.setUint32(cli.strongNameFlagOffset, cli.flags | 0x08, true);
|
|
200
|
+
|
|
201
|
+
const signer = forge.md[hashName].create();
|
|
202
|
+
updateStrongNameData(signer, output, pe, ranges);
|
|
203
|
+
const signature = binaryStringToBytes(privateKey.sign(signer)).reverse();
|
|
204
|
+
if (signature.length !== cli.strongNameSignature.size) {
|
|
205
|
+
throw new FormatError(
|
|
206
|
+
`Strong-name signature size ${signature.length} does not match reserved slot ${cli.strongNameSignature.size}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
output.set(signature, ranges.signatureOffset);
|
|
210
|
+
return output;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function verifyStrongNameImage(bytes, pe, cli, publicKeyBytes, hashAlgorithm) {
|
|
214
|
+
if ((cli.flags & 0x08) === 0) return false;
|
|
215
|
+
const key = decodeStrongNamePublicKey(publicKeyBytes);
|
|
216
|
+
const hashName = getStrongNameHashName(hashAlgorithm || key.hashAlgorithm);
|
|
217
|
+
if (cli.strongNameSignature.size !== key.signatureSize) return false;
|
|
218
|
+
const ranges = getStrongNameRanges(bytes, pe, cli);
|
|
219
|
+
const signature = bytesToBinaryString(
|
|
220
|
+
Uint8Array.from(bytes.subarray(ranges.signatureOffset, ranges.signatureEnd)).reverse()
|
|
221
|
+
);
|
|
222
|
+
const verifier = forge.md[hashName].create();
|
|
223
|
+
updateStrongNameData(verifier, bytes, pe, ranges);
|
|
224
|
+
try {
|
|
225
|
+
return key.publicKey.verify(verifier.digest().getBytes(), signature);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Error && error.message === "Encryption block is invalid.") return false;
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|