@blamejs/pki 0.6.13 → 0.6.15

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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +1 -1
  3. package/index.js +1 -1
  4. package/lib/acme.js +59 -815
  5. package/lib/asn1-der.js +7 -245
  6. package/lib/attrcert-sign.js +12 -260
  7. package/lib/attrcert-verify.js +1 -7
  8. package/lib/byte-reader.js +0 -8
  9. package/lib/byte-writer.js +0 -11
  10. package/lib/cbor-det.js +0 -54
  11. package/lib/cmc-build.js +15 -357
  12. package/lib/cmc-verify.js +6 -239
  13. package/lib/cmp-build.js +41 -317
  14. package/lib/cmp-session.js +73 -760
  15. package/lib/cmp-verify.js +32 -488
  16. package/lib/cms-compress.js +3 -32
  17. package/lib/cms-decrypt.js +29 -294
  18. package/lib/cms-encrypt.js +14 -234
  19. package/lib/cms-sign.js +29 -324
  20. package/lib/cms-verify.js +9 -500
  21. package/lib/composite-kem.js +2 -102
  22. package/lib/composite-sig.js +1 -77
  23. package/lib/constants.js +0 -249
  24. package/lib/crl-sign.js +9 -237
  25. package/lib/crl-verify.js +1 -10
  26. package/lib/crmf-sign.js +19 -198
  27. package/lib/crmf-verify.js +1 -12
  28. package/lib/csr-sign.js +3 -34
  29. package/lib/csr-verify.js +1 -12
  30. package/lib/ct.js +28 -253
  31. package/lib/edwards-point.js +10 -44
  32. package/lib/est.js +28 -764
  33. package/lib/framework-error.js +4 -182
  34. package/lib/guard-all.js +1 -58
  35. package/lib/guard-async.js +1 -36
  36. package/lib/guard-bytes.js +6 -650
  37. package/lib/guard-compress.js +5 -102
  38. package/lib/guard-crypto.js +1 -40
  39. package/lib/guard-der.js +2 -93
  40. package/lib/guard-encoding.js +2 -60
  41. package/lib/guard-header.js +1 -36
  42. package/lib/guard-identifier.js +7 -531
  43. package/lib/guard-intrinsic.js +1 -150
  44. package/lib/guard-json.js +10 -73
  45. package/lib/guard-limits.js +1 -77
  46. package/lib/guard-list.js +1 -49
  47. package/lib/guard-name.js +3 -209
  48. package/lib/guard-parsed.js +7 -335
  49. package/lib/guard-range.js +1 -71
  50. package/lib/guard-secret.js +1 -102
  51. package/lib/guard-text.js +1 -64
  52. package/lib/guard-time.js +1 -97
  53. package/lib/hpke.js +3 -105
  54. package/lib/http-digest.js +14 -142
  55. package/lib/http-retry-after.js +1 -47
  56. package/lib/http-transport.js +21 -131
  57. package/lib/inspect.js +30 -284
  58. package/lib/ip-utils.js +6 -47
  59. package/lib/jose.js +7 -142
  60. package/lib/key.js +2 -67
  61. package/lib/lint.js +9 -194
  62. package/lib/merkle.js +1 -27
  63. package/lib/mime.js +9 -82
  64. package/lib/ocsp-request-verify.js +1 -14
  65. package/lib/ocsp-verify.js +1 -82
  66. package/lib/ocsp.js +11 -169
  67. package/lib/oid.js +5 -337
  68. package/lib/path-validate.js +103 -1492
  69. package/lib/pbes2.js +2 -89
  70. package/lib/pkcs12-build.js +31 -379
  71. package/lib/pki-build.js +64 -290
  72. package/lib/rc2.js +2 -18
  73. package/lib/rfc3339.js +4 -27
  74. package/lib/scep.js +59 -233
  75. package/lib/schema-all.js +0 -122
  76. package/lib/schema-attrcert.js +4 -146
  77. package/lib/schema-c509.js +153 -802
  78. package/lib/schema-cmc.js +5 -220
  79. package/lib/schema-cmp.js +2 -223
  80. package/lib/schema-cms.js +13 -427
  81. package/lib/schema-crl.js +5 -68
  82. package/lib/schema-crmf.js +6 -167
  83. package/lib/schema-csr.js +1 -54
  84. package/lib/schema-csrattrs.js +2 -87
  85. package/lib/schema-engine.js +0 -152
  86. package/lib/schema-ocsp.js +0 -117
  87. package/lib/schema-pkcs12.js +2 -93
  88. package/lib/schema-pkcs8.js +3 -35
  89. package/lib/schema-pkix.js +24 -655
  90. package/lib/schema-smime.js +0 -54
  91. package/lib/schema-tsp.js +3 -99
  92. package/lib/schema-x509.js +4 -63
  93. package/lib/shbs.js +11 -95
  94. package/lib/sign-scheme.js +2 -97
  95. package/lib/sigstore.js +9 -169
  96. package/lib/sleep.js +2 -8
  97. package/lib/smime.js +15 -408
  98. package/lib/tls-cert-compress.js +0 -62
  99. package/lib/trust.js +19 -185
  100. package/lib/tsp-sign.js +16 -270
  101. package/lib/validator-all.js +1 -30
  102. package/lib/validator-attcert.js +2 -46
  103. package/lib/validator-cose.js +2 -153
  104. package/lib/validator-keydesc.js +1 -37
  105. package/lib/validator-sig.js +1 -46
  106. package/lib/validator-tls.js +1 -88
  107. package/lib/validator-tpm.js +19 -122
  108. package/lib/verify-seam.js +1 -24
  109. package/lib/webauthn-mds.js +3 -400
  110. package/lib/webauthn.js +12 -833
  111. package/lib/webcrypto.js +5 -485
  112. package/lib/x509-sign.js +4 -108
  113. package/package.json +1 -1
  114. package/sbom.cdx.json +6 -6
package/lib/webcrypto.js CHANGED
@@ -42,38 +42,18 @@ var guard = require("./guard-all");
42
42
  var constants = require("./constants");
43
43
  var asn1 = require("./asn1-der");
44
44
  var oid = require("./oid");
45
- // What this engine reads from the runtime while it is deciding something, taken at load. The
46
- // decisions here are the ones the whole toolkit signs and verifies behind: whether a value IS a
47
- // CryptoKey, which algorithm a key names, how many octets a modulus has. A caller who receives
48
- // control afterwards -- through an accessor on a key-like object, or while a verb is suspended on
49
- // an await -- can replace any of them, and every check around them still runs and still passes.
50
- // See guard-intrinsic for the whole captured set.
51
45
  var intrinsic = require("./guard-intrinsic");
52
46
  var _isArray = intrinsic.isArray;
53
47
  var _hasOwn = intrinsic.hasOwn;
54
48
  var _stringify = intrinsic.stringify;
55
49
  var _bufferFrom = intrinsic.bufferFrom;
56
50
  var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
57
- // The node Hash instance methods, captured at load. digestStream feeds chunks across `await` points,
58
- // and a co-resident that replaced Hash.prototype.update while the stream was suspended between chunks
59
- // could drop or alter one -- making digest("ab") equal digest("a") -- and, because both streamed
60
- // signing and verification trust this digest, forge a valid verdict over bytes the stream never
61
- // yielded. Calling the captured update/digest keeps the hash bound to the bytes the stream saw. The
62
- // one-shot digest needs no such capture: its single update and digest run with no suspension between.
63
51
  var _hashUpdate = intrinsic.uncurry(nodeCrypto.Hash.prototype.update);
64
52
  var _hashDigest = intrinsic.uncurry(nodeCrypto.Hash.prototype.digest);
65
- // The node Cipheriv instance methods, captured at load for the same reason as the hash methods above:
66
- // encryptStream feeds plaintext chunks through the cipher across `await` points, and a co-resident that
67
- // replaced Cipheriv.prototype.update while the stream was suspended between chunks could drop or alter a
68
- // chunk, so the emitted ciphertext would not be the encryption of the plaintext the caller streamed.
69
- // Calling the captured update/final keeps the ciphertext bound to the bytes the stream actually yielded.
70
53
  var _cipherUpdate = intrinsic.uncurry(nodeCrypto.Cipheriv.prototype.update);
71
54
  var _cipherFinal = intrinsic.uncurry(nodeCrypto.Cipheriv.prototype.final);
72
55
  var _cipherGetAuthTag = intrinsic.uncurry(nodeCrypto.Cipheriv.prototype.getAuthTag);
73
56
  var _cipherSetAAD = intrinsic.uncurry(nodeCrypto.Cipheriv.prototype.setAAD);
74
- // decryptStream drives these across the caller's iteration steps, so it captures them for the same
75
- // reason: a co-resident that replaced Decipheriv.prototype.update between two chunk reads could steer
76
- // the recovered plaintext. The captured pair keeps the yielded chunks bound to the ciphertext.
77
57
  var _decipherUpdate = intrinsic.uncurry(nodeCrypto.Decipheriv.prototype.update);
78
58
  var _decipherFinal = intrinsic.uncurry(nodeCrypto.Decipheriv.prototype.final);
79
59
  var _STREAM_CHUNK = constants.BYTES.kib(64);
@@ -111,19 +91,13 @@ var _numberToString = intrinsic.numberToString;
111
91
  var _bigIntToString = intrinsic.bigIntToString;
112
92
  var _push = intrinsic.push;
113
93
  var _concat = intrinsic.concat;
114
- // Promise.resolve is a constructor method, so it keeps its receiver: `_resolve(Promise, x)`.
115
94
  var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
116
95
  var _types = intrinsic.types;
117
96
 
118
- // Single-owner error class -- co-located with its module (framework-error
119
- // stays the cross-module home; this is webcrypto-private). withCause: a
120
- // failure discovered while processing decrypted/untrusted bytes threads the
121
- // underlying fault instead of discarding it.
122
97
  var WebCryptoError = frameworkError.defineClass("WebCryptoError", { withCause: true });
123
98
 
124
99
  var MAX_RANDOM_BYTES = 65536;
125
100
 
126
- // ---- value helpers ---------------------------------------------------
127
101
 
