react-native-quick-crypto 1.1.1 → 1.1.2

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.
Files changed (181) hide show
  1. package/QuickCrypto.podspec +1 -0
  2. package/android/CMakeLists.txt +4 -0
  3. package/cpp/cipher/CCMCipher.cpp +7 -11
  4. package/cpp/cipher/ChaCha20Cipher.cpp +6 -10
  5. package/cpp/cipher/ChaCha20Poly1305Cipher.cpp +10 -16
  6. package/cpp/cipher/GCMCipher.cpp +3 -5
  7. package/cpp/cipher/HybridCipher.cpp +7 -13
  8. package/cpp/cipher/HybridRsaCipher.cpp +19 -27
  9. package/cpp/cipher/OCBCipher.cpp +2 -3
  10. package/cpp/cipher/XChaCha20Poly1305Cipher.cpp +13 -19
  11. package/cpp/cipher/XSalsa20Cipher.cpp +8 -12
  12. package/cpp/cipher/XSalsa20Poly1305Cipher.cpp +11 -16
  13. package/cpp/keys/HybridKeyObjectHandle.cpp +630 -2
  14. package/cpp/keys/HybridKeyObjectHandle.hpp +21 -1
  15. package/cpp/sign/HybridSignHandle.cpp +26 -8
  16. package/cpp/sign/HybridVerifyHandle.cpp +28 -11
  17. package/cpp/slhdsa/HybridSlhDsaKeyPair.cpp +245 -0
  18. package/cpp/slhdsa/HybridSlhDsaKeyPair.hpp +48 -0
  19. package/cpp/turboshake/HybridTurboShake.cpp +379 -0
  20. package/cpp/turboshake/HybridTurboShake.hpp +28 -0
  21. package/cpp/utils/HybridUtils.cpp +26 -14
  22. package/deps/blake3/README.md +6 -7
  23. package/deps/blake3/c/blake3.c +3 -2
  24. package/deps/blake3/c/blake3.h +2 -2
  25. package/deps/blake3/c/blake3_dispatch.c +2 -2
  26. package/deps/blake3/c/blake3_impl.h +1 -1
  27. package/deps/blake3/c/blake3_neon.c +5 -4
  28. package/deps/ncrypto/include/ncrypto/version.h +2 -2
  29. package/deps/ncrypto/include/ncrypto.h +9 -2
  30. package/deps/ncrypto/src/ncrypto.cpp +130 -35
  31. package/lib/commonjs/dhKeyPair.js +3 -0
  32. package/lib/commonjs/dhKeyPair.js.map +1 -1
  33. package/lib/commonjs/dsa.js +3 -0
  34. package/lib/commonjs/dsa.js.map +1 -1
  35. package/lib/commonjs/ec.js +37 -30
  36. package/lib/commonjs/ec.js.map +1 -1
  37. package/lib/commonjs/ed.js +60 -6
  38. package/lib/commonjs/ed.js.map +1 -1
  39. package/lib/commonjs/hash.js +52 -5
  40. package/lib/commonjs/hash.js.map +1 -1
  41. package/lib/commonjs/keys/classes.js +33 -7
  42. package/lib/commonjs/keys/classes.js.map +1 -1
  43. package/lib/commonjs/keys/generateKeyPair.js +85 -4
  44. package/lib/commonjs/keys/generateKeyPair.js.map +1 -1
  45. package/lib/commonjs/keys/index.js +50 -2
  46. package/lib/commonjs/keys/index.js.map +1 -1
  47. package/lib/commonjs/keys/signVerify.js +9 -2
  48. package/lib/commonjs/keys/signVerify.js.map +1 -1
  49. package/lib/commonjs/keys/utils.js +59 -1
  50. package/lib/commonjs/keys/utils.js.map +1 -1
  51. package/lib/commonjs/random.js +63 -9
  52. package/lib/commonjs/random.js.map +1 -1
  53. package/lib/commonjs/rsa.js +3 -0
  54. package/lib/commonjs/rsa.js.map +1 -1
  55. package/lib/commonjs/slhdsa.js +70 -0
  56. package/lib/commonjs/slhdsa.js.map +1 -0
  57. package/lib/commonjs/specs/slhDsaKeyPair.nitro.js +6 -0
  58. package/lib/commonjs/specs/slhDsaKeyPair.nitro.js.map +1 -0
  59. package/lib/commonjs/specs/turboshake.nitro.js +6 -0
  60. package/lib/commonjs/specs/turboshake.nitro.js.map +1 -0
  61. package/lib/commonjs/subtle.js +926 -275
  62. package/lib/commonjs/subtle.js.map +1 -1
  63. package/lib/commonjs/utils/conversion.js +53 -19
  64. package/lib/commonjs/utils/conversion.js.map +1 -1
  65. package/lib/commonjs/utils/errors.js +63 -4
  66. package/lib/commonjs/utils/errors.js.map +1 -1
  67. package/lib/commonjs/utils/types.js.map +1 -1
  68. package/lib/commonjs/utils/validation.js +46 -0
  69. package/lib/commonjs/utils/validation.js.map +1 -1
  70. package/lib/module/dhKeyPair.js +3 -0
  71. package/lib/module/dhKeyPair.js.map +1 -1
  72. package/lib/module/dsa.js +3 -0
  73. package/lib/module/dsa.js.map +1 -1
  74. package/lib/module/ec.js +38 -31
  75. package/lib/module/ec.js.map +1 -1
  76. package/lib/module/ed.js +61 -7
  77. package/lib/module/ed.js.map +1 -1
  78. package/lib/module/hash.js +52 -5
  79. package/lib/module/hash.js.map +1 -1
  80. package/lib/module/keys/classes.js +31 -5
  81. package/lib/module/keys/classes.js.map +1 -1
  82. package/lib/module/keys/generateKeyPair.js +86 -5
  83. package/lib/module/keys/generateKeyPair.js.map +1 -1
  84. package/lib/module/keys/index.js +50 -2
  85. package/lib/module/keys/index.js.map +1 -1
  86. package/lib/module/keys/signVerify.js +9 -2
  87. package/lib/module/keys/signVerify.js.map +1 -1
  88. package/lib/module/keys/utils.js +57 -1
  89. package/lib/module/keys/utils.js.map +1 -1
  90. package/lib/module/random.js +63 -10
  91. package/lib/module/random.js.map +1 -1
  92. package/lib/module/rsa.js +3 -0
  93. package/lib/module/rsa.js.map +1 -1
  94. package/lib/module/slhdsa.js +64 -0
  95. package/lib/module/slhdsa.js.map +1 -0
  96. package/lib/module/specs/slhDsaKeyPair.nitro.js +4 -0
  97. package/lib/module/specs/slhDsaKeyPair.nitro.js.map +1 -0
  98. package/lib/module/specs/turboshake.nitro.js +4 -0
  99. package/lib/module/specs/turboshake.nitro.js.map +1 -0
  100. package/lib/module/subtle.js +927 -276
  101. package/lib/module/subtle.js.map +1 -1
  102. package/lib/module/utils/conversion.js +51 -19
  103. package/lib/module/utils/conversion.js.map +1 -1
  104. package/lib/module/utils/errors.js +61 -4
  105. package/lib/module/utils/errors.js.map +1 -1
  106. package/lib/module/utils/types.js.map +1 -1
  107. package/lib/module/utils/validation.js +44 -0
  108. package/lib/module/utils/validation.js.map +1 -1
  109. package/lib/typescript/dhKeyPair.d.ts.map +1 -1
  110. package/lib/typescript/dsa.d.ts.map +1 -1
  111. package/lib/typescript/ec.d.ts.map +1 -1
  112. package/lib/typescript/ed.d.ts.map +1 -1
  113. package/lib/typescript/hash.d.ts.map +1 -1
  114. package/lib/typescript/index.d.ts +12 -7
  115. package/lib/typescript/index.d.ts.map +1 -1
  116. package/lib/typescript/keys/classes.d.ts +10 -1
  117. package/lib/typescript/keys/classes.d.ts.map +1 -1
  118. package/lib/typescript/keys/generateKeyPair.d.ts +12 -1
  119. package/lib/typescript/keys/generateKeyPair.d.ts.map +1 -1
  120. package/lib/typescript/keys/index.d.ts +3 -1
  121. package/lib/typescript/keys/index.d.ts.map +1 -1
  122. package/lib/typescript/keys/signVerify.d.ts.map +1 -1
  123. package/lib/typescript/keys/utils.d.ts +21 -4
  124. package/lib/typescript/keys/utils.d.ts.map +1 -1
  125. package/lib/typescript/random.d.ts +5 -1
  126. package/lib/typescript/random.d.ts.map +1 -1
  127. package/lib/typescript/rsa.d.ts.map +1 -1
  128. package/lib/typescript/slhdsa.d.ts +19 -0
  129. package/lib/typescript/slhdsa.d.ts.map +1 -0
  130. package/lib/typescript/specs/keyObjectHandle.nitro.d.ts +9 -0
  131. package/lib/typescript/specs/keyObjectHandle.nitro.d.ts.map +1 -1
  132. package/lib/typescript/specs/slhDsaKeyPair.nitro.d.ts +16 -0
  133. package/lib/typescript/specs/slhDsaKeyPair.nitro.d.ts.map +1 -0
  134. package/lib/typescript/specs/turboshake.nitro.d.ts +11 -0
  135. package/lib/typescript/specs/turboshake.nitro.d.ts.map +1 -0
  136. package/lib/typescript/subtle.d.ts +3 -2
  137. package/lib/typescript/subtle.d.ts.map +1 -1
  138. package/lib/typescript/utils/conversion.d.ts +4 -3
  139. package/lib/typescript/utils/conversion.d.ts.map +1 -1
  140. package/lib/typescript/utils/errors.d.ts +12 -0
  141. package/lib/typescript/utils/errors.d.ts.map +1 -1
  142. package/lib/typescript/utils/types.d.ts +32 -15
  143. package/lib/typescript/utils/types.d.ts.map +1 -1
  144. package/lib/typescript/utils/validation.d.ts +3 -1
  145. package/lib/typescript/utils/validation.d.ts.map +1 -1
  146. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +2 -0
  147. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +20 -0
  148. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +20 -0
  149. package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +48 -0
  150. package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp +9 -0
  151. package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +9 -0
  152. package/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp +29 -0
  153. package/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.hpp +72 -0
  154. package/nitrogen/generated/shared/c++/HybridTurboShakeSpec.cpp +22 -0
  155. package/nitrogen/generated/shared/c++/HybridTurboShakeSpec.hpp +70 -0
  156. package/nitrogen/generated/shared/c++/JWK.hpp +9 -1
  157. package/nitrogen/generated/shared/c++/JWKkty.hpp +4 -0
  158. package/nitrogen/generated/shared/c++/KangarooTwelveVariant.hpp +76 -0
  159. package/nitrogen/generated/shared/c++/TurboShakeVariant.hpp +76 -0
  160. package/package.json +2 -3
  161. package/src/dhKeyPair.ts +8 -0
  162. package/src/dsa.ts +8 -0
  163. package/src/ec.ts +52 -29
  164. package/src/ed.ts +95 -16
  165. package/src/hash.ts +108 -5
  166. package/src/keys/classes.ts +46 -5
  167. package/src/keys/generateKeyPair.ts +151 -5
  168. package/src/keys/index.ts +73 -3
  169. package/src/keys/signVerify.ts +13 -2
  170. package/src/keys/utils.ts +78 -5
  171. package/src/random.ts +93 -9
  172. package/src/rsa.ts +8 -0
  173. package/src/slhdsa.ts +146 -0
  174. package/src/specs/keyObjectHandle.nitro.ts +17 -0
  175. package/src/specs/slhDsaKeyPair.nitro.ts +29 -0
  176. package/src/specs/turboshake.nitro.ts +21 -0
  177. package/src/subtle.ts +1191 -360
  178. package/src/utils/conversion.ts +72 -21
  179. package/src/utils/errors.ts +72 -4
  180. package/src/utils/types.ts +80 -15
  181. package/src/utils/validation.ts +70 -1
@@ -2,13 +2,14 @@
2
2
 
3
3
  /* eslint-disable @typescript-eslint/no-unused-vars */
4
4
  import { Buffer as SBuffer } from 'safe-buffer';
5
- import { KFormatType, KeyEncoding, KeyType } from './utils';
5
+ import { KFormatType, KeyEncoding, KeyType, kNamedCurveAliases } from './utils';
6
+ import { Buffer } from '@craftzdog/react-native-buffer';
6
7
  import { CryptoKey, KeyObject, PublicKeyObject, PrivateKeyObject, SecretKeyObject } from './keys';
7
8
  import { bufferLikeToArrayBuffer } from './utils/conversion';
8
9
  import { argon2Sync } from './argon2';
9
10
  import { lazyDOMException } from './utils/errors';
10
11
  import { normalizeHashName, HashContext } from './utils/hashnames';
11
- import { validateMaxBufferLength } from './utils/validation';
12
+ import { validateJwkStructure, validateMaxBufferLength } from './utils/validation';
12
13
  import { asyncDigest } from './hash';
