react-native-quick-crypto 1.1.3 → 1.1.4

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/src/subtle.ts CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  import { Buffer as SBuffer } from 'safe-buffer';
3
2
  import type {
4
3
  SubtleAlgorithm,
@@ -28,7 +27,10 @@ import {
28
27
  SecretKeyObject,
29
28
  } from './keys';
30
29
  import type { CryptoKeyPair } from './utils/types';
31
- import { bufferLikeToArrayBuffer } from './utils/conversion';
30
+ import {
31
+ binaryLikeToArrayBuffer,
32
+ bufferLikeToArrayBuffer,
33
+ } from './utils/conversion';
32
34
  import { argon2Sync } from './argon2';
33
35
  import { lazyDOMException } from './utils/errors';
34
36
  import { normalizeHashName, HashContext } from './utils/hashnames';
@@ -130,19 +132,705 @@ function getCanonicalAlgorithmNames(): Map<string, AnyAlgorithm> {
130
132
  return _canonicalAlgorithmNames;
131
133
  }
132
134
 
135
+ // Per-algorithm WebIDL converter table. Mirrors Node's kAlgorithmDefinitions
136
+ // (lib/internal/crypto/util.js): each (algorithm, operation) pair maps to a
137
+ // dictionary converter name, or null when only the `name` member is required.
138
+ // Operation keys are missing when an algorithm cannot perform that operation,
139
+ // causing `normalizeAlgorithm` to reject the call.
140
+ const kAlgorithmDefinitions: Record<string, Record<string, string | null>> = {
141
+ 'AES-CBC': {
142
+ generateKey: 'AesKeyGenParams',
143
+ exportKey: null,
144
+ importKey: null,
145
+ encrypt: 'AesCbcParams',
146
+ decrypt: 'AesCbcParams',
147
+ 'get key length': 'AesDerivedKeyParams',
148
+ },
149
+ 'AES-CTR': {
150
+ generateKey: 'AesKeyGenParams',
151
+ exportKey: null,
152
+ importKey: null,
153
+ encrypt: 'AesCtrParams',
154
+ decrypt: 'AesCtrParams',
155
+ 'get key length': 'AesDerivedKeyParams',
156
+ },
157
+ 'AES-GCM': {
158
+ generateKey: 'AesKeyGenParams',
159
+ exportKey: null,
160
+ importKey: null,
161
+ encrypt: 'AeadParams',
162
+ decrypt: 'AeadParams',
163
+ 'get key length': 'AesDerivedKeyParams',
164
+ },
165
+ 'AES-KW': {
166
+ generateKey: 'AesKeyGenParams',
167
+ exportKey: null,
168
+ importKey: null,
169
+ 'get key length': 'AesDerivedKeyParams',
170
+ wrapKey: null,
171
+ unwrapKey: null,
172
+ },
173
+ 'AES-OCB': {
174
+ generateKey: 'AesKeyGenParams',
175
+ exportKey: null,
176
+ importKey: null,
177
+ encrypt: 'AeadParams',
178
+ decrypt: 'AeadParams',
179
+ 'get key length': 'AesDerivedKeyParams',
180
+ },
181
+ Argon2d: {
182
+ deriveBits: 'Argon2Params',
183
+ 'get key length': null,
184
+ importKey: null,
185
+ },
186
+ Argon2i: {
187
+ deriveBits: 'Argon2Params',
188
+ 'get key length': null,
189
+ importKey: null,
190
+ },
191
+ Argon2id: {
192
+ deriveBits: 'Argon2Params',
193
+ 'get key length': null,
194
+ importKey: null,
195
+ },
196
+ 'ChaCha20-Poly1305': {
197
+ generateKey: null,
198
+ exportKey: null,
199
+ importKey: null,
200
+ encrypt: 'AeadParams',
201
+ decrypt: 'AeadParams',
202
+ 'get key length': null,
203
+ },
204
+ ECDH: {
205
+ generateKey: 'EcKeyGenParams',
206
+ exportKey: null,
207
+ importKey: 'EcKeyImportParams',
208
+ deriveBits: 'EcdhKeyDeriveParams',
209
+ },
210
+ ECDSA: {
211
+ generateKey: 'EcKeyGenParams',
212
+ exportKey: null,
213
+ importKey: 'EcKeyImportParams',
214
+ sign: 'EcdsaParams',
215
+ verify: 'EcdsaParams',
216
+ },
217
+ Ed25519: {
218
+ generateKey: null,
219
+ exportKey: null,
220
+ importKey: null,
221
+ sign: null,
222
+ verify: null,
223
+ },
224
+ Ed448: {
225
+ generateKey: null,
226
+ exportKey: null,
227
+ importKey: null,
228
+ sign: 'ContextParams',
229
+ verify: 'ContextParams',
230
+ },
231
+ HKDF: {
232
+ importKey: null,
233
+ deriveBits: 'HkdfParams',
234
+ 'get key length': null,
235
+ },
236
+ HMAC: {
237
+ generateKey: 'HmacKeyGenParams',
238
+ exportKey: null,
239
+ importKey: 'HmacImportParams',
240
+ sign: null,
241
+ verify: null,
242
+ 'get key length': 'HmacImportParams',
243
+ },
244
+ KMAC128: {
245
+ generateKey: 'KmacKeyGenParams',
246
+ exportKey: null,
247
+ importKey: 'KmacImportParams',
248
+ sign: 'KmacParams',
249
+ verify: 'KmacParams',
250
+ 'get key length': 'KmacImportParams',
251
+ },
252
+ KMAC256: {
253
+ generateKey: 'KmacKeyGenParams',
254
+ exportKey: null,
255
+ importKey: 'KmacImportParams',
256
+ sign: 'KmacParams',
257
+ verify: 'KmacParams',
258
+ 'get key length': 'KmacImportParams',
259
+ },
260
+ 'ML-DSA-44': {
261
+ generateKey: null,
262
+ exportKey: null,
263
+ importKey: null,
264
+ sign: 'ContextParams',
265
+ verify: 'ContextParams',
266
+ },
267
+ 'ML-DSA-65': {
268
+ generateKey: null,
269
+ exportKey: null,
270
+ importKey: null,
271
+ sign: 'ContextParams',
272
+ verify: 'ContextParams',
273
+ },
274
+ 'ML-DSA-87': {
275
+ generateKey: null,
276
+ exportKey: null,
277
+ importKey: null,
278
+ sign: 'ContextParams',
279
+ verify: 'ContextParams',
280
+ },
281
+ 'ML-KEM-512': {
282
+ generateKey: null,
283
+ exportKey: null,
284
+ importKey: null,
285
+ encapsulateBits: null,
286
+ decapsulateBits: null,
287
+ encapsulateKey: null,
288
+ decapsulateKey: null,
289
+ },
290
+ 'ML-KEM-768': {
291
+ generateKey: null,
292
+ exportKey: null,
293
+ importKey: null,
294
+ encapsulateBits: null,
295
+ decapsulateBits: null,
296
+ encapsulateKey: null,
297
+ decapsulateKey: null,
298
+ },
299
+ 'ML-KEM-1024': {
300
+ generateKey: null,
301
+ exportKey: null,
302
+ importKey: null,
303
+ encapsulateBits: null,
304
+ decapsulateBits: null,
305
+ encapsulateKey: null,
306
+ decapsulateKey: null,
307
+ },
308
+ PBKDF2: {
309
+ importKey: null,
310
+ deriveBits: 'Pbkdf2Params',
311
+ 'get key length': null,
312
+ },
313
+ 'RSA-OAEP': {
314
+ generateKey: 'RsaHashedKeyGenParams',
315
+ exportKey: null,
316
+ importKey: 'RsaHashedImportParams',
317
+ encrypt: 'RsaOaepParams',
318
+ decrypt: 'RsaOaepParams',
319
+ },
320
+ 'RSA-PSS': {
321
+ generateKey: 'RsaHashedKeyGenParams',
322
+ exportKey: null,
323
+ importKey: 'RsaHashedImportParams',
324
+ sign: 'RsaPssParams',
325
+ verify: 'RsaPssParams',
326
+ },
327
+ 'RSASSA-PKCS1-v1_5': {
328
+ generateKey: 'RsaHashedKeyGenParams',
329
+ exportKey: null,
330
+ importKey: 'RsaHashedImportParams',
331
+ sign: null,
332
+ verify: null,
333
+ },
334
+ 'SHA-1': { digest: null },
335
+ 'SHA-256': { digest: null },
336
+ 'SHA-384': { digest: null },
337
+ 'SHA-512': { digest: null },
338
+ 'SHA3-256': { digest: null },
339
+ 'SHA3-384': { digest: null },
340
+ 'SHA3-512': { digest: null },
341
+ cSHAKE128: { digest: 'CShakeParams' },
342
+ cSHAKE256: { digest: 'CShakeParams' },
343
+ KT128: { digest: 'KangarooTwelveParams' },
344
+ KT256: { digest: 'KangarooTwelveParams' },
345
+ TurboSHAKE128: { digest: 'TurboShakeParams' },
346
+ TurboSHAKE256: { digest: 'TurboShakeParams' },
347
+ X25519: {
348
+ generateKey: null,
349
+ exportKey: null,
350
+ importKey: null,
351
+ deriveBits: 'EcdhKeyDeriveParams',
352
+ },
353
+ X448: {
354
+ generateKey: null,
355
+ exportKey: null,
356
+ importKey: null,
357
+ deriveBits: 'EcdhKeyDeriveParams',
358
+ },
359
+ };
360
+
361
+ for (const v of SLH_DSA_VARIANTS) {
362
+ kAlgorithmDefinitions[v] = {
363
+ generateKey: null,
364
+ exportKey: null,
365
+ importKey: null,
366
+ sign: null,
367
+ verify: null,
368
+ };
369
+ }
370
+
371
+ // WebIDL dictionary member specs. Mirrors Node's per-converter
372
+ // `createDictionaryConverter` definitions in lib/internal/crypto/webidl.js.
373
+ // `required: true` causes `normalizeAlgorithm` to throw a TypeError when the
374
+ // member is missing — matching the spec'd WebCrypto behavior that
375
+ // `SubtleCrypto.supports` relies on via try/catch.
376
+ interface IdlField {
377
+ key: string;
378
+ required?: boolean;
379
+ }
380
+
381
+ type NormalizedAlgorithmRecord = SubtleAlgorithm & Record<string, unknown>;
382
+
383
+ const kRequiredFields: Record<string, IdlField[]> = {
384
+ AesKeyGenParams: [{ key: 'length', required: true }],
385
+ AesDerivedKeyParams: [{ key: 'length', required: true }],
386
+ AesCbcParams: [{ key: 'iv', required: true }],
387
+ AesCtrParams: [
388
+ { key: 'counter', required: true },
389
+ { key: 'length', required: true },
390
+ ],
391
+ AeadParams: [
392
+ { key: 'iv', required: true },
393
+ { key: 'tagLength' },
394
+ { key: 'additionalData' },
395
+ ],
396
+ EcKeyGenParams: [{ key: 'namedCurve', required: true }],
397
+ EcKeyImportParams: [{ key: 'namedCurve', required: true }],
398
+ EcdsaParams: [{ key: 'hash', required: true }],
399
+ EcdhKeyDeriveParams: [{ key: 'public', required: true }],
400
+ HmacKeyGenParams: [{ key: 'hash', required: true }, { key: 'length' }],
401
+ HmacImportParams: [{ key: 'hash', required: true }, { key: 'length' }],
402
+ HkdfParams: [
403
+ { key: 'hash', required: true },
404
+ { key: 'salt', required: true },
405
+ { key: 'info', required: true },
406
+ ],
407
+ Pbkdf2Params: [
408
+ { key: 'hash', required: true },
409
+ { key: 'iterations', required: true },
410
+ { key: 'salt', required: true },
411
+ ],
412
+ RsaHashedKeyGenParams: [
413
+ { key: 'modulusLength', required: true },
414
+ { key: 'publicExponent', required: true },
415
+ { key: 'hash', required: true },
416
+ ],
417
+ RsaHashedImportParams: [{ key: 'hash', required: true }],
418
+ RsaOaepParams: [{ key: 'label' }],
419
+ RsaPssParams: [{ key: 'saltLength', required: true }],
420
+ ContextParams: [{ key: 'context' }],
421
+ Argon2Params: [
422
+ { key: 'nonce', required: true },
423
+ { key: 'parallelism', required: true },
424
+ { key: 'memory', required: true },
425
+ { key: 'passes', required: true },
426
+ { key: 'version' },
427
+ { key: 'secretValue' },
428
+ { key: 'associatedData' },
429
+ ],
430
+ KmacKeyGenParams: [{ key: 'length' }],
431
+ KmacImportParams: [{ key: 'length' }],
432
+ KmacParams: [
433
+ { key: 'outputLength', required: true },
434
+ { key: 'customization' },
435
+ ],
436
+ CShakeParams: [
437
+ { key: 'outputLength', required: true },
438
+ { key: 'functionName' },
439
+ { key: 'customization' },
440
+ ],
441
+ KangarooTwelveParams: [
442
+ { key: 'outputLength', required: true },
443
+ { key: 'customization' },
444
+ ],
445
+ TurboShakeParams: [
446
+ { key: 'outputLength', required: true },
447
+ { key: 'domainSeparation' },
448
+ ],
449
+ };
450
+
451
+ function isBufferSource(value: unknown): value is BufferLike {
452
+ return value instanceof ArrayBuffer || ArrayBuffer.isView(value);
453
+ }
454
+
455
+ function validateBufferSource(
456
+ algorithm: NormalizedAlgorithmRecord,
457
+ key: string,
458
+ ): ArrayBuffer | undefined {
459
+ const value = algorithm[key];
460
+ if (value === undefined) return undefined;
461
+ if (!isBufferSource(value)) {
462
+ throw new TypeError(
463
+ `Failed to normalize algorithm: '${key}' must be a BufferSource`,
464
+ );
465
+ }
466
+ return bufferLikeToArrayBuffer(value);
467
+ }
468
+
469
+ function validateBinaryLike(
470
+ algorithm: NormalizedAlgorithmRecord,
471
+ key: string,
472
+ ): ArrayBuffer | undefined {
473
+ const value = algorithm[key];
474
+ if (value === undefined) return undefined;
475
+ try {
476
+ return binaryLikeToArrayBuffer(value as BinaryLike);
477
+ } catch {
478
+ throw new TypeError(
479
+ `Failed to normalize algorithm: '${key}' must be a BufferSource`,
480
+ );
481
+ }
482
+ }
483
+
484
+ function validateByteLength(
485
+ buffer: ArrayBuffer | undefined,
486
+ key: string,
487
+ length: number,
488
+ message?: string,
489
+ ): void {
490
+ if (buffer !== undefined && buffer.byteLength !== length) {
491
+ throw lazyDOMException(
492
+ message ?? `${key} must be ${length} bytes`,
493
+ 'OperationError',
494
+ );
495
+ }
496
+ }
497
+
498
+ function validateUnsignedInteger(
499
+ algorithm: NormalizedAlgorithmRecord,
500
+ key: string,
501
+ ): number | undefined {
502
+ const value = algorithm[key];
503
+ if (value === undefined) return undefined;
504
+ const numberValue = Number(value);
505
+ if (
506
+ !Number.isFinite(numberValue) ||
507
+ !Number.isInteger(numberValue) ||
508
+ numberValue < 0
509
+ ) {
510
+ throw new TypeError(
511
+ `Failed to normalize algorithm: '${key}' must be an unsigned integer`,
512
+ );
513
+ }
514
+ algorithm[key] = numberValue;
515
+ return numberValue;
516
+ }
517
+
518
+ function validateHashAlgorithm(
519
+ algorithm: NormalizedAlgorithmRecord,
520
+ converterName: string,
521
+ ): void {
522
+ const hash = algorithm.hash as string | { name: string } | undefined;
523
+ if (hash === undefined) return;
524
+ const normalizedHash = normalizeHashName(hash, HashContext.WebCrypto);
525
+ if (
526
+ ![
527
+ 'SHA-1',
528
+ 'SHA-256',
529
+ 'SHA-384',
530
+ 'SHA-512',
531
+ 'SHA3-256',
532
+ 'SHA3-384',
533
+ 'SHA3-512',
534
+ ].includes(normalizedHash)
535
+ ) {
536
+ throw lazyDOMException(
537
+ `Unsupported ${converterName}.hash`,
538
+ 'NotSupportedError',
539
+ );
540
+ }
541
+ algorithm.hash = { name: normalizedHash };
542
+ }
543
+
544
+ function validateAesLength(length: number | undefined): void {
545
+ if (
546
+ length !== undefined &&
547
+ length !== 128 &&
548
+ length !== 192 &&
549
+ length !== 256
550
+ ) {
551
+ throw lazyDOMException('Invalid key length', 'OperationError');
552
+ }
553
+ }
554
+
555
+ function validateMacLength(
556
+ algorithm: NormalizedAlgorithmRecord,
557
+ key: string,
558
+ zeroError: 'DataError' | 'OperationError',
559
+ ): void {
560
+ const length = validateUnsignedInteger(algorithm, key);
561
+ if (length === undefined) return;
562
+ if (length === 0) {
563
+ throw lazyDOMException(`${key} cannot be 0`, zeroError);
564
+ }
565
+ if (length % 8) {
566
+ throw lazyDOMException(`Unsupported ${key}`, 'NotSupportedError');
567
+ }
568
+ }
569
+
570
+ function validateAeadParams(algorithm: NormalizedAlgorithmRecord): void {
571
+ const iv = validateBufferSource(algorithm, 'iv');
572
+ const tagLength = validateUnsignedInteger(algorithm, 'tagLength');
573
+ validateBufferSource(algorithm, 'additionalData');
574
+
575
+ switch (algorithm.name) {
576
+ case 'AES-GCM':
577
+ if (
578
+ tagLength !== undefined &&
579
+ ![32, 64, 96, 104, 112, 120, 128].includes(tagLength)
580
+ ) {
581
+ throw lazyDOMException(
582
+ `${tagLength} is not a valid AES-GCM tag length`,
583
+ 'OperationError',
584
+ );
585
+ }
586
+ break;
587
+ case 'AES-OCB':
588
+ if (iv !== undefined && (iv.byteLength < 1 || iv.byteLength > 15)) {
589
+ throw lazyDOMException(
590
+ 'AES-OCB algorithm.iv must be between 1 and 15 bytes',
591
+ 'OperationError',
592
+ );
593
+ }
594
+ if (tagLength !== undefined && ![64, 96, 128].includes(tagLength)) {
595
+ throw lazyDOMException(
596
+ `${tagLength} is not a valid AES-OCB tag length`,
597
+ 'OperationError',
598
+ );
599
+ }
600
+ break;
601
+ case 'ChaCha20-Poly1305':
602
+ validateByteLength(
603
+ iv,
604
+ 'algorithm.iv',
605
+ 12,
606
+ 'ChaCha20-Poly1305 IV must be exactly 12 bytes',
607
+ );
608
+ if (tagLength !== undefined && tagLength !== 128) {
609
+ throw lazyDOMException(
610
+ `${tagLength} is not a valid ChaCha20-Poly1305 tag length`,
611
+ 'OperationError',
612
+ );
613
+ }
614
+ break;
615
+ }
616
+ }
617
+
618
+ function validateNormalizedAlgorithm(
619
+ converterName: string,
620
+ algorithm: NormalizedAlgorithmRecord,
621
+ ): void {
622
+ switch (converterName) {
623
+ case 'AesKeyGenParams':
624
+ case 'AesDerivedKeyParams':
625
+ validateAesLength(validateUnsignedInteger(algorithm, 'length'));
626
+ break;
627
+ case 'AesCbcParams':
628
+ validateByteLength(
629
+ validateBufferSource(algorithm, 'iv'),
630
+ 'algorithm.iv',
631
+ 16,
632
+ 'algorithm.iv must contain exactly 16 bytes',
633
+ );
634
+ break;
635
+ case 'AesCtrParams': {
636
+ validateByteLength(
637
+ validateBufferSource(algorithm, 'counter'),
638
+ 'algorithm.counter',
639
+ 16,
640
+ );
641
+ const length = validateUnsignedInteger(algorithm, 'length');
642
+ if (length !== undefined && (length === 0 || length > 128)) {
643
+ throw lazyDOMException(
644
+ 'AES-CTR algorithm.length must be between 1 and 128',
645
+ 'OperationError',
646
+ );
647
+ }
648
+ break;
649
+ }
650
+ case 'AeadParams':
651
+ validateAeadParams(algorithm);
652
+ break;
653
+ case 'EcdsaParams':
654
+ case 'HmacKeyGenParams':
655
+ case 'HmacImportParams':
656
+ case 'HkdfParams':
657
+ case 'Pbkdf2Params':
658
+ case 'RsaHashedKeyGenParams':
659
+ case 'RsaHashedImportParams':
660
+ validateHashAlgorithm(algorithm, converterName);
661
+ if (converterName === 'HmacKeyGenParams') {
662
+ validateMacLength(algorithm, 'length', 'OperationError');
663
+ }
664
+ if (converterName === 'HkdfParams') {
665
+ validateBinaryLike(algorithm, 'salt');
666
+ validateBinaryLike(algorithm, 'info');
667
+ } else if (converterName === 'Pbkdf2Params') {
668
+ const iterations = validateUnsignedInteger(algorithm, 'iterations');
669
+ if (iterations === 0) {
670
+ throw lazyDOMException('iterations cannot be zero', 'OperationError');
671
+ }
672
+ validateBinaryLike(algorithm, 'salt');
673
+ } else if (converterName === 'RsaHashedKeyGenParams') {
674
+ validateUnsignedInteger(algorithm, 'modulusLength');
675
+ validateBufferSource(algorithm, 'publicExponent');
676
+ }
677
+ break;
678
+ case 'RsaPssParams':
679
+ validateUnsignedInteger(algorithm, 'saltLength');
680
+ break;
681
+ case 'RsaOaepParams':
682
+ validateBufferSource(algorithm, 'label');
683
+ break;
684
+ case 'ContextParams':
685
+ validateBufferSource(algorithm, 'context');
686
+ break;
687
+ case 'EcdhKeyDeriveParams':
688
+ if (!(algorithm.public instanceof CryptoKey)) {
689
+ throw lazyDOMException(
690
+ 'algorithm.public must be a public key',
691
+ 'InvalidAccessError',
692
+ );
693
+ }
694
+ break;
695
+ case 'Argon2Params': {
696
+ validateBufferSource(algorithm, 'nonce');
697
+ const parallelism = validateUnsignedInteger(algorithm, 'parallelism');
698
+ const memory = validateUnsignedInteger(algorithm, 'memory');
699
+ validateUnsignedInteger(algorithm, 'passes');
700
+ const version = validateUnsignedInteger(algorithm, 'version');
701
+ validateBufferSource(algorithm, 'secretValue');
702
+ validateBufferSource(algorithm, 'associatedData');
703
+ if (
704
+ parallelism !== undefined &&
705
+ (parallelism === 0 || parallelism > 2 ** 24 - 1)
706
+ ) {
707
+ throw lazyDOMException(
708
+ 'parallelism must be > 0 and < 16777215',
709
+ 'OperationError',
710
+ );
711
+ }
712
+ if (
713
+ memory !== undefined &&
714
+ parallelism !== undefined &&
715
+ memory < 8 * parallelism
716
+ ) {
717
+ throw lazyDOMException(
718
+ 'memory must be at least 8 times the degree of parallelism',
719
+ 'OperationError',
720
+ );
721
+ }
722
+ if (version !== undefined && version !== 0x13) {
723
+ throw lazyDOMException(
724
+ `${version} is not a valid Argon2 version`,
725
+ 'OperationError',
726
+ );
727
+ }
728
+ break;
729
+ }
730
+ case 'KmacKeyGenParams':
731
+ validateMacLength(algorithm, 'length', 'OperationError');
732
+ break;
733
+ case 'KmacImportParams':
734
+ validateMacLength(algorithm, 'length', 'DataError');
735
+ break;
736
+ case 'KmacParams':
737
+ validateMacLength(algorithm, 'outputLength', 'OperationError');
738
+ validateBufferSource(algorithm, 'customization');
739
+ break;
740
+ case 'CShakeParams':
741
+ case 'KangarooTwelveParams': {
742
+ const outputLength = validateUnsignedInteger(algorithm, 'outputLength');
743
+ if (
744
+ outputLength !== undefined &&
745
+ (outputLength === 0 || outputLength % 8)
746
+ ) {
747
+ throw lazyDOMException(
748
+ `Invalid ${converterName} outputLength`,
749
+ 'OperationError',
750
+ );
751
+ }
752
+ validateBufferSource(algorithm, 'functionName');
753
+ validateBufferSource(algorithm, 'customization');
754
+ break;
755
+ }
756
+ case 'TurboShakeParams': {
757
+ const outputLength = validateUnsignedInteger(algorithm, 'outputLength');
758
+ const domainSeparation = validateUnsignedInteger(
759
+ algorithm,
760
+ 'domainSeparation',
761
+ );
762
+ if (
763
+ outputLength !== undefined &&
764
+ (outputLength === 0 || outputLength % 8)
765
+ ) {
766
+ throw lazyDOMException(
767
+ 'Invalid TurboShakeParams outputLength',
768
+ 'OperationError',
769
+ );
770
+ }
771
+ if (
772
+ domainSeparation !== undefined &&
773
+ (domainSeparation < 0x01 || domainSeparation > 0x7f)
774
+ ) {
775
+ throw lazyDOMException(
776
+ 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f',
777
+ 'OperationError',
778
+ );
779
+ }
780
+ break;
781
+ }
782
+ }
783
+ }
784
+
785
+ // WebCrypto §18.4.4 algorithm normalization. Mirrors Node's normalizeAlgorithm
786
+ // in lib/internal/crypto/util.js: canonicalizes `name`, looks up the
787
+ // (name, op) → converter mapping, and rejects inputs that omit required
788
+ // dictionary members. Callers in Subtle.supports rely on this throwing for
789
+ // invalid params — without it, supports() over-reports capability (#1025).
133
790
  function normalizeAlgorithm(
134
791
  algorithm: SubtleAlgorithm | AnyAlgorithm,
135
- _operation: Operation,
792
+ operation: Operation | string,
136
793
  ): SubtleAlgorithm {
137
- const map = getCanonicalAlgorithmNames();
138
794
  if (typeof algorithm === 'string') {
139
- return { name: map.get(algorithm.toLowerCase()) ?? algorithm };
795
+ return normalizeAlgorithm({ name: algorithm }, operation);
796
+ }
797
+ const name = (algorithm as { name?: unknown }).name;
798
+ if (typeof name !== 'string') {
799
+ throw new TypeError("Algorithm: 'name' is required");
800
+ }
801
+ const map = getCanonicalAlgorithmNames();
802
+ const canonical = map.get(name.toLowerCase());
803
+ if (canonical === undefined) {
804
+ throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
805
+ }
806
+ const opMap = kAlgorithmDefinitions[canonical];
807
+ if (!opMap || !(operation in opMap)) {
808
+ throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
140
809
  }
141
- if (typeof algorithm.name === 'string') {
142
- const canonical = map.get(algorithm.name.toLowerCase()) ?? algorithm.name;
810
+ const converterName = opMap[operation];
811
+ if (converterName == null) {
812
+ return { name: canonical };
813
+ }
814
+ const fields = kRequiredFields[converterName];
815
+ if (!fields) {
143
816
  return { ...algorithm, name: canonical };
144
817
  }
145
- return algorithm as SubtleAlgorithm;
818
+ const out = { name: canonical } as NormalizedAlgorithmRecord;
819
+ const src = algorithm as Record<string, unknown>;
820
+ for (const field of fields) {
821
+ const value = src[field.key];
822
+ if (value === undefined) {
823
+ if (field.required) {
824
+ throw new TypeError(
825
+ `Failed to normalize algorithm: '${field.key}' is required in '${converterName}'`,
826
+ );
827
+ }
828
+ continue;
829
+ }
830
+ (out as Record<string, unknown>)[field.key] = value;
831
+ }
832
+ validateNormalizedAlgorithm(converterName, out);
833
+ return out;
146
834
  }
147
835
 
148
836
  function getAlgorithmName(name: string, length: number): string {
@@ -2884,10 +3572,7 @@ export class Subtle {
2884
3572
  data: BufferLike,
2885
3573
  ): Promise<ArrayBuffer> {
2886
3574
  requireArgs(arguments.length, 2, 'digest');
2887
- const normalizedAlgorithm = normalizeAlgorithm(
2888
- algorithm,
2889
- 'digest' as Operation,
2890
- );
3575
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest');
2891
3576
  return asyncDigest(normalizedAlgorithm, data);
2892
3577
  }
2893
3578
 
@@ -2897,7 +3582,6 @@ export class Subtle {
2897
3582
  length: number | null = null,
2898
3583
  ): Promise<ArrayBuffer> {
2899
3584
  requireArgs(arguments.length, 2, 'deriveBits');
2900
- const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
2901
3585
  // WebCrypto §SubtleCrypto.deriveBits step 11: throw InvalidAccessError
2902
3586
  // unless `baseKey.[[usages]]` contains "deriveBits" specifically. The
2903
3587
  // previous `deriveBits || deriveKey` accept-either branch silently
@@ -2909,6 +3593,7 @@ export class Subtle {
2909
3593
  'InvalidAccessError',
2910
3594
  );
2911
3595
  }
3596
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
2912
3597
  if (baseKey.algorithm.name !== normalizedAlgorithm.name) {
2913
3598
  throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
2914
3599
  }
@@ -2956,10 +3641,9 @@ export class Subtle {
2956
3641
  ): Promise<CryptoKey> {
2957
3642
  requireArgs(arguments.length, 5, 'deriveKey');
2958
3643
  const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
2959
- const normalizedDerivedKeyAlgorithm = normalizeAlgorithm(
2960
- derivedKeyAlgorithm,
2961
- 'importKey',
2962
- );
3644
+ // Validate the derived-key algorithm up front (mirrors Node webcrypto.js:341).
3645
+ // The normalized form is unused — `this.importKey` re-normalizes below.
3646
+ normalizeAlgorithm(derivedKeyAlgorithm, 'importKey');
2963
3647
 
2964
3648
  // Validate baseKey usage
2965
3649
  if (!baseKey.usages.includes('deriveKey')) {
@@ -2974,7 +3658,12 @@ export class Subtle {
2974
3658
  }
2975
3659
 
2976
3660
  // Calculate required key length (may be null for KDF-derived material).
2977
- const length = getKeyLength(normalizedDerivedKeyAlgorithm);
3661
+ // Mirrors Node webcrypto.js:350 — uses the raw derivedKeyAlgorithm with
3662
+ // op='get key length' so AES `length` survives normalization (the
3663
+ // 'importKey' converter for AES is null and strips dictionary members).
3664
+ const length = getKeyLength(
3665
+ normalizeAlgorithm(derivedKeyAlgorithm, 'get key length'),
3666
+ );
2978
3667
 
2979
3668
  // Step 1: Derive bits
2980
3669
  let derivedBits: ArrayBuffer;
@@ -3114,10 +3803,21 @@ export class Subtle {
3114
3803
  wrapAlgorithm: EncryptDecryptParams,
3115
3804
  ): Promise<ArrayBuffer> {
3116
3805
  requireArgs(arguments.length, 4, 'wrapKey');
3117
- const normalizedWrapAlgorithm = normalizeAlgorithm(
3118
- wrapAlgorithm,
3119
- 'wrapKey',
3120
- ) as EncryptDecryptParams;
3806
+ // Mirrors Node webcrypto.js:923-927: prefer the 'wrapKey' op (only
3807
+ // AES-KW defines it) and fall back to 'encrypt' for cipher-based wrap
3808
+ // algorithms like AES-GCM and RSA-OAEP.
3809
+ let normalizedWrapAlgorithm: EncryptDecryptParams;
3810
+ try {
3811
+ normalizedWrapAlgorithm = normalizeAlgorithm(
3812
+ wrapAlgorithm,
3813
+ 'wrapKey',
3814
+ ) as EncryptDecryptParams;
3815
+ } catch {
3816
+ normalizedWrapAlgorithm = normalizeAlgorithm(
3817
+ wrapAlgorithm,
3818
+ 'encrypt',
3819
+ ) as EncryptDecryptParams;
3820
+ }
3121
3821
 
3122
3822
  if (normalizedWrapAlgorithm.name !== wrappingKey.algorithm.name) {
3123
3823
  throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
@@ -3174,10 +3874,20 @@ export class Subtle {
3174
3874
  keyUsages: KeyUsage[],
3175
3875
  ): Promise<CryptoKey> {
3176
3876
  requireArgs(arguments.length, 7, 'unwrapKey');
3177
- const normalizedUnwrapAlgorithm = normalizeAlgorithm(
3178
- unwrapAlgorithm,
3179
- 'unwrapKey',
3180
- ) as EncryptDecryptParams;
3877
+ // Mirrors Node webcrypto.js:1006-1010: prefer 'unwrapKey', fall back to
3878
+ // 'decrypt' for cipher-based unwrap algorithms.
3879
+ let normalizedUnwrapAlgorithm: EncryptDecryptParams;
3880
+ try {
3881
+ normalizedUnwrapAlgorithm = normalizeAlgorithm(
3882
+ unwrapAlgorithm,
3883
+ 'unwrapKey',
3884
+ ) as EncryptDecryptParams;
3885
+ } catch {
3886
+ normalizedUnwrapAlgorithm = normalizeAlgorithm(
3887
+ unwrapAlgorithm,
3888
+ 'decrypt',
3889
+ ) as EncryptDecryptParams;
3890
+ }
3181
3891
 
3182
3892
  if (normalizedUnwrapAlgorithm.name !== unwrappingKey.algorithm.name) {
3183
3893
  throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');