128
102
  function _toBuf(data, who) {
129
103
  return guard.bytes.source(data, WebCryptoError, "webcrypto/data", who || "input");
@@ -134,11 +108,6 @@ function _toArrayBuffer(buf) {
134
108
  }
135
109
 
136
110
  function _wcErr(code, msg) { return new WebCryptoError(code, msg); }
137
- // STRICT base64url decode via the shared encoding guard: a missing / non-canonical
138
- // / non-alphabet value throws webcrypto/data instead of silently importing wrong
139
- // key material (the lenient Buffer.from(String(undefined),"base64url") returned a
140
- // bogus 6-byte key). Node's own canonical JWK export (_rawPublic) round-trips
141
- // cleanly, so the strictness is benign there and fail-closed on untrusted import.
142
111
  function _b64urlToBuf(s, who) { return guard.encoding.base64url(s, null, _wcErr, "webcrypto/data", who || "base64url value"); }
143
112
  function _bufToB64url(buf) { return _bufToString(_bufferFrom(buf), "base64url"); }
144
113
 
@@ -153,16 +122,9 @@ function _normalizeAlg(algorithm, who) {
153
122
  return out;
154
123
  }
155
124
 
156
- // The counterpart to _normalizeAlg's upper-casing: _normalizeAlg upper-cases every algorithm name for
157
- // case-insensitive INTERNAL matching, but the name a CryptoKey EMITS on `.algorithm.name` MUST carry the
158
- // WebCrypto-registered casing, which for a few algorithms is not all-upper: "RSASSA-PKCS1-v1_5" (lowercase
159
- // v), "Ed25519" / "Ed448" (mixed). Every emitted algorithm.name passes through here so a consumer that
160
- // compares against the standard name (e.g. a signer matching a CryptoKey to a certificate key) matches.
161
125
  var _STD_ALG_NAME = { "RSASSA-PKCS1-V1_5": "RSASSA-PKCS1-v1_5", "ED25519": "Ed25519", "ED448": "Ed448" };
162
126
  function _stdName(name) { return _STD_ALG_NAME[name] || name; }
163
127
 
164
- // WebCrypto hash name -> node digest name. SHA-1 is retained for
165
- // backwards compatibility with legacy certificates and signatures.
166
128
  var HASH_NODE = {
167
129
  "SHA-1": "sha1",
168
130
  "SHA-224": "sha224",
@@ -174,16 +136,6 @@ var HASH_NODE = {
174
136
  "SHA3-512": "sha3-512",
175
137
  };
176
138
 
177
- // FIPS 202 extendable-output functions, as message digests. Deliberately a
178
- // separate table from HASH_NODE, not rows added to it: an XOF's output
179
- // length is a parameter and not a property of its name, and node:crypto
180
- // refuses an XOF at every other HASH_NODE consumer (signature, HMAC, HKDF,
181
- // PBKDF2, OAEP). Widening the shared table would turn those consumers' typed
182
- // "unsupported hash" rejection into a raw error from the underlying library.
183
- // The length is fixed by the name, not chosen by the caller: RFC 8702 sec. 4
184
- // requires the output length of SHAKE128 or SHAKE256 used as a message digest
185
- // to be 32 or 64 bytes respectively, and RFC 9814 sec. 4 and
186
- // draft-ietf-lamps-cms-composite-sigs sec. 3 restate it for their profiles.
187
139
  var XOF_NODE = {
188
140
  "SHAKE128": { node: "shake128", length: 32 },
189
141
  "SHAKE256": { node: "shake256", length: 64 },
@@ -191,21 +143,11 @@ var XOF_NODE = {
191
143
 
192
144
  function _hashNode(h, who) {
193
145
  var name = (typeof h === "string") ? h : (h && h.name);
194
- // The case fold is what picks the registry row, so it runs through the captured method: dispatched
195
- // live, a replaced `toUpperCase` answering "SHA-1" makes a caller who asked for SHA-256 receive a
196
- // SHA-1 digest, and nothing between here and the hash says the algorithm changed.
197
146
  var node = HASH_NODE[_toUpperCase(_String(name))];
198
147
  if (!node) throw new WebCryptoError("webcrypto/not-supported", (who || "operation") + ": unsupported hash " + _stringify(name));
199
148
  return node;
200
149
  }
201
150
 
202
- // W3C HMAC get-key-length: an explicit `length` is used as given (validated
203
- // to a positive multiple of 8, so the byte-level key material is exact and
204
- // never a raw RangeError out of randomBytes); an OMITTED length defaults to
205
- // the block size of the hash, the HMAC key-pad width, and not the digest
206
- // size. A digest-size default would mint different key material than every
207
- // conforming WebCrypto for identical inputs, so MACs keyed through this
208
- // engine would fail to verify elsewhere.
209
151
  var HMAC_BLOCK_BITS = {
210
152
  "SHA-1": 512, "SHA-256": 512, "SHA-384": 1024, "SHA-512": 1024,
211
153
  "SHA3-256": 1088, "SHA3-384": 832, "SHA3-512": 576,
@@ -222,40 +164,25 @@ function _hmacLengthBits(alg, who) {
222
164
  return alg.length;
223
165
  }
224
166
 
225
- // WebCrypto namedCurve -> node namedCurve.
226
167
  var CURVE_NODE = { "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1" };
227
168
  var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
228
169
 
229
170
  var ML_DSA_NODE = { "ML-DSA-44": "ml-dsa-44", "ML-DSA-65": "ml-dsa-65", "ML-DSA-87": "ml-dsa-87" };
230
171
  var ML_KEM_NODE = { "ML-KEM-512": "ml-kem-512", "ML-KEM-768": "ml-kem-768", "ML-KEM-1024": "ml-kem-1024" };
231
172
 
232
- // FIPS 205 SLH-DSA -- stateless hash-based signatures. All twelve
233
- // parameter sets Node exposes; signing is one-shot (null algorithm), the
234
- // same shape as ML-DSA / EdDSA. ML-KEM covers key-generation / encoding /
235
- // import plus encapsulateBits / decapsulateBits over Node's KEM primitive.
236
173
  var SLH_DSA_NODE = {};
237
174
  ["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
238
175
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
239
176
  ].forEach(function (s) { SLH_DSA_NODE["SLH-DSA-" + _toUpperCase(s)] = "slh-dsa-" + s; });
240
177
 
241
- // The algorithm names each keyed operation recognizes. Membership is checked
242
- // before the algorithm/key name binding so an unrecognized algorithm reports
243
- // NotSupportedError while a recognized-but-wrong-for-this-key one reports
244
- // InvalidAccessError, matching the W3C error ordering.
245
178
  var SIGN_VERIFY_NAMES = {};
246
179
  ["RSASSA-PKCS1-V1_5", "RSA-PSS", "ECDSA", "ED25519", "ED448", "HMAC"]
247
180
  .concat(_keys(ML_DSA_NODE), _keys(SLH_DSA_NODE))
248
181
  .forEach(function (n) { SIGN_VERIFY_NAMES[n] = true; });
249
182
  var ENCRYPT_DECRYPT_NAMES = { "RSA-OAEP": true, "AES-GCM": true, "AES-CBC": true, "AES-CTR": true };
250
183
  var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
251
- // The secret-key / KDF algorithms whose key material is raw octets (imported via "raw" or a
252
- // JWK "oct"), never an SPKI / PKCS#8 asymmetric-key structure. importKey("spki"|"pkcs8", ...)
253
- // under one of these names is unsupported (W3C: NotSupportedError) and, without this gate, it
254
- // would mint a mislabeled CryptoKey wrapping an asymmetric handle and dodge the algorithm-keyed
255
- // pkcs8 pre-validation (e.g. the RFC 9935 ML-KEM CHOICE guard).
256
184
  var SECRET_KEY_NAMES = { "AES-GCM": true, "AES-CBC": true, "AES-CTR": true, "AES-KW": true, "HMAC": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
257
185
 
258
- // ---- CryptoKey -------------------------------------------------------
259
186
 
260
187
  /**
261
188
  * @primitive pki.webcrypto.CryptoKey
@@ -280,13 +207,6 @@ var SECRET_KEY_NAMES = { "AES-GCM": true, "AES-CBC": true, "AES-CTR": true, "AES
280
207
  function CryptoKey(type, extractable, algorithm, usages, handle) {
281
208
  this.type = type;
282
209
  this.extractable = !!extractable;
283
- // The algorithm is immutable both ways. The PROPERTY is defined non-writable and non-configurable so
284
- // it cannot be REPLACED wholesale (`key.algorithm = { hash: "SHA-512" }`), and its value is a frozen
285
- // COPY -- a caller's importKey params object is left untouched -- so its fields, the nested `hash` an
286
- // RSA/HMAC signature reads among them, cannot be MUTATED. This engine reads key.algorithm.hash live
287
- // when it signs, and a W3C CryptoKey's [[algorithm]] is immutable for exactly this reason: neither
288
- // swapping the object nor a field of it (e.g. from a microtask during a signing await) can make the
289
- // hash the signature is computed under differ from the one checked against a header algorithm.
290
210
  var frozenAlg = algorithm;
291
211
  if (frozenAlg && typeof frozenAlg === "object") {
292
212
  frozenAlg = _assign({}, algorithm);
@@ -294,11 +214,6 @@ function CryptoKey(type, extractable, algorithm, usages, handle) {
294
214
  frozenAlg = intrinsic.freeze(frozenAlg);
295
215
  }
296
216
  _defineProperty(this, "algorithm", { value: frozenAlg, enumerable: true });
297
- // The copy is what the key will be asked about for the rest of its life, so the method that makes
298
- // it is the captured one: a replaced `slice` hands back a list the caller still controls. The
299
- // list-ness is checked rather than left to the copy to discover -- `Array.prototype.slice` on a
300
- // number yields an empty list, so a non-list would have minted a key permitted for NOTHING
301
- // instead of naming the bad argument.
302
217
  if (usages != null && !_isArray(usages)) {
303
218
  throw new WebCryptoError("webcrypto/syntax", "usages must be an array of key-usage strings");
304
219
  }
@@ -306,11 +221,6 @@ function CryptoKey(type, extractable, algorithm, usages, handle) {
306
221
  _defineProperty(this, "_handle", { value: handle, enumerable: false });
307
222
  }
308
223
 
309
- // This engine reads key material through its own CryptoKey handle, which a key minted by another
310
- // WebCrypto implementation does not carry -- W3C leaves cross-implementation use undefined, and
311
- // reaching for the missing handle raises a bare type error from inside the crypto library rather
312
- // than a verdict. Refuse it here, naming which of the two it is: a foreign CryptoKey (import it
313
- // through this engine, or use the pki.* verbs, which adopt one) or something that is no key at all.
314
224
  function _requireOwnKey(key, who) {
315
225
  if (key instanceof CryptoKey) return;
316
226
  if (isCryptoKeyLike(key)) {
@@ -321,21 +231,11 @@ function _requireOwnKey(key, who) {
321
231
 
322
232
  function _requireUsage(key, usage) {
323
233
  _requireOwnKey(key, usage);
324
- // The membership test IS the permission check, so it runs through the captured method: dispatched
325
- // live, an `Array.prototype.indexOf` replaced after load reports every usage present and a key
326
- // minted to verify signs instead, with this refusal never raised.
327
234
  if (_indexOf(key.usages, usage) === -1) {
328
235
  throw new WebCryptoError("webcrypto/invalid-access", "key is not permitted for '" + usage + "' (usages: " + _join(key.usages, ",") + ")");
329
236
  }
330
237
  }
331
238
 
332
- // W3C WebCrypto sign/verify/encrypt/decrypt/deriveBits/deriveKey/wrapKey/
333
- // unwrapKey MUST throw an InvalidAccessError when the normalized algorithm's
334
- // name differs from the key's own algorithm name. The binding is load-bearing
335
- // for the one-shot signature families (EdDSA / ML-DSA / SLH-DSA): node derives
336
- // the algorithm from the KEY handle, so without this check the requested name
337
- // would be silently ignored and an operation requested under algorithm X could
338
- // be satisfied by a key of algorithm Y (algorithm confusion).
339
239
  function _requireAlgMatch(alg, key, who) {
340
240
  var keyName = key && key.algorithm && key.algorithm.name;
341
241
  if (_toUpperCase(_String(keyName)) !== alg.name) {
@@ -343,16 +243,12 @@ function _requireAlgMatch(alg, key, who) {
343
243
  }
344
244
  }
345
245
 
346
- // ---- SubtleCrypto ----------------------------------------------------
347
246
 
348
247
  function SubtleCrypto() {}
349
248
 
350
249
  SubtleCrypto.prototype.digest = async function digest(algorithm, data) {
351
250
  var name = _normalizeAlg(algorithm, "digest").name;
352
251
  var xof = XOF_NODE[_toUpperCase(_String(name))];
353
- // An XOF is always constructed with its output length stated. Omitting it
354
- // makes node emit a deprecation warning and fall back to a 32-byte squeeze,
355
- // which for SHAKE256 would silently produce half the required digest.
356
252
  var h = xof
357
253
  ? nodeCrypto.createHash(xof.node, { outputLength: xof.length })
358
254
  : nodeCrypto.createHash(_hashNode(name, "digest"));
@@ -360,34 +256,10 @@ SubtleCrypto.prototype.digest = async function digest(algorithm, data) {
360
256
  return _toArrayBuffer(h.digest());
361
257
  };
362
258
 
363
- // Digest an async iterable of byte chunks under several algorithms in ONE pass, returning one
364
- // ArrayBuffer per algorithm in order. Each result is byte-identical to digest(algorithms[i], <the
365
- // concatenated chunks>): the same createHash + XOF handling, fed incrementally so an arbitrarily
366
- // large payload is never held whole. The single pass is what lets a multi-signer detached CMS sign
367
- // hash a single-use stream once for every distinct digest algorithm. Which algorithm each Hash runs
368
- // is fixed up front through the captured name resolution (_normalizeAlg / _toUpperCase / _hashNode),
369
- // exactly as the one-shot digest resolves it, so a replaced toUpperCase cannot swap SHA-256 for SHA-1
370
- // mid-stream. A chunk that is not a BufferSource throws webcrypto/data, before it reaches a hash.
371
259
  SubtleCrypto.prototype.digestStream = async function digestStream(algorithms, source) {
372
260
  if (!_isArray(algorithms) || !algorithms.length) {
373
261
  throw new WebCryptoError("webcrypto/syntax", "digestStream: algorithms must be a non-empty array of digest algorithms");
374
262
  }
375
- // Follow the async-iteration protocol exactly as `for await` does: read the source's
376
- // Symbol.asyncIterator ONCE, call it to obtain the iterator, then capture THAT iterator's `next` with
377
- // a single read and drive it manually. Driving `source` by its own `next` would diverge from
378
- // `for await` whenever Symbol.asyncIterator returns a distinct iterator, and an inherited `next` (say
379
- // from a polluted prototype) would misclassify an ordinary iterable; going through the method avoids
380
- // both. Native `for await` caches `next` after one read, so capturing each method once and reusing it
381
- // -- rather than re-reading to drive the loop -- also consumes a one-shot or stateful accessor a
382
- // direct `for await` accepts.
383
- // Build (and thereby validate) the hashes BEFORE acquiring the iterator: an unsupported algorithm
384
- // must reject before the iterator factory runs, so a factory that opens a file or socket is never
385
- // invoked -- and never leaked outside the cleanup guard -- on a rejection, keeping the lazy-stream
386
- // guarantee that a pre-hash rejection acquires no resource.
387
- // A plain index loop, not `_map`: Array.prototype.map SKIPS holes, so a sparse list such as
388
- // `[, "SHA-256"]` would leave a hole in `hashes` (later read as `undefined` and updated as a raw
389
- // TypeError) or, if fully sparse, yield sparse output. Indexing every slot instead makes a hole read
390
- // as `undefined`, which `_normalizeAlg` rejects with a typed webcrypto/syntax before any chunk is read.
391
263
  var hashes = [];
392
264
  for (var _h = 0; _h < algorithms.length; _h++) {
393
265
  var name = _normalizeAlg(algorithms[_h], "digestStream").name;
@@ -402,9 +274,6 @@ SubtleCrypto.prototype.digestStream = async function digestStream(algorithms, so
402
274
  }
403
275
  var iterator = _apply(acquire, source, _NO_ARGS);
404
276
  try {
405
- // The `next` retrieval and its validation are INSIDE the guard: the factory above may have opened a
406
- // resource, so a non-callable next (or a throwing next accessor) must still reach the cleanup below
407
- // that calls the iterator's return(), rather than leak on a malformed iterator protocol.
408
277
  var nextMethod = iterator == null ? undefined : iterator.next;
409
278
  if (typeof nextMethod !== "function") {
410
279
  throw new WebCryptoError("webcrypto/syntax", "digestStream: source must be an async iterable of byte chunks");
@@ -416,24 +285,15 @@ SubtleCrypto.prototype.digestStream = async function digestStream(algorithms, so
416
285
  for (var i = 0; i < hashes.length; i++) _hashUpdate(hashes[i], buf);
417
286
  }
418
287
  } catch (e) {
419
- // Best-effort completion, as `for await` performs on an early exit or throw: let the source release.
420
- // The retrieval of `return` is INSIDE the guard too, so a throwing `return` accessor cannot replace
421
- // the original error -- the malformed-chunk verdict is what the caller must see, not a cleanup fault.
422
288
  try {
423
289
  var ret = iterator == null ? undefined : iterator.return;
424
290
  if (typeof ret === "function") await _apply(ret, iterator, _NO_ARGS);
425
- } catch (_r) { /* cleanup is best-effort */ }
291
+ } catch (_r) { }
426
292
  throw e;
427
293
  }
428
294
  return _map(hashes, function (h) { return _toArrayBuffer(_hashDigest(h)); });
429
295
  };
430
296
 
431
- // encryptStream(spec, source) -> { ciphertext, tag }. Runs the plaintext chunks of an async iterable
432
- // through the content cipher incrementally, assembling the ciphertext -- its definite length is needed
433
- // for the EnvelopedData encryptedContent OCTET STRING, and the plaintext is never held whole (the memory
434
- // win of streaming encrypt). Mirrors digestStream's captured-method + acquire-iterator-once discipline so
435
- // a chunk-boundary prototype swap cannot steer the ciphertext, and drives the iterator's return() on any
436
- // exit. spec: { algNode, key, iv, aad (optional), authTagLength (optional, AEAD -> a tag is returned) }.
437
297
  SubtleCrypto.prototype.encryptStream = async function encryptStream(spec, source) {
438
298
  var cipher = spec.authTagLength
439
299
  ? nodeCrypto.createCipheriv(spec.algNode, spec.key, spec.iv, { authTagLength: spec.authTagLength })
@@ -461,27 +321,15 @@ SubtleCrypto.prototype.encryptStream = async function encryptStream(spec, source
461
321
  try {
462
322
  var ret = iterator == null ? undefined : iterator.return;
463
323
  if (typeof ret === "function") await _apply(ret, iterator, _NO_ARGS);
464
- } catch (_r) { /* cleanup is best-effort */ }
324
+ } catch (_r) { }
465
325
  throw e;
466
326
  }
467
327
  out[out.length] = _cipherFinal(cipher);
468
328
  return { ciphertext: _bufferConcat(out), tag: spec.authTagLength ? _cipherGetAuthTag(cipher) : null };
469
329
  };
470
330
 
471
- // decryptStream(spec, ct) -> an async iterable of plaintext chunks. Unlike encryptStream the whole
472
- // ciphertext is already in memory (a parsed message states its definite length), so this streams the
473
- // OUTPUT: the plaintext is yielded a chunk at a time and never held whole (the memory win of streaming
474
- // decrypt). The decipher is created EAGERLY here -- node copies the key into the cipher at creation, so
475
- // a caller that clears spec.key after this returns (as the CMS decrypt path does once the content is
476
- // open) does not disturb the running decipher. The chunked drive uses captured update/final + subarray,
477
- // so a prototype swap between the caller's iteration steps cannot steer the recovered bytes. A CBC
478
- // final() rejects a wrong key / bad padding exactly as the buffered path does; the caller translates
479
- // that failure into its own error domain. spec: { algNode, key, iv }.
480
331
  SubtleCrypto.prototype.decryptStream = function decryptStream(spec, ct) {
481
332
  var decipher = nodeCrypto.createDecipheriv(spec.algNode, spec.key, spec.iv);
482
- // Snapshot the ciphertext into a private copy NOW: the drive slices it lazily as the caller iterates,
483
- // so a view aliasing the caller's buffer would let a reuse of that buffer after this returns change the
484
- // recovered plaintext and its padding verdict. The copy is read; the caller's buffer is theirs to reuse.
485
333
  return _decipherDrive(decipher, _bufferFrom(_toBuf(ct, "decryptStream ciphertext")), _STREAM_CHUNK);
486
334
  };
487
335
  async function* _decipherDrive(decipher, ct, chunkSize) {
@@ -496,18 +344,12 @@ async function* _decipherDrive(decipher, ct, chunkSize) {
496
344
 
497
345
  SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extractable, keyUsages) {
498
346
  var alg = _normalizeAlg(algorithm, "generateKey");
499
- // Before the partition below, not only in the CryptoKey constructor: the asymmetric branch splits
500
- // the usages first, and `Array.prototype.filter` treats a non-list as array-like and yields `[]`,
501
- // so both halves reached the constructor already valid and the call minted an unusable PAIR
502
- // instead of naming the argument. The symmetric branches hand `usages` straight to the
503
- // constructor, which is why its own door caught those and not these.
504
347
  if (keyUsages != null && !_isArray(keyUsages)) {
505
348
  throw new WebCryptoError("webcrypto/syntax", "generateKey: keyUsages must be an array of key-usage strings");
506
349
  }
507
350
  var usages = keyUsages || [];
508
351
  var name = alg.name;
509
352
 
510
- // Symmetric keys.
511
353
  if (name === "AES-GCM" || name === "AES-CBC" || name === "AES-CTR" || name === "AES-KW") {
512
354
  var bits = alg.length;
513
355
  if (bits !== 128 && bits !== 192 && bits !== 256) throw new WebCryptoError("webcrypto/syntax", name + ": length must be 128/192/256");
@@ -520,11 +362,8 @@ SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extra
520
362
  return new CryptoKey("secret", extractable, { name: name, hash: { name: (typeof alg.hash === "string" ? alg.hash : alg.hash.name) }, length: lenBits }, usages, hkey);
521
363
  }
522
364
 
523
- // Asymmetric key pairs.
524
365
  var pair = _generateKeyPair(alg);
525
366
  var pubAlg = pair.algorithm;
526
- // Which usages each half of the pair is minted with -- the permission set every later operation
527
- // is checked against -- so the partition runs through the captured method.
528
367
  var privUsages = _filter(usages, function (u) { return u === "sign" || u === "deriveKey" || u === "deriveBits" || u === "decrypt" || u === "unwrapKey" || u === "decapsulateKey" || u === "decapsulateBits"; });
529
368
  var pubUsages = _filter(usages, function (u) { return u === "verify" || u === "encrypt" || u === "wrapKey" || u === "encapsulateKey" || u === "encapsulateBits"; });
530
369
  return {
@@ -536,9 +375,6 @@ SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extra
536
375
  function _generateKeyPair(alg) {
537
376
  var name = alg.name, kp, algorithm;
538
377
  if (name === "RSASSA-PKCS1-V1_5" || name === "RSA-PSS" || name === "RSA-OAEP") {
539
- // Resolve the hash BEFORE generating. An RSA keygen is the expensive step here, and a hash
540
- // this engine cannot use yields a key that can never sign, so the refusal has to come first
541
- // or the caller pays for a keypair only to be told the request was unusable.
542
378
  var rsaHash = _hashObj(alg.hash, "generateKey " + name);
543
379
  kp = nodeCrypto.generateKeyPairSync("rsa", {
544
380
  modulusLength: alg.modulusLength || 2048,
@@ -568,30 +404,13 @@ function _generateKeyPair(alg) {
568
404
  return { publicKey: kp.publicKey, privateKey: kp.privateKey, algorithm: algorithm };
569
405
  }
570
406
 
571
- // The hash recorded on a CryptoKey's algorithm. The name is RESOLVED here, so an
572
- // entry point refuses a hash this engine cannot use instead of minting a key that
573
- // only fails at its first sign / verify / wrap -- after the caller has already paid
574
- // for the key generation. Resolution runs through _hashNode itself rather than a
575
- // second table, so what an entry point accepts cannot drift from what the
576
- // operations support.
577
407
  function _hashObj(h, who) { if (!h) return undefined; var n = (typeof h === "string" ? h : h.name); _hashNode(n, who || "importKey"); return { name: n }; }
578
- // The node key argument for an ML-DSA sign/verify: the bare handle, or a
579
- // { key, context } when the caller supplies an ML-DSA context. FIPS 204 bounds the
580
- // context to 0..255 bytes; a longer one is a DataError at the API boundary, not a
581
- // raw ERR_OUT_OF_RANGE from node.
582
408
  function _mldsaKeyArg(alg, keyHandle, who) {
583
409
  if (alg.context == null) return keyHandle;
584
410
  var ctx = _toBuf(alg.context, who + " context");
585
411
  if (ctx.length > 255) throw new WebCryptoError("webcrypto/data", who + ": an ML-DSA context must be at most 255 bytes (FIPS 204)");
586
412
  return { key: keyHandle, context: ctx };
587
413
  }
588
- // publicExponent arrives as a W3C BigInteger octet string but node:crypto
589
- // takes a JS number, so the value is bounds-checked BEFORE the Number()
590
- // narrowing: an empty buffer has no integer value (BigInt("0x") is a raw
591
- // SyntaxError), and a value above 2^32-1 is outside the interoperable
592
- // WebCrypto exponent range and heads toward Number's exact-integer limit,
593
- // where the narrowing would silently hand node a different exponent than
594
- // the caller requested.
595
414
  function _bufToBigIntNum(exp) {
596
415
  var b = _toBuf(exp, "publicExponent");
597
416
  if (b.length === 0) {
@@ -624,9 +443,6 @@ SubtleCrypto.prototype.sign = async function sign(algorithm, key, data) {
624
443
  return _toArrayBuffer(nodeCrypto.sign(_hashNode(alg.hash, "sign"), buf, { key: key._handle, dsaEncoding: "ieee-p1363" }));
625
444
  }
626
445
  if (ML_DSA_NODE[name]) {
627
- // ML-DSA (FIPS 204) accepts an optional context octet string; thread it through
628
- // signing symmetrically with verify so a context signature round-trips (a
629
- // signature made with a context verifies only under the SAME context).
630
446
  return _toArrayBuffer(nodeCrypto.sign(null, buf, _mldsaKeyArg(alg, key._handle, "sign")));
631
447
  }
632
448
  if (name === "ED25519" || name === "ED448" || SLH_DSA_NODE[name]) {
@@ -661,9 +477,6 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
661
477
  return nodeCrypto.verify(_hashNode(alg.hash, "verify"), buf, { key: key._handle, dsaEncoding: "ieee-p1363" }, sig);
662
478
  }
663
479
  if (ML_DSA_NODE[name]) {
664
- // ML-DSA (FIPS 204) accepts an optional context octet string (the composite
665
- // signature construction passes the composite Domain as the ML-DSA context,
666
- // draft-ietf-lamps-pq-composite-sigs sec. 2). A context mismatch fails.
667
480
  return nodeCrypto.verify(null, buf, _mldsaKeyArg(alg, key._handle, "verify"), sig);
668
481
  }
669
482
  if (name === "ED25519" || name === "ED448" || SLH_DSA_NODE[name]) {
@@ -672,22 +485,12 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
672
485
  if (name === "HMAC") {
673
486
  var hm = nodeCrypto.createHmac(_hashNode(key.algorithm.hash, "verify"), key._handle);
674
487
  hm.update(buf);
675
- // Verify must RESOLVE false for any invalid signature, including one of
676
- // the wrong length. timingSafeEqual throws RangeError on a length
677
- // mismatch, so gate on the (public) length first, then compare the bytes
678
- // in constant time. The length check leaks nothing a constant-time compare
679
- // would protect; the secret-dependent byte comparison is timingSafeEqual.
680
488
  var digest = hm.digest();
681
489
  return guard.crypto.constantTimeEqual(digest, sig);
682
490
  }
683
491
  throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + _stringify(name));
684
492
  };
685
493
 
686
- // Run a node:crypto AES cipher sequence, converting any raw node fault -- a bad IV length,
687
- // a failed GCM authentication tag, bad CBC padding -- into a typed OperationError. A
688
- // decrypt of tampered ciphertext (or a malformed cipher parameter) is a typed webcrypto
689
- // verdict, never a raw Error crossing the public API (input prep that already throws a
690
- // typed error runs OUTSIDE this, so a typed fault is never double-wrapped).
691
494
  function _runCipher(fn, who) {
692
495
  try { return fn(); }
693
496
  catch (e) { throw new WebCryptoError("webcrypto/operation", who + ": AES cipher operation failed (bad key / iv / tag / padding)", e); }
@@ -749,9 +552,6 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
749
552
  if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + _stringify(name));
750
553
  _requireAlgMatch(alg, key, "decrypt");
751
554
  if (name === "RSA-OAEP") {
752
- // For a CMS key-transport recipient this plaintext IS the recovered content-encryption key, and
753
- // _toArrayBuffer copies it -- so the node-allocated buffer is finished with the moment the copy
754
- // exists, and nothing outside this function can reach it to clear it.
755
555
  var oaepOut = nodeCrypto.privateDecrypt({
756
556
  key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
757
557
  oaepHash: _hashNode(key.algorithm.hash, "decrypt"),
@@ -764,8 +564,6 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
764
564
  var tagLen = (alg.tagLength || 128) / 8;
765
565
  var iv = _toBuf(alg.iv, "AES-GCM iv");
766
566
  if (buf.length < tagLen) throw new WebCryptoError("webcrypto/operation", "decrypt: AES-GCM ciphertext is shorter than its authentication tag");
767
- // The split between ciphertext and tag decides which bytes are authenticated, so the window it
768
- // is taken with is the captured one: a replaced `subarray` hands the tag check other bytes.
769
567
  var ct = _subarray(buf, 0, buf.length - tagLen);
770
568
  var tag = _subarray(buf, buf.length - tagLen);
771
569
  var gcmAad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
@@ -774,8 +572,6 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
774
572
  var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", kb, iv, { authTagLength: tagLen });
775
573
  if (gcmAad) d.setAAD(gcmAad);
776
574
  d.setAuthTag(tag);
777
- // The recovered plaintext exists in full before final() judges the tag, so a forged message
778
- // would otherwise abandon a readable copy on the throw path.
779
575
  return guard.secret.cipherFinish(d, ct, WebCryptoError, "webcrypto/operation", "the recovered AES-GCM plaintext");
780
576
  });
781
577
  }, "decrypt"));
@@ -802,28 +598,8 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
802
598
  throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + _stringify(name));
803
599
  };
804
600
 
805
- // _withSecretBytes(key, fn) -- export a secret key's raw material, hand it to fn, and wipe
806
- // the export the moment fn is done with it (NIST SP 800-227 RS5 / sec. 4.2, RFC 9629 sec. 7).
807
- //
808
- // export() allocates a FRESH Buffer this module owns; the CryptoKey keeps its own copy inside
809
- // the node KeyObject, so wiping the export never destroys the caller's key or the key itself.
810
- // Node's cipher and KDF constructors copy the key into their own context, so the export's
811
- // useful life ends when the operation returns.
812
- //
813
- // This is the ONLY way to reach raw secret key material: there is deliberately no unwiped
814
- // `_secretBytes(key)` primitive to call, because a new consumer that forgot the wipe was the
815
- // live defect this replaced -- the AES content-encryption paths and two of the three KDF arms
816
- // each exported the key and left it readable while their sibling arms did not.
817
- //
818
- // When fn returns a PROMISE the wipe rides the settlement rather than the callback's return.
819
- // Node's async primitives copy their inputs when the job is queued, so an eager wipe would not
820
- // corrupt today's derivations -- but that is an implementation detail of the provider, not a
821
- // documented guarantee, and the export's useful life is the operation's, not the callback's.
822
- // Deferring costs nothing and keeps the rule true for any future asynchronous consumer.
823
601
  function _withSecretBytes(key, fn) {
824
602
  var kb = key._handle.export();
825
- // Set once the export's lifetime has been handed to the promise handlers below, so the
826
- // `finally` does not clear a buffer the pending operation is still entitled to.
827
603
  var deferred = false;
828
604
  try {
829
605
  var out = fn(kb);
@@ -838,35 +614,18 @@ function _withSecretBytes(key, fn) {
838
614
  }
839
615
  function _wipeExport(kb) { guard.secret.zeroize(kb, WebCryptoError, "webcrypto/operation", "the exported key material"); }
840
616
 
841
- // AES-CTR: node always treats the full 128-bit block as the counter and
842
- // never reads the spec's `length` (counter-width) parameter. A length < 128
843
- // would silently diverge from the W3C definition, so fail closed -- accept
844
- // only the one value node can honor.
845
617
  function _requireCtrLength128(alg) {
846
618
  if (alg.length !== 128) {
847
619
  throw new WebCryptoError("webcrypto/not-supported", "AES-CTR length must be 128");
848
620
  }
849
621
  }
850
622
 
851
- // W3C deriveBits: the requested length must be a positive multiple of 8 --
852
- // the `length / 8` narrowing in the per-algorithm branches below would
853
- // otherwise silently truncate a fractional byte count.
854
623
  function _requireDeriveLength(length, who) {
855
624
  if (typeof length !== "number" || !_isFinite(length) || length <= 0 || length % 8 !== 0) {
856
625
  throw new WebCryptoError("webcrypto/operation", who + ": length must be a positive multiple of 8 bits");
857
626
  }
858
627
  }
859
628
 
860
- // Raw key-agreement / KDF derivation with NO usage check -- the usage a
861
- // caller must hold differs by entry point (deriveBits requires "deriveBits",
862
- // deriveKey requires "deriveKey"), so each public method checks its own
863
- // usage and then routes the actual derivation through here.
864
- // PBKDF2 on the libuv threadpool (crypto.pbkdf2, NOT pbkdf2Sync): an attacker-controlled iteration count must
865
- // never block the Node event loop (CWE-400 DoS) -- a network peer can request the maximum iterations without
866
- // knowing the secret (e.g. an unauthenticated PBMAC1 message), so the derivation runs off the main thread.
867
- // Concurrency is CAPPED so many high-iteration jobs cannot monopolize the whole worker pool and starve
868
- // unrelated DNS / filesystem / crypto work: at least two pool threads are left free (default pool is four),
869
- // and derivations beyond the cap queue and run as slots free. The cap tracks UV_THREADPOOL_SIZE when raised.
870
629
  var _PBKDF2_MAX_CONCURRENT = _max(1, (_parseInt(process.env.UV_THREADPOOL_SIZE, 10) || 4) - 2);
871
630
  var _pbkdf2InFlight = 0;
872
631
  var _pbkdf2Waiters = [];
@@ -874,14 +633,12 @@ function _pbkdf2Async(pw, salt, iterations, keylen, digest) {
874
633
  return new Promise(function (resolve, reject) {
875
634
  function start() {
876
635
  _pbkdf2InFlight++;
877
- function done(err, derived) { // release the slot + admit the next waiter, THEN settle
636
+ function done(err, derived) {
878
637
  _pbkdf2InFlight--;
879
638
  var next = _pbkdf2Waiters.shift();
880
639
  if (next) next();
881
640
  if (err) reject(err); else resolve(derived);
882
641
  }
883
- // A SYNCHRONOUS argument fault (e.g. iterations 0) never reaches the async callback, so release the slot
884
- // in the catch too -- otherwise a leaked slot would permanently shrink the pool and queue every later job.
885
642
  try { nodeCrypto.pbkdf2(pw, salt, iterations, keylen, digest, done); }
886
643
  catch (e) { done(e); }
887
644
  }
@@ -894,17 +651,10 @@ async function _deriveBitsRaw(alg, key, length) {
894
651
  if (name === "ECDH" || name === "X25519" || name === "X448") {
895
652
  _requireAlgMatch(alg, alg.public, name + " public key");
896
653
  _requireOwnKey(alg.public, "deriveBits public key");
897
- // The provider hands back z (ECDH) / mz (X25519, X448) in a Buffer this module owns. It is
898
- // the raw key-agreement shared secret -- the same class of material as a KEM shared secret --
899
- // so it is wiped once the caller's copy exists, on EVERY exit including the over-request
900
- // throw. _toArrayBuffer copies via ArrayBuffer.slice, so the returned bits are unaffected.
901
654
  var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
902
655
  try {
903
656
  if (length == null) return _toArrayBuffer(secret);
904
657
  _requireDeriveLength(length, name);
905
- // subarray clamps at the end of the secret, so an unchecked over-request
906
- // would silently return fewer bytes than asked. W3C deriveBits: throw an
907
- // OperationError when the requested length cannot be satisfied.
908
658
  if (length / 8 > secret.length) {
909
659
  throw new WebCryptoError("webcrypto/operation", name + ": requested " + length + " bits but the shared secret has " + (secret.length * 8));
910
660
  }
@@ -915,9 +665,6 @@ async function _deriveBitsRaw(alg, key, length) {
915
665
  }
916
666
  if (name === "HKDF") {
917
667
  _requireDeriveLength(length, "HKDF");
918
- // The export is a fresh Buffer this module owns -- for a KEM flow it is the shared secret
919
- // itself. A controllable allocation, not one of the runtime-internal copies the best-effort
920
- // caveat covers, so it is wiped once the derivation has consumed it.
921
668
  return _withSecretBytes(key, function (ikm) {
922
669
  var derived = nodeCrypto.hkdfSync(_hashNode(alg.hash, "HKDF"), ikm, _toBuf(alg.salt, "HKDF salt"), _toBuf(alg.info || _bufferAlloc(0), "HKDF info"), length / 8);
923
670
  return _types.isArrayBuffer(derived) ? derived : _toArrayBuffer(_bufferFrom(derived));
@@ -925,28 +672,18 @@ async function _deriveBitsRaw(alg, key, length) {
925
672
  }
926
673
  if (name === "PBKDF2") {
927
674
  _requireDeriveLength(length, "PBKDF2");
928
- // The derivation is asynchronous; _withSecretBytes defers the wipe to the promise's
929
- // settlement so the export outlives the operation rather than the callback.
930
675
  return _withSecretBytes(key, function (pw) {
931
676
  return _pbkdf2Async(pw, _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"))
932
677
  .then(function (out) {
933
- // _toArrayBuffer COPIES, so the node-allocated derivation buffer is finished with the
934
- // moment the copy exists -- and nothing else can reach it to clear it.
935
678
  try { return _toArrayBuffer(out); }
936
679
  finally { guard.secret.zeroize(out, WebCryptoError, "webcrypto/operation", "the PBKDF2 output"); }
937
680
  });
938
681
  });
939
682
  }
940
683
  if (name === "X963KDF") {
941
- // ANSI-X9.63 / SEC1 sec. 3.6.1 single-step KDF: K = H(Z || INT32(counter) || SharedInfo)
942
- // concatenated over counter = 1, 2, ... The RFC 5753 kari KEK derivation; the base key holds
943
- // the ECDH shared secret Z, alg.info holds the DER ECC-CMS-SharedInfo.
944
684
  _requireDeriveLength(length, "X963KDF");
945
- // The base key here HOLDS the ECDH shared secret Z of an RFC 5753 kari, so this export is
946
- // exactly as sensitive as the raw agreement secret wiped above.
947
685
  return _withSecretBytes(key, function (z) {
948
686
  var derived = _x963Kdf(_hashNode(alg.hash, "X963KDF"), z, _toBuf(alg.info || _bufferAlloc(0), "X963KDF SharedInfo"), length / 8);
949
- // Same as PBKDF2 above: the copy is what the caller receives, so the derived buffer is cleared.
950
687
  try { return _toArrayBuffer(derived); }
951
688
  finally { guard.secret.zeroize(derived, WebCryptoError, "webcrypto/operation", "the X9.63 KDF output"); }
952
689
  });
@@ -954,8 +691,6 @@ async function _deriveBitsRaw(alg, key, length) {
954
691
  throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + _stringify(name));
955
692
  }
956
693
 
957
- // The X9.63 single-step KDF counter loop. Bounded by the 2^32 counter range; every input is a
958
- // public/agreed value, so a plain (non-constant-time) hash concat is correct.
959
694
  function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
960
695
  var blocks = [], counter = 1, got = 0;
961
696
  while (got < lenBytes) {
@@ -964,10 +699,6 @@ function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
964
699
  var h = nodeCrypto.createHash(hashNode).update(z).update(ctr).update(sharedInfo).digest();
965
700
  _push(blocks, h); got += h.length; counter += 1;
966
701
  }
967
- // Return an exact-sized buffer the caller wholly owns, not a VIEW over the joined blocks: a
968
- // caller clearing what it received would otherwise leave the unused tail of the final digest
969
- // block -- derived from the shared secret -- readable behind it. The accumulator and the
970
- // per-block digests are cleared here, where they were allocated.
971
702
  var joined = _bufferConcat(blocks);
972
703
  var exact = _bufferAlloc(lenBytes);
973
704
  joined.copy(exact, 0, 0, lenBytes);
@@ -985,10 +716,6 @@ SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, le
985
716
  };
986
717
 
987
718
  SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages) {
988
- // deriveKey requires the "deriveKey" usage -- NOT "deriveBits". Delegating
989
- // to this.deriveBits would false-reject a key created with ["deriveKey"]
990
- // and fail-open on a ["deriveBits"]-only key, so check the correct usage
991
- // here and route the raw derivation past deriveBits' own usage gate.
992
719
  _requireUsage(baseKey, "deriveKey");
993
720
  var alg = _normalizeAlg(algorithm, "deriveKey");
994
721
  if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveKey: unsupported algorithm " + _stringify(alg.name));
@@ -996,43 +723,20 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
996
723
  var dk = _normalizeAlg(derivedKeyType, "deriveKey");
997
724
  var bits;
998
725
  if (_stringIndexOf(dk.name, "AES") === 0) {
999
- // W3C get-key-length for AES: the derivedKeyType MUST carry a length of
1000
- // 128/192/256. Without this check the derived key would silently take
1001
- // the size of the raw agreement/KDF output (e.g. a 384-bit "AES" key
1002
- // from a P-384 secret) instead of a usable AES size.
1003
726
  if (dk.length !== 128 && dk.length !== 192 && dk.length !== 256) {
1004
727
  throw new WebCryptoError("webcrypto/syntax", "deriveKey: " + dk.name + " length must be 128/192/256");
1005
728
  }
1006
729
  bits = dk.length;
1007
730
  } else if (dk.name === "HMAC") {
1008
- // The same W3C HMAC get-key-length rule as generateKey: an omitted
1009
- // length is the hash's BLOCK size, never a fixed 256.
1010
731
  bits = _hmacLengthBits(dk, "deriveKey");
1011
732
  } else {
1012
- // HKDF / PBKDF2 derived-key types carry no intrinsic size (W3C
1013
- // get-key-length is null): the base derivation decides -- a key
1014
- // agreement yields its full shared secret as the input keying
1015
- // material; a KDF base has no implicit output size and fails closed.
1016
733
  bits = dk.length != null ? dk.length : null;
1017
734
  }
1018
- // The derived bits are the KEY -- importKey copies them into a KeyObject, so this buffer is a
1019
- // transient copy nothing else can reach once the CryptoKey exists, and it is cleared on the
1020
- // failing import too (a bad derivedKeyType is caller-controlled).
1021
735
  var raw = await _deriveBitsRaw(alg, baseKey, bits);
1022
736
  try { return await this.importKey("raw", raw, dk, extractable, keyUsages); }
1023
737
  finally { guard.secret.zeroize(new _Uint8Array(raw), WebCryptoError, "webcrypto/operation", "the derived key material"); }
1024
738
  };
1025
739
 
1026
- // ML-KEM (FIPS 203) key encapsulation over Node's crypto.encapsulate/decapsulate.
1027
- // encapsulateBits takes the recipient's PUBLIC (encapsulation) key and returns a fresh
1028
- // { sharedKey, ciphertext }; decapsulateBits takes the PRIVATE (decapsulation) key + a
1029
- // ciphertext and recovers the shared key. ML-KEM's Fujisaki-Okamoto transform gives IMPLICIT
1030
- // rejection: a tampered but correctly-sized ciphertext decapsulates to a pseudo-random shared
1031
- // key rather than failing, so decapsulateBits only throws (typed) on a malformed / wrong-length
1032
- // ciphertext -- the "wrong key" case is indistinguishable by design, which the CMS uniform
1033
- // decrypt-failure verdict relies on. The usage split (encapsulateBits -> public, decapsulateBits
1034
- // -> private) already lives in the generateKey usage filters; the explicit type check hardens a
1035
- // key imported with custom usages.
1036
740
  SubtleCrypto.prototype.encapsulateBits = async function encapsulateBits(algorithm, encapsulationKey) {
1037
741
  var alg = _normalizeAlg(algorithm, "encapsulateBits");
1038
742
  _requireUsage(encapsulationKey, "encapsulateBits");
@@ -1043,15 +747,8 @@ SubtleCrypto.prototype.encapsulateBits = async function encapsulateBits(algorith
1043
747
  try { r = nodeCrypto.encapsulate(encapsulationKey._handle); }
1044
748
  catch (e) { throw new WebCryptoError("webcrypto/operation", "encapsulateBits: ML-KEM encapsulation failed", e); }
1045
749
  try {
1046
- // _toArrayBuffer copies via ArrayBuffer.slice, so the shared key is passed straight through --
1047
- // an intermediate Buffer.from would be another copy of the secret that nothing wipes. The
1048
- // ciphertext is public and needs no such care, but it is copied the same way for symmetry.
1049
750
  return { sharedKey: _toArrayBuffer(r.sharedKey), ciphertext: _toArrayBuffer(r.ciphertext) };
1050
751
  } finally {
1051
- // Encapsulation produces a shared secret exactly as decapsulation does, so it owes the same
1052
- // duty: the provider's buffer is wiped once the caller's copy exists (NIST SP 800-227 RS5 /
1053
- // sec. 4.2, RFC 9629 sec. 7). Wiping only the decapsulation side would make the guarantee a
1054
- // half-truth -- the sender holds the same secret the recipient does.
1055
752
  guard.secret.zeroize(r.sharedKey, WebCryptoError, "webcrypto/operation", "the KEM shared secret");
1056
753
  }
1057
754
  };
@@ -1063,18 +760,6 @@ SubtleCrypto.prototype.decapsulateBits = async function decapsulateBits(algorith
1063
760
  _requireAlgMatch(alg, decapsulationKey, "decapsulateBits");
1064
761
  if (decapsulationKey.type !== "private") throw new WebCryptoError("webcrypto/invalid-access", "decapsulateBits requires a private (decapsulation) key, got " + _stringify(decapsulationKey.type));
1065
762
  var ct = _toBuf(ciphertext, "decapsulateBits ciphertext");
1066
- // FIPS 203 sec. 7.3 makes the ciphertext-length check the ONE per-execution input check a
1067
- // decapsulating party owes, so it belongs here, at the engine boundary, rather than only in the
1068
- // format module that happens to call this today: a direct caller -- or a future composite-KEM or
1069
- // HPKE consumer -- inherits nothing from a check that lives in cms-decrypt. A distinct code names
1070
- // the real reason, which "the operation failed" cannot.
1071
- //
1072
- // Length ONLY. A correct-length ciphertext that has been tampered with must still resolve to a
1073
- // pseudo-random shared secret (the Fujisaki-Okamoto implicit rejection of FIPS 203 sec. 6.3);
1074
- // turning that into a throw would hand an attacker a decryption oracle, and it is the property
1075
- // the CMS uniform verdict is built on.
1076
- // The registry is keyed by the registered OID name ("id-ml-kem-768"), which is the node
1077
- // algorithm name this module already maps ("ml-kem-768") under its id- prefix.
1078
763
  var kemRow = oid.kemParams("id-" + ML_KEM_NODE[alg.name]);
1079
764
  if (kemRow && ct.length !== kemRow.ct) {
1080
765
  throw new WebCryptoError("webcrypto/bad-kem-ciphertext",
@@ -1084,24 +769,13 @@ SubtleCrypto.prototype.decapsulateBits = async function decapsulateBits(algorith
1084
769
  try { ss = nodeCrypto.decapsulate(decapsulationKey._handle, ct); }
1085
770
  catch (e) { throw new WebCryptoError("webcrypto/operation", "decapsulateBits: ML-KEM decapsulation failed (malformed or wrong-length ciphertext)", e); }
1086
771
  try {
1087
- // ss is already a Buffer, and _toArrayBuffer copies via ArrayBuffer.slice -- so it is passed
1088
- // straight through. An intermediate Buffer.from(ss) would be a THIRD copy of the secret that
1089
- // nothing wipes, which would give back most of what the wipe below is for.
1090
772
  return _toArrayBuffer(ss);
1091
773
  } finally {
1092
- // The shared secret is returned as a COPY, so the buffer the provider handed back would stay
1093
- // readable until collection -- and a caller wiping only what it receives would leave the
1094
- // original behind, which is the whole secret. Wiping here means the engine owns the lifetime of
1095
- // the buffer it allocated, and every caller (CMS today, a composite KEM or HPKE later) inherits
1096
- // it rather than each having to remember (NIST SP 800-227 RS5 / sec. 4.2).
1097
774
  guard.secret.zeroize(ss, WebCryptoError, "webcrypto/operation", "the KEM shared secret");
1098
775
  }
1099
776
  };
1100
777
 
1101
778
  SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey, wrapAlgorithm) {
1102
- // is the PLAINTEXT serialization of the key being wrapped -- the very material the wrap
1103
- // exists to protect. It is this function's allocation and is cleared once the wrap has consumed
1104
- // it, on the delegated branch as well as the AES-KW one.
1105
779
  var exported = await this.exportKey(format, key);
1106
780
  var bytes = (format === "jwk") ? _bufferFrom(_stringify(exported)) : _bufferFrom(exported);
1107
781
  try {
@@ -1110,15 +784,9 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
1110
784
  if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + _stringify(alg.name));
1111
785
  _requireAlgMatch(alg, wrappingKey, "wrapKey");
1112
786
  if (alg.name === "AES-KW") {
1113
- // AES-KW wraps 64-bit blocks (RFC 3394): the serialized key MUST be a multiple of 8
1114
- // bytes and at least 16. A "jwk" serialization (arbitrary-length JSON) or any other
1115
- // non-conforming length is an OperationError -- a typed verdict, never a raw node fault.
1116
787
  if (bytes.length < 16 || bytes.length % 8 !== 0) {
1117
788
  throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW requires the serialized key be a multiple of 8 bytes (>= 16); got " + bytes.length + " -- format " + _stringify(format) + " is not AES-KW-wrappable");
1118
789
  }
1119
- // The mirror of unwrapKey below: in a KEM flow this export is the SENDER's copy of the same
1120
- // key-encryption key, so leaving it unwiped would keep a full copy of the KEK alive for the
1121
- // process lifetime and make the wipes the CMS layer performs pointless in the encrypt direction.
1122
790
  try {
1123
791
  return _withSecretBytes(wrappingKey, function (wkBytes) {
1124
792
  var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", wkBytes, _bufferFrom("A6A6A6A6A6A6A6A6", "hex"));
@@ -1126,44 +794,24 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
1126
794
  });
1127
795
  } catch (e) { throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW key wrap failed", e); }
1128
796
  }
1129
- // Delegate to a content-encryption algorithm (RSA-OAEP / AES-GCM).
1130
797
  var wrapKeyClone = _cloneWithUsage(wrappingKey, "encrypt");
1131
798
  return await this.encrypt(wrapAlgorithm, wrapKeyClone, bytes);
1132
799
  } finally {
1133
- // Clearing also clears the export it came from: for every non-jwk format
1134
- // Buffer.from(<ArrayBuffer>) is a VIEW over that buffer, not a copy, so one wipe covers both.
1135
- // A "jwk" export is the residual -- its key material lives in immutable JavaScript strings that
1136
- // no code can overwrite -- so wrapping a jwk serialization cannot offer this guarantee, and the
1137
- // documentation does not claim it does.
1138
800
  guard.secret.zeroize(bytes, WebCryptoError, "webcrypto/operation", "the exported key being wrapped");
1139
801
  }
1140
802
  };
1141
803
 
1142
804
  SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
1143
805
  var alg = _normalizeAlg(unwrapAlgorithm, "unwrapKey");
1144
- // Enforce the "unwrapKey" usage before EITHER path. The delegated
1145
- // (RSA-OAEP / AES-GCM) branch clones the key with "decrypt" and hands off
1146
- // to this.decrypt, so without this top-level check the else branch would
1147
- // never verify the caller was actually permitted to unwrap (mirrors
1148
- // wrapKey, which checks before both of its paths).
1149
806
  _requireUsage(unwrappingKey, "unwrapKey");
1150
807
  if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "unwrapKey: unsupported algorithm " + _stringify(alg.name));
1151
808
  _requireAlgMatch(alg, unwrappingKey, "unwrapKey");
1152
809
  var bytes;
1153
810
  if (alg.name === "AES-KW") {
1154
811
  var wrapped = _toBuf(wrappedKey, "unwrapKey");
1155
- // The wrapped input is the plaintext plus one 64-bit integrity block: a multiple of 8,
1156
- // at least 24. A wrong length, or a failed integrity check inside final(), is an
1157
- // OperationError -- a typed verdict, never a raw node cipher fault.
1158
812
  if (wrapped.length < 24 || wrapped.length % 8 !== 0) {
1159
813
  throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW wrapped key must be a multiple of 8 bytes (>= 24); got " + wrapped.length);
1160
814
  }
1161
- // The exported wrapping key is a Buffer this module owns; in a KEM flow it is the KEK derived
1162
- // from the shared secret, so it is wiped once the unwrap has consumed it -- on the failing path
1163
- // too, which is the one an attacker induces by tampering with the wrapped key.
1164
- // The export happens INSIDE the try: a key whose handle cannot be exported must still surface
1165
- // the typed verdict this branch promises, not a raw node TypeError -- and a non-PkiError throw
1166
- // would also break the fuzz-harness contract.
1167
815
  try {
1168
816
  bytes = _withSecretBytes(unwrappingKey, function (kwBytes) {
1169
817
  var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", kwBytes, _bufferFrom("A6A6A6A6A6A6A6A6", "hex"));
@@ -1176,11 +824,6 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
1176
824
  }
1177
825
  var keyData;
1178
826
  if (format === "jwk") {
1179
- // A NON-authenticating unwrap algorithm (AES-CBC / AES-CTR) decrypts
1180
- // tampered bytes "successfully", so this parse is the first point that can
1181
- // notice. The shared JSON guard parses strictly (bounded, fatal UTF-8, and a
1182
- // smuggled duplicate member rejected rather than resolved last-wins), and its
1183
- // failure surfaces as the module's typed webcrypto/data (W3C: DataError).
1184
827
  keyData = guard.json.parse(bytes, _wcErr, {
1185
828
  maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
1186
829
  badJson: "webcrypto/data", tooDeep: "webcrypto/data", duplicateMember: "webcrypto/data",
@@ -1192,9 +835,6 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
1192
835
  try {
1193
836
  return await this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
1194
837
  } finally {
1195
- // `bytes` is the UNWRAPPED key in plaintext -- a module-owned buffer, and the last plaintext
1196
- // copy this layer controls once importKey has taken its own. Clearing the caller-visible copy
1197
- // downstream while leaving this one live would make that wipe ceremonial.
1198
838
  guard.secret.zeroize(bytes, WebCryptoError, "webcrypto/operation", "the unwrapped key material");
1199
839
  }
1200
840
  };
@@ -1204,10 +844,6 @@ function _cloneWithUsage(key, usage) {
1204
844
  return k;
1205
845
  }
1206
846
 
1207
- // A raw or JWK-oct AES key MUST be 128/192/256 bits: W3C WebCrypto importKey rejects a
1208
- // non-conforming length as a DataError AT IMPORT, never a CryptoKey that only faults at
1209
- // first use (where node would build an "aes-160-gcm" cipher that fails only then). HMAC /
1210
- // HKDF / PBKDF2 keys carry no length restriction, so the check is scoped to the AES names.
1211
847
  function _assertAesImportLen(name, byteLen) {
1212
848
  if ((name === "AES-GCM" || name === "AES-CBC" || name === "AES-CTR" || name === "AES-KW") &&
1213
849
  byteLen !== 16 && byteLen !== 24 && byteLen !== 32) {
@@ -1215,25 +851,14 @@ function _assertAesImportLen(name, byteLen) {
1215
851
  }
1216
852
  }
1217
853
 
1218
- // Run a node:crypto key-import call, mapping a raw engine error (a malformed DER, an
1219
- // inconsistent key) to a typed webcrypto/data DataError -- importKey over hostile bytes
1220
- // must never leak a bare OpenSSL/Node exception through the public API.
1221
854
  function _nodeKey(fn, who) {
1222
855
  try { return fn(); }
1223
856
  catch (e) {
1224
- // Coverage residual -- every current thunk calls node:crypto (which throws a node error),
1225
- // never a WebCryptoError, so this already-typed passthrough is defensive depth for a future
1226
- // thunk that pre-validates and throws typed; it prevents double-wrapping a typed error.
1227
857
  if (e instanceof WebCryptoError) throw e;
1228
858
  throw new WebCryptoError("webcrypto/data", who + ": the key material is malformed or internally inconsistent", e);
1229
859
  }
1230
860
  }
1231
861
 
1232
- // RFC 9935 sec. 6 ML-KEM-*-PrivateKey CHOICE: the inner sizes, keyed by the OID -- the OID is
1233
- // the SOLE authority for the parameter set (never a length heuristic). ek = 384k+32, dk = the
1234
- // FIPS 203 decapsulation key length.
1235
- // {ek, dk} come from the shared ML-KEM parameter registry (FIPS 203 Table 3) -- the same rows the
1236
- // CMS codec and the linter read, so a parameter set cannot mean one size here and another there.
1237
862
  var ML_KEM_INNER = {};
1238
863
  ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) {
1239
864
  var row = oid.kemParams(n);
@@ -1244,12 +869,6 @@ function _isOctet(node, size) {
1244
869
  return node && node.tagClass === "universal" && node.tagNumber === asn1.TAGS.OCTET_STRING &&
1245
870
  !node.constructed && node.content && node.content.length === size;
1246
871
  }
1247
- // Validate the RFC 9935 sec. 6 inner CHOICE structurally, dispatching on the DER tag (0x80 seed
1248
- // [0] / 0x04 expandedKey / 0x30 both) -- NEVER on the length of the enclosing OCTET STRING. A
1249
- // bare seed, a bare dk, an oqskeypair dk||ek, a constructed [0], or a wrong-size arm is rejected
1250
- // fail-closed (guards never guess a plausible layout). The seed/expandedKey CRYPTOGRAPHIC
1251
- // consistency (sec. 8 / FIPS 203 sec. 7.3) is enforced by the engine on import (a mismatch is a
1252
- // raw node error _nodeKey maps to webcrypto/data).
1253
872
  function _validateMlKemInner(innerBytes, sizes) {
1254
873
  var node;
1255
874
  try { node = asn1.decode(innerBytes); }
@@ -1263,8 +882,6 @@ function _validateMlKemInner(innerBytes, sizes) {
1263
882
  throw new WebCryptoError("webcrypto/data", "an ML-KEM expandedKey must be exactly " + sizes.dk + " octets for this parameter set (RFC 9935 sec. 6)");
1264
883
  }
1265
884
  if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.SEQUENCE) {
1266
- // Coverage residual -- a decoded universal SEQUENCE always carries a children array, so the
1267
- // `|| []` fallback is defensive (asn1.decode never yields a childless constructed node).
1268
885
  var kids = node.children || [];
1269
886
  if (kids.length !== 2 || !_isOctet(kids[0], 64) || !_isOctet(kids[1], sizes.dk)) {
1270
887
  throw new WebCryptoError("webcrypto/data", "an ML-KEM both-arm must be SEQUENCE { seed OCTET STRING(64), expandedKey OCTET STRING(" + sizes.dk + ") } (RFC 9935 sec. 6)");
@@ -1274,15 +891,11 @@ function _validateMlKemInner(innerBytes, sizes) {
1274
891
  throw new WebCryptoError("webcrypto/data", "an ML-KEM private key must be the seed, expandedKey, or both CHOICE (RFC 9935 sec. 6)");
1275
892
  }
1276
893
 
1277
- // PKCS#8 inner-key pre-validation, keyed by the privateKeyAlgorithm OID (a registry row, not a
1278
- // switch). The parse surfaces the inner key as opaque octets by design; the fail-closed
1279
- // per-algorithm structural check lives here, at the one import boundary. Only ML-KEM needs it
1280
- // today: the engine ACCEPTS the OpenSSL-legacy bare-seed layout RFC 9935 sec. 6 forbids.
1281
894
  var PKCS8_INNER_VALIDATORS = {};
1282
895
  _forEach(_keys(ML_KEM_INNER), function (o) { PKCS8_INNER_VALIDATORS[o] = function (inner) { _validateMlKemInner(inner, ML_KEM_INNER[o]); }; });
1283
896
 
1284
897
  function _preValidatePkcs8(p8, name) {
1285
- if (!ML_KEM_NODE[name]) return; // only the ML-KEM CHOICE carries the bare-seed hazard
898
+ if (!ML_KEM_NODE[name]) return;
1286
899
  var algOid, innerBytes;
1287
900
  try {
1288
901
  var root = asn1.decode(p8);
@@ -1299,15 +912,11 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
1299
912
  var usages = keyUsages || [];
1300
913
  var name = alg.name;
1301
914
 
1302
- // spki / pkcs8 carry an asymmetric key; a secret-key / KDF name has no such structure. Reject
1303
- // it up front (W3C NotSupportedError) so it can neither mint a mislabeled key nor bypass the
1304
- // per-algorithm pkcs8 pre-validation below.
1305
915
  if ((format === "spki" || format === "pkcs8") && SECRET_KEY_NAMES[name]) {
1306
916
  throw new WebCryptoError("webcrypto/not-supported", "importKey: " + name + " does not support the " + format + " key format");
1307
917
  }
1308
918
 
1309
919
  if (format === "raw") {
1310
- // Symmetric raw material, or a raw public key for EC/OKP.
1311
920
  if (SECRET_KEY_NAMES[name]) {
1312
921
  var raw = _toBuf(keyData, "importKey raw");
1313
922
  _assertAesImportLen(name, raw.length);
@@ -1315,7 +924,6 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
1315
924
  var symAlg = (name === "HMAC") ? { name: name, hash: _hashObj(alg.hash, "importKey raw HMAC"), length: raw.length * 8 } : { name: name, length: raw.length * 8 };
1316
925
  return new CryptoKey("secret", extractable, symAlg, usages, secret);
1317
926
  }
1318
- // Raw public keys are imported via JWK reconstruction below.
1319
927
  return _importRawPublic(name, alg, _toBuf(keyData, "importKey raw"), extractable, usages);
1320
928
  }
1321
929
 
@@ -1332,9 +940,6 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
1332
940
  }
1333
941
  if (format === "jwk") {
1334
942
  var jwk = keyData;
1335
- // A JWK is a JSON object; null / undefined / an array / a primitive (incl. the JSON token
1336
- // `null` from an unwrapKey over non-authenticating ciphertext) fails closed as a typed
1337
- // DataError, never a raw property-access TypeError.
1338
943
  if (jwk === null || typeof jwk !== "object" || _isArray(jwk)) {
1339
944
  throw new WebCryptoError("webcrypto/data", "importKey jwk: keyData must be a JsonWebKey object");
1340
945
  }
@@ -1345,15 +950,6 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
1345
950
  var a2 = (name === "HMAC") ? { name: name, hash: _hashObj(alg.hash, "importKey jwk HMAC"), length: kbuf.length * 8 } : { name: name, length: kbuf.length * 8 };
1346
951
  return new CryptoKey("secret", extractable, a2, usages, s2);
1347
952
  }
1348
- // The private half is named by the KEY TYPE, not by one spelling. EC and OKP carry it in `d`;
1349
- // an AKP JWK -- how ML-DSA, ML-KEM and SLH-DSA are represented -- carries it in `priv`.
1350
- // Testing `d` alone reads every PQC private JWK as public, so a re-import yields a public key
1351
- // that still announces `usages: ["sign"]` and forces `extractable` true whatever the caller
1352
- // asked, silently dropping the half that signs.
1353
- // `priv` is read ONLY for an AKP key, the type that defines it. RFC 7517 sec. 4 requires an
1354
- // unrecognized member to be ignored, so a member of that name on an EC or OKP JWK is an
1355
- // extension this implementation has no meaning for -- reading it as private material there
1356
- // would turn a valid public-key import into a failure.
1357
953
  var isPrivate = _hasOwn(jwk, "d") ||
1358
954
  (jwk.kty === "AKP" && _hasOwn(jwk, "priv"));
1359
955
  var ko = _nodeKey(function () { return isPrivate ? nodeCrypto.createPrivateKey({ key: jwk, format: "jwk" }) : nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }, "importKey jwk");
@@ -1366,8 +962,6 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
1366
962
  if (name === "ED25519" || name === "ED448" || name === "X25519" || name === "X448") {
1367
963
  var jwk = { kty: "OKP", crv: (name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : name === "X25519" ? "X25519" : "X448"), x: _bufToB64url(raw) };
1368
964
  var ko = nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
1369
- // The canonical mixed-case EdDSA labels, matching _algFromImport -- a raw
1370
- // import must not label the same key differently than an spki/jwk one.
1371
965
  return new CryptoKey("public", true, { name: _stdName(name) }, usages, ko);
1372
966
  }
1373
967
  if (name === "ECDSA" || name === "ECDH") {
@@ -1380,19 +974,12 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
1380
974
  throw new WebCryptoError("webcrypto/not-supported", "importKey raw: unsupported public-key algorithm " + _stringify(name));
1381
975
  }
1382
976
 
1383
- // The node asymmetricKeyType(s) each imported asymmetric algorithm requires. The key TYPE
1384
- // is a property of the key material, not the caller's claim: an RSA key imported under
1385
- // {name:"Ed25519"} must be a DataError, not a CryptoKey mislabeled Ed25519 that then signs
1386
- // under the wrong scheme (algorithm confusion). EC is additionally curve-validated below.
1387
977
  var IMPORT_KEY_TYPE = {
1388
978
  "RSASSA-PKCS1-V1_5": { rsa: 1 }, "RSA-PSS": { rsa: 1, "rsa-pss": 1 }, "RSA-OAEP": { rsa: 1 },
1389
979
  "ECDSA": { ec: 1 }, "ECDH": { ec: 1 },
1390
980
  "ED25519": { ed25519: 1 }, "ED448": { ed448: 1 },
1391
981
  "X25519": { x25519: 1 }, "X448": { x448: 1 },
1392
982
  };
1393
- // The FIPS 203/204/205 PQC families import the same key-is-authority rule: each WebCrypto
1394
- // algorithm name maps to exactly its node asymmetricKeyType, so an RSA (or any) key imported
1395
- // under an ML-DSA / ML-KEM / SLH-DSA name is a DataError, not a mislabeled PQC CryptoKey.
1396
983
  [ML_DSA_NODE, ML_KEM_NODE, SLH_DSA_NODE].forEach(function (m) {
1397
984
  _forEach(_keys(m), function (webName) { var t = {}; t[m[webName]] = 1; IMPORT_KEY_TYPE[webName] = t; });
1398
985
  });
@@ -1403,12 +990,6 @@ function _algFromImport(name, alg, keyObject) {
1403
990
  " key, which is not compatible with algorithm " + name);
1404
991
  }
1405
992
  if (name === "ECDSA" || name === "ECDH") {
1406
- // W3C WebCrypto EC import -- the curve is a property of the KEY, not the
1407
- // caller's claim. Derive it from the imported key material; reject an
1408
- // unsupported curve (NotSupportedError, matching generateKey) and reject a
1409
- // requested namedCurve that disagrees with the key (DataError). Trusting
1410
- // alg.namedCurve would mislabel the CryptoKey (algorithm confusion) and let
1411
- // a non-approved curve import as an approved one.
1412
993
  var actualCurve = _curveFromKey(keyObject);
1413
994
  if (!actualCurve) {
1414
995
  throw new WebCryptoError("webcrypto/not-supported", name + ": imported key uses an unsupported EC curve");
@@ -1429,7 +1010,7 @@ function _curveFromKey(ko) {
1429
1010
  var jwk = ko.export({ format: "jwk" });
1430
1011
  for (var k in CURVE_NODE) { if (jwk.crv === k) return k; }
1431
1012
  }
1432
- catch (_e) { /* best-effort */ }
1013
+ catch (_e) { }
1433
1014
  return undefined;
1434
1015
  }
1435
1016
 
@@ -1465,9 +1046,6 @@ SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
1465
1046
  if (!key.extractable) throw new WebCryptoError("webcrypto/invalid-access", "key is not extractable");
1466
1047
  if (format === "jwk") return key._handle.export({ format: "jwk" });
1467
1048
  if (key.type === "secret") {
1468
- // The export is a fresh Buffer holding the secret key, and _toArrayBuffer copies it via
1469
- // ArrayBuffer.slice -- so the export is a controllable allocation nothing references once the
1470
- // copy exists. The unsupported-format throw below clears it too.
1471
1049
  return _withSecretBytes(key, function (raw) {
1472
1050
  if (format === "raw") return _toArrayBuffer(raw);
1473
1051
  throw new WebCryptoError("webcrypto/not-supported", "exportKey: secret keys support 'raw' / 'jwk' only");
@@ -1476,11 +1054,6 @@ SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
1476
1054
  if (format === "spki") return _toArrayBuffer(key._handle.export({ format: "der", type: "spki" }));
1477
1055
  if (format === "pkcs8") return _toArrayBuffer(key._handle.export({ format: "der", type: "pkcs8" }));
1478
1056
  if (format === "raw") {
1479
- // "raw" is defined for PUBLIC and secret keys; there is no raw private-key serialization for
1480
- // EC or OKP. Answering a private-key request with the public half hands back the opposite of
1481
- // what was asked for, with nothing to notice it by -- and `wrapKey` forwards the caller's
1482
- // format straight here, so a private key wrapped as "raw" escrows the PUBLIC key. Unwrapping
1483
- // that yields a handle announcing it can sign, which cannot, and the private key is gone.
1484
1057
  if (key.type !== "public") {
1485
1058
  throw new WebCryptoError("webcrypto/not-supported",
1486
1059
  "exportKey: 'raw' is defined for public and secret keys only -- a " + key.type +
@@ -1498,7 +1071,6 @@ function _rawPublic(key) {
1498
1071
  throw new WebCryptoError("webcrypto/not-supported", "exportKey raw: unsupported key type " + jwk.kty);
1499
1072
  }
1500
1073
 
1501
- // ---- Crypto ----------------------------------------------------------
1502
1074
 
1503
1075
  /**
1504
1076
  * @primitive pki.webcrypto
@@ -1520,25 +1092,10 @@ function Crypto() {
1520
1092
  }
1521
1093
 
1522
1094
  Crypto.prototype.getRandomValues = function getRandomValues(typedArray) {
1523
- // The kinds W3C WebCrypto refuses here are settled from the value's internal slot, not from an
1524
- // `instanceof` against a replaceable global: replace `Float32Array` after load and the comparison
1525
- // is against a different constructor, so a real Float32Array passes a check written to reject it.
1526
1095
  if (!_isView(typedArray) || _types.isFloat32Array(typedArray) || _types.isFloat64Array(typedArray) || _types.isDataView(typedArray)) {
1527
1096
  throw new WebCryptoError("webcrypto/data", "getRandomValues: expected an integer TypedArray");
1528
1097
  }
1529
- // Re-view the (already integer-TypedArray-validated) output buffer through the OUTPUT door --
1530
- // any TypedArray, not just Uint8Array -- so a detached backing store fails closed here; the
1531
- // returned Buffer aliases typedArray, so randomFillSync writes through to the caller's array.
1532
- //
1533
- // The output door, because the input one refuses shared memory: bytes another thread can rewrite
1534
- // after they have been checked cannot be checked. That answers a question about input. Here the
1535
- // caller is asking for random bytes in memory they chose, and a `SharedArrayBuffer`-backed view
1536
- // is a supported W3C argument that the platform fills; refusing it made a working call an error.
1537
1098
  var out = guard.bytes.outputView(typedArray, WebCryptoError, "webcrypto/data", "getRandomValues");
1538
- // The ceiling is measured on that view, and never on `typedArray.byteLength`. `byteLength` is an
1539
- // accessor on the shared typed-array prototype, so a caller's subclass answers it: one reporting
1540
- // 0 passed the ceiling while the fill below wrote the array's real length, which is the bound
1541
- // being bypassed rather than enforced. The view is built from the internal slot.
1542
1099
  if (out.length > MAX_RANDOM_BYTES) {
1543
1100
  throw new WebCryptoError("webcrypto/data", "getRandomValues: byteLength exceeds " + MAX_RANDOM_BYTES);
1544
1101
  }
@@ -1548,12 +1105,6 @@ Crypto.prototype.getRandomValues = function getRandomValues(typedArray) {
1548
1105
 
1549
1106
  Crypto.prototype.randomUUID = function randomUUID() { return nodeCrypto.randomUUID(); };
1550
1107
 
1551
- // decompressEcPoint(sec1Compressed, nodeCurve) -> the uncompressed SEC1 point 0x04||X||Y for a
1552
- // compressed 0x02/0x03||X input, computing Y on-curve via node's ECDH.convertKey. The crypto-engine
1553
- // home for EC point de-compression (Hard rule #8: EC key material is webcrypto's domain). Fail-closed:
1554
- // convertKey throws on an off-curve X or an unsupported curve, surfaced as the caller's typed error.
1555
- // `nodeCurve` is the OpenSSL curve name (prime256v1 / secp384r1 / secp521r1). The C509-specific
1556
- // 0xFE/0xFD marker -> 0x02/0x03 parity translation stays in the C509 layer (this takes a real SEC1 point).
1557
1108
  function decompressEcPoint(sec1Compressed, nodeCurve, E, code) {
1558
1109
  var head = sec1Compressed[0];
1559
1110
  if (head !== 0x02 && head !== 0x03) throw E(code, "EC point de-compression expects a compressed SEC1 point (0x02/0x03)");
@@ -1564,21 +1115,10 @@ function decompressEcPoint(sec1Compressed, nodeCurve, E, code) {
1564
1115
  }
1565
1116
  }
1566
1117
 
1567
- // A CryptoKey from a DIFFERENT WebCrypto implementation -- node:crypto's global `webcrypto`, a
1568
- // browser's, or a userland polyfill's -- is indistinguishable from one of this engine's by `type`,
1569
- // `algorithm` and `usages`, yet carries none of the key material this engine holds. Handing one to
1570
- // this engine raises a bare error from inside the crypto library, so a caller who followed the
1571
- // documented "pass a CryptoKey" contract is told the argument is the wrong type rather than which
1572
- // implementation it came from. Neither engine can export the other's keys, so a foreign key is
1573
- // exported through its OWN implementation and re-imported here. Both take the CALLER's typed error
1574
- // factory + code so every boundary keeps its own domain/reason.
1575
1118
  var _crypto = new Crypto();
1576
- // Null-prototype: is caller-supplied, and an inherited member ("constructor", "toString")
1577
- // would otherwise resolve to a truthy non-format and slip past the check below.
1578
1119
  var _EXPORT_FORMAT = _assign(_create(null), { "private": "pkcs8", "public": "spki", "secret": "raw" });
1579
1120
 
1580
1121
  // @internal
1581
- // True for any object shaped like a WebCrypto CryptoKey, whichever implementation minted it.
1582
1122
  function isCryptoKeyLike(x) {
1583
1123
  return !!x && typeof x === "object" && typeof x.type === "string" &&
1584
1124
  typeof x.extractable === "boolean" && !!x.algorithm && typeof x.algorithm === "object" &&
@@ -1586,10 +1126,6 @@ function isCryptoKeyLike(x) {
1586
1126
  }
1587
1127
 
1588
1128
  // @internal
1589
- // Export any CryptoKey's material through whichever implementation owns it, in the format its
1590
- // `type` implies. A key this engine minted goes through this engine; a foreign one through
1591
- // node:crypto's. A foreign key that is not extractable cannot be reached at all -- that is a
1592
- // permanent verdict, and it is reported as itself rather than as an export failure.
1593
1129
  function exportAnyKey(key, E, code) {
1594
1130
  var format = _EXPORT_FORMAT[key && key.type];
1595
1131
  if (!format) throw E(code, "a CryptoKey with a private, public, or secret type is required");
@@ -1601,11 +1137,6 @@ function exportAnyKey(key, E, code) {
1601
1137
  if (!key.extractable) {
1602
1138
  throw E(code, "the CryptoKey comes from a different WebCrypto implementation and is not extractable, so its key material cannot be reached; import it through pki.webcrypto.subtle, or pass the key as DER");
1603
1139
  }
1604
- // A separately-installed copy of this engine is the same code under a different class identity, so
1605
- // its keys fail the `instanceof` above while carrying the very handle this engine reads -- and no
1606
- // WebCrypto implementation but that copy would accept them. Read the handle directly. The
1607
- // extractable check above stays AHEAD of this: the handle can always be read, so honoring the
1608
- // key's own extractable promise is this branch's responsibility, not the crypto library's.
1609
1140
  var handle = key._handle;
1610
1141
  if (handle instanceof nodeCrypto.KeyObject && handle.type === key.type) {
1611
1142
  return _resolve(Promise).then(function () {
@@ -1614,9 +1145,6 @@ function exportAnyKey(key, E, code) {
1614
1145
  throw E(code, "the CryptoKey comes from a different copy of this WebCrypto engine and its key material could not be read; import it through pki.webcrypto.subtle, or pass the key as DER", e);
1615
1146
  });
1616
1147
  }
1617
- // Otherwise the only remaining implementation whose keys are reachable from this process is the
1618
- // platform's own. A third party's -- a browser's, a userland polyfill's -- keeps its material
1619
- // behind its own SubtleCrypto, which nothing here holds a reference to, so it is refused below.
1620
1148
  return _resolve(Promise)
1621
1149
  .then(function () { return nodeCrypto.webcrypto.subtle.exportKey(format, key); })
1622
1150
  .then(function (b) { return _bufferFrom(b); }, function (e) {
@@ -1625,16 +1153,8 @@ function exportAnyKey(key, E, code) {
1625
1153
  }
1626
1154
 
1627
1155
  // @internal
1628
- // A CryptoKey this engine can use. One it minted passes through untouched -- so a non-extractable
1629
- // key of its own keeps working -- and a foreign one is re-imported. `importParams` may be null, in
1630
- // which case the key's own `algorithm` is used, which is what the key actually is.
1631
1156
  function adoptKey(key, importParams, usages, E, code) {
1632
1157
  if (key instanceof CryptoKey) return _resolve(Promise, key);
1633
- // A key's usages are a capability restriction it carries, and re-importing it is the one moment
1634
- // that restriction could be widened -- the new key is created with the usages the CALLER wants,
1635
- // not the ones the original was created with. Require every one of them up front, so an adopted
1636
- // key can do no more than it could where it came from, and a verify-only key is refused here
1637
- // exactly as this engine's own verify-only key is refused by _requireUsage.
1638
1158
  if (!_isArray(key.usages)) throw E(code, "a CryptoKey carrying its permitted usages is required");
1639
1159
  for (var i = 0; i < usages.length; i++) {
1640
1160
  if (_indexOf(key.usages, usages[i]) === -1) {