13
14
  import { createSecretKey, createPublicKey } from './keys';
14
15
  import { NitroModules } from 'react-native-nitro-modules';
@@ -21,6 +22,7 @@ import { timingSafeEqual } from './utils/timingSafeEqual';
21
22
  import { createSign, createVerify } from './keys/signVerify';
22
23
  import { ed_generateKeyPairWebCrypto, x_generateKeyPairWebCrypto, xDeriveBits, Ed } from './ed';
23
24
  import { mldsa_generateKeyPairWebCrypto } from './mldsa';
25
+ import { slhdsa_generateKeyPairWebCrypto, SLH_DSA_VARIANTS } from './slhdsa';
24
26
  import { mlkem_generateKeyPairWebCrypto, MlKem } from './mlkem';
25
27
  import { hkdfDeriveBits } from './hkdf';
26
28
  // Temporary enums that need to be defined
@@ -39,6 +41,16 @@ function hasAnyNotIn(usages, allowed) {
39
41
  return usages.some(usage => !allowed.includes(usage));
40
42
  }
41
43
 
44
+ // Mirrors webidl.requiredArguments. Node throws TypeError when a SubtleCrypto
45
+ // method is called with fewer than the spec-required number of arguments
46
+ // (webcrypto.js:866 etc.); we relied on TypeScript types alone, which apps
47
+ // catching `instanceof TypeError` could not see at runtime.
48
+ function requireArgs(actual, required, method) {
49
+ if (actual < required) {
50
+ throw new TypeError(`Failed to execute '${method}' on 'SubtleCrypto': ${required} arguments required, but only ${actual} present.`);
51
+ }
52
+ }
53
+
42
54
  // WebCrypto §18.4.4: algorithm name lookup is case-insensitive, but the
43
55
  // canonical mixed-case form is preserved in the resulting `name` field
44
56
  // (e.g. "aes-gcm" → "AES-GCM"). This map is built lazily on first call so
@@ -80,27 +92,6 @@ function normalizeAlgorithm(algorithm, _operation) {
80
92
  }
81
93
  return algorithm;
82
94
  }
