@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,1881 @@
|
|
|
1
|
+
// Tests for W3C WebCrypto API
|
|
2
|
+
// Ported from refs/deno/tests/unit/webcrypto_test.ts, refs/wpt/WebCryptoAPI/,
|
|
3
|
+
// and refs/node-test/parallel/test-webcrypto-*.js
|
|
4
|
+
// Original: MIT license (Deno), 3-Clause BSD license (WPT), MIT license (Node.js contributors)
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
7
|
+
|
|
8
|
+
export default async () => {
|
|
9
|
+
|
|
10
|
+
// Use global crypto (native on Node.js, polyfill on GJS)
|
|
11
|
+
const subtle = globalThis.crypto.subtle;
|
|
12
|
+
|
|
13
|
+
// ==================== getRandomValues ====================
|
|
14
|
+
|
|
15
|
+
await describe('crypto.getRandomValues', async () => {
|
|
16
|
+
await it('should fill Uint8Array', async () => {
|
|
17
|
+
const arr = new Uint8Array(32);
|
|
18
|
+
const result = crypto.getRandomValues(arr);
|
|
19
|
+
expect(result).toBe(arr);
|
|
20
|
+
// Should not be all zeros (extremely unlikely)
|
|
21
|
+
let allZero = true;
|
|
22
|
+
for (let i = 0; i < arr.length; i++) {
|
|
23
|
+
if (arr[i] !== 0) { allZero = false; break; }
|
|
24
|
+
}
|
|
25
|
+
expect(allZero).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await it('should fill Int8Array', async () => {
|
|
29
|
+
const arr = new Int8Array(10);
|
|
30
|
+
const result = crypto.getRandomValues(arr);
|
|
31
|
+
expect(result).toBe(arr);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await it('should fill Int16Array', async () => {
|
|
35
|
+
const arr = new Int16Array(10);
|
|
36
|
+
const result = crypto.getRandomValues(arr);
|
|
37
|
+
expect(result).toBe(arr);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await it('should fill Int32Array', async () => {
|
|
41
|
+
const arr = new Int32Array(10);
|
|
42
|
+
const result = crypto.getRandomValues(arr);
|
|
43
|
+
expect(result).toBe(arr);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await it('should fill Uint16Array', async () => {
|
|
47
|
+
const arr = new Uint16Array(10);
|
|
48
|
+
const result = crypto.getRandomValues(arr);
|
|
49
|
+
expect(result).toBe(arr);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await it('should fill Uint32Array', async () => {
|
|
53
|
+
const arr = new Uint32Array(10);
|
|
54
|
+
const result = crypto.getRandomValues(arr);
|
|
55
|
+
expect(result).toBe(arr);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await it('should fill Uint8ClampedArray', async () => {
|
|
59
|
+
const arr = new Uint8ClampedArray(10);
|
|
60
|
+
const result = crypto.getRandomValues(arr);
|
|
61
|
+
expect(result).toBe(arr);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await it('should fill BigInt64Array', async () => {
|
|
65
|
+
const arr = new BigInt64Array(4);
|
|
66
|
+
const result = crypto.getRandomValues(arr);
|
|
67
|
+
expect(result).toBe(arr);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await it('should fill BigUint64Array', async () => {
|
|
71
|
+
const arr = new BigUint64Array(4);
|
|
72
|
+
const result = crypto.getRandomValues(arr);
|
|
73
|
+
expect(result).toBe(arr);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await it('should accept zero-length array', async () => {
|
|
77
|
+
const arr = new Uint8Array(0);
|
|
78
|
+
const result = crypto.getRandomValues(arr);
|
|
79
|
+
expect(result).toBe(arr);
|
|
80
|
+
expect(arr.length).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await it('should produce non-zero data for various typed arrays', async () => {
|
|
84
|
+
// Each integer typed array should get random data (not all zeros)
|
|
85
|
+
const constructors = [
|
|
86
|
+
Int8Array, Int16Array, Int32Array,
|
|
87
|
+
Uint8Array, Uint16Array, Uint32Array,
|
|
88
|
+
Uint8ClampedArray,
|
|
89
|
+
] as const;
|
|
90
|
+
for (const Ctor of constructors) {
|
|
91
|
+
const buf = new Ctor(10);
|
|
92
|
+
crypto.getRandomValues(buf);
|
|
93
|
+
let hasNonZero = false;
|
|
94
|
+
for (let i = 0; i < buf.length; i++) {
|
|
95
|
+
if (buf[i] !== 0) { hasNonZero = true; break; }
|
|
96
|
+
}
|
|
97
|
+
expect(hasNonZero).toBe(true);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await it('should throw for Float32Array', async () => {
|
|
102
|
+
let threw = false;
|
|
103
|
+
try {
|
|
104
|
+
crypto.getRandomValues(new Float32Array(1) as unknown as Uint8Array);
|
|
105
|
+
} catch {
|
|
106
|
+
threw = true;
|
|
107
|
+
}
|
|
108
|
+
expect(threw).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await it('should throw for Float64Array', async () => {
|
|
112
|
+
let threw = false;
|
|
113
|
+
try {
|
|
114
|
+
crypto.getRandomValues(new Float64Array(1) as unknown as Uint8Array);
|
|
115
|
+
} catch {
|
|
116
|
+
threw = true;
|
|
117
|
+
}
|
|
118
|
+
expect(threw).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await it('should throw for DataView', async () => {
|
|
122
|
+
let threw = false;
|
|
123
|
+
try {
|
|
124
|
+
crypto.getRandomValues(new DataView(new ArrayBuffer(1)) as unknown as Uint8Array);
|
|
125
|
+
} catch {
|
|
126
|
+
threw = true;
|
|
127
|
+
}
|
|
128
|
+
expect(threw).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await it('should throw when byte length exceeds 65536', async () => {
|
|
132
|
+
let threw = false;
|
|
133
|
+
try {
|
|
134
|
+
// 65537 bytes exceeds quota
|
|
135
|
+
crypto.getRandomValues(new Uint8Array(65537));
|
|
136
|
+
} catch {
|
|
137
|
+
threw = true;
|
|
138
|
+
}
|
|
139
|
+
expect(threw).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await it('should accept exactly 65536 bytes', async () => {
|
|
143
|
+
const arr = new Uint8Array(65536);
|
|
144
|
+
const result = crypto.getRandomValues(arr);
|
|
145
|
+
expect(result).toBe(arr);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await it('should return the same typed array reference', async () => {
|
|
149
|
+
const arr = new Uint32Array(8);
|
|
150
|
+
const returned = crypto.getRandomValues(arr);
|
|
151
|
+
expect(returned).toBe(arr);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ==================== randomUUID ====================
|
|
156
|
+
|
|
157
|
+
await describe('crypto.randomUUID', async () => {
|
|
158
|
+
await it('should return valid UUID v4 format', async () => {
|
|
159
|
+
const uuid = crypto.randomUUID();
|
|
160
|
+
expect(typeof uuid).toBe('string');
|
|
161
|
+
expect(uuid.length).toBe(36);
|
|
162
|
+
// Check format: 8-4-4-4-12
|
|
163
|
+
const parts = uuid.split('-');
|
|
164
|
+
expect(parts.length).toBe(5);
|
|
165
|
+
expect(parts[0].length).toBe(8);
|
|
166
|
+
expect(parts[1].length).toBe(4);
|
|
167
|
+
expect(parts[2].length).toBe(4);
|
|
168
|
+
expect(parts[3].length).toBe(4);
|
|
169
|
+
expect(parts[4].length).toBe(12);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await it('should contain only hex characters and dashes', async () => {
|
|
173
|
+
const uuid = crypto.randomUUID();
|
|
174
|
+
expect(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uuid)).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await it('should have version 4 indicator', async () => {
|
|
178
|
+
const uuid = crypto.randomUUID();
|
|
179
|
+
// The 13th character (index 14) should be '4' (version 4)
|
|
180
|
+
expect(uuid[14]).toBe('4');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await it('should have correct variant bits', async () => {
|
|
184
|
+
const uuid = crypto.randomUUID();
|
|
185
|
+
// The 17th character (index 19) should be one of 8, 9, a, b
|
|
186
|
+
expect(['8', '9', 'a', 'b'].indexOf(uuid[19]) >= 0).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await it('should produce unique UUIDs', async () => {
|
|
190
|
+
const set = new Set<string>();
|
|
191
|
+
for (let i = 0; i < 100; i++) {
|
|
192
|
+
set.add(crypto.randomUUID());
|
|
193
|
+
}
|
|
194
|
+
expect(set.size).toBe(100);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ==================== digest ====================
|
|
199
|
+
|
|
200
|
+
await describe('SubtleCrypto.digest', async () => {
|
|
201
|
+
await it('should hash with SHA-256', async () => {
|
|
202
|
+
const data = new TextEncoder().encode('hello');
|
|
203
|
+
const hash = await subtle.digest('SHA-256', data);
|
|
204
|
+
expect(hash instanceof ArrayBuffer).toBe(true);
|
|
205
|
+
expect(hash.byteLength).toBe(32);
|
|
206
|
+
// Known SHA-256 of 'hello'
|
|
207
|
+
const bytes = new Uint8Array(hash);
|
|
208
|
+
expect(bytes[0]).toBe(0x2c);
|
|
209
|
+
expect(bytes[1]).toBe(0xf2);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await it('should hash with SHA-1', async () => {
|
|
213
|
+
const data = new TextEncoder().encode('hello');
|
|
214
|
+
const hash = await subtle.digest('SHA-1', data);
|
|
215
|
+
expect(hash.byteLength).toBe(20);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await it('should hash with SHA-384', async () => {
|
|
219
|
+
const data = new TextEncoder().encode('hello');
|
|
220
|
+
const hash = await subtle.digest('SHA-384', data);
|
|
221
|
+
expect(hash.byteLength).toBe(48);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await it('should hash with SHA-512', async () => {
|
|
225
|
+
const data = new TextEncoder().encode('hello');
|
|
226
|
+
const hash = await subtle.digest('SHA-512', data);
|
|
227
|
+
expect(hash.byteLength).toBe(64);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await it('should accept algorithm as object', async () => {
|
|
231
|
+
const data = new TextEncoder().encode('hello');
|
|
232
|
+
const hash = await subtle.digest({ name: 'SHA-256' }, data);
|
|
233
|
+
expect(hash.byteLength).toBe(32);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await it('should hash empty data', async () => {
|
|
237
|
+
const hash = await subtle.digest('SHA-256', new Uint8Array(0));
|
|
238
|
+
expect(hash.byteLength).toBe(32);
|
|
239
|
+
// SHA-256 of empty string: e3b0c44298fc1c14...
|
|
240
|
+
const bytes = new Uint8Array(hash);
|
|
241
|
+
expect(bytes[0]).toBe(0xe3);
|
|
242
|
+
expect(bytes[1]).toBe(0xb0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await it('should produce known SHA-1 digest', async () => {
|
|
246
|
+
// SHA-1('') = da39a3ee5e6b4b0d3255bfef95601890afd80709
|
|
247
|
+
const hash = await subtle.digest('SHA-1', new Uint8Array(0));
|
|
248
|
+
const bytes = new Uint8Array(hash);
|
|
249
|
+
expect(bytes[0]).toBe(0xda);
|
|
250
|
+
expect(bytes[1]).toBe(0x39);
|
|
251
|
+
expect(bytes[2]).toBe(0xa3);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await it('should produce known SHA-384 digest of empty', async () => {
|
|
255
|
+
// SHA-384('') starts with 38b060a751ac9638...
|
|
256
|
+
const hash = await subtle.digest('SHA-384', new Uint8Array(0));
|
|
257
|
+
const bytes = new Uint8Array(hash);
|
|
258
|
+
expect(bytes[0]).toBe(0x38);
|
|
259
|
+
expect(bytes[1]).toBe(0xb0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await it('should produce known SHA-512 digest of empty', async () => {
|
|
263
|
+
// SHA-512('') starts with cf83e1357eefb8bd...
|
|
264
|
+
const hash = await subtle.digest('SHA-512', new Uint8Array(0));
|
|
265
|
+
const bytes = new Uint8Array(hash);
|
|
266
|
+
expect(bytes[0]).toBe(0xcf);
|
|
267
|
+
expect(bytes[1]).toBe(0x83);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await it('should accept ArrayBuffer as input', async () => {
|
|
271
|
+
const data = new TextEncoder().encode('hello');
|
|
272
|
+
const hash = await subtle.digest('SHA-256', data.buffer);
|
|
273
|
+
expect(hash.byteLength).toBe(32);
|
|
274
|
+
const bytes = new Uint8Array(hash);
|
|
275
|
+
expect(bytes[0]).toBe(0x2c);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await it('should accept DataView as input', async () => {
|
|
279
|
+
const data = new TextEncoder().encode('hello');
|
|
280
|
+
const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
281
|
+
const hash = await subtle.digest('SHA-256', dv);
|
|
282
|
+
expect(hash.byteLength).toBe(32);
|
|
283
|
+
const bytes = new Uint8Array(hash);
|
|
284
|
+
expect(bytes[0]).toBe(0x2c);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await it('should produce consistent results for same input', async () => {
|
|
288
|
+
const data = new TextEncoder().encode('consistent');
|
|
289
|
+
const h1 = await subtle.digest('SHA-256', data);
|
|
290
|
+
const h2 = await subtle.digest('SHA-256', data);
|
|
291
|
+
const a = new Uint8Array(h1);
|
|
292
|
+
const b = new Uint8Array(h2);
|
|
293
|
+
for (let i = 0; i < a.length; i++) {
|
|
294
|
+
expect(a[i]).toBe(b[i]);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await it('should handle moderately large input', async () => {
|
|
299
|
+
// 64KB of data
|
|
300
|
+
const data = new Uint8Array(65536);
|
|
301
|
+
for (let i = 0; i < data.length; i++) data[i] = i & 0xff;
|
|
302
|
+
const hash = await subtle.digest('SHA-256', data);
|
|
303
|
+
expect(hash.byteLength).toBe(32);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ==================== generateKey (AES) ====================
|
|
308
|
+
|
|
309
|
+
await describe('SubtleCrypto.generateKey (AES)', async () => {
|
|
310
|
+
await it('should generate AES-CBC 256 key', async () => {
|
|
311
|
+
const key = await subtle.generateKey(
|
|
312
|
+
{ name: 'AES-CBC', length: 256 },
|
|
313
|
+
true,
|
|
314
|
+
['encrypt', 'decrypt'],
|
|
315
|
+
) as CryptoKey;
|
|
316
|
+
expect(key.type).toBe('secret');
|
|
317
|
+
expect(key.extractable).toBe(true);
|
|
318
|
+
expect(key.algorithm.name).toBe('AES-CBC');
|
|
319
|
+
expect(key.usages).toContain('encrypt');
|
|
320
|
+
expect(key.usages).toContain('decrypt');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await it('should generate AES-GCM 128 key', async () => {
|
|
324
|
+
const key = await subtle.generateKey(
|
|
325
|
+
{ name: 'AES-GCM', length: 128 },
|
|
326
|
+
true,
|
|
327
|
+
['encrypt', 'decrypt'],
|
|
328
|
+
) as CryptoKey;
|
|
329
|
+
expect(key.type).toBe('secret');
|
|
330
|
+
expect((key.algorithm as any).length).toBe(128);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await it('should generate AES-CTR 192 key', async () => {
|
|
334
|
+
const key = await subtle.generateKey(
|
|
335
|
+
{ name: 'AES-CTR', length: 192 },
|
|
336
|
+
true,
|
|
337
|
+
['encrypt', 'decrypt'],
|
|
338
|
+
) as CryptoKey;
|
|
339
|
+
expect(key.type).toBe('secret');
|
|
340
|
+
expect((key.algorithm as any).length).toBe(192);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await it('should generate AES-GCM 256 key', async () => {
|
|
344
|
+
const key = await subtle.generateKey(
|
|
345
|
+
{ name: 'AES-GCM', length: 256 },
|
|
346
|
+
true,
|
|
347
|
+
['encrypt', 'decrypt'],
|
|
348
|
+
) as CryptoKey;
|
|
349
|
+
expect(key.type).toBe('secret');
|
|
350
|
+
expect(key.algorithm.name).toBe('AES-GCM');
|
|
351
|
+
expect((key.algorithm as any).length).toBe(256);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await it('should generate AES-CBC 128 key', async () => {
|
|
355
|
+
const key = await subtle.generateKey(
|
|
356
|
+
{ name: 'AES-CBC', length: 128 },
|
|
357
|
+
true,
|
|
358
|
+
['encrypt', 'decrypt'],
|
|
359
|
+
) as CryptoKey;
|
|
360
|
+
expect(key.type).toBe('secret');
|
|
361
|
+
expect((key.algorithm as any).length).toBe(128);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await it('should generate non-extractable key', async () => {
|
|
365
|
+
const key = await subtle.generateKey(
|
|
366
|
+
{ name: 'AES-CBC', length: 256 },
|
|
367
|
+
false,
|
|
368
|
+
['encrypt', 'decrypt'],
|
|
369
|
+
) as CryptoKey;
|
|
370
|
+
expect(key.extractable).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await it('should produce unique keys each time', async () => {
|
|
374
|
+
const key1 = await subtle.generateKey(
|
|
375
|
+
{ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt'],
|
|
376
|
+
) as CryptoKey;
|
|
377
|
+
const key2 = await subtle.generateKey(
|
|
378
|
+
{ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt'],
|
|
379
|
+
) as CryptoKey;
|
|
380
|
+
const raw1 = new Uint8Array(await subtle.exportKey('raw', key1) as ArrayBuffer);
|
|
381
|
+
const raw2 = new Uint8Array(await subtle.exportKey('raw', key2) as ArrayBuffer);
|
|
382
|
+
let same = true;
|
|
383
|
+
for (let i = 0; i < raw1.length; i++) {
|
|
384
|
+
if (raw1[i] !== raw2[i]) { same = false; break; }
|
|
385
|
+
}
|
|
386
|
+
expect(same).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ==================== generateKey (HMAC) ====================
|
|
391
|
+
|
|
392
|
+
await describe('SubtleCrypto.generateKey (HMAC)', async () => {
|
|
393
|
+
await it('should generate HMAC key with SHA-256', async () => {
|
|
394
|
+
const key = await subtle.generateKey(
|
|
395
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
396
|
+
true,
|
|
397
|
+
['sign', 'verify'],
|
|
398
|
+
) as CryptoKey;
|
|
399
|
+
expect(key.type).toBe('secret');
|
|
400
|
+
expect(key.algorithm.name).toBe('HMAC');
|
|
401
|
+
expect((key.algorithm as any).hash.name).toBe('SHA-256');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await it('should generate HMAC key with SHA-1', async () => {
|
|
405
|
+
const key = await subtle.generateKey(
|
|
406
|
+
{ name: 'HMAC', hash: 'SHA-1' },
|
|
407
|
+
true,
|
|
408
|
+
['sign', 'verify'],
|
|
409
|
+
) as CryptoKey;
|
|
410
|
+
expect(key.type).toBe('secret');
|
|
411
|
+
expect((key.algorithm as any).hash.name).toBe('SHA-1');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await it('should generate HMAC key with SHA-384', async () => {
|
|
415
|
+
const key = await subtle.generateKey(
|
|
416
|
+
{ name: 'HMAC', hash: 'SHA-384' },
|
|
417
|
+
true,
|
|
418
|
+
['sign', 'verify'],
|
|
419
|
+
) as CryptoKey;
|
|
420
|
+
expect(key.type).toBe('secret');
|
|
421
|
+
expect((key.algorithm as any).hash.name).toBe('SHA-384');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await it('should generate HMAC key with SHA-512', async () => {
|
|
425
|
+
const key = await subtle.generateKey(
|
|
426
|
+
{ name: 'HMAC', hash: 'SHA-512' },
|
|
427
|
+
true,
|
|
428
|
+
['sign', 'verify'],
|
|
429
|
+
) as CryptoKey;
|
|
430
|
+
expect(key.type).toBe('secret');
|
|
431
|
+
expect((key.algorithm as any).hash.name).toBe('SHA-512');
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ==================== importKey / exportKey (raw, jwk) ====================
|
|
436
|
+
|
|
437
|
+
await describe('SubtleCrypto.importKey / exportKey', async () => {
|
|
438
|
+
await it('should import raw AES key and export back', async () => {
|
|
439
|
+
const rawKey = crypto.getRandomValues(new Uint8Array(32));
|
|
440
|
+
const key = await subtle.importKey(
|
|
441
|
+
'raw',
|
|
442
|
+
rawKey,
|
|
443
|
+
{ name: 'AES-CBC' },
|
|
444
|
+
true,
|
|
445
|
+
['encrypt', 'decrypt'],
|
|
446
|
+
);
|
|
447
|
+
expect(key.type).toBe('secret');
|
|
448
|
+
|
|
449
|
+
const exported = await subtle.exportKey('raw', key) as ArrayBuffer;
|
|
450
|
+
const exportedBytes = new Uint8Array(exported);
|
|
451
|
+
expect(exportedBytes.length).toBe(32);
|
|
452
|
+
for (let i = 0; i < 32; i++) {
|
|
453
|
+
expect(exportedBytes[i]).toBe(rawKey[i]);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
await it('should import raw HMAC key', async () => {
|
|
458
|
+
const rawKey = new Uint8Array(32);
|
|
459
|
+
crypto.getRandomValues(rawKey);
|
|
460
|
+
const key = await subtle.importKey(
|
|
461
|
+
'raw',
|
|
462
|
+
rawKey,
|
|
463
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
464
|
+
true,
|
|
465
|
+
['sign', 'verify'],
|
|
466
|
+
);
|
|
467
|
+
expect(key.type).toBe('secret');
|
|
468
|
+
expect(key.algorithm.name).toBe('HMAC');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await it('should import and export JWK AES key', async () => {
|
|
472
|
+
const key = await subtle.generateKey(
|
|
473
|
+
{ name: 'AES-GCM', length: 256 },
|
|
474
|
+
true,
|
|
475
|
+
['encrypt', 'decrypt'],
|
|
476
|
+
) as CryptoKey;
|
|
477
|
+
|
|
478
|
+
const jwk = await subtle.exportKey('jwk', key) as JsonWebKey;
|
|
479
|
+
expect(jwk.kty).toBe('oct');
|
|
480
|
+
expect(jwk.k).toBeDefined();
|
|
481
|
+
expect(jwk.ext).toBe(true);
|
|
482
|
+
|
|
483
|
+
const imported = await subtle.importKey(
|
|
484
|
+
'jwk',
|
|
485
|
+
jwk,
|
|
486
|
+
{ name: 'AES-GCM' },
|
|
487
|
+
true,
|
|
488
|
+
['encrypt', 'decrypt'],
|
|
489
|
+
);
|
|
490
|
+
expect(imported.type).toBe('secret');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await it('should reject non-extractable key export', async () => {
|
|
494
|
+
const key = await subtle.generateKey(
|
|
495
|
+
{ name: 'AES-CBC', length: 256 },
|
|
496
|
+
false,
|
|
497
|
+
['encrypt', 'decrypt'],
|
|
498
|
+
) as CryptoKey;
|
|
499
|
+
let threw = false;
|
|
500
|
+
try {
|
|
501
|
+
await subtle.exportKey('raw', key);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
threw = true;
|
|
504
|
+
}
|
|
505
|
+
expect(threw).toBe(true);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await it('should import raw PBKDF2 key', async () => {
|
|
509
|
+
const password = new TextEncoder().encode('password');
|
|
510
|
+
const key = await subtle.importKey(
|
|
511
|
+
'raw',
|
|
512
|
+
password,
|
|
513
|
+
{ name: 'PBKDF2' },
|
|
514
|
+
false,
|
|
515
|
+
['deriveBits', 'deriveKey'],
|
|
516
|
+
);
|
|
517
|
+
expect(key.type).toBe('secret');
|
|
518
|
+
expect(key.algorithm.name).toBe('PBKDF2');
|
|
519
|
+
expect(key.extractable).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await it('should import raw HKDF key', async () => {
|
|
523
|
+
const ikm = new TextEncoder().encode('input keying material');
|
|
524
|
+
const key = await subtle.importKey(
|
|
525
|
+
'raw',
|
|
526
|
+
ikm,
|
|
527
|
+
{ name: 'HKDF' },
|
|
528
|
+
false,
|
|
529
|
+
['deriveBits'],
|
|
530
|
+
);
|
|
531
|
+
expect(key.type).toBe('secret');
|
|
532
|
+
expect(key.algorithm.name).toBe('HKDF');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
await it('should import raw AES-128 key', async () => {
|
|
536
|
+
const rawKey = crypto.getRandomValues(new Uint8Array(16));
|
|
537
|
+
const key = await subtle.importKey(
|
|
538
|
+
'raw', rawKey, { name: 'AES-CBC' }, true, ['encrypt', 'decrypt'],
|
|
539
|
+
);
|
|
540
|
+
expect(key.type).toBe('secret');
|
|
541
|
+
expect((key.algorithm as any).length).toBe(128);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await it('should import raw AES-192 key', async () => {
|
|
545
|
+
const rawKey = crypto.getRandomValues(new Uint8Array(24));
|
|
546
|
+
const key = await subtle.importKey(
|
|
547
|
+
'raw', rawKey, { name: 'AES-CBC' }, true, ['encrypt', 'decrypt'],
|
|
548
|
+
);
|
|
549
|
+
expect(key.type).toBe('secret');
|
|
550
|
+
expect((key.algorithm as any).length).toBe(192);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await it('should import and export JWK HMAC key', async () => {
|
|
554
|
+
const key = await subtle.generateKey(
|
|
555
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
556
|
+
true,
|
|
557
|
+
['sign', 'verify'],
|
|
558
|
+
) as CryptoKey;
|
|
559
|
+
|
|
560
|
+
const jwk = await subtle.exportKey('jwk', key) as JsonWebKey;
|
|
561
|
+
expect(jwk.kty).toBe('oct');
|
|
562
|
+
expect(jwk.k).toBeDefined();
|
|
563
|
+
expect(jwk.alg).toBe('HS256');
|
|
564
|
+
|
|
565
|
+
const imported = await subtle.importKey(
|
|
566
|
+
'jwk', jwk, { name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify'],
|
|
567
|
+
);
|
|
568
|
+
expect(imported.type).toBe('secret');
|
|
569
|
+
expect(imported.algorithm.name).toBe('HMAC');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await it('should import and export JWK AES-CBC key', async () => {
|
|
573
|
+
const key = await subtle.generateKey(
|
|
574
|
+
{ name: 'AES-CBC', length: 256 },
|
|
575
|
+
true,
|
|
576
|
+
['encrypt', 'decrypt'],
|
|
577
|
+
) as CryptoKey;
|
|
578
|
+
|
|
579
|
+
const jwk = await subtle.exportKey('jwk', key) as JsonWebKey;
|
|
580
|
+
expect(jwk.kty).toBe('oct');
|
|
581
|
+
expect(jwk.alg).toBe('A256CBC');
|
|
582
|
+
|
|
583
|
+
const imported = await subtle.importKey(
|
|
584
|
+
'jwk', jwk, { name: 'AES-CBC' }, true, ['encrypt', 'decrypt'],
|
|
585
|
+
);
|
|
586
|
+
expect(imported.type).toBe('secret');
|
|
587
|
+
expect(imported.algorithm.name).toBe('AES-CBC');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
await it('should export ECDSA public key as raw', async () => {
|
|
591
|
+
const keyPair = await subtle.generateKey(
|
|
592
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
593
|
+
true,
|
|
594
|
+
['sign', 'verify'],
|
|
595
|
+
) as CryptoKeyPair;
|
|
596
|
+
|
|
597
|
+
const raw = await subtle.exportKey('raw', keyPair.publicKey) as ArrayBuffer;
|
|
598
|
+
// Uncompressed EC P-256 public key: 1 + 32 + 32 = 65 bytes
|
|
599
|
+
expect(raw.byteLength).toBe(65);
|
|
600
|
+
// First byte should be 0x04 (uncompressed point)
|
|
601
|
+
expect(new Uint8Array(raw)[0]).toBe(0x04);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
await it('should round-trip ECDSA JWK private key', async () => {
|
|
605
|
+
const keyPair = await subtle.generateKey(
|
|
606
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
607
|
+
true,
|
|
608
|
+
['sign', 'verify'],
|
|
609
|
+
) as CryptoKeyPair;
|
|
610
|
+
|
|
611
|
+
const privJwk = await subtle.exportKey('jwk', keyPair.privateKey) as JsonWebKey;
|
|
612
|
+
expect(privJwk.kty).toBe('EC');
|
|
613
|
+
expect(privJwk.crv).toBe('P-256');
|
|
614
|
+
expect(privJwk.d).toBeDefined();
|
|
615
|
+
expect(privJwk.x).toBeDefined();
|
|
616
|
+
expect(privJwk.y).toBeDefined();
|
|
617
|
+
|
|
618
|
+
const imported = await subtle.importKey(
|
|
619
|
+
'jwk', privJwk, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign'],
|
|
620
|
+
);
|
|
621
|
+
expect(imported.type).toBe('private');
|
|
622
|
+
|
|
623
|
+
// Verify the imported key can sign
|
|
624
|
+
const data = new TextEncoder().encode('round-trip test');
|
|
625
|
+
const sig = await subtle.sign(
|
|
626
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
627
|
+
imported,
|
|
628
|
+
data,
|
|
629
|
+
);
|
|
630
|
+
expect(sig.byteLength).toBe(64);
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// ==================== encrypt / decrypt (AES) ====================
|
|
635
|
+
|
|
636
|
+
await describe('SubtleCrypto.encrypt / decrypt (AES)', async () => {
|
|
637
|
+
await it('should encrypt and decrypt with AES-CBC', async () => {
|
|
638
|
+
const key = await subtle.generateKey(
|
|
639
|
+
{ name: 'AES-CBC', length: 256 },
|
|
640
|
+
true,
|
|
641
|
+
['encrypt', 'decrypt'],
|
|
642
|
+
) as CryptoKey;
|
|
643
|
+
|
|
644
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
645
|
+
const plaintext = new TextEncoder().encode('Hello, WebCrypto!');
|
|
646
|
+
|
|
647
|
+
const ciphertext = await subtle.encrypt(
|
|
648
|
+
{ name: 'AES-CBC', iv },
|
|
649
|
+
key,
|
|
650
|
+
plaintext,
|
|
651
|
+
);
|
|
652
|
+
expect(ciphertext.byteLength).toBeGreaterThan(0);
|
|
653
|
+
|
|
654
|
+
const decrypted = await subtle.decrypt(
|
|
655
|
+
{ name: 'AES-CBC', iv },
|
|
656
|
+
key,
|
|
657
|
+
ciphertext,
|
|
658
|
+
);
|
|
659
|
+
const result = new TextDecoder().decode(decrypted);
|
|
660
|
+
expect(result).toBe('Hello, WebCrypto!');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
await it('should encrypt and decrypt with AES-GCM', async () => {
|
|
664
|
+
const key = await subtle.generateKey(
|
|
665
|
+
{ name: 'AES-GCM', length: 256 },
|
|
666
|
+
true,
|
|
667
|
+
['encrypt', 'decrypt'],
|
|
668
|
+
) as CryptoKey;
|
|
669
|
+
|
|
670
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
671
|
+
const plaintext = new TextEncoder().encode('AES-GCM test');
|
|
672
|
+
|
|
673
|
+
const ciphertext = await subtle.encrypt(
|
|
674
|
+
{ name: 'AES-GCM', iv },
|
|
675
|
+
key,
|
|
676
|
+
plaintext,
|
|
677
|
+
);
|
|
678
|
+
// GCM adds 16-byte auth tag
|
|
679
|
+
expect(ciphertext.byteLength).toBe(plaintext.length + 16);
|
|
680
|
+
|
|
681
|
+
const decrypted = await subtle.decrypt(
|
|
682
|
+
{ name: 'AES-GCM', iv },
|
|
683
|
+
key,
|
|
684
|
+
ciphertext,
|
|
685
|
+
);
|
|
686
|
+
expect(new TextDecoder().decode(decrypted)).toBe('AES-GCM test');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
await it('should encrypt and decrypt with AES-CTR', async () => {
|
|
690
|
+
const key = await subtle.generateKey(
|
|
691
|
+
{ name: 'AES-CTR', length: 128 },
|
|
692
|
+
true,
|
|
693
|
+
['encrypt', 'decrypt'],
|
|
694
|
+
) as CryptoKey;
|
|
695
|
+
|
|
696
|
+
const counter = crypto.getRandomValues(new Uint8Array(16));
|
|
697
|
+
const plaintext = new TextEncoder().encode('CTR mode');
|
|
698
|
+
|
|
699
|
+
const ciphertext = await subtle.encrypt(
|
|
700
|
+
{ name: 'AES-CTR', counter, length: 64 },
|
|
701
|
+
key,
|
|
702
|
+
plaintext,
|
|
703
|
+
);
|
|
704
|
+
expect(ciphertext.byteLength).toBe(plaintext.length);
|
|
705
|
+
|
|
706
|
+
const decrypted = await subtle.decrypt(
|
|
707
|
+
{ name: 'AES-CTR', counter, length: 64 },
|
|
708
|
+
key,
|
|
709
|
+
ciphertext,
|
|
710
|
+
);
|
|
711
|
+
expect(new TextDecoder().decode(decrypted)).toBe('CTR mode');
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
await it('AES-GCM should support additionalData', async () => {
|
|
715
|
+
const key = await subtle.generateKey(
|
|
716
|
+
{ name: 'AES-GCM', length: 256 },
|
|
717
|
+
true,
|
|
718
|
+
['encrypt', 'decrypt'],
|
|
719
|
+
) as CryptoKey;
|
|
720
|
+
|
|
721
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
722
|
+
const aad = new TextEncoder().encode('additional data');
|
|
723
|
+
const plaintext = new TextEncoder().encode('authenticated');
|
|
724
|
+
|
|
725
|
+
const ciphertext = await subtle.encrypt(
|
|
726
|
+
{ name: 'AES-GCM', iv, additionalData: aad },
|
|
727
|
+
key,
|
|
728
|
+
plaintext,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
const decrypted = await subtle.decrypt(
|
|
732
|
+
{ name: 'AES-GCM', iv, additionalData: aad },
|
|
733
|
+
key,
|
|
734
|
+
ciphertext,
|
|
735
|
+
);
|
|
736
|
+
expect(new TextDecoder().decode(decrypted)).toBe('authenticated');
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await it('AES-CBC 128-bit key round-trip', async () => {
|
|
740
|
+
const key = await subtle.generateKey(
|
|
741
|
+
{ name: 'AES-CBC', length: 128 },
|
|
742
|
+
true,
|
|
743
|
+
['encrypt', 'decrypt'],
|
|
744
|
+
) as CryptoKey;
|
|
745
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
746
|
+
const pt = new TextEncoder().encode('128-bit key test');
|
|
747
|
+
const ct = await subtle.encrypt({ name: 'AES-CBC', iv }, key, pt);
|
|
748
|
+
const dec = await subtle.decrypt({ name: 'AES-CBC', iv }, key, ct);
|
|
749
|
+
expect(new TextDecoder().decode(dec)).toBe('128-bit key test');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
await it('AES-CBC 192-bit key round-trip', async () => {
|
|
753
|
+
const key = await subtle.generateKey(
|
|
754
|
+
{ name: 'AES-CBC', length: 192 },
|
|
755
|
+
true,
|
|
756
|
+
['encrypt', 'decrypt'],
|
|
757
|
+
) as CryptoKey;
|
|
758
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
759
|
+
const pt = new TextEncoder().encode('192-bit key test');
|
|
760
|
+
const ct = await subtle.encrypt({ name: 'AES-CBC', iv }, key, pt);
|
|
761
|
+
const dec = await subtle.decrypt({ name: 'AES-CBC', iv }, key, ct);
|
|
762
|
+
expect(new TextDecoder().decode(dec)).toBe('192-bit key test');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
await it('AES-GCM 128-bit key round-trip', async () => {
|
|
766
|
+
const key = await subtle.generateKey(
|
|
767
|
+
{ name: 'AES-GCM', length: 128 },
|
|
768
|
+
true,
|
|
769
|
+
['encrypt', 'decrypt'],
|
|
770
|
+
) as CryptoKey;
|
|
771
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
772
|
+
const pt = new TextEncoder().encode('GCM-128');
|
|
773
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, key, pt);
|
|
774
|
+
expect(ct.byteLength).toBe(pt.length + 16);
|
|
775
|
+
const dec = await subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
|
|
776
|
+
expect(new TextDecoder().decode(dec)).toBe('GCM-128');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
await it('AES-CTR 256-bit key round-trip', async () => {
|
|
780
|
+
const key = await subtle.generateKey(
|
|
781
|
+
{ name: 'AES-CTR', length: 256 },
|
|
782
|
+
true,
|
|
783
|
+
['encrypt', 'decrypt'],
|
|
784
|
+
) as CryptoKey;
|
|
785
|
+
const counter = crypto.getRandomValues(new Uint8Array(16));
|
|
786
|
+
const pt = new TextEncoder().encode('CTR-256 mode test data');
|
|
787
|
+
const ct = await subtle.encrypt({ name: 'AES-CTR', counter, length: 64 }, key, pt);
|
|
788
|
+
expect(ct.byteLength).toBe(pt.length);
|
|
789
|
+
const dec = await subtle.decrypt({ name: 'AES-CTR', counter, length: 64 }, key, ct);
|
|
790
|
+
expect(new TextDecoder().decode(dec)).toBe('CTR-256 mode test data');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
await it('AES-GCM should fail with tampered ciphertext', async () => {
|
|
794
|
+
const key = await subtle.generateKey(
|
|
795
|
+
{ name: 'AES-GCM', length: 256 },
|
|
796
|
+
true,
|
|
797
|
+
['encrypt', 'decrypt'],
|
|
798
|
+
) as CryptoKey;
|
|
799
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
800
|
+
const pt = new TextEncoder().encode('tamper test');
|
|
801
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, key, pt);
|
|
802
|
+
|
|
803
|
+
// Corrupt the ciphertext
|
|
804
|
+
const tampered = new Uint8Array(ct);
|
|
805
|
+
tampered[0] ^= 0xff;
|
|
806
|
+
|
|
807
|
+
let threw = false;
|
|
808
|
+
try {
|
|
809
|
+
await subtle.decrypt({ name: 'AES-GCM', iv }, key, tampered);
|
|
810
|
+
} catch {
|
|
811
|
+
threw = true;
|
|
812
|
+
}
|
|
813
|
+
expect(threw).toBe(true);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
await it('AES-GCM wrong AAD should fail decryption', async () => {
|
|
817
|
+
const key = await subtle.generateKey(
|
|
818
|
+
{ name: 'AES-GCM', length: 256 },
|
|
819
|
+
true,
|
|
820
|
+
['encrypt', 'decrypt'],
|
|
821
|
+
) as CryptoKey;
|
|
822
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
823
|
+
const aad = new TextEncoder().encode('correct aad');
|
|
824
|
+
const wrongAad = new TextEncoder().encode('wrong aad');
|
|
825
|
+
const pt = new TextEncoder().encode('aad check');
|
|
826
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv, additionalData: aad }, key, pt);
|
|
827
|
+
|
|
828
|
+
let threw = false;
|
|
829
|
+
try {
|
|
830
|
+
await subtle.decrypt({ name: 'AES-GCM', iv, additionalData: wrongAad }, key, ct);
|
|
831
|
+
} catch {
|
|
832
|
+
threw = true;
|
|
833
|
+
}
|
|
834
|
+
expect(threw).toBe(true);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
await it('AES-CBC should encrypt empty data', async () => {
|
|
838
|
+
const key = await subtle.generateKey(
|
|
839
|
+
{ name: 'AES-CBC', length: 256 },
|
|
840
|
+
true,
|
|
841
|
+
['encrypt', 'decrypt'],
|
|
842
|
+
) as CryptoKey;
|
|
843
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
844
|
+
const pt = new Uint8Array(0);
|
|
845
|
+
const ct = await subtle.encrypt({ name: 'AES-CBC', iv }, key, pt);
|
|
846
|
+
// CBC with empty input produces one block of padding (16 bytes)
|
|
847
|
+
expect(ct.byteLength).toBe(16);
|
|
848
|
+
const dec = await subtle.decrypt({ name: 'AES-CBC', iv }, key, ct);
|
|
849
|
+
expect(dec.byteLength).toBe(0);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
await it('AES-CTR should encrypt empty data', async () => {
|
|
853
|
+
const key = await subtle.generateKey(
|
|
854
|
+
{ name: 'AES-CTR', length: 256 },
|
|
855
|
+
true,
|
|
856
|
+
['encrypt', 'decrypt'],
|
|
857
|
+
) as CryptoKey;
|
|
858
|
+
const counter = crypto.getRandomValues(new Uint8Array(16));
|
|
859
|
+
const pt = new Uint8Array(0);
|
|
860
|
+
const ct = await subtle.encrypt({ name: 'AES-CTR', counter, length: 64 }, key, pt);
|
|
861
|
+
expect(ct.byteLength).toBe(0);
|
|
862
|
+
const dec = await subtle.decrypt({ name: 'AES-CTR', counter, length: 64 }, key, ct);
|
|
863
|
+
expect(dec.byteLength).toBe(0);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
await it('AES-GCM should encrypt empty data', async () => {
|
|
867
|
+
const key = await subtle.generateKey(
|
|
868
|
+
{ name: 'AES-GCM', length: 256 },
|
|
869
|
+
true,
|
|
870
|
+
['encrypt', 'decrypt'],
|
|
871
|
+
) as CryptoKey;
|
|
872
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
873
|
+
const pt = new Uint8Array(0);
|
|
874
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, key, pt);
|
|
875
|
+
// GCM with empty input = 16-byte auth tag only
|
|
876
|
+
expect(ct.byteLength).toBe(16);
|
|
877
|
+
const dec = await subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
|
|
878
|
+
expect(dec.byteLength).toBe(0);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
await it('different keys should produce different ciphertext', async () => {
|
|
882
|
+
const key1 = await subtle.generateKey(
|
|
883
|
+
{ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt'],
|
|
884
|
+
) as CryptoKey;
|
|
885
|
+
const key2 = await subtle.generateKey(
|
|
886
|
+
{ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt'],
|
|
887
|
+
) as CryptoKey;
|
|
888
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
889
|
+
const pt = new TextEncoder().encode('same plaintext');
|
|
890
|
+
const ct1 = new Uint8Array(await subtle.encrypt({ name: 'AES-CBC', iv }, key1, pt));
|
|
891
|
+
const ct2 = new Uint8Array(await subtle.encrypt({ name: 'AES-CBC', iv }, key2, pt));
|
|
892
|
+
let same = true;
|
|
893
|
+
for (let i = 0; i < ct1.length; i++) {
|
|
894
|
+
if (ct1[i] !== ct2[i]) { same = false; break; }
|
|
895
|
+
}
|
|
896
|
+
expect(same).toBe(false);
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// ==================== sign / verify (HMAC) ====================
|
|
901
|
+
|
|
902
|
+
await describe('SubtleCrypto.sign / verify (HMAC)', async () => {
|
|
903
|
+
await it('should sign and verify with HMAC SHA-256', async () => {
|
|
904
|
+
const key = await subtle.generateKey(
|
|
905
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
906
|
+
true,
|
|
907
|
+
['sign', 'verify'],
|
|
908
|
+
) as CryptoKey;
|
|
909
|
+
|
|
910
|
+
const data = new TextEncoder().encode('message to sign');
|
|
911
|
+
const signature = await subtle.sign('HMAC', key, data);
|
|
912
|
+
expect(signature.byteLength).toBe(32); // SHA-256 = 32 bytes
|
|
913
|
+
|
|
914
|
+
const valid = await subtle.verify('HMAC', key, signature, data);
|
|
915
|
+
expect(valid).toBe(true);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
await it('should reject wrong signature', async () => {
|
|
919
|
+
const key = await subtle.generateKey(
|
|
920
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
921
|
+
true,
|
|
922
|
+
['sign', 'verify'],
|
|
923
|
+
) as CryptoKey;
|
|
924
|
+
|
|
925
|
+
const data = new TextEncoder().encode('message');
|
|
926
|
+
const signature = await subtle.sign('HMAC', key, data);
|
|
927
|
+
|
|
928
|
+
// Corrupt signature
|
|
929
|
+
const badSig = new Uint8Array(signature);
|
|
930
|
+
badSig[0] ^= 0xFF;
|
|
931
|
+
|
|
932
|
+
const valid = await subtle.verify('HMAC', key, badSig, data);
|
|
933
|
+
expect(valid).toBe(false);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
await it('should reject wrong data', async () => {
|
|
937
|
+
const key = await subtle.generateKey(
|
|
938
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
939
|
+
true,
|
|
940
|
+
['sign', 'verify'],
|
|
941
|
+
) as CryptoKey;
|
|
942
|
+
|
|
943
|
+
const data = new TextEncoder().encode('original');
|
|
944
|
+
const signature = await subtle.sign('HMAC', key, data);
|
|
945
|
+
|
|
946
|
+
const wrongData = new TextEncoder().encode('modified');
|
|
947
|
+
const valid = await subtle.verify('HMAC', key, signature, wrongData);
|
|
948
|
+
expect(valid).toBe(false);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
await it('should sign with HMAC SHA-512', async () => {
|
|
952
|
+
const key = await subtle.generateKey(
|
|
953
|
+
{ name: 'HMAC', hash: 'SHA-512' },
|
|
954
|
+
true,
|
|
955
|
+
['sign', 'verify'],
|
|
956
|
+
) as CryptoKey;
|
|
957
|
+
|
|
958
|
+
const data = new TextEncoder().encode('test');
|
|
959
|
+
const signature = await subtle.sign('HMAC', key, data);
|
|
960
|
+
expect(signature.byteLength).toBe(64); // SHA-512 = 64 bytes
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
await it('should sign and verify with HMAC SHA-1', async () => {
|
|
964
|
+
const key = await subtle.generateKey(
|
|
965
|
+
{ name: 'HMAC', hash: 'SHA-1' },
|
|
966
|
+
true,
|
|
967
|
+
['sign', 'verify'],
|
|
968
|
+
) as CryptoKey;
|
|
969
|
+
|
|
970
|
+
const data = new TextEncoder().encode('SHA-1 HMAC test');
|
|
971
|
+
const sig = await subtle.sign('HMAC', key, data);
|
|
972
|
+
expect(sig.byteLength).toBe(20); // SHA-1 = 20 bytes
|
|
973
|
+
const valid = await subtle.verify('HMAC', key, sig, data);
|
|
974
|
+
expect(valid).toBe(true);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
await it('should sign and verify with HMAC SHA-384', async () => {
|
|
978
|
+
const key = await subtle.generateKey(
|
|
979
|
+
{ name: 'HMAC', hash: 'SHA-384' },
|
|
980
|
+
true,
|
|
981
|
+
['sign', 'verify'],
|
|
982
|
+
) as CryptoKey;
|
|
983
|
+
|
|
984
|
+
const data = new TextEncoder().encode('SHA-384 HMAC test');
|
|
985
|
+
const sig = await subtle.sign('HMAC', key, data);
|
|
986
|
+
expect(sig.byteLength).toBe(48); // SHA-384 = 48 bytes
|
|
987
|
+
const valid = await subtle.verify('HMAC', key, sig, data);
|
|
988
|
+
expect(valid).toBe(true);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
await it('HMAC SHA-512 sign and verify', async () => {
|
|
992
|
+
const key = await subtle.generateKey(
|
|
993
|
+
{ name: 'HMAC', hash: 'SHA-512' },
|
|
994
|
+
true,
|
|
995
|
+
['sign', 'verify'],
|
|
996
|
+
) as CryptoKey;
|
|
997
|
+
|
|
998
|
+
const data = new TextEncoder().encode('SHA-512 verify test');
|
|
999
|
+
const sig = await subtle.sign('HMAC', key, data);
|
|
1000
|
+
expect(sig.byteLength).toBe(64);
|
|
1001
|
+
const valid = await subtle.verify('HMAC', key, sig, data);
|
|
1002
|
+
expect(valid).toBe(true);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
await it('should sign empty data with HMAC', async () => {
|
|
1006
|
+
const key = await subtle.generateKey(
|
|
1007
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
1008
|
+
true,
|
|
1009
|
+
['sign', 'verify'],
|
|
1010
|
+
) as CryptoKey;
|
|
1011
|
+
|
|
1012
|
+
const data = new Uint8Array(0);
|
|
1013
|
+
const sig = await subtle.sign('HMAC', key, data);
|
|
1014
|
+
expect(sig.byteLength).toBe(32);
|
|
1015
|
+
const valid = await subtle.verify('HMAC', key, sig, data);
|
|
1016
|
+
expect(valid).toBe(true);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
await it('should produce deterministic HMAC signatures', async () => {
|
|
1020
|
+
const key = await subtle.generateKey(
|
|
1021
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
1022
|
+
true,
|
|
1023
|
+
['sign', 'verify'],
|
|
1024
|
+
) as CryptoKey;
|
|
1025
|
+
|
|
1026
|
+
const data = new TextEncoder().encode('deterministic');
|
|
1027
|
+
const sig1 = new Uint8Array(await subtle.sign('HMAC', key, data));
|
|
1028
|
+
const sig2 = new Uint8Array(await subtle.sign('HMAC', key, data));
|
|
1029
|
+
for (let i = 0; i < sig1.length; i++) {
|
|
1030
|
+
expect(sig1[i]).toBe(sig2[i]);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
await it('should produce different signatures with different keys', async () => {
|
|
1035
|
+
const key1 = await subtle.generateKey(
|
|
1036
|
+
{ name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify'],
|
|
1037
|
+
) as CryptoKey;
|
|
1038
|
+
const key2 = await subtle.generateKey(
|
|
1039
|
+
{ name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify'],
|
|
1040
|
+
) as CryptoKey;
|
|
1041
|
+
|
|
1042
|
+
const data = new TextEncoder().encode('same data');
|
|
1043
|
+
const sig1 = new Uint8Array(await subtle.sign('HMAC', key1, data));
|
|
1044
|
+
const sig2 = new Uint8Array(await subtle.sign('HMAC', key2, data));
|
|
1045
|
+
let same = true;
|
|
1046
|
+
for (let i = 0; i < sig1.length; i++) {
|
|
1047
|
+
if (sig1[i] !== sig2[i]) { same = false; break; }
|
|
1048
|
+
}
|
|
1049
|
+
expect(same).toBe(false);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
await it('should reject truncated signature', async () => {
|
|
1053
|
+
const key = await subtle.generateKey(
|
|
1054
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
1055
|
+
true,
|
|
1056
|
+
['sign', 'verify'],
|
|
1057
|
+
) as CryptoKey;
|
|
1058
|
+
|
|
1059
|
+
const data = new TextEncoder().encode('truncation test');
|
|
1060
|
+
const sig = await subtle.sign('HMAC', key, data);
|
|
1061
|
+
// Truncated signature (only half)
|
|
1062
|
+
const truncated = new Uint8Array(sig).slice(0, 16);
|
|
1063
|
+
const valid = await subtle.verify('HMAC', key, truncated, data);
|
|
1064
|
+
expect(valid).toBe(false);
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// ==================== deriveBits (PBKDF2) ====================
|
|
1069
|
+
|
|
1070
|
+
await describe('SubtleCrypto.deriveBits (PBKDF2)', async () => {
|
|
1071
|
+
await it('should derive 256 bits with PBKDF2', async () => {
|
|
1072
|
+
const password = new TextEncoder().encode('password');
|
|
1073
|
+
const key = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1074
|
+
|
|
1075
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1076
|
+
const bits = await subtle.deriveBits(
|
|
1077
|
+
{ name: 'PBKDF2', salt, iterations: 1000, hash: 'SHA-256' },
|
|
1078
|
+
key,
|
|
1079
|
+
256,
|
|
1080
|
+
);
|
|
1081
|
+
expect(bits.byteLength).toBe(32);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
await it('should produce deterministic output', async () => {
|
|
1085
|
+
const password = new TextEncoder().encode('password');
|
|
1086
|
+
const salt = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
|
1087
|
+
|
|
1088
|
+
const key1 = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1089
|
+
const bits1 = await subtle.deriveBits(
|
|
1090
|
+
{ name: 'PBKDF2', salt, iterations: 100, hash: 'SHA-256' },
|
|
1091
|
+
key1,
|
|
1092
|
+
256,
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
const key2 = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1096
|
+
const bits2 = await subtle.deriveBits(
|
|
1097
|
+
{ name: 'PBKDF2', salt, iterations: 100, hash: 'SHA-256' },
|
|
1098
|
+
key2,
|
|
1099
|
+
256,
|
|
1100
|
+
);
|
|
1101
|
+
|
|
1102
|
+
const a = new Uint8Array(bits1);
|
|
1103
|
+
const b = new Uint8Array(bits2);
|
|
1104
|
+
for (let i = 0; i < 32; i++) {
|
|
1105
|
+
expect(a[i]).toBe(b[i]);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
await it('should derive 128 bits with PBKDF2', async () => {
|
|
1110
|
+
const password = new TextEncoder().encode('short');
|
|
1111
|
+
const key = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1112
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1113
|
+
const bits = await subtle.deriveBits(
|
|
1114
|
+
{ name: 'PBKDF2', salt, iterations: 1000, hash: 'SHA-256' },
|
|
1115
|
+
key,
|
|
1116
|
+
128,
|
|
1117
|
+
);
|
|
1118
|
+
expect(bits.byteLength).toBe(16);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
await it('should derive 512 bits with PBKDF2', async () => {
|
|
1122
|
+
const password = new TextEncoder().encode('long-password');
|
|
1123
|
+
const key = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1124
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1125
|
+
const bits = await subtle.deriveBits(
|
|
1126
|
+
{ name: 'PBKDF2', salt, iterations: 1000, hash: 'SHA-512' },
|
|
1127
|
+
key,
|
|
1128
|
+
512,
|
|
1129
|
+
);
|
|
1130
|
+
expect(bits.byteLength).toBe(64);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
await it('different passwords produce different output', async () => {
|
|
1134
|
+
const salt = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
|
1135
|
+
const params = { name: 'PBKDF2', salt, iterations: 100, hash: 'SHA-256' } as const;
|
|
1136
|
+
|
|
1137
|
+
const key1 = await subtle.importKey('raw', new TextEncoder().encode('pass1'), 'PBKDF2', false, ['deriveBits']);
|
|
1138
|
+
const key2 = await subtle.importKey('raw', new TextEncoder().encode('pass2'), 'PBKDF2', false, ['deriveBits']);
|
|
1139
|
+
const bits1 = new Uint8Array(await subtle.deriveBits(params, key1, 256));
|
|
1140
|
+
const bits2 = new Uint8Array(await subtle.deriveBits(params, key2, 256));
|
|
1141
|
+
let same = true;
|
|
1142
|
+
for (let i = 0; i < bits1.length; i++) {
|
|
1143
|
+
if (bits1[i] !== bits2[i]) { same = false; break; }
|
|
1144
|
+
}
|
|
1145
|
+
expect(same).toBe(false);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
await it('different salts produce different output', async () => {
|
|
1149
|
+
const password = new TextEncoder().encode('password');
|
|
1150
|
+
const salt1 = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
|
|
1151
|
+
const salt2 = new Uint8Array([16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
|
|
1152
|
+
|
|
1153
|
+
const key1 = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1154
|
+
const key2 = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits']);
|
|
1155
|
+
const bits1 = new Uint8Array(await subtle.deriveBits(
|
|
1156
|
+
{ name: 'PBKDF2', salt: salt1, iterations: 100, hash: 'SHA-256' }, key1, 256,
|
|
1157
|
+
));
|
|
1158
|
+
const bits2 = new Uint8Array(await subtle.deriveBits(
|
|
1159
|
+
{ name: 'PBKDF2', salt: salt2, iterations: 100, hash: 'SHA-256' }, key2, 256,
|
|
1160
|
+
));
|
|
1161
|
+
let same = true;
|
|
1162
|
+
for (let i = 0; i < bits1.length; i++) {
|
|
1163
|
+
if (bits1[i] !== bits2[i]) { same = false; break; }
|
|
1164
|
+
}
|
|
1165
|
+
expect(same).toBe(false);
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
// ==================== deriveBits (HKDF) ====================
|
|
1170
|
+
|
|
1171
|
+
await describe('SubtleCrypto.deriveBits (HKDF)', async () => {
|
|
1172
|
+
await it('should derive 256 bits with HKDF', async () => {
|
|
1173
|
+
const ikm = new TextEncoder().encode('input keying material');
|
|
1174
|
+
const key = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
1175
|
+
|
|
1176
|
+
const salt = new Uint8Array(16);
|
|
1177
|
+
const info = new TextEncoder().encode('info');
|
|
1178
|
+
const bits = await subtle.deriveBits(
|
|
1179
|
+
{ name: 'HKDF', salt, info, hash: 'SHA-256' },
|
|
1180
|
+
key,
|
|
1181
|
+
256,
|
|
1182
|
+
);
|
|
1183
|
+
expect(bits.byteLength).toBe(32);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
await it('should derive 128 bits with HKDF', async () => {
|
|
1187
|
+
const ikm = new TextEncoder().encode('ikm');
|
|
1188
|
+
const key = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
1189
|
+
const bits = await subtle.deriveBits(
|
|
1190
|
+
{ name: 'HKDF', salt: new Uint8Array(16), info: new Uint8Array(0), hash: 'SHA-256' },
|
|
1191
|
+
key,
|
|
1192
|
+
128,
|
|
1193
|
+
);
|
|
1194
|
+
expect(bits.byteLength).toBe(16);
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
await it('should produce deterministic HKDF output', async () => {
|
|
1198
|
+
const ikm = new TextEncoder().encode('deterministic ikm');
|
|
1199
|
+
const salt = new Uint8Array([10, 20, 30, 40]);
|
|
1200
|
+
const info = new TextEncoder().encode('deterministic info');
|
|
1201
|
+
|
|
1202
|
+
const key1 = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
1203
|
+
const key2 = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
1204
|
+
const bits1 = new Uint8Array(await subtle.deriveBits(
|
|
1205
|
+
{ name: 'HKDF', salt, info, hash: 'SHA-256' }, key1, 256,
|
|
1206
|
+
));
|
|
1207
|
+
const bits2 = new Uint8Array(await subtle.deriveBits(
|
|
1208
|
+
{ name: 'HKDF', salt, info, hash: 'SHA-256' }, key2, 256,
|
|
1209
|
+
));
|
|
1210
|
+
for (let i = 0; i < bits1.length; i++) {
|
|
1211
|
+
expect(bits1[i]).toBe(bits2[i]);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
await it('different info produces different output', async () => {
|
|
1216
|
+
const ikm = new TextEncoder().encode('ikm');
|
|
1217
|
+
const salt = new Uint8Array(16);
|
|
1218
|
+
|
|
1219
|
+
const key1 = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
1220
|
+
const key2 = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
|
|
1221
|
+
const bits1 = new Uint8Array(await subtle.deriveBits(
|
|
1222
|
+
{ name: 'HKDF', salt, info: new TextEncoder().encode('info1'), hash: 'SHA-256' }, key1, 256,
|
|
1223
|
+
));
|
|
1224
|
+
const bits2 = new Uint8Array(await subtle.deriveBits(
|
|
1225
|
+
{ name: 'HKDF', salt, info: new TextEncoder().encode('info2'), hash: 'SHA-256' }, key2, 256,
|
|
1226
|
+
));
|
|
1227
|
+
let same = true;
|
|
1228
|
+
for (let i = 0; i < bits1.length; i++) {
|
|
1229
|
+
if (bits1[i] !== bits2[i]) { same = false; break; }
|
|
1230
|
+
}
|
|
1231
|
+
expect(same).toBe(false);
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// ==================== deriveKey ====================
|
|
1236
|
+
|
|
1237
|
+
await describe('SubtleCrypto.deriveKey', async () => {
|
|
1238
|
+
await it('should derive AES key from PBKDF2', async () => {
|
|
1239
|
+
const password = new TextEncoder().encode('password');
|
|
1240
|
+
const baseKey = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveKey']);
|
|
1241
|
+
|
|
1242
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1243
|
+
const derivedKey = await subtle.deriveKey(
|
|
1244
|
+
{ name: 'PBKDF2', salt, iterations: 1000, hash: 'SHA-256' },
|
|
1245
|
+
baseKey,
|
|
1246
|
+
{ name: 'AES-GCM', length: 256 },
|
|
1247
|
+
true,
|
|
1248
|
+
['encrypt', 'decrypt'],
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
expect(derivedKey.type).toBe('secret');
|
|
1252
|
+
expect(derivedKey.algorithm.name).toBe('AES-GCM');
|
|
1253
|
+
expect((derivedKey.algorithm as any).length).toBe(256);
|
|
1254
|
+
|
|
1255
|
+
// Verify derived key works for encryption
|
|
1256
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1257
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, derivedKey, new TextEncoder().encode('test'));
|
|
1258
|
+
const pt = await subtle.decrypt({ name: 'AES-GCM', iv }, derivedKey, ct);
|
|
1259
|
+
expect(new TextDecoder().decode(pt)).toBe('test');
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
await it('should derive AES-CBC key from PBKDF2', async () => {
|
|
1263
|
+
const password = new TextEncoder().encode('cbc-password');
|
|
1264
|
+
const baseKey = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveKey']);
|
|
1265
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
1266
|
+
|
|
1267
|
+
const derivedKey = await subtle.deriveKey(
|
|
1268
|
+
{ name: 'PBKDF2', salt, iterations: 1000, hash: 'SHA-256' },
|
|
1269
|
+
baseKey,
|
|
1270
|
+
{ name: 'AES-CBC', length: 128 },
|
|
1271
|
+
true,
|
|
1272
|
+
['encrypt', 'decrypt'],
|
|
1273
|
+
);
|
|
1274
|
+
expect(derivedKey.type).toBe('secret');
|
|
1275
|
+
expect(derivedKey.algorithm.name).toBe('AES-CBC');
|
|
1276
|
+
expect((derivedKey.algorithm as any).length).toBe(128);
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
await it('should derive HMAC key from HKDF', async () => {
|
|
1280
|
+
const ikm = new TextEncoder().encode('input keying material');
|
|
1281
|
+
const baseKey = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveKey']);
|
|
1282
|
+
|
|
1283
|
+
const derivedKey = await subtle.deriveKey(
|
|
1284
|
+
{ name: 'HKDF', salt: new Uint8Array(16), info: new TextEncoder().encode('hmac-info'), hash: 'SHA-256' },
|
|
1285
|
+
baseKey,
|
|
1286
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
1287
|
+
true,
|
|
1288
|
+
['sign', 'verify'],
|
|
1289
|
+
);
|
|
1290
|
+
expect(derivedKey.type).toBe('secret');
|
|
1291
|
+
expect(derivedKey.algorithm.name).toBe('HMAC');
|
|
1292
|
+
|
|
1293
|
+
// Verify derived key works for signing
|
|
1294
|
+
const data = new TextEncoder().encode('derived hmac test');
|
|
1295
|
+
const sig = await subtle.sign('HMAC', derivedKey, data);
|
|
1296
|
+
expect(sig.byteLength).toBe(32);
|
|
1297
|
+
const valid = await subtle.verify('HMAC', derivedKey, sig, data);
|
|
1298
|
+
expect(valid).toBe(true);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
await it('should derive AES key from HKDF', async () => {
|
|
1302
|
+
const ikm = new TextEncoder().encode('hkdf ikm');
|
|
1303
|
+
const baseKey = await subtle.importKey('raw', ikm, 'HKDF', false, ['deriveKey']);
|
|
1304
|
+
|
|
1305
|
+
const derivedKey = await subtle.deriveKey(
|
|
1306
|
+
{ name: 'HKDF', salt: new Uint8Array(16), info: new TextEncoder().encode('aes-info'), hash: 'SHA-256' },
|
|
1307
|
+
baseKey,
|
|
1308
|
+
{ name: 'AES-GCM', length: 256 },
|
|
1309
|
+
true,
|
|
1310
|
+
['encrypt', 'decrypt'],
|
|
1311
|
+
);
|
|
1312
|
+
expect(derivedKey.type).toBe('secret');
|
|
1313
|
+
expect(derivedKey.algorithm.name).toBe('AES-GCM');
|
|
1314
|
+
|
|
1315
|
+
// Verify the derived key works
|
|
1316
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1317
|
+
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, derivedKey, new TextEncoder().encode('hkdf-aes'));
|
|
1318
|
+
const pt = await subtle.decrypt({ name: 'AES-GCM', iv }, derivedKey, ct);
|
|
1319
|
+
expect(new TextDecoder().decode(pt)).toBe('hkdf-aes');
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
// ==================== generateKey / deriveBits (ECDH) ====================
|
|
1324
|
+
|
|
1325
|
+
await describe('SubtleCrypto ECDH', async () => {
|
|
1326
|
+
await it('should generate ECDH P-256 key pair', async () => {
|
|
1327
|
+
const keyPair = await subtle.generateKey(
|
|
1328
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1329
|
+
true,
|
|
1330
|
+
['deriveBits'],
|
|
1331
|
+
) as CryptoKeyPair;
|
|
1332
|
+
|
|
1333
|
+
expect(keyPair.publicKey.type).toBe('public');
|
|
1334
|
+
expect(keyPair.privateKey.type).toBe('private');
|
|
1335
|
+
expect(keyPair.publicKey.algorithm.name).toBe('ECDH');
|
|
1336
|
+
expect((keyPair.publicKey.algorithm as any).namedCurve).toBe('P-256');
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
await it('should derive shared secret between two key pairs', async () => {
|
|
1340
|
+
const keyPairA = await subtle.generateKey(
|
|
1341
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1342
|
+
true,
|
|
1343
|
+
['deriveBits'],
|
|
1344
|
+
) as CryptoKeyPair;
|
|
1345
|
+
|
|
1346
|
+
const keyPairB = await subtle.generateKey(
|
|
1347
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1348
|
+
true,
|
|
1349
|
+
['deriveBits'],
|
|
1350
|
+
) as CryptoKeyPair;
|
|
1351
|
+
|
|
1352
|
+
const secretA = await subtle.deriveBits(
|
|
1353
|
+
{ name: 'ECDH', public: keyPairB.publicKey },
|
|
1354
|
+
keyPairA.privateKey,
|
|
1355
|
+
256,
|
|
1356
|
+
);
|
|
1357
|
+
|
|
1358
|
+
const secretB = await subtle.deriveBits(
|
|
1359
|
+
{ name: 'ECDH', public: keyPairA.publicKey },
|
|
1360
|
+
keyPairB.privateKey,
|
|
1361
|
+
256,
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
const a = new Uint8Array(secretA);
|
|
1365
|
+
const b = new Uint8Array(secretB);
|
|
1366
|
+
expect(a.length).toBe(32);
|
|
1367
|
+
for (let i = 0; i < a.length; i++) {
|
|
1368
|
+
expect(a[i]).toBe(b[i]);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
await it('should export and import ECDH JWK key', async () => {
|
|
1373
|
+
const keyPair = await subtle.generateKey(
|
|
1374
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1375
|
+
true,
|
|
1376
|
+
['deriveBits'],
|
|
1377
|
+
) as CryptoKeyPair;
|
|
1378
|
+
|
|
1379
|
+
const pubJwk = await subtle.exportKey('jwk', keyPair.publicKey) as JsonWebKey;
|
|
1380
|
+
expect(pubJwk.kty).toBe('EC');
|
|
1381
|
+
expect(pubJwk.crv).toBe('P-256');
|
|
1382
|
+
expect(pubJwk.x).toBeDefined();
|
|
1383
|
+
expect(pubJwk.y).toBeDefined();
|
|
1384
|
+
|
|
1385
|
+
// Re-import
|
|
1386
|
+
const imported = await subtle.importKey(
|
|
1387
|
+
'jwk',
|
|
1388
|
+
pubJwk,
|
|
1389
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1390
|
+
true,
|
|
1391
|
+
[],
|
|
1392
|
+
);
|
|
1393
|
+
expect(imported.type).toBe('public');
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
await it('should generate ECDH P-384 key pair', async () => {
|
|
1397
|
+
const keyPair = await subtle.generateKey(
|
|
1398
|
+
{ name: 'ECDH', namedCurve: 'P-384' },
|
|
1399
|
+
true,
|
|
1400
|
+
['deriveBits'],
|
|
1401
|
+
) as CryptoKeyPair;
|
|
1402
|
+
|
|
1403
|
+
expect(keyPair.publicKey.type).toBe('public');
|
|
1404
|
+
expect(keyPair.privateKey.type).toBe('private');
|
|
1405
|
+
expect((keyPair.publicKey.algorithm as any).namedCurve).toBe('P-384');
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
await it('ECDH P-384 shared secret', async () => {
|
|
1409
|
+
const keyPairA = await subtle.generateKey(
|
|
1410
|
+
{ name: 'ECDH', namedCurve: 'P-384' },
|
|
1411
|
+
true,
|
|
1412
|
+
['deriveBits'],
|
|
1413
|
+
) as CryptoKeyPair;
|
|
1414
|
+
const keyPairB = await subtle.generateKey(
|
|
1415
|
+
{ name: 'ECDH', namedCurve: 'P-384' },
|
|
1416
|
+
true,
|
|
1417
|
+
['deriveBits'],
|
|
1418
|
+
) as CryptoKeyPair;
|
|
1419
|
+
|
|
1420
|
+
const secretA = await subtle.deriveBits(
|
|
1421
|
+
{ name: 'ECDH', public: keyPairB.publicKey },
|
|
1422
|
+
keyPairA.privateKey,
|
|
1423
|
+
384,
|
|
1424
|
+
);
|
|
1425
|
+
const secretB = await subtle.deriveBits(
|
|
1426
|
+
{ name: 'ECDH', public: keyPairA.publicKey },
|
|
1427
|
+
keyPairB.privateKey,
|
|
1428
|
+
384,
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
const a = new Uint8Array(secretA);
|
|
1432
|
+
const b = new Uint8Array(secretB);
|
|
1433
|
+
expect(a.length).toBe(48);
|
|
1434
|
+
for (let i = 0; i < a.length; i++) {
|
|
1435
|
+
expect(a[i]).toBe(b[i]);
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
await it('should derive key from ECDH', async () => {
|
|
1440
|
+
const keyPairA = await subtle.generateKey(
|
|
1441
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1442
|
+
true,
|
|
1443
|
+
['deriveKey'],
|
|
1444
|
+
) as CryptoKeyPair;
|
|
1445
|
+
const keyPairB = await subtle.generateKey(
|
|
1446
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
1447
|
+
true,
|
|
1448
|
+
['deriveKey'],
|
|
1449
|
+
) as CryptoKeyPair;
|
|
1450
|
+
|
|
1451
|
+
const derivedKey = await subtle.deriveKey(
|
|
1452
|
+
{ name: 'ECDH', public: keyPairB.publicKey },
|
|
1453
|
+
keyPairA.privateKey,
|
|
1454
|
+
{ name: 'AES-GCM', length: 256 },
|
|
1455
|
+
true,
|
|
1456
|
+
['encrypt', 'decrypt'],
|
|
1457
|
+
);
|
|
1458
|
+
expect(derivedKey.type).toBe('secret');
|
|
1459
|
+
expect(derivedKey.algorithm.name).toBe('AES-GCM');
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// ==================== CryptoKey ====================
|
|
1464
|
+
|
|
1465
|
+
await describe('CryptoKey', async () => {
|
|
1466
|
+
await it('should have correct properties', async () => {
|
|
1467
|
+
const key = await subtle.generateKey(
|
|
1468
|
+
{ name: 'AES-CBC', length: 256 },
|
|
1469
|
+
true,
|
|
1470
|
+
['encrypt', 'decrypt'],
|
|
1471
|
+
) as CryptoKey;
|
|
1472
|
+
expect(key.type).toBe('secret');
|
|
1473
|
+
expect(key.extractable).toBe(true);
|
|
1474
|
+
expect(key.algorithm).toBeDefined();
|
|
1475
|
+
expect(key.usages).toBeDefined();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
await it('should have immutable algorithm and usages', async () => {
|
|
1479
|
+
const key = await subtle.generateKey(
|
|
1480
|
+
{ name: 'AES-CBC', length: 256 },
|
|
1481
|
+
true,
|
|
1482
|
+
['encrypt'],
|
|
1483
|
+
) as CryptoKey;
|
|
1484
|
+
expect(key.algorithm.name).toBe('AES-CBC');
|
|
1485
|
+
expect(key.usages.length).toBe(1);
|
|
1486
|
+
expect(key.usages[0]).toBe('encrypt');
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
await it('should reject wrong key usage', async () => {
|
|
1490
|
+
const key = await subtle.generateKey(
|
|
1491
|
+
{ name: 'AES-CBC', length: 256 },
|
|
1492
|
+
true,
|
|
1493
|
+
['encrypt'],
|
|
1494
|
+
) as CryptoKey;
|
|
1495
|
+
let threw = false;
|
|
1496
|
+
try {
|
|
1497
|
+
const iv = crypto.getRandomValues(new Uint8Array(16));
|
|
1498
|
+
await subtle.decrypt({ name: 'AES-CBC', iv }, key, new Uint8Array(32));
|
|
1499
|
+
} catch {
|
|
1500
|
+
threw = true;
|
|
1501
|
+
}
|
|
1502
|
+
expect(threw).toBe(true);
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
await it('HMAC key should have hash in algorithm', async () => {
|
|
1506
|
+
const key = await subtle.generateKey(
|
|
1507
|
+
{ name: 'HMAC', hash: 'SHA-512' },
|
|
1508
|
+
true,
|
|
1509
|
+
['sign', 'verify'],
|
|
1510
|
+
) as CryptoKey;
|
|
1511
|
+
expect(key.algorithm.name).toBe('HMAC');
|
|
1512
|
+
expect((key.algorithm as any).hash.name).toBe('SHA-512');
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
await it('EC key pair should have namedCurve in algorithm', async () => {
|
|
1516
|
+
const keyPair = await subtle.generateKey(
|
|
1517
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1518
|
+
true,
|
|
1519
|
+
['sign', 'verify'],
|
|
1520
|
+
) as CryptoKeyPair;
|
|
1521
|
+
expect((keyPair.publicKey.algorithm as any).namedCurve).toBe('P-256');
|
|
1522
|
+
expect((keyPair.privateKey.algorithm as any).namedCurve).toBe('P-256');
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
// ==================== sign / verify (ECDSA) ====================
|
|
1527
|
+
|
|
1528
|
+
await describe('SubtleCrypto ECDSA', async () => {
|
|
1529
|
+
await it('should generate ECDSA P-256 key pair', async () => {
|
|
1530
|
+
const keyPair = await subtle.generateKey(
|
|
1531
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1532
|
+
true,
|
|
1533
|
+
['sign', 'verify'],
|
|
1534
|
+
) as CryptoKeyPair;
|
|
1535
|
+
|
|
1536
|
+
expect(keyPair.publicKey.type).toBe('public');
|
|
1537
|
+
expect(keyPair.privateKey.type).toBe('private');
|
|
1538
|
+
expect(keyPair.publicKey.algorithm.name).toBe('ECDSA');
|
|
1539
|
+
expect((keyPair.publicKey.algorithm as any).namedCurve).toBe('P-256');
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
await it('should sign and verify with ECDSA P-256 SHA-256', async () => {
|
|
1543
|
+
const keyPair = await subtle.generateKey(
|
|
1544
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1545
|
+
true,
|
|
1546
|
+
['sign', 'verify'],
|
|
1547
|
+
) as CryptoKeyPair;
|
|
1548
|
+
|
|
1549
|
+
const data = new TextEncoder().encode('ECDSA test message');
|
|
1550
|
+
const signature = await subtle.sign(
|
|
1551
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1552
|
+
keyPair.privateKey,
|
|
1553
|
+
data,
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
// P-256 signature = 64 bytes (32 bytes r + 32 bytes s)
|
|
1557
|
+
expect(signature.byteLength).toBe(64);
|
|
1558
|
+
|
|
1559
|
+
const valid = await subtle.verify(
|
|
1560
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1561
|
+
keyPair.publicKey,
|
|
1562
|
+
signature,
|
|
1563
|
+
data,
|
|
1564
|
+
);
|
|
1565
|
+
expect(valid).toBe(true);
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
await it('should reject corrupted ECDSA signature', async () => {
|
|
1569
|
+
const keyPair = await subtle.generateKey(
|
|
1570
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1571
|
+
true,
|
|
1572
|
+
['sign', 'verify'],
|
|
1573
|
+
) as CryptoKeyPair;
|
|
1574
|
+
|
|
1575
|
+
const data = new TextEncoder().encode('test');
|
|
1576
|
+
const signature = await subtle.sign(
|
|
1577
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1578
|
+
keyPair.privateKey,
|
|
1579
|
+
data,
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
const badSig = new Uint8Array(signature);
|
|
1583
|
+
badSig[0] ^= 0xFF;
|
|
1584
|
+
const valid = await subtle.verify(
|
|
1585
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1586
|
+
keyPair.publicKey,
|
|
1587
|
+
badSig,
|
|
1588
|
+
data,
|
|
1589
|
+
);
|
|
1590
|
+
expect(valid).toBe(false);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
await it('should reject wrong data in ECDSA verify', async () => {
|
|
1594
|
+
const keyPair = await subtle.generateKey(
|
|
1595
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1596
|
+
true,
|
|
1597
|
+
['sign', 'verify'],
|
|
1598
|
+
) as CryptoKeyPair;
|
|
1599
|
+
|
|
1600
|
+
const data = new TextEncoder().encode('original');
|
|
1601
|
+
const signature = await subtle.sign(
|
|
1602
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1603
|
+
keyPair.privateKey,
|
|
1604
|
+
data,
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
const wrongData = new TextEncoder().encode('modified');
|
|
1608
|
+
const valid = await subtle.verify(
|
|
1609
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1610
|
+
keyPair.publicKey,
|
|
1611
|
+
signature,
|
|
1612
|
+
wrongData,
|
|
1613
|
+
);
|
|
1614
|
+
expect(valid).toBe(false);
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
await it('should sign different messages with different signatures', async () => {
|
|
1618
|
+
const keyPair = await subtle.generateKey(
|
|
1619
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1620
|
+
true,
|
|
1621
|
+
['sign', 'verify'],
|
|
1622
|
+
) as CryptoKeyPair;
|
|
1623
|
+
|
|
1624
|
+
const sig1 = await subtle.sign(
|
|
1625
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1626
|
+
keyPair.privateKey,
|
|
1627
|
+
new TextEncoder().encode('message A'),
|
|
1628
|
+
);
|
|
1629
|
+
const sig2 = await subtle.sign(
|
|
1630
|
+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
|
|
1631
|
+
keyPair.privateKey,
|
|
1632
|
+
new TextEncoder().encode('message B'),
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
// Different messages should produce different signatures
|
|
1636
|
+
const a = new Uint8Array(sig1);
|
|
1637
|
+
const b = new Uint8Array(sig2);
|
|
1638
|
+
let same = true;
|
|
1639
|
+
for (let i = 0; i < a.length; i++) {
|
|
1640
|
+
if (a[i] !== b[i]) { same = false; break; }
|
|
1641
|
+
}
|
|
1642
|
+
expect(same).toBe(false);
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
await it('should sign and verify with ECDSA P-384', async () => {
|
|
1646
|
+
const keyPair = await subtle.generateKey(
|
|
1647
|
+
{ name: 'ECDSA', namedCurve: 'P-384' },
|
|
1648
|
+
true,
|
|
1649
|
+
['sign', 'verify'],
|
|
1650
|
+
) as CryptoKeyPair;
|
|
1651
|
+
|
|
1652
|
+
const data = new TextEncoder().encode('P-384 test');
|
|
1653
|
+
const signature = await subtle.sign(
|
|
1654
|
+
{ name: 'ECDSA', hash: { name: 'SHA-384' } },
|
|
1655
|
+
keyPair.privateKey,
|
|
1656
|
+
data,
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
// P-384 signature = 96 bytes (48 bytes r + 48 bytes s)
|
|
1660
|
+
expect(signature.byteLength).toBe(96);
|
|
1661
|
+
|
|
1662
|
+
const valid = await subtle.verify(
|
|
1663
|
+
{ name: 'ECDSA', hash: { name: 'SHA-384' } },
|
|
1664
|
+
keyPair.publicKey,
|
|
1665
|
+
signature,
|
|
1666
|
+
data,
|
|
1667
|
+
);
|
|
1668
|
+
expect(valid).toBe(true);
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
await it('ECDSA should verify with imported public key', async () => {
|
|
1672
|
+
const keyPair = await subtle.generateKey(
|
|
1673
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
1674
|
+
true,
|
|
1675
|
+
['sign', 'verify'],
|
|
1676
|
+
) as CryptoKeyPair;
|
|
1677
|
+
|
|
1678
|
+
// Export and re-import public key
|
|
1679
|
+
const pubJwk = await subtle.exportKey('jwk', keyPair.publicKey) as JsonWebKey;
|
|
1680
|
+
const importedPub = await subtle.importKey(
|
|
1681
|
+
'jwk', pubJwk, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify'],
|
|
1682
|
+
);
|
|
1683
|
+
|
|
1684
|
+
const data = new TextEncoder().encode('imported key verify');
|
|
1685
|
+
const sig = await subtle.sign(
|
|
1686
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
1687
|
+
keyPair.privateKey,
|
|
1688
|
+
data,
|
|
1689
|
+
);
|
|
1690
|
+
const valid = await subtle.verify(
|
|
1691
|
+
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
1692
|
+
importedPub,
|
|
1693
|
+
sig,
|
|
1694
|
+
data,
|
|
1695
|
+
);
|
|
1696
|
+
expect(valid).toBe(true);
|
|
1697
|
+
});
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// ==================== Error handling ====================
|
|
1701
|
+
|
|
1702
|
+
await describe('SubtleCrypto error handling', async () => {
|
|
1703
|
+
await it('should reject unsupported digest algorithm', async () => {
|
|
1704
|
+
let threw = false;
|
|
1705
|
+
try {
|
|
1706
|
+
await subtle.digest('MD5', new Uint8Array(0));
|
|
1707
|
+
} catch {
|
|
1708
|
+
threw = true;
|
|
1709
|
+
}
|
|
1710
|
+
expect(threw).toBe(true);
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
await it('should reject unsupported generateKey algorithm', async () => {
|
|
1714
|
+
let threw = false;
|
|
1715
|
+
try {
|
|
1716
|
+
await subtle.generateKey(
|
|
1717
|
+
{ name: 'CHACHA20' } as any, true, ['encrypt', 'decrypt'],
|
|
1718
|
+
);
|
|
1719
|
+
} catch {
|
|
1720
|
+
threw = true;
|
|
1721
|
+
}
|
|
1722
|
+
expect(threw).toBe(true);
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
await it('should reject encrypt with sign-only key', async () => {
|
|
1726
|
+
const key = await subtle.generateKey(
|
|
1727
|
+
{ name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify'],
|
|
1728
|
+
) as CryptoKey;
|
|
1729
|
+
let threw = false;
|
|
1730
|
+
try {
|
|
1731
|
+
await subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(16) }, key, new Uint8Array(16));
|
|
1732
|
+
} catch {
|
|
1733
|
+
threw = true;
|
|
1734
|
+
}
|
|
1735
|
+
expect(threw).toBe(true);
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
await it('should reject sign with encrypt-only key', async () => {
|
|
1739
|
+
const key = await subtle.generateKey(
|
|
1740
|
+
{ name: 'AES-CBC', length: 256 }, true, ['encrypt'],
|
|
1741
|
+
) as CryptoKey;
|
|
1742
|
+
let threw = false;
|
|
1743
|
+
try {
|
|
1744
|
+
await subtle.sign('HMAC', key, new Uint8Array(16));
|
|
1745
|
+
} catch {
|
|
1746
|
+
threw = true;
|
|
1747
|
+
}
|
|
1748
|
+
expect(threw).toBe(true);
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
await it('should reject invalid AES key length in generateKey', async () => {
|
|
1752
|
+
let threw = false;
|
|
1753
|
+
try {
|
|
1754
|
+
await subtle.generateKey(
|
|
1755
|
+
{ name: 'AES-CBC', length: 100 } as any, true, ['encrypt', 'decrypt'],
|
|
1756
|
+
);
|
|
1757
|
+
} catch {
|
|
1758
|
+
threw = true;
|
|
1759
|
+
}
|
|
1760
|
+
expect(threw).toBe(true);
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
await it('should reject invalid AES raw import key length', async () => {
|
|
1764
|
+
let threw = false;
|
|
1765
|
+
try {
|
|
1766
|
+
// 15 bytes is not a valid AES key length
|
|
1767
|
+
await subtle.importKey('raw', new Uint8Array(15), { name: 'AES-CBC' }, true, ['encrypt', 'decrypt']);
|
|
1768
|
+
} catch {
|
|
1769
|
+
threw = true;
|
|
1770
|
+
}
|
|
1771
|
+
expect(threw).toBe(true);
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
await it('should reject empty key usages for AES', async () => {
|
|
1775
|
+
let threw = false;
|
|
1776
|
+
try {
|
|
1777
|
+
await subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, []);
|
|
1778
|
+
} catch {
|
|
1779
|
+
threw = true;
|
|
1780
|
+
}
|
|
1781
|
+
expect(threw).toBe(true);
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
await it('should reject invalid key usages for HMAC', async () => {
|
|
1785
|
+
let threw = false;
|
|
1786
|
+
try {
|
|
1787
|
+
// encrypt is not valid for HMAC
|
|
1788
|
+
await subtle.generateKey(
|
|
1789
|
+
{ name: 'HMAC', hash: 'SHA-256' }, true, ['encrypt'] as any,
|
|
1790
|
+
);
|
|
1791
|
+
} catch {
|
|
1792
|
+
threw = true;
|
|
1793
|
+
}
|
|
1794
|
+
expect(threw).toBe(true);
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
await it('should reject invalid key usages for AES', async () => {
|
|
1798
|
+
let threw = false;
|
|
1799
|
+
try {
|
|
1800
|
+
// sign is not valid for AES
|
|
1801
|
+
await subtle.generateKey(
|
|
1802
|
+
{ name: 'AES-CBC', length: 256 }, true, ['sign'] as any,
|
|
1803
|
+
);
|
|
1804
|
+
} catch {
|
|
1805
|
+
threw = true;
|
|
1806
|
+
}
|
|
1807
|
+
expect(threw).toBe(true);
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
await it('should reject unsupported import format', async () => {
|
|
1811
|
+
let threw = false;
|
|
1812
|
+
try {
|
|
1813
|
+
await subtle.importKey(
|
|
1814
|
+
'pkcs8' as any, new Uint8Array(32), { name: 'AES-CBC' }, true, ['encrypt'],
|
|
1815
|
+
);
|
|
1816
|
+
} catch {
|
|
1817
|
+
threw = true;
|
|
1818
|
+
}
|
|
1819
|
+
expect(threw).toBe(true);
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
await it('should reject JWK with wrong kty for AES', async () => {
|
|
1823
|
+
let threw = false;
|
|
1824
|
+
try {
|
|
1825
|
+
await subtle.importKey(
|
|
1826
|
+
'jwk',
|
|
1827
|
+
{ kty: 'EC', k: 'AAAA' } as JsonWebKey,
|
|
1828
|
+
{ name: 'AES-CBC' },
|
|
1829
|
+
true,
|
|
1830
|
+
['encrypt', 'decrypt'],
|
|
1831
|
+
);
|
|
1832
|
+
} catch {
|
|
1833
|
+
threw = true;
|
|
1834
|
+
}
|
|
1835
|
+
expect(threw).toBe(true);
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
await it('should reject JWK with wrong kty for HMAC', async () => {
|
|
1839
|
+
let threw = false;
|
|
1840
|
+
try {
|
|
1841
|
+
await subtle.importKey(
|
|
1842
|
+
'jwk',
|
|
1843
|
+
{ kty: 'EC', k: 'AAAA' } as JsonWebKey,
|
|
1844
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
1845
|
+
true,
|
|
1846
|
+
['sign', 'verify'],
|
|
1847
|
+
);
|
|
1848
|
+
} catch {
|
|
1849
|
+
threw = true;
|
|
1850
|
+
}
|
|
1851
|
+
expect(threw).toBe(true);
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
// ==================== subtle property existence ====================
|
|
1856
|
+
|
|
1857
|
+
await describe('crypto / subtle structure', async () => {
|
|
1858
|
+
await it('crypto should have subtle property', async () => {
|
|
1859
|
+
expect(globalThis.crypto).toBeDefined();
|
|
1860
|
+
expect(globalThis.crypto.subtle).toBeDefined();
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
await it('subtle should have standard methods', async () => {
|
|
1864
|
+
expect(typeof subtle.digest).toBe('function');
|
|
1865
|
+
expect(typeof subtle.generateKey).toBe('function');
|
|
1866
|
+
expect(typeof subtle.importKey).toBe('function');
|
|
1867
|
+
expect(typeof subtle.exportKey).toBe('function');
|
|
1868
|
+
expect(typeof subtle.encrypt).toBe('function');
|
|
1869
|
+
expect(typeof subtle.decrypt).toBe('function');
|
|
1870
|
+
expect(typeof subtle.sign).toBe('function');
|
|
1871
|
+
expect(typeof subtle.verify).toBe('function');
|
|
1872
|
+
expect(typeof subtle.deriveBits).toBe('function');
|
|
1873
|
+
expect(typeof subtle.deriveKey).toBe('function');
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
await it('crypto should have getRandomValues and randomUUID', async () => {
|
|
1877
|
+
expect(typeof crypto.getRandomValues).toBe('function');
|
|
1878
|
+
expect(typeof crypto.randomUUID).toBe('function');
|
|
1879
|
+
});
|
|
1880
|
+
});
|
|
1881
|
+
};
|