@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
package/src/subtle.ts
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
// W3C SubtleCrypto for GJS
|
|
2
|
+
// Reference: refs/deno/ext/crypto/00_crypto.js
|
|
3
|
+
// Copyright (c) 2018-2026 the Deno authors. MIT license.
|
|
4
|
+
// Reimplemented for GJS using @gjsify/crypto primitives.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CryptoKey,
|
|
8
|
+
type CryptoKeyPair,
|
|
9
|
+
type KeyUsage,
|
|
10
|
+
type KeyAlgorithm,
|
|
11
|
+
type AlgorithmIdentifier,
|
|
12
|
+
type AesKeyGenParams,
|
|
13
|
+
type HmacKeyGenParams,
|
|
14
|
+
type EcKeyGenParams,
|
|
15
|
+
type HmacImportParams,
|
|
16
|
+
type EcKeyImportParams,
|
|
17
|
+
type AesCbcParams,
|
|
18
|
+
type AesCtrParams,
|
|
19
|
+
type AesGcmParams,
|
|
20
|
+
type RsaOaepParams,
|
|
21
|
+
type EcdsaParams,
|
|
22
|
+
type RsaPssParams,
|
|
23
|
+
type Pbkdf2Params,
|
|
24
|
+
type HkdfParams,
|
|
25
|
+
type EcdhKeyDeriveParams,
|
|
26
|
+
type AesKeyAlgorithm,
|
|
27
|
+
type HmacKeyAlgorithm,
|
|
28
|
+
type EcKeyAlgorithm,
|
|
29
|
+
type RsaHashedKeyAlgorithm,
|
|
30
|
+
type EcKeyPairHandle,
|
|
31
|
+
type RsaPemHandle,
|
|
32
|
+
type HashLike,
|
|
33
|
+
type HmacLike,
|
|
34
|
+
type CipherLike,
|
|
35
|
+
type DecipherLike,
|
|
36
|
+
type SignerLike,
|
|
37
|
+
type VerifierLike,
|
|
38
|
+
type EcdhLike,
|
|
39
|
+
} from './crypto-key.js';
|
|
40
|
+
import {
|
|
41
|
+
normalizeAlgorithm,
|
|
42
|
+
toNodeHashName,
|
|
43
|
+
toNodeCurveName,
|
|
44
|
+
hashSize,
|
|
45
|
+
validateUsages,
|
|
46
|
+
checkUsage,
|
|
47
|
+
base64urlEncode,
|
|
48
|
+
base64urlDecode,
|
|
49
|
+
toUint8Array,
|
|
50
|
+
} from './util.js';
|
|
51
|
+
|
|
52
|
+
// Lazy-loaded crypto imports to avoid circular deps during bundling
|
|
53
|
+
let _cryptoLoaded = false;
|
|
54
|
+
let _createHash: (alg: string) => HashLike;
|
|
55
|
+
let _createHmac: (alg: string, key: Uint8Array) => HmacLike;
|
|
56
|
+
let _createCipheriv: (alg: string, key: Uint8Array, iv: Uint8Array) => CipherLike;
|
|
57
|
+
let _createDecipheriv: (alg: string, key: Uint8Array, iv: Uint8Array) => DecipherLike;
|
|
58
|
+
let _createSign: (alg: string) => SignerLike;
|
|
59
|
+
let _createVerify: (alg: string) => VerifierLike;
|
|
60
|
+
let _pbkdf2Sync: (pass: Uint8Array, salt: Uint8Array, iter: number, keylen: number, digest: string) => Uint8Array;
|
|
61
|
+
let _hkdfSync: (digest: string, ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, keylen: number) => ArrayBuffer;
|
|
62
|
+
let _createECDH: (curve: string) => EcdhLike;
|
|
63
|
+
let _randomBytes: (size: number) => Uint8Array;
|
|
64
|
+
let _publicEncrypt: (key: unknown, buffer: Uint8Array) => Uint8Array;
|
|
65
|
+
let _privateDecrypt: (key: unknown, buffer: Uint8Array) => Uint8Array;
|
|
66
|
+
let _createPublicKey: (key: unknown) => unknown;
|
|
67
|
+
let _createPrivateKey: (key: unknown) => unknown;
|
|
68
|
+
let _ecdsaSign: (hashAlgo: string, privKeyBytes: Uint8Array, data: Uint8Array, curveName: string) => Uint8Array;
|
|
69
|
+
let _ecdsaVerify: (hashAlgo: string, pubKeyBytes: Uint8Array, signature: Uint8Array, data: Uint8Array, curveName: string) => boolean;
|
|
70
|
+
let _rsaPssSign: (hashAlgo: string, privKeyPem: string, data: Uint8Array, saltLength: number) => Uint8Array;
|
|
71
|
+
let _rsaPssVerify: (hashAlgo: string, pubKeyPem: string, signature: Uint8Array, data: Uint8Array, saltLength: number) => boolean;
|
|
72
|
+
let _rsaOaepEncrypt: (hashAlgo: string, pubKeyPem: string, plaintext: Uint8Array, label?: Uint8Array) => Uint8Array;
|
|
73
|
+
let _rsaOaepDecrypt: (hashAlgo: string, privKeyPem: string, ciphertext: Uint8Array, label?: Uint8Array) => Uint8Array;
|
|
74
|
+
|
|
75
|
+
async function loadCrypto(): Promise<void> {
|
|
76
|
+
if (_cryptoLoaded) return;
|
|
77
|
+
const crypto = await import('crypto') as Record<string, unknown>;
|
|
78
|
+
_createHash = crypto.createHash as typeof _createHash;
|
|
79
|
+
_createHmac = crypto.createHmac as typeof _createHmac;
|
|
80
|
+
_createCipheriv = crypto.createCipheriv as typeof _createCipheriv;
|
|
81
|
+
_createDecipheriv = crypto.createDecipheriv as typeof _createDecipheriv;
|
|
82
|
+
_createSign = crypto.createSign as typeof _createSign;
|
|
83
|
+
_createVerify = crypto.createVerify as typeof _createVerify;
|
|
84
|
+
_pbkdf2Sync = crypto.pbkdf2Sync as typeof _pbkdf2Sync;
|
|
85
|
+
_hkdfSync = crypto.hkdfSync as typeof _hkdfSync;
|
|
86
|
+
_createECDH = crypto.createECDH as typeof _createECDH;
|
|
87
|
+
_randomBytes = crypto.randomBytes as typeof _randomBytes;
|
|
88
|
+
_publicEncrypt = crypto.publicEncrypt as typeof _publicEncrypt;
|
|
89
|
+
_privateDecrypt = crypto.privateDecrypt as typeof _privateDecrypt;
|
|
90
|
+
_createPublicKey = crypto.createPublicKey as typeof _createPublicKey;
|
|
91
|
+
_createPrivateKey = crypto.createPrivateKey as typeof _createPrivateKey;
|
|
92
|
+
_ecdsaSign = crypto.ecdsaSign as typeof _ecdsaSign;
|
|
93
|
+
_ecdsaVerify = crypto.ecdsaVerify as typeof _ecdsaVerify;
|
|
94
|
+
_rsaPssSign = crypto.rsaPssSign as typeof _rsaPssSign;
|
|
95
|
+
_rsaPssVerify = crypto.rsaPssVerify as typeof _rsaPssVerify;
|
|
96
|
+
_rsaOaepEncrypt = crypto.rsaOaepEncrypt as typeof _rsaOaepEncrypt;
|
|
97
|
+
_rsaOaepDecrypt = crypto.rsaOaepDecrypt as typeof _rsaOaepDecrypt;
|
|
98
|
+
_cryptoLoaded = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Eagerly start loading
|
|
102
|
+
const cryptoReady = loadCrypto();
|
|
103
|
+
|
|
104
|
+
function ensureCrypto(): void {
|
|
105
|
+
if (!_cryptoLoaded) {
|
|
106
|
+
throw new Error('crypto not yet loaded. Ensure module initialization is complete.');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* SubtleCrypto provides cryptographic primitives per the W3C WebCrypto API.
|
|
112
|
+
*/
|
|
113
|
+
export class SubtleCrypto {
|
|
114
|
+
|
|
115
|
+
// ==================== digest ====================
|
|
116
|
+
|
|
117
|
+
async digest(
|
|
118
|
+
algorithm: string | { name: string },
|
|
119
|
+
data: BufferSource,
|
|
120
|
+
): Promise<ArrayBuffer> {
|
|
121
|
+
await cryptoReady;
|
|
122
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
123
|
+
const nodeName = toNodeHashName(alg.name);
|
|
124
|
+
const bytes = toUint8Array(data);
|
|
125
|
+
const hash = _createHash(nodeName);
|
|
126
|
+
hash.update(bytes);
|
|
127
|
+
const result = hash.digest();
|
|
128
|
+
return (result.buffer as ArrayBuffer).slice(result.byteOffset, result.byteOffset + result.byteLength);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ==================== generateKey ====================
|
|
132
|
+
|
|
133
|
+
async generateKey(
|
|
134
|
+
algorithm: AlgorithmIdentifier,
|
|
135
|
+
extractable: boolean,
|
|
136
|
+
keyUsages: KeyUsage[],
|
|
137
|
+
): Promise<CryptoKey | CryptoKeyPair> {
|
|
138
|
+
await cryptoReady;
|
|
139
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
140
|
+
const name = alg.name.toUpperCase();
|
|
141
|
+
|
|
142
|
+
switch (name) {
|
|
143
|
+
case 'AES-CBC':
|
|
144
|
+
case 'AES-CTR':
|
|
145
|
+
case 'AES-GCM': {
|
|
146
|
+
const length = (algorithm as AesKeyGenParams).length;
|
|
147
|
+
if (![128, 192, 256].includes(length)) {
|
|
148
|
+
throw new DOMException(`Invalid AES key length: ${length}`, 'OperationError');
|
|
149
|
+
}
|
|
150
|
+
validateUsages(keyUsages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']);
|
|
151
|
+
const keyData = _randomBytes(length / 8);
|
|
152
|
+
return new CryptoKey('secret', extractable, { name: alg.name, length }, keyUsages, new Uint8Array(keyData));
|
|
153
|
+
}
|
|
154
|
+
case 'HMAC': {
|
|
155
|
+
const hmacParams = algorithm as HmacKeyGenParams;
|
|
156
|
+
const hashAlg = normalizeAlgorithm(hmacParams.hash);
|
|
157
|
+
const length = hmacParams.length || hashSize(hashAlg.name) * 8;
|
|
158
|
+
validateUsages(keyUsages, ['sign', 'verify']);
|
|
159
|
+
const keyData = _randomBytes(Math.ceil(length / 8));
|
|
160
|
+
return new CryptoKey('secret', extractable, { name: 'HMAC', hash: { name: hashAlg.name }, length }, keyUsages, new Uint8Array(keyData));
|
|
161
|
+
}
|
|
162
|
+
case 'ECDH': {
|
|
163
|
+
const namedCurve = (algorithm as EcKeyGenParams).namedCurve;
|
|
164
|
+
const nodeCurve = toNodeCurveName(namedCurve);
|
|
165
|
+
validateUsages(keyUsages, ['deriveKey', 'deriveBits']);
|
|
166
|
+
const ecdh = _createECDH(nodeCurve);
|
|
167
|
+
ecdh.generateKeys();
|
|
168
|
+
const pubBytes = new Uint8Array(ecdh.getPublicKey());
|
|
169
|
+
const privBytes = new Uint8Array(ecdh.getPrivateKey());
|
|
170
|
+
const pubKey = new CryptoKey('public', true, { name: 'ECDH', namedCurve }, [], pubBytes);
|
|
171
|
+
const privKey = new CryptoKey('private', extractable, { name: 'ECDH', namedCurve }, keyUsages, { pub: pubBytes, priv: privBytes } satisfies EcKeyPairHandle);
|
|
172
|
+
return { publicKey: pubKey, privateKey: privKey } as CryptoKeyPair;
|
|
173
|
+
}
|
|
174
|
+
case 'ECDSA': {
|
|
175
|
+
const namedCurve = (algorithm as EcKeyGenParams).namedCurve;
|
|
176
|
+
const nodeCurve = toNodeCurveName(namedCurve);
|
|
177
|
+
validateUsages(keyUsages, ['sign', 'verify']);
|
|
178
|
+
const ecdh = _createECDH(nodeCurve);
|
|
179
|
+
ecdh.generateKeys();
|
|
180
|
+
const pubBytes = new Uint8Array(ecdh.getPublicKey());
|
|
181
|
+
const privBytes = new Uint8Array(ecdh.getPrivateKey());
|
|
182
|
+
const pubKey = new CryptoKey('public', true, { name: 'ECDSA', namedCurve }, ['verify'], pubBytes);
|
|
183
|
+
const privKey = new CryptoKey('private', extractable, { name: 'ECDSA', namedCurve }, ['sign'], { pub: pubBytes, priv: privBytes } satisfies EcKeyPairHandle);
|
|
184
|
+
return { publicKey: pubKey, privateKey: privKey } as CryptoKeyPair;
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ==================== importKey ====================
|
|
192
|
+
|
|
193
|
+
async importKey(
|
|
194
|
+
format: 'raw' | 'jwk' | 'pkcs8' | 'spki',
|
|
195
|
+
keyData: BufferSource | JsonWebKey,
|
|
196
|
+
algorithm: AlgorithmIdentifier,
|
|
197
|
+
extractable: boolean,
|
|
198
|
+
keyUsages: KeyUsage[],
|
|
199
|
+
): Promise<CryptoKey> {
|
|
200
|
+
await cryptoReady;
|
|
201
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
202
|
+
const name = alg.name.toUpperCase();
|
|
203
|
+
|
|
204
|
+
switch (name) {
|
|
205
|
+
case 'AES-CBC':
|
|
206
|
+
case 'AES-CTR':
|
|
207
|
+
case 'AES-GCM': {
|
|
208
|
+
if (format === 'raw') {
|
|
209
|
+
const bytes = toUint8Array(keyData as BufferSource);
|
|
210
|
+
if (![16, 24, 32].includes(bytes.length)) {
|
|
211
|
+
throw new DOMException(`Invalid AES key length: ${bytes.length * 8}`, 'DataError');
|
|
212
|
+
}
|
|
213
|
+
validateUsages(keyUsages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']);
|
|
214
|
+
return new CryptoKey('secret', extractable, { name: alg.name, length: bytes.length * 8 }, keyUsages, new Uint8Array(bytes));
|
|
215
|
+
}
|
|
216
|
+
if (format === 'jwk') {
|
|
217
|
+
const jwk = keyData as JsonWebKey;
|
|
218
|
+
if (jwk.kty !== 'oct') throw new DOMException('JWK kty must be "oct"', 'DataError');
|
|
219
|
+
const bytes = base64urlDecode(jwk.k!);
|
|
220
|
+
if (![16, 24, 32].includes(bytes.length)) {
|
|
221
|
+
throw new DOMException(`Invalid AES key length: ${bytes.length * 8}`, 'DataError');
|
|
222
|
+
}
|
|
223
|
+
validateUsages(keyUsages, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']);
|
|
224
|
+
return new CryptoKey('secret', extractable, { name: alg.name, length: bytes.length * 8 }, keyUsages, bytes);
|
|
225
|
+
}
|
|
226
|
+
throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
|
|
227
|
+
}
|
|
228
|
+
case 'HMAC': {
|
|
229
|
+
const hmacParams = algorithm as HmacImportParams;
|
|
230
|
+
const hashAlg = normalizeAlgorithm(hmacParams.hash);
|
|
231
|
+
if (format === 'raw') {
|
|
232
|
+
const bytes = toUint8Array(keyData as BufferSource);
|
|
233
|
+
validateUsages(keyUsages, ['sign', 'verify']);
|
|
234
|
+
return new CryptoKey('secret', extractable, { name: 'HMAC', hash: { name: hashAlg.name }, length: bytes.length * 8 }, keyUsages, new Uint8Array(bytes));
|
|
235
|
+
}
|
|
236
|
+
if (format === 'jwk') {
|
|
237
|
+
const jwk = keyData as JsonWebKey;
|
|
238
|
+
if (jwk.kty !== 'oct') throw new DOMException('JWK kty must be "oct"', 'DataError');
|
|
239
|
+
const bytes = base64urlDecode(jwk.k!);
|
|
240
|
+
validateUsages(keyUsages, ['sign', 'verify']);
|
|
241
|
+
return new CryptoKey('secret', extractable, { name: 'HMAC', hash: { name: hashAlg.name }, length: bytes.length * 8 }, keyUsages, bytes);
|
|
242
|
+
}
|
|
243
|
+
throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
|
|
244
|
+
}
|
|
245
|
+
case 'PBKDF2': {
|
|
246
|
+
if (format === 'raw') {
|
|
247
|
+
const bytes = toUint8Array(keyData as BufferSource);
|
|
248
|
+
validateUsages(keyUsages, ['deriveKey', 'deriveBits']);
|
|
249
|
+
return new CryptoKey('secret', false, { name: 'PBKDF2' }, keyUsages, new Uint8Array(bytes));
|
|
250
|
+
}
|
|
251
|
+
throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
|
|
252
|
+
}
|
|
253
|
+
case 'HKDF': {
|
|
254
|
+
if (format === 'raw') {
|
|
255
|
+
const bytes = toUint8Array(keyData as BufferSource);
|
|
256
|
+
validateUsages(keyUsages, ['deriveKey', 'deriveBits']);
|
|
257
|
+
return new CryptoKey('secret', false, { name: 'HKDF' }, keyUsages, new Uint8Array(bytes));
|
|
258
|
+
}
|
|
259
|
+
throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
|
|
260
|
+
}
|
|
261
|
+
case 'ECDH':
|
|
262
|
+
case 'ECDSA': {
|
|
263
|
+
const namedCurve = (algorithm as EcKeyImportParams).namedCurve;
|
|
264
|
+
if (format === 'raw') {
|
|
265
|
+
// Raw import of public key (uncompressed point)
|
|
266
|
+
const bytes = toUint8Array(keyData as BufferSource);
|
|
267
|
+
// ECDH public keys are allowed to have empty usages per the W3C spec
|
|
268
|
+
if (name === 'ECDH') {
|
|
269
|
+
if (keyUsages.length > 0) {
|
|
270
|
+
validateUsages(keyUsages, ['deriveKey', 'deriveBits']);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
validateUsages(keyUsages, ['verify']);
|
|
274
|
+
}
|
|
275
|
+
return new CryptoKey('public', extractable, { name: alg.name, namedCurve }, keyUsages, new Uint8Array(bytes));
|
|
276
|
+
}
|
|
277
|
+
if (format === 'jwk') {
|
|
278
|
+
const jwk = keyData as JsonWebKey;
|
|
279
|
+
if (jwk.kty !== 'EC') throw new DOMException('JWK kty must be "EC"', 'DataError');
|
|
280
|
+
if (jwk.d) {
|
|
281
|
+
// Private key
|
|
282
|
+
const privBytes = base64urlDecode(jwk.d);
|
|
283
|
+
const xBytes = base64urlDecode(jwk.x!);
|
|
284
|
+
const yBytes = base64urlDecode(jwk.y!);
|
|
285
|
+
// Reconstruct uncompressed public key point: 0x04 || x || y
|
|
286
|
+
const pubBytes = new Uint8Array(1 + xBytes.length + yBytes.length);
|
|
287
|
+
pubBytes[0] = 0x04;
|
|
288
|
+
pubBytes.set(xBytes, 1);
|
|
289
|
+
pubBytes.set(yBytes, 1 + xBytes.length);
|
|
290
|
+
const allowedUsages: KeyUsage[] = name === 'ECDH' ? ['deriveKey', 'deriveBits'] : ['sign'];
|
|
291
|
+
validateUsages(keyUsages, allowedUsages);
|
|
292
|
+
return new CryptoKey('private', extractable, { name: alg.name, namedCurve }, keyUsages, { pub: pubBytes, priv: privBytes } satisfies EcKeyPairHandle);
|
|
293
|
+
} else {
|
|
294
|
+
// Public key
|
|
295
|
+
const xBytes = base64urlDecode(jwk.x!);
|
|
296
|
+
const yBytes = base64urlDecode(jwk.y!);
|
|
297
|
+
const pubBytes = new Uint8Array(1 + xBytes.length + yBytes.length);
|
|
298
|
+
pubBytes[0] = 0x04;
|
|
299
|
+
pubBytes.set(xBytes, 1);
|
|
300
|
+
pubBytes.set(yBytes, 1 + xBytes.length);
|
|
301
|
+
// ECDH public keys are allowed to have empty usages per the W3C spec
|
|
302
|
+
if (name === 'ECDH') {
|
|
303
|
+
if (keyUsages.length > 0) {
|
|
304
|
+
validateUsages(keyUsages, ['deriveKey', 'deriveBits']);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
validateUsages(keyUsages, ['verify']);
|
|
308
|
+
}
|
|
309
|
+
return new CryptoKey('public', extractable, { name: alg.name, namedCurve }, keyUsages, pubBytes);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
|
|
313
|
+
}
|
|
314
|
+
default:
|
|
315
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ==================== exportKey ====================
|
|
320
|
+
|
|
321
|
+
async exportKey(
|
|
322
|
+
format: 'raw' | 'jwk' | 'pkcs8' | 'spki',
|
|
323
|
+
key: CryptoKey,
|
|
324
|
+
): Promise<ArrayBuffer | JsonWebKey> {
|
|
325
|
+
if (!key.extractable) {
|
|
326
|
+
throw new DOMException('Key is not extractable', 'InvalidAccessError');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const name = key.algorithm.name.toUpperCase();
|
|
330
|
+
|
|
331
|
+
if (format === 'raw') {
|
|
332
|
+
if (key.type === 'secret') {
|
|
333
|
+
const handle = key._handle as Uint8Array;
|
|
334
|
+
return (handle.buffer as ArrayBuffer).slice(handle.byteOffset, handle.byteOffset + handle.byteLength);
|
|
335
|
+
}
|
|
336
|
+
if (key.type === 'public' && (name === 'ECDH' || name === 'ECDSA')) {
|
|
337
|
+
const handle = key._handle as Uint8Array;
|
|
338
|
+
return (handle.buffer as ArrayBuffer).slice(handle.byteOffset, handle.byteOffset + handle.byteLength);
|
|
339
|
+
}
|
|
340
|
+
throw new DOMException('Cannot export in raw format', 'InvalidAccessError');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (format === 'jwk') {
|
|
344
|
+
if (key.type === 'secret') {
|
|
345
|
+
const handle = key._handle as Uint8Array;
|
|
346
|
+
const jwk: JsonWebKey = {
|
|
347
|
+
kty: 'oct',
|
|
348
|
+
k: base64urlEncode(handle),
|
|
349
|
+
ext: key.extractable,
|
|
350
|
+
key_ops: [...key.usages],
|
|
351
|
+
};
|
|
352
|
+
if (name.startsWith('AES-')) {
|
|
353
|
+
jwk.alg = `A${(key.algorithm as AesKeyAlgorithm).length}${name.replace('AES-', '')}`;
|
|
354
|
+
} else if (name === 'HMAC') {
|
|
355
|
+
const hashName = (key.algorithm as HmacKeyAlgorithm).hash.name;
|
|
356
|
+
jwk.alg = `HS${hashName.replace('SHA-', '')}`;
|
|
357
|
+
}
|
|
358
|
+
return jwk;
|
|
359
|
+
}
|
|
360
|
+
if ((name === 'ECDH' || name === 'ECDSA') && (key.type === 'public' || key.type === 'private')) {
|
|
361
|
+
const namedCurve = (key.algorithm as EcKeyAlgorithm).namedCurve;
|
|
362
|
+
let pubBytes: Uint8Array;
|
|
363
|
+
if (key.type === 'public') {
|
|
364
|
+
pubBytes = key._handle as Uint8Array;
|
|
365
|
+
} else {
|
|
366
|
+
pubBytes = (key._handle as EcKeyPairHandle).pub;
|
|
367
|
+
}
|
|
368
|
+
// Parse uncompressed point: 0x04 || x || y
|
|
369
|
+
const coordLen = (pubBytes.length - 1) / 2;
|
|
370
|
+
const x = pubBytes.slice(1, 1 + coordLen);
|
|
371
|
+
const y = pubBytes.slice(1 + coordLen);
|
|
372
|
+
const jwk: JsonWebKey = {
|
|
373
|
+
kty: 'EC',
|
|
374
|
+
crv: namedCurve,
|
|
375
|
+
x: base64urlEncode(x),
|
|
376
|
+
y: base64urlEncode(y),
|
|
377
|
+
ext: key.extractable,
|
|
378
|
+
key_ops: [...key.usages],
|
|
379
|
+
};
|
|
380
|
+
if (key.type === 'private') {
|
|
381
|
+
jwk.d = base64urlEncode((key._handle as EcKeyPairHandle).priv);
|
|
382
|
+
}
|
|
383
|
+
return jwk;
|
|
384
|
+
}
|
|
385
|
+
throw new DOMException(`JWK export not supported for ${name} ${key.type}`, 'NotSupportedError');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
throw new DOMException(`Unsupported export format: ${format}`, 'NotSupportedError');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ==================== encrypt ====================
|
|
392
|
+
|
|
393
|
+
async encrypt(
|
|
394
|
+
algorithm: AlgorithmIdentifier,
|
|
395
|
+
key: CryptoKey,
|
|
396
|
+
data: BufferSource,
|
|
397
|
+
): Promise<ArrayBuffer> {
|
|
398
|
+
await cryptoReady;
|
|
399
|
+
checkUsage(key, 'encrypt');
|
|
400
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
401
|
+
const name = alg.name.toUpperCase();
|
|
402
|
+
const plaintext = toUint8Array(data);
|
|
403
|
+
|
|
404
|
+
switch (name) {
|
|
405
|
+
case 'AES-CBC': {
|
|
406
|
+
const iv = toUint8Array((algorithm as AesCbcParams).iv);
|
|
407
|
+
const keyBytes = key._handle as Uint8Array;
|
|
408
|
+
const keyLen = keyBytes.length * 8;
|
|
409
|
+
const cipher = _createCipheriv(`aes-${keyLen}-cbc`, keyBytes, iv);
|
|
410
|
+
const part1 = cipher.update(plaintext);
|
|
411
|
+
const part2 = cipher.final();
|
|
412
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
413
|
+
result.set(new Uint8Array(part1), 0);
|
|
414
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
415
|
+
return result.buffer;
|
|
416
|
+
}
|
|
417
|
+
case 'AES-CTR': {
|
|
418
|
+
const counter = toUint8Array((algorithm as AesCtrParams).counter);
|
|
419
|
+
const keyBytes = key._handle as Uint8Array;
|
|
420
|
+
const keyLen = keyBytes.length * 8;
|
|
421
|
+
const cipher = _createCipheriv(`aes-${keyLen}-ctr`, keyBytes, counter);
|
|
422
|
+
const part1 = cipher.update(plaintext);
|
|
423
|
+
const part2 = cipher.final();
|
|
424
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
425
|
+
result.set(new Uint8Array(part1), 0);
|
|
426
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
427
|
+
return result.buffer;
|
|
428
|
+
}
|
|
429
|
+
case 'AES-GCM': {
|
|
430
|
+
const gcmParams = algorithm as AesGcmParams;
|
|
431
|
+
const iv = toUint8Array(gcmParams.iv);
|
|
432
|
+
const aad = gcmParams.additionalData ? toUint8Array(gcmParams.additionalData) : undefined;
|
|
433
|
+
const keyBytes = key._handle as Uint8Array;
|
|
434
|
+
const keyLen = keyBytes.length * 8;
|
|
435
|
+
const cipher = _createCipheriv(`aes-${keyLen}-gcm`, keyBytes, iv);
|
|
436
|
+
if (aad) cipher.setAAD!(aad);
|
|
437
|
+
const part1 = cipher.update(plaintext);
|
|
438
|
+
const part2 = cipher.final();
|
|
439
|
+
const tag = cipher.getAuthTag!();
|
|
440
|
+
const result = new Uint8Array(part1.length + part2.length + tag.length);
|
|
441
|
+
result.set(new Uint8Array(part1), 0);
|
|
442
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
443
|
+
result.set(new Uint8Array(tag), part1.length + part2.length);
|
|
444
|
+
return result.buffer;
|
|
445
|
+
}
|
|
446
|
+
case 'RSA-OAEP': {
|
|
447
|
+
const hashName = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
448
|
+
const nodeHash = toNodeHashName(hashName);
|
|
449
|
+
const handle = key._handle as RsaPemHandle;
|
|
450
|
+
const rsaOaepParams = algorithm as RsaOaepParams;
|
|
451
|
+
const label = rsaOaepParams.label ? toUint8Array(rsaOaepParams.label) : undefined;
|
|
452
|
+
const ct = _rsaOaepEncrypt(nodeHash, handle.pem, plaintext, label);
|
|
453
|
+
return (ct.buffer as ArrayBuffer).slice(ct.byteOffset, ct.byteOffset + ct.byteLength);
|
|
454
|
+
}
|
|
455
|
+
default:
|
|
456
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ==================== decrypt ====================
|
|
461
|
+
|
|
462
|
+
async decrypt(
|
|
463
|
+
algorithm: AlgorithmIdentifier,
|
|
464
|
+
key: CryptoKey,
|
|
465
|
+
data: BufferSource,
|
|
466
|
+
): Promise<ArrayBuffer> {
|
|
467
|
+
await cryptoReady;
|
|
468
|
+
checkUsage(key, 'decrypt');
|
|
469
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
470
|
+
const name = alg.name.toUpperCase();
|
|
471
|
+
const ciphertext = toUint8Array(data);
|
|
472
|
+
|
|
473
|
+
switch (name) {
|
|
474
|
+
case 'AES-CBC': {
|
|
475
|
+
const iv = toUint8Array((algorithm as AesCbcParams).iv);
|
|
476
|
+
const keyBytes = key._handle as Uint8Array;
|
|
477
|
+
const keyLen = keyBytes.length * 8;
|
|
478
|
+
const decipher = _createDecipheriv(`aes-${keyLen}-cbc`, keyBytes, iv);
|
|
479
|
+
const part1 = decipher.update(ciphertext);
|
|
480
|
+
const part2 = decipher.final();
|
|
481
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
482
|
+
result.set(new Uint8Array(part1), 0);
|
|
483
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
484
|
+
return result.buffer;
|
|
485
|
+
}
|
|
486
|
+
case 'AES-CTR': {
|
|
487
|
+
const counter = toUint8Array((algorithm as AesCtrParams).counter);
|
|
488
|
+
const keyBytes = key._handle as Uint8Array;
|
|
489
|
+
const keyLen = keyBytes.length * 8;
|
|
490
|
+
const decipher = _createDecipheriv(`aes-${keyLen}-ctr`, keyBytes, counter);
|
|
491
|
+
const part1 = decipher.update(ciphertext);
|
|
492
|
+
const part2 = decipher.final();
|
|
493
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
494
|
+
result.set(new Uint8Array(part1), 0);
|
|
495
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
496
|
+
return result.buffer;
|
|
497
|
+
}
|
|
498
|
+
case 'AES-GCM': {
|
|
499
|
+
const gcmParams = algorithm as AesGcmParams;
|
|
500
|
+
const iv = toUint8Array(gcmParams.iv);
|
|
501
|
+
const tagLength = (gcmParams.tagLength || 128) / 8;
|
|
502
|
+
const aad = gcmParams.additionalData ? toUint8Array(gcmParams.additionalData) : undefined;
|
|
503
|
+
const keyBytes = key._handle as Uint8Array;
|
|
504
|
+
const keyLen = keyBytes.length * 8;
|
|
505
|
+
const actualCiphertext = ciphertext.slice(0, ciphertext.length - tagLength);
|
|
506
|
+
const tag = ciphertext.slice(ciphertext.length - tagLength);
|
|
507
|
+
const decipher = _createDecipheriv(`aes-${keyLen}-gcm`, keyBytes, iv);
|
|
508
|
+
decipher.setAuthTag!(tag);
|
|
509
|
+
if (aad) decipher.setAAD!(aad);
|
|
510
|
+
const part1 = decipher.update(actualCiphertext);
|
|
511
|
+
const part2 = decipher.final();
|
|
512
|
+
const result = new Uint8Array(part1.length + part2.length);
|
|
513
|
+
result.set(new Uint8Array(part1), 0);
|
|
514
|
+
result.set(new Uint8Array(part2), part1.length);
|
|
515
|
+
return result.buffer;
|
|
516
|
+
}
|
|
517
|
+
case 'RSA-OAEP': {
|
|
518
|
+
const hashName = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
519
|
+
const nodeHash = toNodeHashName(hashName);
|
|
520
|
+
const handle = key._handle as RsaPemHandle;
|
|
521
|
+
const rsaOaepParams = algorithm as RsaOaepParams;
|
|
522
|
+
const label = rsaOaepParams.label ? toUint8Array(rsaOaepParams.label) : undefined;
|
|
523
|
+
const pt = _rsaOaepDecrypt(nodeHash, handle.pem, ciphertext, label);
|
|
524
|
+
return (pt.buffer as ArrayBuffer).slice(pt.byteOffset, pt.byteOffset + pt.byteLength);
|
|
525
|
+
}
|
|
526
|
+
default:
|
|
527
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ==================== sign ====================
|
|
532
|
+
|
|
533
|
+
async sign(
|
|
534
|
+
algorithm: AlgorithmIdentifier,
|
|
535
|
+
key: CryptoKey,
|
|
536
|
+
data: BufferSource,
|
|
537
|
+
): Promise<ArrayBuffer> {
|
|
538
|
+
await cryptoReady;
|
|
539
|
+
checkUsage(key, 'sign');
|
|
540
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
541
|
+
const name = alg.name.toUpperCase();
|
|
542
|
+
const bytes = toUint8Array(data);
|
|
543
|
+
|
|
544
|
+
switch (name) {
|
|
545
|
+
case 'HMAC': {
|
|
546
|
+
const hashName = (key.algorithm as HmacKeyAlgorithm).hash.name;
|
|
547
|
+
const nodeHash = toNodeHashName(hashName);
|
|
548
|
+
const keyBytes = key._handle as Uint8Array;
|
|
549
|
+
const hmac = _createHmac(nodeHash, keyBytes);
|
|
550
|
+
hmac.update(bytes);
|
|
551
|
+
const sig = hmac.digest();
|
|
552
|
+
return (sig.buffer as ArrayBuffer).slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
553
|
+
}
|
|
554
|
+
case 'RSASSA-PKCS1-V1_5': {
|
|
555
|
+
const hashName = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
556
|
+
const nodeHash = toNodeHashName(hashName);
|
|
557
|
+
const handle = key._handle as RsaPemHandle;
|
|
558
|
+
const signer = _createSign(nodeHash);
|
|
559
|
+
signer.update(bytes);
|
|
560
|
+
const sig = signer.sign(handle.pem);
|
|
561
|
+
return (sig.buffer as ArrayBuffer).slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
562
|
+
}
|
|
563
|
+
case 'ECDSA': {
|
|
564
|
+
const ecdsaParams = algorithm as EcdsaParams;
|
|
565
|
+
const hashName = typeof ecdsaParams.hash === 'string' ? ecdsaParams.hash : ecdsaParams.hash.name;
|
|
566
|
+
const nodeHash = toNodeHashName(hashName);
|
|
567
|
+
const namedCurve = (key.algorithm as EcKeyAlgorithm).namedCurve;
|
|
568
|
+
const handle = key._handle as EcKeyPairHandle;
|
|
569
|
+
const sig = _ecdsaSign(nodeHash, handle.priv, bytes, namedCurve);
|
|
570
|
+
return (sig.buffer as ArrayBuffer).slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
571
|
+
}
|
|
572
|
+
case 'RSA-PSS': {
|
|
573
|
+
const hashName = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
574
|
+
const nodeHash = toNodeHashName(hashName);
|
|
575
|
+
const handle = key._handle as RsaPemHandle;
|
|
576
|
+
const saltLen = (algorithm as RsaPssParams).saltLength ?? hashSize(hashName);
|
|
577
|
+
const sig = _rsaPssSign(nodeHash, handle.pem, bytes, saltLen);
|
|
578
|
+
return (sig.buffer as ArrayBuffer).slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
|
|
579
|
+
}
|
|
580
|
+
default:
|
|
581
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ==================== verify ====================
|
|
586
|
+
|
|
587
|
+
async verify(
|
|
588
|
+
algorithm: AlgorithmIdentifier,
|
|
589
|
+
key: CryptoKey,
|
|
590
|
+
signature: BufferSource,
|
|
591
|
+
data: BufferSource,
|
|
592
|
+
): Promise<boolean> {
|
|
593
|
+
await cryptoReady;
|
|
594
|
+
checkUsage(key, 'verify');
|
|
595
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
596
|
+
const name = alg.name.toUpperCase();
|
|
597
|
+
const bytes = toUint8Array(data);
|
|
598
|
+
const sig = toUint8Array(signature);
|
|
599
|
+
|
|
600
|
+
switch (name) {
|
|
601
|
+
case 'HMAC': {
|
|
602
|
+
const hashName = (key.algorithm as HmacKeyAlgorithm).hash.name;
|
|
603
|
+
const nodeHash = toNodeHashName(hashName);
|
|
604
|
+
const keyBytes = key._handle as Uint8Array;
|
|
605
|
+
const hmac = _createHmac(nodeHash, keyBytes);
|
|
606
|
+
hmac.update(bytes);
|
|
607
|
+
const expected = new Uint8Array(hmac.digest());
|
|
608
|
+
if (expected.length !== sig.length) return false;
|
|
609
|
+
// Constant-time compare
|
|
610
|
+
let diff = 0;
|
|
611
|
+
for (let i = 0; i < expected.length; i++) {
|
|
612
|
+
diff |= expected[i] ^ sig[i];
|
|
613
|
+
}
|
|
614
|
+
return diff === 0;
|
|
615
|
+
}
|
|
616
|
+
case 'RSASSA-PKCS1-V1_5': {
|
|
617
|
+
const hashName = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
618
|
+
const nodeHash = toNodeHashName(hashName);
|
|
619
|
+
const handle = key._handle as RsaPemHandle;
|
|
620
|
+
const verifier = _createVerify(nodeHash);
|
|
621
|
+
verifier.update(bytes);
|
|
622
|
+
return verifier.verify(handle.pem, sig);
|
|
623
|
+
}
|
|
624
|
+
case 'ECDSA': {
|
|
625
|
+
const ecdsaParams = algorithm as EcdsaParams;
|
|
626
|
+
const hashName = typeof ecdsaParams.hash === 'string' ? ecdsaParams.hash : ecdsaParams.hash.name;
|
|
627
|
+
const nodeHash = toNodeHashName(hashName);
|
|
628
|
+
const namedCurve = (key.algorithm as EcKeyAlgorithm).namedCurve;
|
|
629
|
+
const pubBytes = key._handle as Uint8Array;
|
|
630
|
+
return _ecdsaVerify(nodeHash, pubBytes, sig, bytes, namedCurve);
|
|
631
|
+
}
|
|
632
|
+
case 'RSA-PSS': {
|
|
633
|
+
const hashName = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
634
|
+
const nodeHash = toNodeHashName(hashName);
|
|
635
|
+
const handle = key._handle as RsaPemHandle;
|
|
636
|
+
const saltLen = (algorithm as RsaPssParams).saltLength ?? hashSize(hashName);
|
|
637
|
+
return _rsaPssVerify(nodeHash, handle.pem, sig, bytes, saltLen);
|
|
638
|
+
}
|
|
639
|
+
default:
|
|
640
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ==================== deriveBits ====================
|
|
645
|
+
|
|
646
|
+
/** Internal deriveBits without usage check (used by deriveKey) */
|
|
647
|
+
private async _deriveBitsInternal(
|
|
648
|
+
algorithm: AlgorithmIdentifier,
|
|
649
|
+
baseKey: CryptoKey,
|
|
650
|
+
length: number,
|
|
651
|
+
): Promise<ArrayBuffer> {
|
|
652
|
+
await cryptoReady;
|
|
653
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
654
|
+
const name = alg.name.toUpperCase();
|
|
655
|
+
|
|
656
|
+
switch (name) {
|
|
657
|
+
case 'PBKDF2': {
|
|
658
|
+
const pbkdf2Params = algorithm as Pbkdf2Params;
|
|
659
|
+
const hashName = toNodeHashName(normalizeAlgorithm(pbkdf2Params.hash).name);
|
|
660
|
+
const salt = toUint8Array(pbkdf2Params.salt);
|
|
661
|
+
const iterations = pbkdf2Params.iterations;
|
|
662
|
+
const keyBytes = baseKey._handle as Uint8Array;
|
|
663
|
+
const result = _pbkdf2Sync(keyBytes, salt, iterations, length / 8, hashName);
|
|
664
|
+
return (result.buffer as ArrayBuffer).slice(result.byteOffset, result.byteOffset + result.byteLength);
|
|
665
|
+
}
|
|
666
|
+
case 'HKDF': {
|
|
667
|
+
const hkdfParams = algorithm as HkdfParams;
|
|
668
|
+
const hashName = toNodeHashName(normalizeAlgorithm(hkdfParams.hash).name);
|
|
669
|
+
const salt = toUint8Array(hkdfParams.salt);
|
|
670
|
+
const info = toUint8Array(hkdfParams.info);
|
|
671
|
+
const keyBytes = baseKey._handle as Uint8Array;
|
|
672
|
+
const result = _hkdfSync(hashName, keyBytes, salt, info, length / 8);
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
case 'ECDH': {
|
|
676
|
+
const ecdhParams = algorithm as EcdhKeyDeriveParams;
|
|
677
|
+
const publicKey = ecdhParams.public;
|
|
678
|
+
const namedCurve = (baseKey.algorithm as EcKeyAlgorithm).namedCurve;
|
|
679
|
+
const nodeCurve = toNodeCurveName(namedCurve);
|
|
680
|
+
const ecdh = _createECDH(nodeCurve);
|
|
681
|
+
const privHandle = baseKey._handle as EcKeyPairHandle;
|
|
682
|
+
ecdh.setPrivateKey(privHandle.priv);
|
|
683
|
+
const pubBytes = publicKey._handle instanceof Uint8Array ? publicKey._handle : (publicKey._handle as EcKeyPairHandle).pub;
|
|
684
|
+
const secret = ecdh.computeSecret(pubBytes);
|
|
685
|
+
const secretBytes = new Uint8Array(secret);
|
|
686
|
+
if (length) {
|
|
687
|
+
return secretBytes.buffer.slice(0, length / 8);
|
|
688
|
+
}
|
|
689
|
+
return secretBytes.buffer.slice(secretBytes.byteOffset, secretBytes.byteOffset + secretBytes.byteLength);
|
|
690
|
+
}
|
|
691
|
+
default:
|
|
692
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async deriveBits(
|
|
697
|
+
algorithm: AlgorithmIdentifier,
|
|
698
|
+
baseKey: CryptoKey,
|
|
699
|
+
length: number,
|
|
700
|
+
): Promise<ArrayBuffer> {
|
|
701
|
+
checkUsage(baseKey, 'deriveBits');
|
|
702
|
+
return this._deriveBitsInternal(algorithm, baseKey, length);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ==================== deriveKey ====================
|
|
706
|
+
|
|
707
|
+
async deriveKey(
|
|
708
|
+
algorithm: AlgorithmIdentifier,
|
|
709
|
+
baseKey: CryptoKey,
|
|
710
|
+
derivedKeyAlgorithm: AlgorithmIdentifier,
|
|
711
|
+
extractable: boolean,
|
|
712
|
+
keyUsages: KeyUsage[],
|
|
713
|
+
): Promise<CryptoKey> {
|
|
714
|
+
checkUsage(baseKey, 'deriveKey');
|
|
715
|
+
const derivedAlg = normalizeAlgorithm(derivedKeyAlgorithm);
|
|
716
|
+
let length: number;
|
|
717
|
+
const dName = derivedAlg.name.toUpperCase();
|
|
718
|
+
|
|
719
|
+
if (dName === 'AES-CBC' || dName === 'AES-CTR' || dName === 'AES-GCM') {
|
|
720
|
+
length = (derivedKeyAlgorithm as AesKeyGenParams).length;
|
|
721
|
+
} else if (dName === 'HMAC') {
|
|
722
|
+
const hmacParams = derivedKeyAlgorithm as HmacKeyGenParams;
|
|
723
|
+
const hashAlg = normalizeAlgorithm(hmacParams.hash);
|
|
724
|
+
length = hmacParams.length || hashSize(hashAlg.name) * 8;
|
|
725
|
+
} else {
|
|
726
|
+
throw new DOMException(`Unsupported derived key algorithm: ${derivedAlg.name}`, 'NotSupportedError');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const bits = await this._deriveBitsInternal(algorithm, baseKey, length);
|
|
730
|
+
return this.importKey('raw', bits, derivedKeyAlgorithm, extractable, keyUsages);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ==================== wrapKey / unwrapKey (stubs) ====================
|
|
734
|
+
|
|
735
|
+
async wrapKey(
|
|
736
|
+
_format: 'raw' | 'jwk' | 'pkcs8' | 'spki',
|
|
737
|
+
_key: CryptoKey,
|
|
738
|
+
_wrappingKey: CryptoKey,
|
|
739
|
+
_wrapAlgorithm: AlgorithmIdentifier,
|
|
740
|
+
): Promise<ArrayBuffer> {
|
|
741
|
+
throw new DOMException('wrapKey not yet implemented', 'NotSupportedError');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async unwrapKey(
|
|
745
|
+
_format: 'raw' | 'jwk' | 'pkcs8' | 'spki',
|
|
746
|
+
_wrappedKey: BufferSource,
|
|
747
|
+
_unwrappingKey: CryptoKey,
|
|
748
|
+
_unwrapAlgorithm: AlgorithmIdentifier,
|
|
749
|
+
_unwrappedKeyAlgorithm: AlgorithmIdentifier,
|
|
750
|
+
_extractable: boolean,
|
|
751
|
+
_keyUsages: KeyUsage[],
|
|
752
|
+
): Promise<CryptoKey> {
|
|
753
|
+
throw new DOMException('unwrapKey not yet implemented', 'NotSupportedError');
|
|
754
|
+
}
|
|
755
|
+
}
|