83
-
84
- // WebCrypto §25.7.6 (JWK import): if the JWK's `ext` member is present and
85
- // false, the requested `extractable` parameter must also be false. If the
86
- // JWK's `key_ops` member is present, every requested usage must appear in
87
- // it. We centralize the check here so every importKey path that accepts
88
- // `format === 'jwk'` can reuse it.
89
- function validateJwkExtAndKeyOps(jwk, extractable, keyUsages) {
90
- if (jwk.ext === false && extractable) {
91
- throw lazyDOMException('JWK "ext" is false but extractable was requested', 'DataError');
92
- }
93
- if (jwk.key_ops !== undefined) {
94
- if (!Array.isArray(jwk.key_ops)) {
95
- throw lazyDOMException('JWK "key_ops" must be an array', 'DataError');
96
- }
97
- for (const usage of keyUsages) {
98
- if (!jwk.key_ops.includes(usage)) {
99
- throw lazyDOMException(`JWK "key_ops" does not include requested usage "${usage}"`, 'DataError');
100
- }
101
- }
102
- }
103
- }
104
95
  function getAlgorithmName(name, length) {
105
96
  switch (name) {
106
97
  case 'AES-CBC':
@@ -120,17 +111,55 @@ function getAlgorithmName(name, length) {
120
111
  }
121
112
  }
122
113
 
123
- // Placeholder implementations for missing functions
114
+ // Mirrors Node's aliasKeyFormat (lib/internal/crypto/webcrypto.js): for
115
+ // algorithms whose import/export accepts both 'raw' and the disambiguated
116
+ // 'raw-secret' / 'raw-public', collapse the latter to 'raw'. Used per-algorithm
117
+ // — algorithms that demand the disambiguated form (KMAC, AES-OCB,
118
+ // ChaCha20-Poly1305, Argon2*, ML-DSA, ML-KEM) MUST NOT alias.
119
+ function aliasKeyFormat(format) {
120
+ if (format === 'raw-secret' || format === 'raw-public') return 'raw';
121
+ return format;
122
+ }
123
+ const kUncompressedSpkiLength = {
124
+ 'P-256': 91,
125
+ 'P-384': 120,
126
+ 'P-521': 158
127
+ };
124
128
  function ecExportKey(key, format) {
125
129
  const keyObject = key.keyObject;
126
130
  if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw) {
127
131
  return bufferLikeToArrayBuffer(keyObject.handle.exportKey());
128
132
  } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) {
129
- const exported = keyObject.export({
133
+ const exported = bufferLikeToArrayBuffer(keyObject.export({
130
134
  format: 'der',
131
135
  type: 'spki'
132
- });
133
- return bufferLikeToArrayBuffer(exported);
136
+ }));
137
+
138
+ // WebCrypto requires uncompressed point format for SPKI exports.
139
+ // If the key was imported in compressed form, re-export as uncompressed
140
+ // by reconstructing the point from the JWK x,y coordinates and
141
+ // round-tripping through initECRaw.
142
+ const namedCurve = key.algorithm.namedCurve;
143
+ const expected = namedCurve === undefined ? undefined : kUncompressedSpkiLength[namedCurve];
144
+ if (expected !== undefined && exported.byteLength !== expected) {
145
+ const jwk = keyObject.handle.exportJwk({}, false);
146
+ if (!jwk.x || !jwk.y) {
147
+ throw lazyDOMException('Failed to re-export EC public key as uncompressed SPKI', 'OperationError');
148
+ }
149
+ const x = Buffer.from(jwk.x, 'base64url');
150
+ const y = Buffer.from(jwk.y, 'base64url');
151
+ const raw = new Uint8Array(1 + x.length + y.length);
152
+ raw[0] = 0x04;
153
+ raw.set(x, 1);
154
+ raw.set(y, 1 + x.length);
155
+ const tmp = NitroModules.createHybridObject('KeyObjectHandle');
156
+ const curveAlias = kNamedCurveAliases[namedCurve];
157
+ if (!tmp.initECRaw(curveAlias, raw.buffer)) {
158
+ throw lazyDOMException('Failed to re-export EC public key as uncompressed SPKI', 'OperationError');
159
+ }
160
+ return bufferLikeToArrayBuffer(tmp.exportKey(KFormatType.DER, KeyEncoding.SPKI));
161
+ }
162
+ return exported;
134
163
  } else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) {
135
164
  const exported = keyObject.export({
136
165
  format: 'der',
@@ -565,10 +594,17 @@ function kmacSignVerify(key, data, algorithm, signature) {
565
594
  const {
566
595
  name
567
596
  } = algorithm;
568
- const defaultLength = name === 'KMAC128' ? 256 : 512;
569
- const outputLengthBits = algorithm.length ?? defaultLength;
597
+
598
+ // KmacParams.outputLength is required per
599
+ // https://wicg.github.io/webcrypto-modern-algos/#KmacParams-dictionary
600
+ // and the rename from `length` (commit ab8dc2b84c2). Mirror Node's
601
+ // mac.js:213-223 by reading `outputLength` (in bits).
602
+ if (typeof algorithm.outputLength !== 'number') {
603
+ throw lazyDOMException(`${name}Params.outputLength is required`, 'OperationError');
604
+ }
605
+ const outputLengthBits = algorithm.outputLength;
570
606
  if (outputLengthBits % 8 !== 0) {
571
- throw lazyDOMException('KMAC output length must be a multiple of 8', 'OperationError');
607
+ throw lazyDOMException(`Unsupported ${name}Params outputLength`, 'NotSupportedError');
572
608
  }
573
609
  const outputLengthBytes = outputLengthBits / 8;
574
610
  const keyData = key.keyObject.export();
@@ -593,30 +629,43 @@ async function kmacImportKey(algorithm, format, data, extractable, keyUsages) {
593
629
  const {
594
630
  name
595
631
  } = algorithm;
596
- if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
597
- throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
598
- }
599
632
  let keyObject;
600
633
  if (format === 'jwk') {
601
634
  const jwk = data;
602
635
  if (!jwk || typeof jwk !== 'object') {
603
636
  throw lazyDOMException('Invalid keyData', 'DataError');
604
637
  }
605
- validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
606
638
  if (jwk.kty !== 'oct') {
607
- throw lazyDOMException('Invalid JWK format for KMAC key', 'DataError');
639
+ throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
608
640
  }
641
+ validateJwkStructure(jwk, extractable, keyUsages, 'sig');
609
642
  const expectedAlg = name === 'KMAC128' ? 'K128' : 'K256';
610
643
  if (jwk.alg !== undefined && jwk.alg !== expectedAlg) {
611
- throw lazyDOMException('JWK "alg" does not match the requested algorithm', 'DataError');
644
+ throw lazyDOMException('JWK "alg" Parameter and algorithm name mismatch', 'DataError');
645
+ }
646
+ if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
647
+ throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
612
648
  }
613
649
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
614
- const keyType = handle.initJwk(jwk, undefined);
650
+ let keyType;
651
+ try {
652
+ keyType = handle.initJwk(jwk, undefined);
653
+ } catch (err) {
654
+ throw lazyDOMException('Invalid keyData', {
655
+ name: 'DataError',
656
+ cause: err
657
+ });
658
+ }
615
659
  if (keyType === undefined || keyType !== 0) {
616
- throw lazyDOMException('Failed to import KMAC JWK', 'DataError');
660
+ throw lazyDOMException('Invalid keyData', 'DataError');
617
661
  }
618
662
  keyObject = new SecretKeyObject(handle);
619
- } else if (format === 'raw' || format === 'raw-secret') {
663
+ } else if (format === 'raw-secret') {
664
+ // KMAC accepts only the disambiguated 'raw-secret' form (Node mac.js:141-145
665
+ // returns undefined for plain 'raw' when not HMAC).
666
+ if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
667
+ throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
668
+ }
620
669
  keyObject = createSecretKey(data);
621
670
  } else {
622
671
  throw lazyDOMException(`Unable to import ${name} key with format ${format}`, 'NotSupportedError');
@@ -639,8 +688,6 @@ function rsaImportKey(format, data, algorithm, extractable, keyUsages) {
639
688
  const {
640
689
  name
641
690
  } = algorithm;
642
-
643
- // Validate usages
644
691
  let checkSet;
645
692
  switch (name) {
646
693
  case 'RSASSA-PKCS1-v1_5':
@@ -653,40 +700,70 @@ function rsaImportKey(format, data, algorithm, extractable, keyUsages) {
653
700
  default:
654
701
  throw new Error(`Unsupported RSA algorithm: ${name}`);
655
702
  }
656
- if (hasAnyNotIn(keyUsages, checkSet)) {
657
- throw new Error(`Unsupported key usage for ${name}`);
658
- }
703
+ const checkUsages = () => {
704
+ if (hasAnyNotIn(keyUsages, checkSet)) {
705
+ throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
706
+ }
707
+ };
659
708
  let keyObject;
660
709
  if (format === 'jwk') {
661
710
  const jwk = data;
662
-
663
- // Validate JWK
711
+ if (!jwk || typeof jwk !== 'object') {
712
+ throw lazyDOMException('Invalid keyData', 'DataError');
713
+ }
664
714
  if (jwk.kty !== 'RSA') {
665
- throw new Error('Invalid JWK format for RSA key');
715
+ throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
716
+ }
717
+ const expectedUse = name === 'RSA-OAEP' ? 'enc' : 'sig';
718
+ validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
719
+ checkUsages();
720
+ if (jwk.alg !== undefined) {
721
+ let jwkContext;
722
+ switch (name) {
723
+ case 'RSASSA-PKCS1-v1_5':
724
+ jwkContext = HashContext.JwkRsa;
725
+ break;
726
+ case 'RSA-PSS':
727
+ jwkContext = HashContext.JwkRsaPss;
728
+ break;
729
+ default:
730
+ jwkContext = HashContext.JwkRsaOaep;
731
+ }
732
+ const expectedAlg = normalizeHashName(algorithm.hash, jwkContext);
733
+ if (jwk.alg !== expectedAlg) {
734
+ throw lazyDOMException('JWK "alg" does not match the requested algorithm', 'DataError');
735
+ }
666
736
  }
667
- validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
668
737
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
669
- const keyType = handle.initJwk(jwk, undefined);
738
+ let keyType;
739
+ try {
740
+ keyType = handle.initJwk(jwk, undefined);
741
+ } catch (err) {
742
+ throw lazyDOMException('Invalid keyData', {
743
+ name: 'DataError',
744
+ cause: err
745
+ });
746
+ }
670
747
  if (keyType === undefined) {
671
- throw new Error('Failed to import RSA JWK');
748
+ throw lazyDOMException('Invalid keyData', 'DataError');
672
749
  }
673
-
674
- // Create the appropriate KeyObject based on type
675
- if (keyType === 1) {
750
+ if (keyType === KeyType.PUBLIC) {
676
751
  keyObject = new PublicKeyObject(handle);
677
- } else if (keyType === 2) {
752
+ } else if (keyType === KeyType.PRIVATE) {
678
753
  keyObject = new PrivateKeyObject(handle);
679
754
  } else {
680
- throw new Error('Unexpected key type from RSA JWK import');
755
+ throw lazyDOMException('Invalid keyData', 'DataError');
681
756
  }
682
757
  } else if (format === 'spki') {
758
+ checkUsages();
683
759
  const keyData = bufferLikeToArrayBuffer(data);
684
760
  keyObject = KeyObject.createKeyObject('public', keyData, KFormatType.DER, KeyEncoding.SPKI);
685
761
  } else if (format === 'pkcs8') {
762
+ checkUsages();
686
763
  const keyData = bufferLikeToArrayBuffer(data);
687
764
  keyObject = KeyObject.createKeyObject('private', keyData, KFormatType.DER, KeyEncoding.PKCS8);
688
765
  } else {
689
- throw new Error(`Unsupported format for RSA import: ${format}`);
766
+ throw lazyDOMException(`Unsupported format for ${name} import: ${format}`, 'NotSupportedError');
690
767
  }
691
768
 
692
769
  // Get the modulus length from the key and add it to the algorithm
@@ -720,29 +797,26 @@ function rsaImportKey(format, data, algorithm, extractable, keyUsages) {
720
797
  return new CryptoKey(keyObject, algorithmWithDetails, keyUsages, extractable);
721
798
  }
722
799
  async function hmacImportKey(algorithm, format, data, extractable, keyUsages) {
723
- // Validate usages
724
- if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
725
- throw new Error('Unsupported key usage for an HMAC key');
726
- }
800
+ const checkUsages = () => {
801
+ if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
802
+ throw new Error('Unsupported key usage for an HMAC key');
803
+ }
804
+ };
727
805
  let keyObject;
728
806
  if (format === 'jwk') {
729
807
  const jwk = data;
730
-
731
- // Validate JWK
732
808
  if (!jwk || typeof jwk !== 'object') {
733
809
  throw new Error('Invalid keyData');
734
810
  }
735
- validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
736
811
  if (jwk.kty !== 'oct') {
737
812
  throw new Error('Invalid JWK format for HMAC key');
738
813
  }
739
-
740
- // Validate key length if specified
814
+ validateJwkStructure(jwk, extractable, keyUsages, 'sig');
815
+ checkUsages();
741
816
  if (algorithm.length !== undefined) {
742
817
  if (!jwk.k) {
743
818
  throw new Error('JWK missing key data');
744
819
  }
745
- // Decode to check length
746
820
  const decoded = SBuffer.from(jwk.k, 'base64');
747
821
  const keyBitLength = decoded.length * 8;
748
822
  if (algorithm.length === 0) {
@@ -753,15 +827,25 @@ async function hmacImportKey(algorithm, format, data, extractable, keyUsages) {
753
827
  }
754
828
  }
755
829
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
756
- const keyType = handle.initJwk(jwk, undefined);
830
+ let keyType;
831
+ try {
832
+ keyType = handle.initJwk(jwk, undefined);
833
+ } catch (err) {
834
+ throw lazyDOMException('Invalid keyData', {
835
+ name: 'DataError',
836
+ cause: err
837
+ });
838
+ }
757
839
  if (keyType === undefined || keyType !== 0) {
758
- throw new Error('Failed to import HMAC JWK');
840
+ throw lazyDOMException('Invalid keyData', 'DataError');
759
841
  }
760
842
  keyObject = new SecretKeyObject(handle);
761
- } else if (format === 'raw') {
843
+ } else if (format === 'raw' || format === 'raw-secret') {
844
+ // HMAC accepts both 'raw' and 'raw-secret' (Node mac.js:141-145).
845
+ checkUsages();
762
846
  keyObject = createSecretKey(data);
763
847
  } else {
764
- throw new Error(`Unable to import HMAC key with format ${format}`);
848
+ throw lazyDOMException(`Unable to import HMAC key with format ${format}`, 'NotSupportedError');
765
849
  }
766
850
 
767
851
  // Normalize hash to { name: string } format per WebCrypto spec
@@ -780,43 +864,57 @@ async function aesImportKey(algorithm, format, data, extractable, keyUsages) {
780
864
  name,
781
865
  length
782
866
  } = algorithm;
783
-
784
- // Validate usages
785
867
  const validUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'];
786
- if (hasAnyNotIn(keyUsages, validUsages)) {
787
- throw new Error(`Unsupported key usage for ${name}`);
788
- }
868
+ const checkUsages = () => {
869
+ if (hasAnyNotIn(keyUsages, validUsages)) {
870
+ throw new Error(`Unsupported key usage for ${name}`);
871
+ }
872
+ };
873
+
874
+ // AES-OCB and ChaCha20-Poly1305 require the disambiguated 'raw-secret' form
875
+ // and reject 'raw' (Node aes.js:243-249, chacha20_poly1305.js:104-134).
876
+ // Other AES variants accept both 'raw' and 'raw-secret'.
877
+ const requiresRawSecret = name === 'AES-OCB' || name === 'ChaCha20-Poly1305';
878
+ const acceptsRaw = format === 'raw-secret' || format === 'raw' && !requiresRawSecret;
789
879
  let keyObject;
790
880
  let actualLength;
791
881
  if (format === 'jwk') {
792
882
  const jwk = data;
793
-
794
- // Validate JWK
795
883
  if (jwk.kty !== 'oct') {
796
884
  throw new Error('Invalid JWK format for AES key');
797
885
  }
798
- validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
886
+ validateJwkStructure(jwk, extractable, keyUsages, 'enc');
887
+ checkUsages();
799
888
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
800
- const keyType = handle.initJwk(jwk, undefined);
889
+ let keyType;
890
+ try {
891
+ keyType = handle.initJwk(jwk, undefined);
892
+ } catch (err) {
893
+ throw lazyDOMException('Invalid keyData', {
894
+ name: 'DataError',
895
+ cause: err
896
+ });
897
+ }
801
898
  if (keyType === undefined || keyType !== 0) {
802
- throw new Error('Failed to import AES JWK');
899
+ throw lazyDOMException('Invalid keyData', 'DataError');
803
900
  }
804
901
  keyObject = new SecretKeyObject(handle);
805
-
806
- // Get actual key length from imported key
807
902
  const exported = keyObject.export();
808
903
  actualLength = exported.byteLength * 8;
809
- } else if (format === 'raw') {
904
+ } else if (acceptsRaw) {
905
+ checkUsages();
810
906
  const keyData = bufferLikeToArrayBuffer(data);
811
907
  actualLength = keyData.byteLength * 8;
812
-
813
- // Validate key length
814
- if (![128, 192, 256].includes(actualLength)) {
908
+ if (name === 'ChaCha20-Poly1305') {
909
+ if (actualLength !== 256) {
910
+ throw lazyDOMException('Invalid ChaCha20-Poly1305 key length', 'DataError');
911
+ }
912
+ } else if (![128, 192, 256].includes(actualLength)) {
815
913
  throw new Error('Invalid AES key length');
816
914
  }
817
915
  keyObject = createSecretKey(keyData);
818
916
  } else {
819
- throw new Error(`Unsupported format for AES import: ${format}`);
917
+ throw lazyDOMException(`Unable to import ${name} key with format ${format}`, 'NotSupportedError');
820
918
  }
821
919
 
822
920
  // Validate length if specified
@@ -832,35 +930,57 @@ function edImportKey(format, data, algorithm, extractable, keyUsages) {
832
930
  const {
833
931
  name
834
932
  } = algorithm;
835
-
836
- // Validate usages
837
933
  const isX = name === 'X25519' || name === 'X448';
838
934
  const allowedUsages = isX ? ['deriveKey', 'deriveBits'] : ['sign', 'verify'];
839
- if (hasAnyNotIn(keyUsages, allowedUsages)) {
840
- throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
841
- }
935
+ const checkUsages = () => {
936
+ if (hasAnyNotIn(keyUsages, allowedUsages)) {
937
+ throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
938
+ }
939
+ };
842
940
  let keyObject;
843
941
  if (format === 'spki') {
844
- // Import public key
942
+ checkUsages();
845
943
  const keyData = bufferLikeToArrayBuffer(data);
846
944
  keyObject = KeyObject.createKeyObject('public', keyData, KFormatType.DER, KeyEncoding.SPKI);
847
945
  } else if (format === 'pkcs8') {
848
- // Import private key
946
+ checkUsages();
849
947
  const keyData = bufferLikeToArrayBuffer(data);
850
948
  keyObject = KeyObject.createKeyObject('private', keyData, KFormatType.DER, KeyEncoding.PKCS8);
851
949
  } else if (format === 'raw') {
852
- // Raw format - public key only for Ed keys
950
+ checkUsages();
853
951
  const keyData = bufferLikeToArrayBuffer(data);
854
952
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
855
- // For raw Ed keys, we need to create them differently
856
- // Raw public keys are just the key bytes
857
- handle.init(1, keyData); // 1 = public key type
953
+ handle.init(1, keyData);
858
954
  keyObject = new PublicKeyObject(handle);
859
955
  } else if (format === 'jwk') {
860
956
  const jwkData = data;
861
- validateJwkExtAndKeyOps(jwkData, extractable, keyUsages);
957
+ if (!jwkData || typeof jwkData !== 'object') {
958
+ throw lazyDOMException('Invalid keyData', 'DataError');
959
+ }
960
+ if (jwkData.kty !== 'OKP') {
961
+ throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
962
+ }
963
+ const expectedUse = isX ? 'enc' : 'sig';
964
+ validateJwkStructure(jwkData, extractable, keyUsages, expectedUse);
965
+ if (jwkData.crv !== name) {
966
+ throw lazyDOMException('JWK "crv" Parameter and algorithm name mismatch', 'DataError');
967
+ }
968
+ if (!isX && jwkData.alg !== undefined) {
969
+ if (jwkData.alg !== name && jwkData.alg !== 'EdDSA') {
970
+ throw lazyDOMException('JWK "alg" does not match the requested algorithm', 'DataError');
971
+ }
972
+ }
973
+ checkUsages();
862
974
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
863
- const keyType = handle.initJwk(jwkData);
975
+ let keyType;
976
+ try {
977
+ keyType = handle.initJwk(jwkData);
978
+ } catch (err) {
979
+ throw lazyDOMException('Invalid JWK data', {
980
+ name: 'DataError',
981
+ cause: err
982
+ });
983
+ }
864
984
  if (keyType === undefined) {
865
985
  throw lazyDOMException('Invalid JWK data', 'DataError');
866
986
  }
@@ -876,48 +996,171 @@ function edImportKey(format, data, algorithm, extractable, keyUsages) {
876
996
  name
877
997
  }, keyUsages, extractable);
878
998
  }
999
+
1000
+ // Lengths (in bytes) of seedless ML-DSA / ML-KEM PKCS#8 encodings. A PKCS#8
1001
+ // blob of exactly this length contains only the expanded private key with no
1002
+ // seed; Node rejects these to keep cross-implementation interop intact.
1003
+ // Refs: node lib/internal/crypto/ml_dsa.js (mlDsaImportKey, pkcs8 case)
1004
+ // node lib/internal/crypto/ml_kem.js (mlKemImportKey, pkcs8 case)
1005
+ export const PQC_SEEDLESS_PKCS8_LENGTHS = {
1006
+ 'ML-DSA-44': 2588,
1007
+ 'ML-DSA-65': 4060,
1008
+ 'ML-DSA-87': 4924,
1009
+ 'ML-KEM-512': 1660,
1010
+ 'ML-KEM-768': 2428,
1011
+ 'ML-KEM-1024': 3196
1012
+ };
1013
+
1014
+ // Map from PQC algorithm name to display family. Used to render the
1015
+ // import-rejection error message in the same form Node emits.
1016
+ const PQC_FAMILY = {
1017
+ 'ML-DSA-44': 'ML-DSA',
1018
+ 'ML-DSA-65': 'ML-DSA',
1019
+ 'ML-DSA-87': 'ML-DSA',
1020
+ 'ML-KEM-512': 'ML-KEM',
1021
+ 'ML-KEM-768': 'ML-KEM',
1022
+ 'ML-KEM-1024': 'ML-KEM',
1023
+ 'SLH-DSA-SHA2-128s': 'SLH-DSA',
1024
+ 'SLH-DSA-SHA2-128f': 'SLH-DSA',
1025
+ 'SLH-DSA-SHA2-192s': 'SLH-DSA',
1026
+ 'SLH-DSA-SHA2-192f': 'SLH-DSA',
1027
+ 'SLH-DSA-SHA2-256s': 'SLH-DSA',
1028
+ 'SLH-DSA-SHA2-256f': 'SLH-DSA',
1029
+ 'SLH-DSA-SHAKE-128s': 'SLH-DSA',
1030
+ 'SLH-DSA-SHAKE-128f': 'SLH-DSA',
1031
+ 'SLH-DSA-SHAKE-192s': 'SLH-DSA',
1032
+ 'SLH-DSA-SHAKE-192f': 'SLH-DSA',
1033
+ 'SLH-DSA-SHAKE-256s': 'SLH-DSA',
1034
+ 'SLH-DSA-SHAKE-256f': 'SLH-DSA'
1035
+ };
879
1036
  function pqcImportKeyObject(format, data, name) {
880
1037
  if (format === 'spki') {
881
- return KeyObject.createKeyObject('public', bufferLikeToArrayBuffer(data), KFormatType.DER, KeyEncoding.SPKI);
1038
+ return {
1039
+ keyObject: KeyObject.createKeyObject('public', bufferLikeToArrayBuffer(data), KFormatType.DER, KeyEncoding.SPKI),
1040
+ isPublic: true
1041
+ };
882
1042
  } else if (format === 'pkcs8') {
883
- return KeyObject.createKeyObject('private', bufferLikeToArrayBuffer(data), KFormatType.DER, KeyEncoding.PKCS8);
884
- } else if (format === 'raw') {
1043
+ const ab = bufferLikeToArrayBuffer(data);
1044
+ const family = PQC_FAMILY[name];
1045
+ if (family !== undefined && ab.byteLength === PQC_SEEDLESS_PKCS8_LENGTHS[name]) {
1046
+ throw lazyDOMException(`Importing an ${family} PKCS#8 key without a seed is not supported`, 'NotSupportedError');
1047
+ }
1048
+ return {
1049
+ keyObject: KeyObject.createKeyObject('private', ab, KFormatType.DER, KeyEncoding.PKCS8),
1050
+ isPublic: false
1051
+ };
1052
+ } else if (format === 'raw-public') {
1053
+ // ML-DSA / ML-KEM reject plain 'raw' — only 'raw-public' is accepted for
1054
+ // public-key import (Node webcrypto.js:493-499, 506-511).
885
1055
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
886
1056
  if (!handle.initPqcRaw(name, bufferLikeToArrayBuffer(data), true)) {
887
1057
  throw lazyDOMException(`Failed to import ${name} raw public key`, 'DataError');
888
1058
  }
889
- return new PublicKeyObject(handle);
1059
+ return {
1060
+ keyObject: new PublicKeyObject(handle),
1061
+ isPublic: true
1062
+ };
890
1063
  } else if (format === 'raw-seed') {
891
1064
  const handle = NitroModules.createHybridObject('KeyObjectHandle');
892
1065
  if (!handle.initPqcRaw(name, bufferLikeToArrayBuffer(data), false)) {
893
1066
  throw lazyDOMException(`Failed to import ${name} raw seed`, 'DataError');
894
1067
  }
895
- return new PrivateKeyObject(handle);
1068
+ return {
1069
+ keyObject: new PrivateKeyObject(handle),
1070
+ isPublic: false
1071
+ };
1072
+ } else if (format === 'jwk') {
1073
+ const jwkData = data;
1074
+ const isPublic = jwkData.priv === undefined;
1075
+ const handle = NitroModules.createHybridObject('KeyObjectHandle');
1076
+ let keyType;
1077
+ try {
1078
+ keyType = handle.initJwk(jwkData);
1079
+ } catch (err) {
1080
+ throw lazyDOMException('Invalid JWK data', {
1081
+ name: 'DataError',
1082
+ cause: err
1083
+ });
1084
+ }
1085
+ if (keyType === undefined) {
1086
+ throw lazyDOMException('Invalid JWK data', 'DataError');
1087
+ }
1088
+ return {
1089
+ keyObject: isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle),
1090
+ isPublic
1091
+ };
896
1092
  }
897
1093
  throw lazyDOMException(`Unsupported format for ${name} import: ${format}`, 'NotSupportedError');
898
1094
  }
1095
+
1096
+ // Per WebCrypto AKP JWK rules, public-vs-private is determined by the presence
1097
+ // of `priv`. For binary formats it follows from the format itself.
1098
+ function pqcIsPublicImport(format, data) {
1099
+ if (format === 'jwk') {
1100
+ return typeof data === 'object' && data !== null && data.priv === undefined;
1101
+ }
1102
+ return format === 'spki' || format === 'raw-public';
1103
+ }
1104
+ function validatePqcJwk(data, name, extractable, keyUsages, expectedUse) {
1105
+ if (typeof data !== 'object' || data === null) {
1106
+ throw lazyDOMException('Invalid keyData', 'DataError');
1107
+ }
1108
+ const jwk = data;
1109
+ if (jwk.kty !== 'AKP') {
1110
+ throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
1111
+ }
1112
+ validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
1113
+ if (jwk.alg !== name) {
1114
+ throw lazyDOMException('JWK "alg" Parameter and algorithm name mismatch', 'DataError');
1115
+ }
1116
+ }
1117
+
1118
+ // Validates that `format` is one of the formats PQC algorithms accept; rejects
1119
+ // plain 'raw' early so the format error wins over usage-based errors.
1120
+ function validatePqcFormat(format, name) {
1121
+ if (format !== 'spki' && format !== 'pkcs8' && format !== 'raw-public' && format !== 'raw-seed' && format !== 'jwk') {
1122
+ throw lazyDOMException(`Unsupported format for ${name} import: ${format}`, 'NotSupportedError');
1123
+ }
1124
+ }
899
1125
  function mldsaImportKey(format, data, algorithm, extractable, keyUsages) {
900
1126
  const {
901
1127
  name
902
1128
  } = algorithm;
903
- const isPublicFormat = format === 'spki' || format === 'raw';
904
- if (hasAnyNotIn(keyUsages, isPublicFormat ? ['verify'] : ['sign'])) {
1129
+ validatePqcFormat(format, name);
1130
+ if (format === 'jwk') {
1131
+ validatePqcJwk(data, name, extractable, keyUsages, 'sig');
1132
+ }
1133
+ const isPublic = pqcIsPublicImport(format, data);
1134
+ if (hasAnyNotIn(keyUsages, isPublic ? ['verify'] : ['sign'])) {
905
1135
  throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
906
1136
  }
907
- return new CryptoKey(pqcImportKeyObject(format, data, name), {
1137
+ const {
1138
+ keyObject
1139
+ } = pqcImportKeyObject(format, data, name);
1140
+ return new CryptoKey(keyObject, {
908
1141
  name
909
1142
  }, keyUsages, extractable);
910
1143
  }
1144
+ function slhdsaImportKey(format, data, algorithm, extractable, keyUsages) {
1145
+ return mldsaImportKey(format, data, algorithm, extractable, keyUsages);
1146
+ }
911
1147
  function mlkemImportKey(format, data, algorithm, extractable, keyUsages) {
912
1148
  const {
913
1149
  name
914
1150
  } = algorithm;
915
- const isPublicFormat = format === 'spki' || format === 'raw';
916
- const allowedUsages = isPublicFormat ? ['encapsulateBits', 'encapsulateKey'] : ['decapsulateBits', 'decapsulateKey'];
1151
+ validatePqcFormat(format, name);
1152
+ if (format === 'jwk') {
1153
+ validatePqcJwk(data, name, extractable, keyUsages, 'enc');
1154
+ }
1155
+ const isPublic = pqcIsPublicImport(format, data);
1156
+ const allowedUsages = isPublic ? ['encapsulateBits', 'encapsulateKey'] : ['decapsulateBits', 'decapsulateKey'];
917
1157
  if (hasAnyNotIn(keyUsages, allowedUsages)) {
918
1158
  throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
919
1159
  }
920
- return new CryptoKey(pqcImportKeyObject(format, data, name), {
1160
+ const {
1161
+ keyObject
1162
+ } = pqcImportKeyObject(format, data, name);
1163
+ return new CryptoKey(keyObject, {
921
1164
  name
922
1165
  }, keyUsages, extractable);
923
1166
  }
@@ -956,8 +1199,21 @@ const exportKeySpki = async key => {
956
1199
  case 'ML-DSA-65':
957
1200
  // Fall through
958
1201
  case 'ML-DSA-87':
1202
+ // Fall through
1203
+ case 'SLH-DSA-SHA2-128s':
1204
+ case 'SLH-DSA-SHA2-128f':
1205
+ case 'SLH-DSA-SHA2-192s':
1206
+ case 'SLH-DSA-SHA2-192f':
1207
+ case 'SLH-DSA-SHA2-256s':
1208
+ case 'SLH-DSA-SHA2-256f':
1209
+ case 'SLH-DSA-SHAKE-128s':
1210
+ case 'SLH-DSA-SHAKE-128f':
1211
+ case 'SLH-DSA-SHAKE-192s':
1212
+ case 'SLH-DSA-SHAKE-192f':
1213
+ case 'SLH-DSA-SHAKE-256s':
1214
+ case 'SLH-DSA-SHAKE-256f':
959
1215
  if (key.type === 'public') {
960
- // Export ML-DSA key in SPKI DER format
1216
+ // Export ML-DSA / SLH-DSA key in SPKI DER format
961
1217
  return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI));
962
1218
  }
963
1219
  break;
@@ -1008,16 +1264,35 @@ const exportKeyPkcs8 = async key => {
1008
1264
  case 'ML-DSA-65':
1009
1265
  // Fall through
1010
1266
  case 'ML-DSA-87':
1011
- if (key.type === 'private') {
1012
- // Export ML-DSA key in PKCS8 DER format
1013
- return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8));
1014
- }
1015
- break;
1267
+ // Fall through
1016
1268
  case 'ML-KEM-512':
1017
1269
  // Fall through
1018
1270
  case 'ML-KEM-768':
1019
1271
  // Fall through
1020
1272
  case 'ML-KEM-1024':
1273
+ if (key.type === 'private') {
1274
+ const ab = bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8));
1275
+ // 22 bytes of PKCS#8 ASN.1 + seed (32 ML-DSA, 64 ML-KEM). Guards
1276
+ // against a seedless KeyObject that was wrapped via toCryptoKey.
1277
+ const expected = key.algorithm.name.startsWith('ML-DSA') ? 54 : 86;
1278
+ if (ab.byteLength !== expected) {
1279
+ throw lazyDOMException('The operation failed for an operation-specific reason', 'OperationError');
1280
+ }
1281
+ return ab;
1282
+ }
1283
+ break;
1284
+ case 'SLH-DSA-SHA2-128s':
1285
+ case 'SLH-DSA-SHA2-128f':
1286
+ case 'SLH-DSA-SHA2-192s':
1287
+ case 'SLH-DSA-SHA2-192f':
1288
+ case 'SLH-DSA-SHA2-256s':
1289
+ case 'SLH-DSA-SHA2-256f':
1290
+ case 'SLH-DSA-SHAKE-128s':
1291
+ case 'SLH-DSA-SHAKE-128f':
1292
+ case 'SLH-DSA-SHAKE-192s':
1293
+ case 'SLH-DSA-SHAKE-192f':
1294
+ case 'SLH-DSA-SHAKE-256s':
1295
+ case 'SLH-DSA-SHAKE-256f':
1021
1296
  if (key.type === 'private') {
1022
1297
  return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8));
1023
1298
  }
@@ -1025,71 +1300,87 @@ const exportKeyPkcs8 = async key => {
1025
1300
  }
1026
1301
  throw new Error(`Unable to export a pkcs8 ${key.algorithm.name} ${key.type} key`);
1027
1302
  };
1028
- const exportKeyRaw = key => {
1029
- switch (key.algorithm.name) {
1303
+
1304
+ // Mirrors Node's export key matrix (lib/internal/crypto/webcrypto.js
1305
+ // exportKeyRawSecret / exportKeyRawPublic, lines 472-563):
1306
+ //
1307
+ // raw — AES-CTR/CBC/GCM/KW + HMAC (secret); ECDSA/ECDH/Ed/X (public)
1308
+ // raw-secret — AES-CTR/CBC/GCM/KW + HMAC + AES-OCB + KMAC + ChaCha20-Poly1305
1309
+ // raw-public — ECDSA/ECDH + Ed/X + ML-DSA + ML-KEM (public)
1310
+ const exportKeyRaw = (key, format) => {
1311
+ const name = key.algorithm.name;
1312
+ const isPublic = key.type === 'public';
1313
+ const isSecret = key.type === 'secret';
1314
+ const exportSecret = () => {
1315
+ const exported = key.keyObject.export();
1316
+ return exported.buffer.slice(exported.byteOffset, exported.byteOffset + exported.byteLength);
1317
+ };
1318
+ const exportRawPublic = () => bufferLikeToArrayBuffer(key.keyObject.handle.exportKey());
1319
+ const fail = () => {
1320
+ throw lazyDOMException(`Unable to export ${name} ${key.type} key using ${format} format`, 'NotSupportedError');
1321
+ };
1322
+
1323
+ // Symmetric: AES-CTR/CBC/GCM/KW and HMAC accept both 'raw' and 'raw-secret';
1324
+ // AES-OCB / KMAC* / ChaCha20-Poly1305 only 'raw-secret'.
1325
+ switch (name) {
1326
+ case 'AES-CTR':
1327
+ case 'AES-CBC':
1328
+ case 'AES-GCM':
1329
+ case 'AES-KW':
1330
+ case 'HMAC':
1331
+ if (!isSecret) return fail();
1332
+ if (format === 'raw' || format === 'raw-secret') return exportSecret();
1333
+ return fail();
1334
+ case 'AES-OCB':
1335
+ case 'KMAC128':
1336
+ case 'KMAC256':
1337
+ case 'ChaCha20-Poly1305':
1338
+ if (!isSecret) return fail();
1339
+ if (format === 'raw-secret') return exportSecret();
1340
+ return fail();
1030
1341
  case 'ECDSA':
1031
- // Fall through
1032
1342
  case 'ECDH':
1033
- if (key.type === 'public') {
1343
+ if (!isPublic) return fail();
1344
+ if (format === 'raw' || format === 'raw-public') {
1034
1345
  return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
1035
1346
  }
1036
- break;
1347
+ return fail();
1037
1348
  case 'Ed25519':
1038
- // Fall through
1039
1349
  case 'Ed448':
1040
- // Fall through
1041
1350
  case 'X25519':
1042
- // Fall through
1043
1351
  case 'X448':
1044
- if (key.type === 'public') {
1045
- const exported = key.keyObject.handle.exportKey();
1046
- return bufferLikeToArrayBuffer(exported);
1047
- }
1048
- break;
1049
- case 'ML-KEM-512':
1050
- // Fall through
1051
- case 'ML-KEM-768':
1052
- // Fall through
1053
- case 'ML-KEM-1024':
1054
- // Fall through
1352
+ if (!isPublic) return fail();
1353
+ if (format === 'raw' || format === 'raw-public') return exportRawPublic();
1354
+ return fail();
1055
1355
  case 'ML-DSA-44':
1056
- // Fall through
1057
1356
  case 'ML-DSA-65':
1058
- // Fall through
1059
1357
  case 'ML-DSA-87':
1060
- if (key.type === 'public') {
1061
- const exported = key.keyObject.handle.exportKey();
1062
- return bufferLikeToArrayBuffer(exported);
1063
- }
1064
- break;
1065
- case 'AES-CTR':
1066
- // Fall through
1067
- case 'AES-CBC':
1068
- // Fall through
1069
- case 'AES-GCM':
1070
- // Fall through
1071
- case 'AES-KW':
1072
- // Fall through
1073
- case 'AES-OCB':
1074
- // Fall through
1075
- case 'ChaCha20-Poly1305':
1076
- // Fall through
1077
- case 'HMAC':
1078
- // Fall through
1079
- case 'KMAC128':
1080
- // Fall through
1081
- case 'KMAC256':
1082
- {
1083
- const exported = key.keyObject.export();
1084
- // Convert Buffer to ArrayBuffer
1085
- return exported.buffer.slice(exported.byteOffset, exported.byteOffset + exported.byteLength);
1086
- }
1087
- }
1088
- throw lazyDOMException(`Unable to export a raw ${key.algorithm.name} ${key.type} key`, 'InvalidAccessError');
1358
+ case 'ML-KEM-512':
1359
+ case 'ML-KEM-768':
1360
+ case 'ML-KEM-1024':
1361
+ case 'SLH-DSA-SHA2-128s':
1362
+ case 'SLH-DSA-SHA2-128f':
1363
+ case 'SLH-DSA-SHA2-192s':
1364
+ case 'SLH-DSA-SHA2-192f':
1365
+ case 'SLH-DSA-SHA2-256s':
1366
+ case 'SLH-DSA-SHA2-256f':
1367
+ case 'SLH-DSA-SHAKE-128s':
1368
+ case 'SLH-DSA-SHAKE-128f':
1369
+ case 'SLH-DSA-SHAKE-192s':
1370
+ case 'SLH-DSA-SHAKE-192f':
1371
+ case 'SLH-DSA-SHAKE-256s':
1372
+ case 'SLH-DSA-SHAKE-256f':
1373
+ // ML-DSA / ML-KEM / SLH-DSA keys do not recognize plain 'raw' (Node
1374
+ // webcrypto.js lines 488-510).
1375
+ if (!isPublic) return fail();
1376
+ if (format === 'raw-public') return exportRawPublic();
1377
+ return fail();
1378
+ }
1379
+ return fail();
1089
1380
  };
1090
1381
  const exportKeyJWK = key => {
1091
1382
  const jwk = key.keyObject.handle.exportJwk({
1092
- key_ops: key.usages,
1383
+ key_ops: [...key.usages],
1093
1384
  ext: key.extractable
1094
1385
  }, true);
1095
1386
  switch (key.algorithm.name) {
@@ -1124,6 +1415,31 @@ const exportKeyJWK = key => {
1124
1415
  // Fall through
1125
1416
  case 'X448':
1126
1417
  return jwk;
1418
+ case 'ML-DSA-44':
1419
+ // Fall through
1420
+ case 'ML-DSA-65':
1421
+ // Fall through
1422
+ case 'ML-DSA-87':
1423
+ // Fall through
1424
+ case 'ML-KEM-512':
1425
+ // Fall through
1426
+ case 'ML-KEM-768':
1427
+ // Fall through
1428
+ case 'ML-KEM-1024':
1429
+ // Fall through
1430
+ case 'SLH-DSA-SHA2-128s':
1431
+ case 'SLH-DSA-SHA2-128f':
1432
+ case 'SLH-DSA-SHA2-192s':
1433
+ case 'SLH-DSA-SHA2-192f':
1434
+ case 'SLH-DSA-SHA2-256s':
1435
+ case 'SLH-DSA-SHA2-256f':
1436
+ case 'SLH-DSA-SHAKE-128s':
1437
+ case 'SLH-DSA-SHAKE-128f':
1438
+ case 'SLH-DSA-SHAKE-192s':
1439
+ case 'SLH-DSA-SHAKE-192f':
1440
+ case 'SLH-DSA-SHAKE-256s':
1441
+ case 'SLH-DSA-SHAKE-256f':
1442
+ return jwk;
1127
1443
  case 'AES-CTR':
1128
1444
  // Fall through
1129
1445
  case 'AES-CBC':
@@ -1145,33 +1461,59 @@ const exportKeyJWK = key => {
1145
1461
  }
1146
1462
  throw lazyDOMException(`JWK export not yet supported: ${key.algorithm.name}`, 'NotSupportedError');
1147
1463
  };
1148
- const importGenericSecretKey = async ({
1464
+
1465
+ // PBKDF2 import. Mirrors Node's importGenericSecretKey ordering
1466
+ // (keys.js:945-971): extractable → usage → format → length. Callers pre-alias
1467
+ // 'raw-secret' / 'raw-public' to 'raw' via aliasKeyFormat
1468
+ // (webcrypto.js:798-808).
1469
+ const pbkdf2ImportKey = async ({
1149
1470
  name,
1150
1471
  length
1151
1472
  }, format, keyData, extractable, keyUsages) => {
1152
1473
  if (extractable) {
1153
- throw new Error(`${name} keys are not extractable`);
1474
+ throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
1154
1475
  }
1155
1476
  if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
1156
- throw new Error(`Unsupported key usage for a ${name} key`);
1477
+ throw lazyDOMException(`Unsupported key usage for a ${name} key`, 'SyntaxError');
1157
1478
  }
1158
- switch (format) {
1159
- case 'raw':
1160
- {
1161
- if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
1162
- throw new Error(`Unsupported key usage for a ${name} key`);
1163
- }
1164
- const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8;
1165
- if (length !== undefined && length !== checkLength) {
1166
- throw new Error('Invalid key length');
1167
- }
1168
- const keyObject = createSecretKey(keyData);
1169
- return new CryptoKey(keyObject, {
1170
- name
1171
- }, keyUsages, false);
1172
- }
1479
+ if (format !== 'raw') {
1480
+ throw lazyDOMException(`Unable to import ${name} key with format ${format}`, 'NotSupportedError');
1481
+ }
1482
+ const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8;
1483
+ if (length !== undefined && length !== checkLength) {
1484
+ throw lazyDOMException('Invalid key length', 'DataError');
1485
+ }
1486
+ const keyObject = createSecretKey(keyData);
1487
+ return new CryptoKey(keyObject, {
1488
+ name
1489
+ }, keyUsages, false);
1490
+ };
1491
+
1492
+ // Argon2 import. Node gates the format at the dispatcher level — only
1493
+ // 'raw-secret' enters importGenericSecretKey (webcrypto.js:813-822). To match
1494
+ // that, format is the first check here; remaining ordering matches Node's
1495
+ // importGenericSecretKey.
1496
+ const argon2ImportKey = async ({
1497
+ name,
1498
+ length
1499
+ }, format, keyData, extractable, keyUsages) => {
1500
+ if (format !== 'raw-secret') {
1501
+ throw lazyDOMException(`Unable to import ${name} key with format ${format}`, 'NotSupportedError');
1502
+ }
1503
+ if (extractable) {
1504
+ throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
1173
1505
  }
1174
- throw new Error(`Unable to import ${name} key with format ${format}`);
1506
+ if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
1507
+ throw lazyDOMException(`Unsupported key usage for a ${name} key`, 'SyntaxError');
1508
+ }
1509
+ const checkLength = typeof keyData === 'string' || SBuffer.isBuffer(keyData) ? keyData.length * 8 : keyData.byteLength * 8;
1510
+ if (length !== undefined && length !== checkLength) {
1511
+ throw lazyDOMException('Invalid key length', 'DataError');
1512
+ }
1513
+ const keyObject = createSecretKey(keyData);
1514
+ return new CryptoKey(keyObject, {
1515
+ name
1516
+ }, keyUsages, false);
1175
1517
  };
1176
1518
  const hkdfImportKey = async (format, keyData, algorithm, extractable, keyUsages) => {
1177
1519
  const {
@@ -1345,7 +1687,10 @@ function mldsaSignVerify(key, data, signature) {
1345
1687
  const signVerify = (algorithm, key, data, signature) => {
1346
1688
  const usage = signature === undefined ? 'sign' : 'verify';
1347
1689
  algorithm = normalizeAlgorithm(algorithm, usage);
1348
- if (!key.usages.includes(usage) || algorithm.name !== key.algorithm.name) {
1690
+ if (algorithm.name !== key.algorithm.name) {
1691
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
1692
+ }
1693
+ if (!key.usages.includes(usage)) {
1349
1694
  throw lazyDOMException(`Unable to use this key to ${usage}`, 'InvalidAccessError');
1350
1695
  }
1351
1696
  switch (algorithm.name) {
@@ -1363,6 +1708,18 @@ const signVerify = (algorithm, key, data, signature) => {
1363
1708
  case 'ML-DSA-44':
1364
1709
  case 'ML-DSA-65':
1365
1710
  case 'ML-DSA-87':
1711
+ case 'SLH-DSA-SHA2-128s':
1712
+ case 'SLH-DSA-SHA2-128f':
1713
+ case 'SLH-DSA-SHA2-192s':
1714
+ case 'SLH-DSA-SHA2-192f':
1715
+ case 'SLH-DSA-SHA2-256s':
1716
+ case 'SLH-DSA-SHA2-256f':
1717
+ case 'SLH-DSA-SHAKE-128s':
1718
+ case 'SLH-DSA-SHAKE-128f':
1719
+ case 'SLH-DSA-SHAKE-192s':
1720
+ case 'SLH-DSA-SHAKE-192f':
1721
+ case 'SLH-DSA-SHAKE-256s':
1722
+ case 'SLH-DSA-SHAKE-256f':
1366
1723
  return mldsaSignVerify(key, data, signature);
1367
1724
  case 'KMAC128':
1368
1725
  case 'KMAC256':
@@ -1370,10 +1727,12 @@ const signVerify = (algorithm, key, data, signature) => {
1370
1727
  }
1371
1728
  throw lazyDOMException(`Unrecognized algorithm name '${algorithm.name}' for '${usage}'`, 'NotSupportedError');
1372
1729
  };
1373
- const cipherOrWrap = async (mode, algorithm, key, data, op) => {
1374
- if (key.algorithm.name !== algorithm.name || !key.usages.includes(op)) {
1375
- throw lazyDOMException('The requested operation is not valid for the provided key', 'InvalidAccessError');
1376
- }
1730
+
1731
+ // Algorithm-mismatch and usage checks live at the public-method call sites
1732
+ // (encrypt / decrypt / wrapKey / unwrapKey) so spec-mandated message and
1733
+ // ordering — algorithm-mismatch first, then usage — is preserved
1734
+ // (Node webcrypto.js, commit 4cb1f284136).
1735
+ const cipherOrWrap = async (mode, algorithm, key, data) => {
1377
1736
  validateMaxBufferLength(data, 'data');
1378
1737
  switch (algorithm.name) {
1379
1738
  case 'RSA-OAEP':
@@ -1395,12 +1754,12 @@ const cipherOrWrap = async (mode, algorithm, key, data, op) => {
1395
1754
  const SUPPORTED_ALGORITHMS = {
1396
1755
  encrypt: new Set(['RSA-OAEP', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-OCB', 'ChaCha20-Poly1305']),
1397
1756
  decrypt: new Set(['RSA-OAEP', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-OCB', 'ChaCha20-Poly1305']),
1398
- sign: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA', 'HMAC', 'KMAC128', 'KMAC256', 'Ed25519', 'Ed448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']),
1399
- verify: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA', 'HMAC', 'KMAC128', 'KMAC256', 'Ed25519', 'Ed448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']),
1400
- digest: new Set(['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512', 'SHA3-256', 'SHA3-384', 'SHA3-512', 'cSHAKE128', 'cSHAKE256']),
1401
- generateKey: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']),
1402
- importKey: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'HKDF', 'PBKDF2', 'Argon2d', 'Argon2i', 'Argon2id', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']),
1403
- exportKey: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']),
1757
+ sign: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA', 'HMAC', 'KMAC128', 'KMAC256', 'Ed25519', 'Ed448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', ...SLH_DSA_VARIANTS]),
1758
+ verify: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'ECDSA', 'HMAC', 'KMAC128', 'KMAC256', 'Ed25519', 'Ed448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', ...SLH_DSA_VARIANTS]),
1759
+ digest: new Set(['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512', 'SHA3-256', 'SHA3-384', 'SHA3-512', 'cSHAKE128', 'cSHAKE256', 'TurboSHAKE128', 'TurboSHAKE256', 'KT128', 'KT256']),
1760
+ generateKey: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS]),
1761
+ importKey: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'HKDF', 'PBKDF2', 'Argon2d', 'Argon2i', 'Argon2id', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS]),
1762
+ exportKey: new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'HMAC', 'KMAC128', 'KMAC256', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS]),
1404
1763
  deriveBits: new Set(['PBKDF2', 'HKDF', 'ECDH', 'X25519', 'X448', 'Argon2d', 'Argon2i', 'Argon2id']),
1405
1764
  wrapKey: new Set(['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'RSA-OAEP']),
1406
1765
  unwrapKey: new Set(['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW', 'AES-OCB', 'ChaCha20-Poly1305', 'RSA-OAEP']),
@@ -1409,119 +1768,315 @@ const SUPPORTED_ALGORITHMS = {
1409
1768
  encapsulateKey: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']),
1410
1769
  decapsulateKey: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024'])
1411
1770
  };
1412
- const ASYMMETRIC_ALGORITHMS = new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']);
1413
- export class Subtle {
1414
- static supports(operation, algorithm, _lengthOrAdditionalAlgorithm) {
1415
- let normalizedAlgorithm;
1771
+ const ASYMMETRIC_ALGORITHMS = new Set(['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP', 'ECDSA', 'ECDH', 'Ed25519', 'Ed448', 'X25519', 'X448', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', 'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', ...SLH_DSA_VARIANTS]);
1772
+
1773
+ // Per-algorithm validators for deriveBits (mirrors Node's hkdf.js:141-149,
1774
+ // pbkdf2.js:96-105, argon2.js:194-209). Used by Subtle.supports to reject
1775
+ // length values that the actual deriveBits implementation would reject.
1776
+ function validateKdfDeriveBitsLength(length, algName) {
1777
+ if (length === null || length === undefined) {
1778
+ throw lazyDOMException('length cannot be null', 'OperationError');
1779
+ }
1780
+ if (length % 8) {
1781
+ throw lazyDOMException('length must be a multiple of 8', 'OperationError');
1782
+ }
1783
+ if (algName.startsWith('Argon2') && length < 32) {
1784
+ throw lazyDOMException('length must be >= 32', 'OperationError');
1785
+ }
1786
+ }
1787
+
1788
+ // Mirrors Node's webcrypto.js:1652-1731 `check`. Normalizes the algorithm,
1789
+ // looks it up in SUPPORTED_ALGORITHMS, and runs per-algorithm validation
1790
+ // (deriveBits length validators, HMAC+SHA3 generateKey rejection).
1791
+ // `op` is the operation key in SUPPORTED_ALGORITHMS — wrapKey/unwrapKey fall
1792
+ // back to encrypt/decrypt to mirror Node's normalize fallback.
1793
+ function supportsCheck(op, alg, length) {
1794
+ let normalizedAlgorithm;
1795
+ try {
1796
+ normalizedAlgorithm = normalizeAlgorithm(alg, op);
1797
+ } catch {
1798
+ if (op === 'wrapKey') return supportsCheck('encrypt', alg);
1799
+ if (op === 'unwrapKey') return supportsCheck('decrypt', alg);
1800
+ return false;
1801
+ }
1802
+ const supported = SUPPORTED_ALGORITHMS[op];
1803
+ if (!supported || !supported.has(normalizedAlgorithm.name)) {
1804
+ if (op === 'wrapKey') return supportsCheck('encrypt', alg);
1805
+ if (op === 'unwrapKey') return supportsCheck('decrypt', alg);
1806
+ return false;
1807
+ }
1808
+ if (op === 'deriveBits') {
1809
+ const name = normalizedAlgorithm.name;
1810
+ if (name === 'HKDF' || name === 'PBKDF2' || name.startsWith('Argon2')) {
1811
+ try {
1812
+ validateKdfDeriveBitsLength(length, name);
1813
+ } catch {
1814
+ return false;
1815
+ }
1816
+ }
1817
+ }
1818
+ if (op === 'generateKey' && normalizedAlgorithm.name === 'HMAC') {
1819
+ const hashName = typeof normalizedAlgorithm.hash === 'string' ? normalizedAlgorithm.hash : normalizedAlgorithm.hash?.name;
1820
+ if (normalizedAlgorithm.length === undefined && typeof hashName === 'string' && hashName.startsWith('SHA3-')) {
1821
+ return false;
1822
+ }
1823
+ }
1824
+ return true;
1825
+ }
1826
+ function supportsImpl(operation, algorithm, lengthOrAdditionalAlgorithm) {
1827
+ switch (operation) {
1828
+ case 'decapsulateBits':
1829
+ case 'decapsulateKey':
1830
+ case 'decrypt':
1831
+ case 'deriveBits':
1832
+ case 'deriveKey':
1833
+ case 'digest':
1834
+ case 'encapsulateBits':
1835
+ case 'encapsulateKey':
1836
+ case 'encrypt':
1837
+ case 'exportKey':
1838
+ case 'generateKey':
1839
+ case 'getPublicKey':
1840
+ case 'importKey':
1841
+ case 'sign':
1842
+ case 'unwrapKey':
1843
+ case 'verify':
1844
+ case 'wrapKey':
1845
+ break;
1846
+ default:
1847
+ return false;
1848
+ }
1849
+ let length;
1850
+ if (operation === 'deriveKey') {
1851
+ // deriveKey decomposes to importKey of derived alg + deriveBits with that
1852
+ // alg's key length. Node webcrypto.js:1547-1563.
1853
+ if (lengthOrAdditionalAlgorithm != null) {
1854
+ const additional = lengthOrAdditionalAlgorithm;
1855
+ if (!supportsCheck('importKey', additional)) return false;
1856
+ try {
1857
+ length = getKeyLength(normalizeAlgorithm(additional, 'get key length'));
1858
+ } catch {
1859
+ return false;
1860
+ }
1861
+ return supportsCheck('deriveBits', algorithm, length);
1862
+ }
1863
+ // No additional algorithm given — only check the deriveBits side.
1864
+ return supportsCheck('deriveBits', algorithm);
1865
+ }
1866
+ if (operation === 'wrapKey') {
1867
+ // wrapKey decomposes to encrypt of wrapping alg + exportKey of wrapped alg.
1868
+ // Node webcrypto.js:1564-1572.
1869
+ if (lengthOrAdditionalAlgorithm != null) {
1870
+ const additional = lengthOrAdditionalAlgorithm;
1871
+ if (!supportsCheck('exportKey', additional)) return false;
1872
+ }
1873
+ return supportsCheck('wrapKey', algorithm);
1874
+ }
1875
+ if (operation === 'unwrapKey') {
1876
+ // unwrapKey decomposes to decrypt of wrapping alg + importKey of wrapped
1877
+ // alg. Node webcrypto.js:1573-1581.
1878
+ if (lengthOrAdditionalAlgorithm != null) {
1879
+ const additional = lengthOrAdditionalAlgorithm;
1880
+ if (!supportsCheck('importKey', additional)) return false;
1881
+ }
1882
+ return supportsCheck('unwrapKey', algorithm);
1883
+ }
1884
+ if (operation === 'deriveBits') {
1885
+ if (lengthOrAdditionalAlgorithm == null) {
1886
+ length = null;
1887
+ } else if (typeof lengthOrAdditionalAlgorithm === 'number') {
1888
+ length = lengthOrAdditionalAlgorithm;
1889
+ } else {
1890
+ return false;
1891
+ }
1892
+ return supportsCheck('deriveBits', algorithm, length);
1893
+ }
1894
+ if (operation === 'getPublicKey') {
1895
+ let normalized;
1416
1896
  try {
1417
- normalizedAlgorithm = normalizeAlgorithm(algorithm, operation === 'getPublicKey' ? 'exportKey' : operation);
1897
+ normalized = normalizeAlgorithm(algorithm, 'exportKey');
1418
1898
  } catch {
1419
1899
  return false;
1420
1900
  }
1421
- const name = normalizedAlgorithm.name;
1422
- if (operation === 'getPublicKey') {
1423
- return ASYMMETRIC_ALGORITHMS.has(name);
1424
- }
1425
- if (operation === 'deriveKey') {
1426
- // deriveKey decomposes to deriveBits + importKey of additional algorithm
1427
- if (!SUPPORTED_ALGORITHMS.deriveBits?.has(name)) return false;
1428
- if (_lengthOrAdditionalAlgorithm != null) {
1429
- try {
1430
- const additionalAlg = normalizeAlgorithm(_lengthOrAdditionalAlgorithm, 'importKey');
1431
- return SUPPORTED_ALGORITHMS.importKey?.has(additionalAlg.name) ?? false;
1432
- } catch {
1433
- return false;
1901
+ return ASYMMETRIC_ALGORITHMS.has(normalized.name);
1902
+ }
1903
+ if (operation === 'encapsulateKey' || operation === 'decapsulateKey') {
1904
+ // sharedKeyAlgorithm must support importKey, with HMAC/KMAC limited to
1905
+ // length === undefined or 256 (Node webcrypto.js:1610-1645).
1906
+ const additional = lengthOrAdditionalAlgorithm;
1907
+ let normalizedAdd;
1908
+ try {
1909
+ normalizedAdd = normalizeAlgorithm(additional, 'importKey');
1910
+ } catch {
1911
+ return false;
1912
+ }
1913
+ switch (normalizedAdd.name) {
1914
+ case 'AES-OCB':
1915
+ case 'AES-KW':
1916
+ case 'AES-GCM':
1917
+ case 'AES-CTR':
1918
+ case 'AES-CBC':
1919
+ case 'ChaCha20-Poly1305':
1920
+ case 'HKDF':
1921
+ case 'PBKDF2':
1922
+ case 'Argon2i':
1923
+ case 'Argon2d':
1924
+ case 'Argon2id':
1925
+ break;
1926
+ case 'HMAC':
1927
+ case 'KMAC128':
1928
+ case 'KMAC256':
1929
+ {
1930
+ const addLen = normalizedAdd.length;
1931
+ if (addLen !== undefined && addLen !== 256) return false;
1932
+ break;
1434
1933
  }
1435
- }
1436
- return true;
1934
+ default:
1935
+ return false;
1437
1936
  }
1438
- const supported = SUPPORTED_ALGORITHMS[operation];
1439
- if (!supported) return false;
1440
- return supported.has(name);
1937
+ return supportsCheck(operation, algorithm);
1938
+ }
1939
+ return supportsCheck(operation, algorithm);
1940
+ }
1941
+ export class Subtle {
1942
+ // Spec-compliant capability detector. Mirrors Node's webcrypto.js:1506-1649
1943
+ // `SubtleCrypto.supports`, including:
1944
+ // • composed-operation decomposition (deriveKey, wrapKey, unwrapKey,
1945
+ // encapsulateKey, decapsulateKey, getPublicKey)
1946
+ // • per-algorithm length validators for deriveBits (HKDF/PBKDF2/Argon2)
1947
+ // • HMAC + SHA3 generateKey with no length → false
1948
+ // Static-only per the WICG spec.
1949
+ static supports(operation, algorithm, lengthOrAdditionalAlgorithm = null) {
1950
+ return supportsImpl(operation, algorithm, lengthOrAdditionalAlgorithm);
1441
1951
  }
1442
1952
  async decrypt(algorithm, key, data) {
1953
+ requireArgs(arguments.length, 3, 'decrypt');
1443
1954
  const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt');
1444
- return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), 'decrypt');
1955
+ if (normalizedAlgorithm.name !== key.algorithm.name) {
1956
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
1957
+ }
1958
+ if (!key.usages.includes('decrypt')) {
1959
+ throw lazyDOMException('Unable to use this key to decrypt', 'InvalidAccessError');
1960
+ }
1961
+ return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data));
1445
1962
  }
1446
1963
  async digest(algorithm, data) {
1964
+ requireArgs(arguments.length, 2, 'digest');
1447
1965
  const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest');
1448
1966
  return asyncDigest(normalizedAlgorithm, data);
1449
1967
  }
1450
- async deriveBits(algorithm, baseKey, length) {
1968
+ async deriveBits(algorithm, baseKey, length = null) {
1969
+ requireArgs(arguments.length, 2, 'deriveBits');
1970
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
1451
1971
  // WebCrypto §SubtleCrypto.deriveBits step 11: throw InvalidAccessError
1452
1972
  // unless `baseKey.[[usages]]` contains "deriveBits" specifically. The
1453
1973
  // previous `deriveBits || deriveKey` accept-either branch silently
1454
1974
  // promoted deriveKey-only keys into deriveBits use, contradicting the
1455
1975
  // spec usage gate.
1456
- if (!baseKey.keyUsages.includes('deriveBits')) {
1976
+ if (!baseKey.usages.includes('deriveBits')) {
1457
1977
  throw lazyDOMException('baseKey does not have deriveBits usage', 'InvalidAccessError');
1458
1978
  }
1459
- if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch');
1460
- switch (algorithm.name) {
1979
+ if (baseKey.algorithm.name !== normalizedAlgorithm.name) {
1980
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
1981
+ }
1982
+ switch (normalizedAlgorithm.name) {
1461
1983
  case 'PBKDF2':
1462
- return pbkdf2DeriveBits(algorithm, baseKey, length);
1984
+ if (length === null) {
1985
+ throw lazyDOMException('length cannot be null', 'OperationError');
1986
+ }
1987
+ return pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length);
1463
1988
  case 'X25519':
1464
1989
  // Fall through
1465
1990
  case 'X448':
1466
- return xDeriveBits(algorithm, baseKey, length);
1991
+ return xDeriveBits(normalizedAlgorithm, baseKey, length);
1467
1992
  case 'ECDH':
1468
- return ecDeriveBits(algorithm, baseKey, length);
1993
+ return ecDeriveBits(normalizedAlgorithm, baseKey, length);
1469
1994
  case 'HKDF':
1470
- return hkdfDeriveBits(algorithm, baseKey, length);
1995
+ if (length === null) {
1996
+ throw lazyDOMException('length cannot be null', 'OperationError');
1997
+ }
1998
+ return hkdfDeriveBits(normalizedAlgorithm, baseKey, length);
1471
1999
  case 'Argon2d':
1472
2000
  case 'Argon2i':
1473
2001
  case 'Argon2id':
1474
- return argon2DeriveBits(algorithm, baseKey, length);
2002
+ if (length === null) {
2003
+ throw lazyDOMException('length cannot be null', 'OperationError');
2004
+ }
2005
+ return argon2DeriveBits(normalizedAlgorithm, baseKey, length);
1475
2006
  }
1476
- throw new Error(`'subtle.deriveBits()' for ${algorithm.name} is not implemented.`);
2007
+ throw lazyDOMException(`'subtle.deriveBits()' for ${normalizedAlgorithm.name} is not implemented.`, 'NotSupportedError');
1477
2008
  }
1478
2009
  async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
2010
+ requireArgs(arguments.length, 5, 'deriveKey');
2011
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
2012
+ const normalizedDerivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey');
2013
+
1479
2014
  // Validate baseKey usage
1480
- if (!baseKey.usages.includes('deriveKey') && !baseKey.usages.includes('deriveBits')) {
1481
- throw lazyDOMException('baseKey does not have deriveKey or deriveBits usage', 'InvalidAccessError');
2015
+ if (!baseKey.usages.includes('deriveKey')) {
2016
+ throw lazyDOMException('baseKey does not have deriveKey usage', 'InvalidAccessError');
2017
+ }
2018
+ if (baseKey.algorithm.name !== normalizedAlgorithm.name) {
2019
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
1482
2020
  }
1483
2021
 
1484
- // Calculate required key length
1485
- const length = getKeyLength(derivedKeyAlgorithm);
2022
+ // Calculate required key length (may be null for KDF-derived material).
2023
+ const length = getKeyLength(normalizedDerivedKeyAlgorithm);
1486
2024
 
1487
2025
  // Step 1: Derive bits
1488
2026
  let derivedBits;
1489
- if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch');
1490
- switch (algorithm.name) {
2027
+ switch (normalizedAlgorithm.name) {
1491
2028
  case 'PBKDF2':
1492
- derivedBits = await pbkdf2DeriveBits(algorithm, baseKey, length);
2029
+ if (length === null) {
2030
+ throw lazyDOMException('length cannot be null', 'OperationError');
2031
+ }
2032
+ derivedBits = await pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length);
1493
2033
  break;
1494
2034
  case 'X25519':
1495
2035
  // Fall through
1496
2036
  case 'X448':
1497
- derivedBits = await xDeriveBits(algorithm, baseKey, length);
2037
+ derivedBits = await xDeriveBits(normalizedAlgorithm, baseKey, length);
1498
2038
  break;
1499
2039
  case 'ECDH':
1500
- derivedBits = await ecDeriveBits(algorithm, baseKey, length);
2040
+ derivedBits = await ecDeriveBits(normalizedAlgorithm, baseKey, length);
1501
2041
  break;
1502
2042
  case 'HKDF':
1503
- derivedBits = hkdfDeriveBits(algorithm, baseKey, length);
2043
+ if (length === null) {
2044
+ throw lazyDOMException('length cannot be null', 'OperationError');
2045
+ }
2046
+ derivedBits = hkdfDeriveBits(normalizedAlgorithm, baseKey, length);
1504
2047
  break;
1505
2048
  case 'Argon2d':
1506
2049
  case 'Argon2i':
1507
2050
  case 'Argon2id':
1508
- derivedBits = argon2DeriveBits(algorithm, baseKey, length);
2051
+ if (length === null) {
2052
+ throw lazyDOMException('length cannot be null', 'OperationError');
2053
+ }
2054
+ derivedBits = argon2DeriveBits(normalizedAlgorithm, baseKey, length);
1509
2055
  break;
1510
2056
  default:
1511
- throw new Error(`'subtle.deriveKey()' for ${algorithm.name} is not implemented.`);
2057
+ throw lazyDOMException(`'subtle.deriveKey()' for ${normalizedAlgorithm.name} is not implemented.`, 'NotSupportedError');
1512
2058
  }
1513
2059
 
1514
- // Step 2: Import as key
1515
- return this.importKey('raw', derivedBits, derivedKeyAlgorithm, extractable, keyUsages);
2060
+ // Step 2: Import as key. Use 'raw-secret' so derived material flows into
2061
+ // AEADs / KMAC correctly — they reject plain 'raw' (Node webcrypto.js:381-385).
2062
+ return this.importKey('raw-secret', derivedBits, derivedKeyAlgorithm, extractable, keyUsages);
1516
2063
  }
1517
2064
  async encrypt(algorithm, key, data) {
2065
+ requireArgs(arguments.length, 3, 'encrypt');
1518
2066
  const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt');
1519
- return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), 'encrypt');
2067
+ if (normalizedAlgorithm.name !== key.algorithm.name) {
2068
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
2069
+ }
2070
+ if (!key.usages.includes('encrypt')) {
2071
+ throw lazyDOMException('Unable to use this key to encrypt', 'InvalidAccessError');
2072
+ }
2073
+ return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data));
1520
2074
  }
1521
2075
  async exportKey(format, key) {
1522
- if (!key.extractable) throw new Error('key is not extractable');
2076
+ requireArgs(arguments.length, 2, 'exportKey');
2077
+ if (!key.extractable) throw lazyDOMException('key is not extractable', 'InvalidAccessError');
1523
2078
  if (format === 'raw-seed') {
1524
- const pqcAlgos = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'];
2079
+ const pqcAlgos = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024', 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87', ...SLH_DSA_VARIANTS];
1525
2080
  if (!pqcAlgos.includes(key.algorithm.name)) {
1526
2081
  throw lazyDOMException('raw-seed export only supported for PQC keys', 'NotSupportedError');
1527
2082
  }
@@ -1530,9 +2085,6 @@ export class Subtle {
1530
2085
  }
1531
2086
  return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey());
1532
2087
  }
1533
-
1534
- // Note: 'raw-seed' is handled above; do NOT normalize it here
1535
- if (format === 'raw-secret' || format === 'raw-public') format = 'raw';
1536
2088
  switch (format) {
1537
2089
  case 'spki':
1538
2090
  return await exportKeySpki(key);
@@ -1541,13 +2093,19 @@ export class Subtle {
1541
2093
  case 'jwk':
1542
2094
  return exportKeyJWK(key);
1543
2095
  case 'raw':
1544
- return exportKeyRaw(key);
2096
+ case 'raw-secret':
2097
+ case 'raw-public':
2098
+ return exportKeyRaw(key, format);
1545
2099
  }
1546
2100
  }
1547
2101
  async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
1548
- // Validate wrappingKey usage
2102
+ requireArgs(arguments.length, 4, 'wrapKey');
2103
+ const normalizedWrapAlgorithm = normalizeAlgorithm(wrapAlgorithm, 'wrapKey');
2104
+ if (normalizedWrapAlgorithm.name !== wrappingKey.algorithm.name) {
2105
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
2106
+ }
1549
2107
  if (!wrappingKey.usages.includes('wrapKey')) {
1550
- throw lazyDOMException('wrappingKey does not have wrapKey usage', 'InvalidAccessError');
2108
+ throw lazyDOMException('Unable to use this key to wrapKey', 'InvalidAccessError');
1551
2109
  }
1552
2110
 
1553
2111
  // Step 1: Export the key
@@ -1560,7 +2118,7 @@ export class Subtle {
1560
2118
  const buffer = SBuffer.from(jwkString, 'utf8');
1561
2119
 
1562
2120
  // For AES-KW, pad to multiple of 8 bytes (accounting for null terminator)
1563
- if (wrapAlgorithm.name === 'AES-KW') {
2121
+ if (normalizedWrapAlgorithm.name === 'AES-KW') {
1564
2122
  const length = buffer.length;
1565
2123
  // Add 1 for null terminator, then pad to multiple of 8
1566
2124
  const paddedLength = Math.ceil((length + 1) / 8) * 8;
@@ -1577,16 +2135,20 @@ export class Subtle {
1577
2135
  }
1578
2136
 
1579
2137
  // Step 3: Encrypt the exported key
1580
- return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, wrapAlgorithm, wrappingKey, keyData, 'wrapKey');
2138
+ return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedWrapAlgorithm, wrappingKey, keyData);
1581
2139
  }
1582
2140
  async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
1583
- // Validate unwrappingKey usage
2141
+ requireArgs(arguments.length, 7, 'unwrapKey');
2142
+ const normalizedUnwrapAlgorithm = normalizeAlgorithm(unwrapAlgorithm, 'unwrapKey');
2143
+ if (normalizedUnwrapAlgorithm.name !== unwrappingKey.algorithm.name) {
2144
+ throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
2145
+ }
1584
2146
  if (!unwrappingKey.usages.includes('unwrapKey')) {
1585
- throw lazyDOMException('unwrappingKey does not have unwrapKey usage', 'InvalidAccessError');
2147
+ throw lazyDOMException('Unable to use this key to unwrapKey', 'InvalidAccessError');
1586
2148
  }
1587
2149
 
1588
2150
  // Step 1: Decrypt the wrapped key
1589
- const decrypted = await cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, unwrapAlgorithm, unwrappingKey, bufferLikeToArrayBuffer(wrappedKey), 'unwrapKey');
2151
+ const decrypted = await cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, normalizedUnwrapAlgorithm, unwrappingKey, bufferLikeToArrayBuffer(wrappedKey));
1590
2152
 
1591
2153
  // Step 2: Convert to appropriate format
1592
2154
  let keyData;
@@ -1594,7 +2156,7 @@ export class Subtle {
1594
2156
  const buffer = SBuffer.from(decrypted);
1595
2157
  // For AES-KW, the data may be padded - find the null terminator
1596
2158
  let jwkString;
1597
- if (unwrapAlgorithm.name === 'AES-KW') {
2159
+ if (normalizedUnwrapAlgorithm.name === 'AES-KW') {
1598
2160
  // Find the null terminator (if present) to get the original string
1599
2161
  const nullIndex = buffer.indexOf(0);
1600
2162
  if (nullIndex !== -1) {
@@ -1615,6 +2177,7 @@ export class Subtle {
1615
2177
  return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
1616
2178
  }
1617
2179
  async generateKey(algorithm, extractable, keyUsages) {
2180
+ requireArgs(arguments.length, 3, 'generateKey');
1618
2181
  algorithm = normalizeAlgorithm(algorithm, 'generateKey');
1619
2182
  let result;
1620
2183
  switch (algorithm.name) {
@@ -1676,6 +2239,21 @@ export class Subtle {
1676
2239
  result = await mldsa_generateKeyPairWebCrypto(algorithm.name, extractable, keyUsages);
1677
2240
  checkCryptoKeyPairUsages(result);
1678
2241
  break;
2242
+ case 'SLH-DSA-SHA2-128s':
2243
+ case 'SLH-DSA-SHA2-128f':
2244
+ case 'SLH-DSA-SHA2-192s':
2245
+ case 'SLH-DSA-SHA2-192f':
2246
+ case 'SLH-DSA-SHA2-256s':
2247
+ case 'SLH-DSA-SHA2-256f':
2248
+ case 'SLH-DSA-SHAKE-128s':
2249
+ case 'SLH-DSA-SHAKE-128f':
2250
+ case 'SLH-DSA-SHAKE-192s':
2251
+ case 'SLH-DSA-SHAKE-192f':
2252
+ case 'SLH-DSA-SHAKE-256s':
2253
+ case 'SLH-DSA-SHAKE-256f':
2254
+ result = await slhdsa_generateKeyPairWebCrypto(algorithm.name, extractable, keyUsages);
2255
+ checkCryptoKeyPairUsages(result);
2256
+ break;
1679
2257
  case 'X25519':
1680
2258
  // Fall through
1681
2259
  case 'X448':
@@ -1697,6 +2275,7 @@ export class Subtle {
1697
2275
  return result;
1698
2276
  }
1699
2277
  async getPublicKey(key, keyUsages) {
2278
+ requireArgs(arguments.length, 2, 'getPublicKey');
1700
2279
  if (key.type === 'secret') {
1701
2280
  throw lazyDOMException('key must be a private key', 'NotSupportedError');
1702
2281
  }
@@ -1707,8 +2286,11 @@ export class Subtle {
1707
2286
  return publicKeyObject.toCryptoKey(key.algorithm, true, keyUsages);
1708
2287
  }
1709
2288
  async importKey(format, data, algorithm, extractable, keyUsages) {
1710
- // Note: 'raw-seed' is NOT normalized — PQC import functions handle it directly
1711
- if (format === 'raw-secret' || format === 'raw-public') format = 'raw';
2289
+ requireArgs(arguments.length, 5, 'importKey');
2290
+ // Per-algorithm format handling. Some algorithms alias raw-secret/raw-public
2291
+ // to 'raw' (RSA, EC, Ed/X, HMAC, HKDF, PBKDF2); others demand the
2292
+ // disambiguated form (KMAC, AES-OCB, ChaCha20-Poly1305, Argon2, ML-DSA,
2293
+ // ML-KEM). 'raw-seed' is never normalized — PQC import handles it directly.
1712
2294
  const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey');
1713
2295
  let result;
1714
2296
  switch (normalizedAlgorithm.name) {
@@ -1717,14 +2299,17 @@ export class Subtle {
1717
2299
  case 'RSA-PSS':
1718
2300
  // Fall through
1719
2301
  case 'RSA-OAEP':
1720
- result = rsaImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
2302
+ result = rsaImportKey(aliasKeyFormat(format), data, normalizedAlgorithm, extractable, keyUsages);
1721
2303
  break;
1722
2304
  case 'ECDSA':
1723
2305
  // Fall through
1724
2306
  case 'ECDH':
1725
- result = ecImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
2307
+ result = ecImportKey(aliasKeyFormat(format), data, normalizedAlgorithm, extractable, keyUsages);
1726
2308
  break;
1727
2309
  case 'HMAC':
2310
+ // No aliasing — Node routes HMAC straight into mac.js, which accepts
2311
+ // 'raw' / 'raw-secret' / 'jwk' and rejects everything else
2312
+ // (webcrypto.js:774-781, mac.js:136-174).
1728
2313
  result = await hmacImportKey(normalizedAlgorithm, format, data, extractable, keyUsages);
1729
2314
  break;
1730
2315
  case 'KMAC128':
@@ -1746,13 +2331,15 @@ export class Subtle {
1746
2331
  result = await aesImportKey(normalizedAlgorithm, format, data, extractable, keyUsages);
1747
2332
  break;
1748
2333
  case 'PBKDF2':
2334
+ result = await pbkdf2ImportKey(normalizedAlgorithm, aliasKeyFormat(format), data, extractable, keyUsages);
2335
+ break;
1749
2336
  case 'Argon2d':
1750
2337
  case 'Argon2i':
1751
2338
  case 'Argon2id':
1752
- result = await importGenericSecretKey(normalizedAlgorithm, format, data, extractable, keyUsages);
2339
+ result = await argon2ImportKey(normalizedAlgorithm, format, data, extractable, keyUsages);
1753
2340
  break;
1754
2341
  case 'HKDF':
1755
- result = await hkdfImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
2342
+ result = await hkdfImportKey(aliasKeyFormat(format), data, normalizedAlgorithm, extractable, keyUsages);
1756
2343
  break;
1757
2344
  case 'X25519':
1758
2345
  // Fall through
@@ -1761,7 +2348,21 @@ export class Subtle {
1761
2348
  case 'Ed25519':
1762
2349
  // Fall through
1763
2350
  case 'Ed448':
1764
- result = edImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
2351
+ result = edImportKey(aliasKeyFormat(format), data, normalizedAlgorithm, extractable, keyUsages);
2352
+ break;
2353
+ case 'SLH-DSA-SHA2-128s':
2354
+ case 'SLH-DSA-SHA2-128f':
2355
+ case 'SLH-DSA-SHA2-192s':
2356
+ case 'SLH-DSA-SHA2-192f':
2357
+ case 'SLH-DSA-SHA2-256s':
2358
+ case 'SLH-DSA-SHA2-256f':
2359
+ case 'SLH-DSA-SHAKE-128s':
2360
+ case 'SLH-DSA-SHAKE-128f':
2361
+ case 'SLH-DSA-SHAKE-192s':
2362
+ case 'SLH-DSA-SHAKE-192f':
2363
+ case 'SLH-DSA-SHAKE-256s':
2364
+ case 'SLH-DSA-SHAKE-256f':
2365
+ result = slhdsaImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
1765
2366
  break;
1766
2367
  case 'ML-DSA-44':
1767
2368
  // Fall through
@@ -1786,9 +2387,11 @@ export class Subtle {
1786
2387
  return result;
1787
2388
  }
1788
2389
  async sign(algorithm, key, data) {
2390
+ requireArgs(arguments.length, 3, 'sign');
1789
2391
  return signVerify(normalizeAlgorithm(algorithm, 'sign'), key, data);
1790
2392
  }
1791
2393
  async verify(algorithm, key, signature, data) {
2394
+ requireArgs(arguments.length, 4, 'verify');
1792
2395
  return signVerify(normalizeAlgorithm(algorithm, 'verify'), key, data, signature);
1793
2396
  }
1794
2397
  _encapsulateCore(algorithm, key) {
@@ -1814,12 +2417,14 @@ export class Subtle {
1814
2417
  return mlkem.decapsulateSync(bufferLikeToArrayBuffer(ciphertext));
1815
2418
  }
1816
2419
  async encapsulateBits(algorithm, key) {
2420
+ requireArgs(arguments.length, 2, 'encapsulateBits');
1817
2421
  if (!key.usages.includes('encapsulateBits')) {
1818
2422
  throw lazyDOMException('Key does not have encapsulateBits usage', 'InvalidAccessError');
1819
2423
  }
1820
2424
  return this._encapsulateCore(algorithm, key);
1821
2425
  }
1822
2426
  async encapsulateKey(algorithm, key, sharedKeyAlgorithm, extractable, usages) {
2427
+ requireArgs(arguments.length, 5, 'encapsulateKey');
1823
2428
  if (!key.usages.includes('encapsulateKey')) {
1824
2429
  throw lazyDOMException('Key does not have encapsulateKey usage', 'InvalidAccessError');
1825
2430
  }
@@ -1827,48 +2432,94 @@ export class Subtle {
1827
2432
  sharedKey,
1828
2433
  ciphertext
1829
2434
  } = this._encapsulateCore(algorithm, key);
1830
- const importedKey = await this.importKey('raw', sharedKey, sharedKeyAlgorithm, extractable, usages);
2435
+ // Node imports the encapsulated shared bits as 'raw-secret'
2436
+ // (webcrypto.js:1370-1374) so AEADs / KMAC accept the result.
2437
+ const importedKey = await this.importKey('raw-secret', sharedKey, sharedKeyAlgorithm, extractable, usages);
1831
2438
  return {
1832
2439
  key: importedKey,
1833
2440
  ciphertext
1834
2441
  };
1835
2442
  }
1836
2443
  async decapsulateBits(algorithm, key, ciphertext) {
2444
+ requireArgs(arguments.length, 3, 'decapsulateBits');
1837
2445
  if (!key.usages.includes('decapsulateBits')) {
1838
2446
  throw lazyDOMException('Key does not have decapsulateBits usage', 'InvalidAccessError');
1839
2447
  }
1840
2448
  return this._decapsulateCore(algorithm, key, ciphertext);
1841
2449
  }
1842
2450
  async decapsulateKey(algorithm, key, ciphertext, sharedKeyAlgorithm, extractable, usages) {
2451
+ requireArgs(arguments.length, 6, 'decapsulateKey');
1843
2452
  if (!key.usages.includes('decapsulateKey')) {
1844
2453
  throw lazyDOMException('Key does not have decapsulateKey usage', 'InvalidAccessError');
1845
2454
  }
1846
2455
  const sharedKey = this._decapsulateCore(algorithm, key, ciphertext);
1847
- return this.importKey('raw', sharedKey, sharedKeyAlgorithm, extractable, usages);
2456
+ // Node imports the decapsulated shared bits as 'raw-secret'
2457
+ // (webcrypto.js:1490-1494).
2458
+ return this.importKey('raw-secret', sharedKey, sharedKeyAlgorithm, extractable, usages);
1848
2459
  }
1849
2460
  }
1850
2461
  export const subtle = new Subtle();
2462
+
2463
+ // Returns the number of bits to derive for an `importKey` algorithm, mirroring
2464
+ // Node's webcrypto.js:269-306 `getKeyLength`. Returns null for KDF algorithms
2465
+ // (HKDF / PBKDF2 / Argon2) — those carry their full derived secret without a
2466
+ // fixed key length. Throws OperationError on invalid AES / HMAC inputs rather
2467
+ // than silently coercing to a default (Node commit 4cb1f284136 behavior).
1851
2468
  function getKeyLength(algorithm) {
1852
2469
  const name = algorithm.name;
2470
+ const length = algorithm.length;
1853
2471
  switch (name) {
1854
2472
  case 'AES-CTR':
1855
2473
  case 'AES-CBC':
1856
2474
  case 'AES-GCM':
1857
2475
  case 'AES-KW':
1858
2476
  case 'AES-OCB':
1859
- case 'ChaCha20-Poly1305':
1860
- return algorithm.length || 256;
2477
+ if (length !== 128 && length !== 192 && length !== 256) {
2478
+ throw lazyDOMException('Invalid key length', 'OperationError');
2479
+ }
2480
+ return length;
1861
2481
  case 'HMAC':
1862
2482
  {
1863
- const hmacAlg = algorithm;
1864
- return hmacAlg.length || 256;
2483
+ if (length === undefined) {
2484
+ return getHmacBlockSize(algorithm.hash?.name ?? algorithm.hash);
2485
+ }
2486
+ if (typeof length === 'number' && length !== 0) {
2487
+ return length;
2488
+ }
2489
+ throw lazyDOMException('Invalid key length', 'OperationError');
1865
2490
  }
1866
2491
  case 'KMAC128':
1867
- return algorithm.length || 128;
2492
+ return typeof length === 'number' ? length : 128;
1868
2493
  case 'KMAC256':
1869
- return algorithm.length || 256;
2494
+ return typeof length === 'number' ? length : 256;
2495
+ case 'ChaCha20-Poly1305':
2496
+ return 256;
2497
+ case 'HKDF':
2498
+ case 'PBKDF2':
2499
+ case 'Argon2d':
2500
+ case 'Argon2i':
2501
+ case 'Argon2id':
2502
+ return null;
1870
2503
  default:
1871
2504
  throw lazyDOMException(`Cannot determine key length for ${name}`, 'NotSupportedError');
1872
2505
  }
1873
2506
  }
2507
+ function getHmacBlockSize(name) {
2508
+ switch (name) {
2509
+ case 'SHA-1':
2510
+ case 'SHA-256':
2511
+ return 512;
2512
+ case 'SHA-384':
2513
+ case 'SHA-512':
2514
+ return 1024;
2515
+ case 'SHA3-256':
2516
+ case 'SHA3-384':
2517
+ case 'SHA3-512':
2518
+ // SHA-3 / HMAC interaction undefined — Node throws here too
2519
+ // (webcrypto-modern-algos issue #23).
2520
+ throw lazyDOMException('Explicit algorithm length member is required', 'NotSupportedError');
2521
+ default:
2522
+ throw lazyDOMException('Invalid key length', 'OperationError');
2523
+ }
2524
+ }
1874
2525
  //# sourceMappingURL=subtle.js.map