@gjsify/webcrypto 0.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.
- package/README.md +28 -0
- package/globals.mjs +6 -0
- package/lib/esm/crypto-key.js +21 -0
- package/lib/esm/index.js +49 -0
- package/lib/esm/subtle.js +607 -0
- package/lib/esm/util.js +133 -0
- package/lib/types/crypto-key.d.ts +182 -0
- package/lib/types/index.d.ts +17 -0
- package/lib/types/subtle.d.ts +22 -0
- package/lib/types/util.d.ts +27 -0
- package/package.json +43 -0
- package/src/crypto-key.ts +226 -0
- package/src/index.spec.ts +1881 -0
- package/src/index.ts +78 -0
- package/src/subtle.ts +755 -0
- package/src/test.mts +8 -0
- package/src/util.ts +152 -0
- package/tsconfig.json +32 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CryptoKey
|
|
3
|
+
} from "./crypto-key.js";
|
|
4
|
+
import {
|
|
5
|
+
normalizeAlgorithm,
|
|
6
|
+
toNodeHashName,
|
|
7
|
+
toNodeCurveName,
|
|
8
|
+
hashSize,
|
|
9
|
+
validateUsages,
|
|
10
|
+
checkUsage,
|
|
11
|
+
base64urlEncode,
|
|
12
|
+
base64urlDecode,
|
|
13
|
+
toUint8Array
|
|
14
|
+
} from "./util.js";
|
|
15
|
+
let _cryptoLoaded = false;
|
|
16
|
+
let _createHash;
|
|
17
|
+
let _createHmac;
|
|
18
|
+
let _createCipheriv;
|
|
19
|
+
let _createDecipheriv;
|
|
20
|
+
let _createSign;
|
|
21
|
+
let _createVerify;
|
|
22
|
+
let _pbkdf2Sync;
|
|
23
|
+
let _hkdfSync;
|
|
24
|
+
let _createECDH;
|
|
25
|
+
let _randomBytes;
|
|
26
|
+
let _publicEncrypt;
|
|
27
|
+
let _privateDecrypt;
|
|
28
|
+
let _createPublicKey;
|
|
29
|
+
let _createPrivateKey;
|
|
30
|
+
let _ecdsaSign;
|
|
31
|
+
let _ecdsaVerify;
|
|
32
|
+
let _rsaPssSign;
|
|
33
|
+
let _rsaPssVerify;
|
|
34
|
+
let _rsaOaepEncrypt;
|
|
35
|
+
let _rsaOaepDecrypt;
|
|
36
|
+
async function loadCrypto() {
|
|
37
|
+
if (_cryptoLoaded) return;
|
|
38
|
+
const crypto = await import("crypto");
|
|
39
|
+
_createHash = crypto.createHash;
|
|
40
|
+
_createHmac = crypto.createHmac;
|
|
41
|
+
_createCipheriv = crypto.createCipheriv;
|
|
42
|
+
_createDecipheriv = crypto.createDecipheriv;
|
|
43
|
+
_createSign = crypto.createSign;
|
|
44
|
+
_createVerify = crypto.createVerify;
|
|
45
|
+
_pbkdf2Sync = crypto.pbkdf2Sync;
|
|
46
|
+
_hkdfSync = crypto.hkdfSync;
|
|
47
|
+
_createECDH = crypto.createECDH;
|
|
48
|
+
_randomBytes = crypto.randomBytes;
|
|
49
|
+
_publicEncrypt = crypto.publicEncrypt;
|
|
50
|
+
_privateDecrypt = crypto.privateDecrypt;
|
|
51
|
+
_createPublicKey = crypto.createPublicKey;
|
|
52
|
+
_createPrivateKey = crypto.createPrivateKey;
|
|
53
|
+
_ecdsaSign = crypto.ecdsaSign;
|
|
54
|
+
_ecdsaVerify = crypto.ecdsaVerify;
|
|
55
|
+
_rsaPssSign = crypto.rsaPssSign;
|
|
56
|
+
_rsaPssVerify = crypto.rsaPssVerify;
|
|
57
|
+
_rsaOaepEncrypt = crypto.rsaOaepEncrypt;
|
|
58
|
+
_rsaOaepDecrypt = crypto.rsaOaepDecrypt;
|
|
59
|
+
_cryptoLoaded = true;
|
|
60
|
+
}
|
|
61
|
+
const cryptoReady = loadCrypto();
|
|
62
|
+
function ensureCrypto() {
|
|
63
|
+
if (!_cryptoLoaded) {
|
|
64
|
+
throw new Error("crypto not yet loaded. Ensure module initialization is complete.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class SubtleCrypto {
|
|
68
|
+
// ==================== digest ====================
|
|
69
|
+
async digest(algorithm, data) {
|
|
70
|
+
await cryptoReady;
|
|
71
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
72
|
+
const nodeName = toNodeHashName(alg.name);
|
|
73
|
+
const bytes = toUint8Array(data);
|
|
74
|
+
const hash = _createHash(nodeName);
|
|
75
|
+
hash.update(bytes);
|
|
76
|
+
const result = hash.digest();
|
|
77
|
+
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
|
|
78
|
+
}
|
|
79
|
+
// ==================== generateKey ====================
|
|
80
|
+
async generateKey(algorithm, extractable, keyUsages) {
|
|
81
|
+
await cryptoReady;
|
|
82
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
83
|
+
const name = alg.name.toUpperCase();
|
|
84
|
+
switch (name) {
|
|
85
|
+
case "AES-CBC":
|
|
86
|
+
case "AES-CTR":
|
|
87
|
+
case "AES-GCM": {
|
|
88
|
+
const length = algorithm.length;
|
|
89
|
+
if (![128, 192, 256].includes(length)) {
|
|
90
|
+
throw new DOMException(`Invalid AES key length: ${length}`, "OperationError");
|
|
91
|
+
}
|
|
92
|
+
validateUsages(keyUsages, ["encrypt", "decrypt", "wrapKey", "unwrapKey"]);
|
|
93
|
+
const keyData = _randomBytes(length / 8);
|
|
94
|
+
return new CryptoKey("secret", extractable, { name: alg.name, length }, keyUsages, new Uint8Array(keyData));
|
|
95
|
+
}
|
|
96
|
+
case "HMAC": {
|
|
97
|
+
const hmacParams = algorithm;
|
|
98
|
+
const hashAlg = normalizeAlgorithm(hmacParams.hash);
|
|
99
|
+
const length = hmacParams.length || hashSize(hashAlg.name) * 8;
|
|
100
|
+
validateUsages(keyUsages, ["sign", "verify"]);
|
|
101
|
+
const keyData = _randomBytes(Math.ceil(length / 8));
|
|
102
|
+
return new CryptoKey("secret", extractable, { name: "HMAC", hash: { name: hashAlg.name }, length }, keyUsages, new Uint8Array(keyData));
|
|
103
|
+
}
|
|
104
|
+
case "ECDH": {
|
|
105
|
+
const namedCurve = algorithm.namedCurve;
|
|
106
|
+
const nodeCurve = toNodeCurveName(namedCurve);
|
|
107
|
+
validateUsages(keyUsages, ["deriveKey", "deriveBits"]);
|
|
108
|
+
const ecdh = _createECDH(nodeCurve);
|
|
109
|
+
ecdh.generateKeys();
|
|
110
|
+
const pubBytes = new Uint8Array(ecdh.getPublicKey());
|
|
111
|
+
const privBytes = new Uint8Array(ecdh.getPrivateKey());
|
|
112
|
+
const pubKey = new CryptoKey("public", true, { name: "ECDH", namedCurve }, [], pubBytes);
|
|
113
|
+
const privKey = new CryptoKey("private", extractable, { name: "ECDH", namedCurve }, keyUsages, { pub: pubBytes, priv: privBytes });
|
|
114
|
+
return { publicKey: pubKey, privateKey: privKey };
|
|
115
|
+
}
|
|
116
|
+
case "ECDSA": {
|
|
117
|
+
const namedCurve = algorithm.namedCurve;
|
|
118
|
+
const nodeCurve = toNodeCurveName(namedCurve);
|
|
119
|
+
validateUsages(keyUsages, ["sign", "verify"]);
|
|
120
|
+
const ecdh = _createECDH(nodeCurve);
|
|
121
|
+
ecdh.generateKeys();
|
|
122
|
+
const pubBytes = new Uint8Array(ecdh.getPublicKey());
|
|
123
|
+
const privBytes = new Uint8Array(ecdh.getPrivateKey());
|
|
124
|
+
const pubKey = new CryptoKey("public", true, { name: "ECDSA", namedCurve }, ["verify"], pubBytes);
|
|
125
|
+
const privKey = new CryptoKey("private", extractable, { name: "ECDSA", namedCurve }, ["sign"], { pub: pubBytes, priv: privBytes });
|
|
126
|
+
return { publicKey: pubKey, privateKey: privKey };
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ==================== importKey ====================
|
|
133
|
+
async importKey(format, keyData, algorithm, extractable, keyUsages) {
|
|
134
|
+
await cryptoReady;
|
|
135
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
136
|
+
const name = alg.name.toUpperCase();
|
|
137
|
+
switch (name) {
|
|
138
|
+
case "AES-CBC":
|
|
139
|
+
case "AES-CTR":
|
|
140
|
+
case "AES-GCM": {
|
|
141
|
+
if (format === "raw") {
|
|
142
|
+
const bytes = toUint8Array(keyData);
|
|
143
|
+
if (![16, 24, 32].includes(bytes.length)) {
|
|
144
|
+
throw new DOMException(`Invalid AES key length: ${bytes.length * 8}`, "DataError");
|
|
145
|
+
}
|
|
146
|
+
validateUsages(keyUsages, ["encrypt", "decrypt", "wrapKey", "unwrapKey"]);
|
|
147
|
+
return new CryptoKey("secret", extractable, { name: alg.name, length: bytes.length * 8 }, keyUsages, new Uint8Array(bytes));
|
|
148
|
+
}
|
|
149
|
+
if (format === "jwk") {
|
|
150
|
+
const jwk = keyData;
|
|
151
|
+
if (jwk.kty !== "oct") throw new DOMException('JWK kty must be "oct"', "DataError");
|
|
152
|
+
const bytes = base64urlDecode(jwk.k);
|
|
153
|
+
if (![16, 24, 32].includes(bytes.length)) {
|
|
154
|
+
throw new DOMException(`Invalid AES key length: ${bytes.length * 8}`, "DataError");
|
|
155
|
+
}
|
|
156
|
+
validateUsages(keyUsages, ["encrypt", "decrypt", "wrapKey", "unwrapKey"]);
|
|
157
|
+
return new CryptoKey("secret", extractable, { name: alg.name, length: bytes.length * 8 }, keyUsages, bytes);
|
|
158
|
+
}
|
|
159
|
+
throw new DOMException(`Unsupported format: ${format}`, "NotSupportedError");
|
|
160
|
+
}
|
|
161
|
+
case "HMAC": {
|
|
162
|
+
const hmacParams = algorithm;
|
|
163
|
+
const hashAlg = normalizeAlgorithm(hmacParams.hash);
|
|
164
|
+
if (format === "raw") {
|
|
165
|
+
const bytes = toUint8Array(keyData);
|
|
166
|
+
validateUsages(keyUsages, ["sign", "verify"]);
|
|
167
|
+
return new CryptoKey("secret", extractable, { name: "HMAC", hash: { name: hashAlg.name }, length: bytes.length * 8 }, keyUsages, new Uint8Array(bytes));
|
|
168
|
+
}
|
|
169
|
+
if (format === "jwk") {
|
|
170
|
+
const jwk = keyData;
|
|
171
|
+
if (jwk.kty !== "oct") throw new DOMException('JWK kty must be "oct"', "DataError");
|
|
172
|
+
const bytes = base64urlDecode(jwk.k);
|
|
173
|
+
validateUsages(keyUsages, ["sign", "verify"]);
|
|
174
|
+
return new CryptoKey("secret", extractable, { name: "HMAC", hash: { name: hashAlg.name }, length: bytes.length * 8 }, keyUsages, bytes);
|
|
175
|
+
}
|
|
176
|
+
throw new DOMException(`Unsupported format: ${format}`, "NotSupportedError");
|
|
177
|
+
}
|
|
178
|
+
case "PBKDF2": {
|
|
179
|
+
if (format === "raw") {
|
|
180
|
+
const bytes = toUint8Array(keyData);
|
|
181
|
+
validateUsages(keyUsages, ["deriveKey", "deriveBits"]);
|
|
182
|
+
return new CryptoKey("secret", false, { name: "PBKDF2" }, keyUsages, new Uint8Array(bytes));
|
|
183
|
+
}
|
|
184
|
+
throw new DOMException(`Unsupported format: ${format}`, "NotSupportedError");
|
|
185
|
+
}
|
|
186
|
+
case "HKDF": {
|
|
187
|
+
if (format === "raw") {
|
|
188
|
+
const bytes = toUint8Array(keyData);
|
|
189
|
+
validateUsages(keyUsages, ["deriveKey", "deriveBits"]);
|
|
190
|
+
return new CryptoKey("secret", false, { name: "HKDF" }, keyUsages, new Uint8Array(bytes));
|
|
191
|
+
}
|
|
192
|
+
throw new DOMException(`Unsupported format: ${format}`, "NotSupportedError");
|
|
193
|
+
}
|
|
194
|
+
case "ECDH":
|
|
195
|
+
case "ECDSA": {
|
|
196
|
+
const namedCurve = algorithm.namedCurve;
|
|
197
|
+
if (format === "raw") {
|
|
198
|
+
const bytes = toUint8Array(keyData);
|
|
199
|
+
if (name === "ECDH") {
|
|
200
|
+
if (keyUsages.length > 0) {
|
|
201
|
+
validateUsages(keyUsages, ["deriveKey", "deriveBits"]);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
validateUsages(keyUsages, ["verify"]);
|
|
205
|
+
}
|
|
206
|
+
return new CryptoKey("public", extractable, { name: alg.name, namedCurve }, keyUsages, new Uint8Array(bytes));
|
|
207
|
+
}
|
|
208
|
+
if (format === "jwk") {
|
|
209
|
+
const jwk = keyData;
|
|
210
|
+
if (jwk.kty !== "EC") throw new DOMException('JWK kty must be "EC"', "DataError");
|
|
211
|
+
if (jwk.d) {
|
|
212
|
+
const privBytes = base64urlDecode(jwk.d);
|
|
213
|
+
const xBytes = base64urlDecode(jwk.x);
|
|
214
|
+
const yBytes = base64urlDecode(jwk.y);
|
|
215
|
+
const pubBytes = new Uint8Array(1 + xBytes.length + yBytes.length);
|
|
216
|
+
pubBytes[0] = 4;
|
|
217
|
+
pubBytes.set(xBytes, 1);
|
|
218
|
+
pubBytes.set(yBytes, 1 + xBytes.length);
|
|
219
|
+
const allowedUsages = name === "ECDH" ? ["deriveKey", "deriveBits"] : ["sign"];
|
|
220
|
+
validateUsages(keyUsages, allowedUsages);
|
|
221
|
+
return new CryptoKey("private", extractable, { name: alg.name, namedCurve }, keyUsages, { pub: pubBytes, priv: privBytes });
|
|
222
|
+
} else {
|
|
223
|
+
const xBytes = base64urlDecode(jwk.x);
|
|
224
|
+
const yBytes = base64urlDecode(jwk.y);
|
|
225
|
+
const pubBytes = new Uint8Array(1 + xBytes.length + yBytes.length);
|
|
226
|
+
pubBytes[0] = 4;
|
|
227
|
+
pubBytes.set(xBytes, 1);
|
|
228
|
+
pubBytes.set(yBytes, 1 + xBytes.length);
|
|
229
|
+
if (name === "ECDH") {
|
|
230
|
+
if (keyUsages.length > 0) {
|
|
231
|
+
validateUsages(keyUsages, ["deriveKey", "deriveBits"]);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
validateUsages(keyUsages, ["verify"]);
|
|
235
|
+
}
|
|
236
|
+
return new CryptoKey("public", extractable, { name: alg.name, namedCurve }, keyUsages, pubBytes);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw new DOMException(`Unsupported format: ${format}`, "NotSupportedError");
|
|
240
|
+
}
|
|
241
|
+
default:
|
|
242
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ==================== exportKey ====================
|
|
246
|
+
async exportKey(format, key) {
|
|
247
|
+
if (!key.extractable) {
|
|
248
|
+
throw new DOMException("Key is not extractable", "InvalidAccessError");
|
|
249
|
+
}
|
|
250
|
+
const name = key.algorithm.name.toUpperCase();
|
|
251
|
+
if (format === "raw") {
|
|
252
|
+
if (key.type === "secret") {
|
|
253
|
+
const handle = key._handle;
|
|
254
|
+
return handle.buffer.slice(handle.byteOffset, handle.byteOffset + handle.byteLength);
|
|
255
|
+
}
|
|
256
|
+
if (key.type === "public" && (name === "ECDH" || name === "ECDSA")) {
|
|
257
|
+
const handle = key._handle;
|
|
258
|
+
return handle.buffer.slice(handle.byteOffset, handle.byteOffset + handle.byteLength);
|
|
259
|
+
}
|
|
260
|
+
throw new DOMException("Cannot export in raw format", "InvalidAccessError");
|
|
261
|
+
}
|
|
262
|
+
if (format === "jwk") {
|
|
263
|
+
if (key.type === "secret") {
|
|
264
|
+
const handle = key._handle;
|
|
265
|
+
const jwk = {
|
|
266
|
+
kty: "oct",
|
|
267
|
+
k: base64urlEncode(handle),
|
|
268
|
+
ext: key.extractable,
|
|
269
|
+
key_ops: [...key.usages]
|
|
270
|
+
};
|
|
271
|
+
if (name.startsWith("AES-")) {
|
|
272
|
+
jwk.alg = `A${key.algorithm.length}${name.replace("AES-", "")}`;
|
|
273
|
+
} else if (name === "HMAC") {
|
|
274
|
+
const hashName = key.algorithm.hash.name;
|
|
275
|
+
jwk.alg = `HS${hashName.replace("SHA-", "")}`;
|
|
276
|
+
}
|
|
277
|
+
return jwk;
|
|
278
|
+
}
|
|
279
|
+
if ((name === "ECDH" || name === "ECDSA") && (key.type === "public" || key.type === "private")) {
|
|
280
|
+
const namedCurve = key.algorithm.namedCurve;
|
|
281
|
+
let pubBytes;
|
|
282
|
+
if (key.type === "public") {
|
|
283
|
+
pubBytes = key._handle;
|
|
284
|
+
} else {
|
|
285
|
+
pubBytes = key._handle.pub;
|
|
286
|
+
}
|
|
287
|
+
const coordLen = (pubBytes.length - 1) / 2;
|
|
288
|
+
const x = pubBytes.slice(1, 1 + coordLen);
|
|
289
|
+
const y = pubBytes.slice(1 + coordLen);
|
|
290
|
+
const jwk = {
|
|
291
|
+
kty: "EC",
|
|
292
|
+
crv: namedCurve,
|
|
293
|
+
x: base64urlEncode(x),
|
|
294
|
+
y: base64urlEncode(y),
|
|
295
|
+
ext: key.extractable,
|
|
296
|
+
key_ops: [...key.usages]
|
|
297
|
+
};
|
|
298
|
+
if (key.type === "private") {
|
|
299
|
+
jwk.d = base64urlEncode(key._handle.priv);
|
|
300
|
+
}
|
|
301
|
+
return jwk;
|
|
302
|
+
}
|
|
303
|
+
throw new DOMException(`JWK export not supported for ${name} ${key.type}`, "NotSupportedError");
|
|
304
|
+
}
|
|
305
|
+
throw new DOMException(`Unsupported export format: ${format}`, "NotSupportedError");
|
|
306
|
+
}
|
|
307
|
+
// ==================== encrypt ====================
|
|
308
|
+
async encrypt(algorithm, key, data) {
|
|
309
|
+
await cryptoReady;
|
|
310
|
+
checkUsage(key, "encrypt");
|
|
311
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
312
|
+
const name = alg.name.toUpperCase();
|
|
313
|
+
const plaintext = toUint8Array(data);
|
|
314
|
+
switch (name) {
|
|
315
|
+
case "AES-CBC": {
|
|
316
|
+
const iv = toUint8Array(algorithm.iv);
|
|
317
|
+
const keyBytes = key._handle;
|
|
318
|
+
const keyLen = keyBytes.length * 8;
|
|
319
|
+
const cipher = _createCipheriv(`aes-${keyLen}-cbc`, keyBytes, iv);
|
|
320
|
+
const part1 = cipher.update(plaintext);
|
|
321
|
+
const part2 = cipher.final();
|
|
322
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
323
|
+
result.set(new Uint8Array(part1), 0);
|
|
324
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
325
|
+
return result.buffer;
|
|
326
|
+
}
|
|
327
|
+
case "AES-CTR": {
|
|
328
|
+
const counter = toUint8Array(algorithm.counter);
|
|
329
|
+
const keyBytes = key._handle;
|
|
330
|
+
const keyLen = keyBytes.length * 8;
|
|
331
|
+
const cipher = _createCipheriv(`aes-${keyLen}-ctr`, keyBytes, counter);
|
|
332
|
+
const part1 = cipher.update(plaintext);
|
|
333
|
+
const part2 = cipher.final();
|
|
334
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
335
|
+
result.set(new Uint8Array(part1), 0);
|
|
336
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
337
|
+
return result.buffer;
|
|
338
|
+
}
|
|
339
|
+
case "AES-GCM": {
|
|
340
|
+
const gcmParams = algorithm;
|
|
341
|
+
const iv = toUint8Array(gcmParams.iv);
|
|
342
|
+
const aad = gcmParams.additionalData ? toUint8Array(gcmParams.additionalData) : void 0;
|
|
343
|
+
const keyBytes = key._handle;
|
|
344
|
+
const keyLen = keyBytes.length * 8;
|
|
345
|
+
const cipher = _createCipheriv(`aes-${keyLen}-gcm`, keyBytes, iv);
|
|
346
|
+
if (aad) cipher.setAAD(aad);
|
|
347
|
+
const part1 = cipher.update(plaintext);
|
|
348
|
+
const part2 = cipher.final();
|
|
349
|
+
const tag = cipher.getAuthTag();
|
|
350
|
+
const result = new Uint8Array(part1.length + part2.length + tag.length);
|
|
351
|
+
result.set(new Uint8Array(part1), 0);
|
|
352
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
353
|
+
result.set(new Uint8Array(tag), part1.length + part2.length);
|
|
354
|
+
return result.buffer;
|
|
355
|
+
}
|
|
356
|
+
case "RSA-OAEP": {
|
|
357
|
+
const hashName = key.algorithm.hash.name;
|
|
358
|
+
const nodeHash = toNodeHashName(hashName);
|
|
359
|
+
const handle = key._handle;
|
|
360
|
+
const rsaOaepParams = algorithm;
|
|
361
|
+
const label = rsaOaepParams.label ? toUint8Array(rsaOaepParams.label) : void 0;
|
|
362
|
+
const ct = _rsaOaepEncrypt(nodeHash, handle.pem, plaintext, label);
|
|
363
|
+
return ct.buffer.slice(ct.byteOffset, ct.byteOffset + ct.byteLength);
|
|
364
|
+
}
|
|
365
|
+
default:
|
|
366
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ==================== decrypt ====================
|
|
370
|
+
async decrypt(algorithm, key, data) {
|
|
371
|
+
await cryptoReady;
|
|
372
|
+
checkUsage(key, "decrypt");
|
|
373
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
374
|
+
const name = alg.name.toUpperCase();
|
|
375
|
+
const ciphertext = toUint8Array(data);
|
|
376
|
+
switch (name) {
|
|
377
|
+
case "AES-CBC": {
|
|
378
|
+
const iv = toUint8Array(algorithm.iv);
|
|
379
|
+
const keyBytes = key._handle;
|
|
380
|
+
const keyLen = keyBytes.length * 8;
|
|
381
|
+
const decipher = _createDecipheriv(`aes-${keyLen}-cbc`, keyBytes, iv);
|
|
382
|
+
const part1 = decipher.update(ciphertext);
|
|
383
|
+
const part2 = decipher.final();
|
|
384
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
385
|
+
result.set(new Uint8Array(part1), 0);
|
|
386
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
387
|
+
return result.buffer;
|
|
388
|
+
}
|
|
389
|
+
case "AES-CTR": {
|
|
390
|
+
const counter = toUint8Array(algorithm.counter);
|
|
391
|
+
const keyBytes = key._handle;
|
|
392
|
+
const keyLen = keyBytes.length * 8;
|
|
393
|
+
const decipher = _createDecipheriv(`aes-${keyLen}-ctr`, keyBytes, counter);
|
|
394
|
+
const part1 = decipher.update(ciphertext);
|
|
395
|
+
const part2 = decipher.final();
|
|
396
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
397
|
+
result.set(new Uint8Array(part1), 0);
|
|
398
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
399
|
+
return result.buffer;
|
|
400
|
+
}
|
|
401
|
+
case "AES-GCM": {
|
|
402
|
+
const gcmParams = algorithm;
|
|
403
|
+
const iv = toUint8Array(gcmParams.iv);
|
|
404
|
+
const tagLength = (gcmParams.tagLength || 128) / 8;
|
|
405
|
+
const aad = gcmParams.additionalData ? toUint8Array(gcmParams.additionalData) : void 0;
|
|
406
|
+
const keyBytes = key._handle;
|
|
407
|
+
const keyLen = keyBytes.length * 8;
|
|
408
|
+
const actualCiphertext = ciphertext.slice(0, ciphertext.length - tagLength);
|
|
409
|
+
const tag = ciphertext.slice(ciphertext.length - tagLength);
|
|
410
|
+
const decipher = _createDecipheriv(`aes-${keyLen}-gcm`, keyBytes, iv);
|
|
411
|
+
decipher.setAuthTag(tag);
|
|
412
|
+
if (aad) decipher.setAAD(aad);
|
|
413
|
+
const part1 = decipher.update(actualCiphertext);
|
|
414
|
+
const part2 = decipher.final();
|
|
415
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
416
|
+
result.set(new Uint8Array(part1), 0);
|
|
417
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
418
|
+
return result.buffer;
|
|
419
|
+
}
|
|
420
|
+
case "RSA-OAEP": {
|
|
421
|
+
const hashName = key.algorithm.hash.name;
|
|
422
|
+
const nodeHash = toNodeHashName(hashName);
|
|
423
|
+
const handle = key._handle;
|
|
424
|
+
const rsaOaepParams = algorithm;
|
|
425
|
+
const label = rsaOaepParams.label ? toUint8Array(rsaOaepParams.label) : void 0;
|
|
426
|
+
const pt = _rsaOaepDecrypt(nodeHash, handle.pem, ciphertext, label);
|
|
427
|
+
return pt.buffer.slice(pt.byteOffset, pt.byteOffset + pt.byteLength);
|
|
428
|
+
}
|
|
429
|
+
default:
|
|
430
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ==================== sign ====================
|
|
434
|
+
async sign(algorithm, key, data) {
|
|
435
|
+
await cryptoReady;
|
|
436
|
+
checkUsage(key, "sign");
|
|
437
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
438
|
+
const name = alg.name.toUpperCase();
|
|
439
|
+
const bytes = toUint8Array(data);
|
|
440
|
+
switch (name) {
|
|
441
|
+
case "HMAC": {
|
|
442
|
+
const hashName = key.algorithm.hash.name;
|
|
443
|
+
const nodeHash = toNodeHashName(hashName);
|
|
444
|
+
const keyBytes = key._handle;
|
|
445
|
+
const hmac = _createHmac(nodeHash, keyBytes);
|
|
446
|
+
hmac.update(bytes);
|
|
447
|
+
const sig = hmac.digest();
|
|
448
|
+
return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
449
|
+
}
|
|
450
|
+
case "RSASSA-PKCS1-V1_5": {
|
|
451
|
+
const hashName = key.algorithm.hash.name;
|
|
452
|
+
const nodeHash = toNodeHashName(hashName);
|
|
453
|
+
const handle = key._handle;
|
|
454
|
+
const signer = _createSign(nodeHash);
|
|
455
|
+
signer.update(bytes);
|
|
456
|
+
const sig = signer.sign(handle.pem);
|
|
457
|
+
return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
458
|
+
}
|
|
459
|
+
case "ECDSA": {
|
|
460
|
+
const ecdsaParams = algorithm;
|
|
461
|
+
const hashName = typeof ecdsaParams.hash === "string" ? ecdsaParams.hash : ecdsaParams.hash.name;
|
|
462
|
+
const nodeHash = toNodeHashName(hashName);
|
|
463
|
+
const namedCurve = key.algorithm.namedCurve;
|
|
464
|
+
const handle = key._handle;
|
|
465
|
+
const sig = _ecdsaSign(nodeHash, handle.priv, bytes, namedCurve);
|
|
466
|
+
return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
467
|
+
}
|
|
468
|
+
case "RSA-PSS": {
|
|
469
|
+
const hashName = key.algorithm.hash.name;
|
|
470
|
+
const nodeHash = toNodeHashName(hashName);
|
|
471
|
+
const handle = key._handle;
|
|
472
|
+
const saltLen = algorithm.saltLength ?? hashSize(hashName);
|
|
473
|
+
const sig = _rsaPssSign(nodeHash, handle.pem, bytes, saltLen);
|
|
474
|
+
return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
475
|
+
}
|
|
476
|
+
default:
|
|
477
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ==================== verify ====================
|
|
481
|
+
async verify(algorithm, key, signature, data) {
|
|
482
|
+
await cryptoReady;
|
|
483
|
+
checkUsage(key, "verify");
|
|
484
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
485
|
+
const name = alg.name.toUpperCase();
|
|
486
|
+
const bytes = toUint8Array(data);
|
|
487
|
+
const sig = toUint8Array(signature);
|
|
488
|
+
switch (name) {
|
|
489
|
+
case "HMAC": {
|
|
490
|
+
const hashName = key.algorithm.hash.name;
|
|
491
|
+
const nodeHash = toNodeHashName(hashName);
|
|
492
|
+
const keyBytes = key._handle;
|
|
493
|
+
const hmac = _createHmac(nodeHash, keyBytes);
|
|
494
|
+
hmac.update(bytes);
|
|
495
|
+
const expected = new Uint8Array(hmac.digest());
|
|
496
|
+
if (expected.length !== sig.length) return false;
|
|
497
|
+
let diff = 0;
|
|
498
|
+
for (let i = 0; i < expected.length; i++) {
|
|
499
|
+
diff |= expected[i] ^ sig[i];
|
|
500
|
+
}
|
|
501
|
+
return diff === 0;
|
|
502
|
+
}
|
|
503
|
+
case "RSASSA-PKCS1-V1_5": {
|
|
504
|
+
const hashName = key.algorithm.hash.name;
|
|
505
|
+
const nodeHash = toNodeHashName(hashName);
|
|
506
|
+
const handle = key._handle;
|
|
507
|
+
const verifier = _createVerify(nodeHash);
|
|
508
|
+
verifier.update(bytes);
|
|
509
|
+
return verifier.verify(handle.pem, sig);
|
|
510
|
+
}
|
|
511
|
+
case "ECDSA": {
|
|
512
|
+
const ecdsaParams = algorithm;
|
|
513
|
+
const hashName = typeof ecdsaParams.hash === "string" ? ecdsaParams.hash : ecdsaParams.hash.name;
|
|
514
|
+
const nodeHash = toNodeHashName(hashName);
|
|
515
|
+
const namedCurve = key.algorithm.namedCurve;
|
|
516
|
+
const pubBytes = key._handle;
|
|
517
|
+
return _ecdsaVerify(nodeHash, pubBytes, sig, bytes, namedCurve);
|
|
518
|
+
}
|
|
519
|
+
case "RSA-PSS": {
|
|
520
|
+
const hashName = key.algorithm.hash.name;
|
|
521
|
+
const nodeHash = toNodeHashName(hashName);
|
|
522
|
+
const handle = key._handle;
|
|
523
|
+
const saltLen = algorithm.saltLength ?? hashSize(hashName);
|
|
524
|
+
return _rsaPssVerify(nodeHash, handle.pem, sig, bytes, saltLen);
|
|
525
|
+
}
|
|
526
|
+
default:
|
|
527
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// ==================== deriveBits ====================
|
|
531
|
+
/** Internal deriveBits without usage check (used by deriveKey) */
|
|
532
|
+
async _deriveBitsInternal(algorithm, baseKey, length) {
|
|
533
|
+
await cryptoReady;
|
|
534
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
535
|
+
const name = alg.name.toUpperCase();
|
|
536
|
+
switch (name) {
|
|
537
|
+
case "PBKDF2": {
|
|
538
|
+
const pbkdf2Params = algorithm;
|
|
539
|
+
const hashName = toNodeHashName(normalizeAlgorithm(pbkdf2Params.hash).name);
|
|
540
|
+
const salt = toUint8Array(pbkdf2Params.salt);
|
|
541
|
+
const iterations = pbkdf2Params.iterations;
|
|
542
|
+
const keyBytes = baseKey._handle;
|
|
543
|
+
const result = _pbkdf2Sync(keyBytes, salt, iterations, length / 8, hashName);
|
|
544
|
+
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
|
|
545
|
+
}
|
|
546
|
+
case "HKDF": {
|
|
547
|
+
const hkdfParams = algorithm;
|
|
548
|
+
const hashName = toNodeHashName(normalizeAlgorithm(hkdfParams.hash).name);
|
|
549
|
+
const salt = toUint8Array(hkdfParams.salt);
|
|
550
|
+
const info = toUint8Array(hkdfParams.info);
|
|
551
|
+
const keyBytes = baseKey._handle;
|
|
552
|
+
const result = _hkdfSync(hashName, keyBytes, salt, info, length / 8);
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
case "ECDH": {
|
|
556
|
+
const ecdhParams = algorithm;
|
|
557
|
+
const publicKey = ecdhParams.public;
|
|
558
|
+
const namedCurve = baseKey.algorithm.namedCurve;
|
|
559
|
+
const nodeCurve = toNodeCurveName(namedCurve);
|
|
560
|
+
const ecdh = _createECDH(nodeCurve);
|
|
561
|
+
const privHandle = baseKey._handle;
|
|
562
|
+
ecdh.setPrivateKey(privHandle.priv);
|
|
563
|
+
const pubBytes = publicKey._handle instanceof Uint8Array ? publicKey._handle : publicKey._handle.pub;
|
|
564
|
+
const secret = ecdh.computeSecret(pubBytes);
|
|
565
|
+
const secretBytes = new Uint8Array(secret);
|
|
566
|
+
if (length) {
|
|
567
|
+
return secretBytes.buffer.slice(0, length / 8);
|
|
568
|
+
}
|
|
569
|
+
return secretBytes.buffer.slice(secretBytes.byteOffset, secretBytes.byteOffset + secretBytes.byteLength);
|
|
570
|
+
}
|
|
571
|
+
default:
|
|
572
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, "NotSupportedError");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async deriveBits(algorithm, baseKey, length) {
|
|
576
|
+
checkUsage(baseKey, "deriveBits");
|
|
577
|
+
return this._deriveBitsInternal(algorithm, baseKey, length);
|
|
578
|
+
}
|
|
579
|
+
// ==================== deriveKey ====================
|
|
580
|
+
async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
|
|
581
|
+
checkUsage(baseKey, "deriveKey");
|
|
582
|
+
const derivedAlg = normalizeAlgorithm(derivedKeyAlgorithm);
|
|
583
|
+
let length;
|
|
584
|
+
const dName = derivedAlg.name.toUpperCase();
|
|
585
|
+
if (dName === "AES-CBC" || dName === "AES-CTR" || dName === "AES-GCM") {
|
|
586
|
+
length = derivedKeyAlgorithm.length;
|
|
587
|
+
} else if (dName === "HMAC") {
|
|
588
|
+
const hmacParams = derivedKeyAlgorithm;
|
|
589
|
+
const hashAlg = normalizeAlgorithm(hmacParams.hash);
|
|
590
|
+
length = hmacParams.length || hashSize(hashAlg.name) * 8;
|
|
591
|
+
} else {
|
|
592
|
+
throw new DOMException(`Unsupported derived key algorithm: ${derivedAlg.name}`, "NotSupportedError");
|
|
593
|
+
}
|
|
594
|
+
const bits = await this._deriveBitsInternal(algorithm, baseKey, length);
|
|
595
|
+
return this.importKey("raw", bits, derivedKeyAlgorithm, extractable, keyUsages);
|
|
596
|
+
}
|
|
597
|
+
// ==================== wrapKey / unwrapKey (stubs) ====================
|
|
598
|
+
async wrapKey(_format, _key, _wrappingKey, _wrapAlgorithm) {
|
|
599
|
+
throw new DOMException("wrapKey not yet implemented", "NotSupportedError");
|
|
600
|
+
}
|
|
601
|
+
async unwrapKey(_format, _wrappedKey, _unwrappingKey, _unwrapAlgorithm, _unwrappedKeyAlgorithm, _extractable, _keyUsages) {
|
|
602
|
+
throw new DOMException("unwrapKey not yet implemented", "NotSupportedError");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
export {
|
|
606
|
+
SubtleCrypto
|
|
607
|
+
};
|