@blamejs/pki 0.5.18 → 0.5.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/lib/guard-intrinsic.js +27 -0
- package/lib/jose.js +55 -35
- package/lib/key.js +6 -0
- package/lib/webcrypto.js +165 -90
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.5.19 — 2026-08-20
|
|
8
|
+
|
|
9
|
+
The crypto engine and the JWS signer decide with operations captured at load, so code that runs after the module does cannot change what a permission check concludes.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `pki.webcrypto` and `pki.jose` are held to the live-read rule the guard family already follows: the check over `lib/` that refuses reading an operation from the runtime at call time covers every module that takes the captures, and these two now do. Operator-visible behavior is unchanged except where a refusal that could be bypassed now holds.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- The WebCrypto key-usage check holds against a replaced `Array.prototype.indexOf`. The membership test IS the permission decision, so it runs through the captured method: dispatched live, a key permitted only for `verify` passed the engine's gate for `sign` and failed later, in the platform's own crypto, with an untyped fault instead of `webcrypto/invalid-access`.
|
|
18
|
+
- A CryptoKey's usage list is a copy this engine took with a captured `slice`, so a replaced one cannot leave the caller holding the array the key is checked against for the rest of its life. The same applies to the partition that decides which usages each half of a generated pair is minted with.
|
|
19
|
+
- `pki.key.import`, `pki.key.generate` and `subtle.generateKey` refuse a non-array `usages` by name. It reached the key constructor, where `Array.prototype.slice` on a number yields an empty list, so the call minted a key permitted for nothing rather than naming the argument that was wrong. The asymmetric branch of `generateKey` partitions the usages before either half is constructed and `filter` treats a non-list the same way, so it is checked at that verb's own door and the whole pair is refused.
|
|
20
|
+
- The AES-GCM ciphertext and authentication tag are split with a captured `subarray`, so a replaced one cannot hand the tag check a different range of the same buffer.
|
|
21
|
+
- A JWK thumbprint (RFC 7638 sec. 3.3) is built with a captured serializer. The canonical JSON is the thumbprint's preimage, so a replaced `JSON.stringify` changes the identity every consumer compares keys by.
|
|
22
|
+
- The JOSE header checks -- own-name reads, the `crit` list walk, the embedded-JWK shape test -- and the protected-header serialization all run on captured operations, so a caller who reaches the header through an accessor cannot decide which of them apply.
|
|
23
|
+
- A digest runs under the algorithm the caller named. The hash name is case-folded to pick its registry row, and dispatched live a replaced `String.prototype.toUpperCase` answering `SHA-1` gave a caller who asked for SHA-256 a 20-byte digest. The same fold picks the HMAC block size and decides whether a key's algorithm matches the operation's.
|
|
24
|
+
- `getRandomValues` settles the kinds WebCrypto refuses from the value's internal slot rather than by `instanceof` against a replaceable global, so a real `Float32Array` cannot pass a check written to reject it by swapping the constructor the comparison names.
|
|
25
|
+
|
|
7
26
|
## v0.5.18 — 2026-08-20
|
|
8
27
|
|
|
9
28
|
Every producing verb refuses an authoring field it does not read, so a misspelling is a refusal at the call instead of a signed artifact that quietly omits what was asked for.
|
package/lib/guard-intrinsic.js
CHANGED
|
@@ -84,6 +84,10 @@ var _jsonStringify = JSON.stringify;
|
|
|
84
84
|
var _mathFloor = Math.floor;
|
|
85
85
|
var _mathCeil = Math.ceil;
|
|
86
86
|
var _mathMin = Math.min;
|
|
87
|
+
var _mathMax = Math.max;
|
|
88
|
+
// A numeric parse a bound is then compared against: one answering a larger value widens the bound
|
|
89
|
+
// the comparison was written to enforce.
|
|
90
|
+
var _parseInt = parseInt;
|
|
87
91
|
var _promiseResolve = Promise.resolve;
|
|
88
92
|
var _promiseReject = Promise.reject;
|
|
89
93
|
|
|
@@ -102,6 +106,19 @@ var _arrSort = uncurry(Array.prototype.sort);
|
|
|
102
106
|
var _arrConcat = uncurry(Array.prototype.concat);
|
|
103
107
|
var _arrSlice = uncurry(Array.prototype.slice);
|
|
104
108
|
var _arrJoin = uncurry(Array.prototype.join);
|
|
109
|
+
var _arrPush = uncurry(Array.prototype.push);
|
|
110
|
+
|
|
111
|
+
// The string and byte-view operations a decision is then made on. A `toUpperCase` or `toLowerCase`
|
|
112
|
+
// replaced after load changes which registry row an algorithm name resolves to; a `subarray`
|
|
113
|
+
// returning a different window hands the next step bytes from somewhere else in the same buffer;
|
|
114
|
+
// and a radix conversion is how a key's own numbers are read back before they are compared.
|
|
115
|
+
var _strToUpperCase = uncurry(String.prototype.toUpperCase);
|
|
116
|
+
var _strToLowerCase = uncurry(String.prototype.toLowerCase);
|
|
117
|
+
var _strIndexOf = uncurry(String.prototype.indexOf);
|
|
118
|
+
var _taSubarray = uncurry(Uint8Array.prototype.subarray);
|
|
119
|
+
var _abSlice = uncurry(ArrayBuffer.prototype.slice);
|
|
120
|
+
var _numToString = uncurry(Number.prototype.toString);
|
|
121
|
+
var _bigIntToString = uncurry(BigInt.prototype.toString);
|
|
105
122
|
|
|
106
123
|
// The collection operations, uncurried. A guard's cycle-safe chain walk is a `Set` it owns, and
|
|
107
124
|
// `has` is what stops it: one answering `true` ends the walk at the first prototype, so a scan that
|
|
@@ -270,6 +287,16 @@ module.exports = {
|
|
|
270
287
|
floor: _mathFloor,
|
|
271
288
|
ceil: _mathCeil,
|
|
272
289
|
min: _mathMin,
|
|
290
|
+
max: _mathMax,
|
|
291
|
+
parseInt: _parseInt,
|
|
292
|
+
push: _arrPush,
|
|
293
|
+
toUpperCase: _strToUpperCase,
|
|
294
|
+
toLowerCase: _strToLowerCase,
|
|
295
|
+
stringIndexOf: _strIndexOf,
|
|
296
|
+
subarray: _taSubarray,
|
|
297
|
+
arrayBufferSlice: _abSlice,
|
|
298
|
+
numberToString: _numToString,
|
|
299
|
+
bigIntToString: _bigIntToString,
|
|
273
300
|
promiseResolve: _promiseResolve,
|
|
274
301
|
promiseReject: _promiseReject,
|
|
275
302
|
forEach: _arrForEach,
|
package/lib/jose.js
CHANGED
|
@@ -42,6 +42,24 @@ var webcrypto = wcEngine.webcrypto;
|
|
|
42
42
|
var frameworkError = require("./framework-error");
|
|
43
43
|
var guard = require("./guard-all");
|
|
44
44
|
var edwardsPoint = require("./edwards-point");
|
|
45
|
+
// What this module reads from the runtime while it is deciding something, taken at load. A JWS is
|
|
46
|
+
// signed and verified against a header the caller owns, and the reads below are the ones that decide
|
|
47
|
+
// what that header IS: whether a value is a list, whether a name is the caller's own, which
|
|
48
|
+
// conversion turns a claim into the string compared against a profile. A caller who receives control
|
|
49
|
+
// afterwards -- through an accessor on the header, or a `toJSON` while it is being serialized -- can
|
|
50
|
+
// replace any of them, and every check around them still runs and still passes. See guard-intrinsic.
|
|
51
|
+
var intrinsic = require("./guard-intrinsic");
|
|
52
|
+
var _isArray = intrinsic.isArray;
|
|
53
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
54
|
+
var _keys = intrinsic.keys;
|
|
55
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
56
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
57
|
+
var _stringify = intrinsic.stringify;
|
|
58
|
+
var _ObjectProto = intrinsic.ObjectProto;
|
|
59
|
+
var _push = intrinsic.uncurry(Array.prototype.push);
|
|
60
|
+
var _join = intrinsic.join;
|
|
61
|
+
var _map = intrinsic.map;
|
|
62
|
+
var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
45
63
|
|
|
46
64
|
var JoseError = frameworkError.JoseError;
|
|
47
65
|
function E(code, message, cause) { return new JoseError(code, message, cause); }
|
|
@@ -65,11 +83,11 @@ var LIMITS = constants.LIMITS;
|
|
|
65
83
|
* pki.jose.base64url.encode(Buffer.from([1, 2, 3])); // -> "AQID"
|
|
66
84
|
*/
|
|
67
85
|
function b64uEncode(bytes) {
|
|
68
|
-
if (!
|
|
86
|
+
if (!_isBuffer(bytes)) throw E("jose/bad-input", "base64url.encode requires a Buffer");
|
|
69
87
|
// Re-view through the byte guard: a detached-backed Buffer reads as zero-length,
|
|
70
88
|
// so a bare .toString would silently encode "" -- fail closed instead.
|
|
71
89
|
bytes = guard.bytes.view(bytes, JoseError, "jose/bad-input", "base64url.encode input");
|
|
72
|
-
return bytes
|
|
90
|
+
return _bufToString(bytes, "base64url");
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
/**
|
|
@@ -182,19 +200,19 @@ var PROFILES = {
|
|
|
182
200
|
// { alg, algRow, header }; throws a typed jose/* fault on any violation.
|
|
183
201
|
function _checkHeader(header, profileName) {
|
|
184
202
|
var profile = PROFILES[profileName];
|
|
185
|
-
if (!profile) throw E("jose/bad-input", "unknown JWS profile " +
|
|
186
|
-
if (!header || typeof header !== "object" ||
|
|
203
|
+
if (!profile) throw E("jose/bad-input", "unknown JWS profile " + _stringify(profileName));
|
|
204
|
+
if (!header || typeof header !== "object" || _isArray(header)) throw E("jose/bad-header", "the protected header must be a JSON object");
|
|
187
205
|
// Every field is read as an OWN property. JSON.stringify (used by sign) omits
|
|
188
206
|
// inherited members, so an alg/nonce/url living on the prototype (a polluted or
|
|
189
207
|
// Object.create(...) header) would pass here yet serialize to a header MISSING
|
|
190
208
|
// them -- so require own-ness, matching what the serialized JWS will actually carry.
|
|
191
|
-
var own = function (k) { return
|
|
209
|
+
var own = function (k) { return _hasOwn(header, k); };
|
|
192
210
|
// b64 (RFC 7797) MUST NOT be present at all -- the unencoded-payload downgrade.
|
|
193
211
|
if (own("b64")) throw E("jose/bad-jws", "the RFC 7797 b64 header parameter is forbidden");
|
|
194
212
|
var alg = header.alg;
|
|
195
213
|
if (!own("alg") || typeof alg !== "string") throw E("jose/bad-header", "the protected header must carry a string alg");
|
|
196
214
|
var algRow = profile.algs[alg];
|
|
197
|
-
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " +
|
|
215
|
+
if (!algRow) throw E("jose/bad-alg", "unsupported or forbidden algorithm " + _stringify(alg) + " for this JWS profile");
|
|
198
216
|
if (profile.requireUrl && (!own("url") || typeof header.url !== "string" || header.url.length === 0)) throw E("jose/bad-header", "the protected header must carry a non-empty string url");
|
|
199
217
|
// nonce discipline.
|
|
200
218
|
var hasNonce = own("nonce");
|
|
@@ -219,20 +237,20 @@ function _checkHeader(header, profileName) {
|
|
|
219
237
|
} else if (profile.keyId === "jwk") {
|
|
220
238
|
if (!hasJwk || hasKid) throw E("jose/bad-header", "this JWS must carry an embedded jwk, not kid");
|
|
221
239
|
}
|
|
222
|
-
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" ||
|
|
240
|
+
if (hasJwk && (!header.jwk || typeof header.jwk !== "object" || _isArray(header.jwk))) throw E("jose/bad-header", "jwk must be a JSON object");
|
|
223
241
|
// crit: protected-only, non-empty, no standard names, no duplicates, all understood.
|
|
224
|
-
if (
|
|
242
|
+
if (_hasOwn(header, "crit")) {
|
|
225
243
|
var crit = header.crit;
|
|
226
|
-
if (!
|
|
244
|
+
if (!_isArray(crit) || crit.length === 0) throw E("jose/bad-crit", "crit must be a non-empty array");
|
|
227
245
|
var seen = {};
|
|
228
246
|
for (var c = 0; c < crit.length; c++) {
|
|
229
247
|
var name = crit[c];
|
|
230
248
|
if (typeof name !== "string") throw E("jose/bad-crit", "crit entries must be strings");
|
|
231
|
-
if (
|
|
249
|
+
if (_hasOwn(seen, name)) throw E("jose/bad-crit", "duplicate crit entry " + _stringify(name));
|
|
232
250
|
seen[name] = 1;
|
|
233
|
-
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " +
|
|
251
|
+
if (UNDERSTOOD_HEADER[name]) throw E("jose/bad-crit", "crit must not name a standard header parameter " + _stringify(name));
|
|
234
252
|
// Anything else is an extension parameter this implementation does not process.
|
|
235
|
-
throw E("jose/bad-crit", "unprocessed critical header parameter " +
|
|
253
|
+
throw E("jose/bad-crit", "unprocessed critical header parameter " + _stringify(name));
|
|
236
254
|
}
|
|
237
255
|
}
|
|
238
256
|
return { alg: alg, algRow: algRow, header: header };
|
|
@@ -267,9 +285,9 @@ function _cryptoAlg(algRow, jwk, key) {
|
|
|
267
285
|
// The key type the JWK MUST have for this alg (fail closed on confusion).
|
|
268
286
|
function _assertKeyType(algRow, jwk) {
|
|
269
287
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
270
|
-
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " +
|
|
288
|
+
if (jwk.kty !== algRow.kty) throw E("jose/bad-alg", "the key type " + _stringify(jwk.kty) + " does not match the algorithm");
|
|
271
289
|
if (algRow.kty === "EC" && jwk.crv !== algRow.crv) throw E("jose/bad-alg", "the EC curve does not match the algorithm");
|
|
272
|
-
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " +
|
|
290
|
+
if (algRow.kty === "OKP" && jwk.crv !== "Ed25519" && jwk.crv !== "Ed448") throw E("jose/bad-alg", "unsupported OKP curve " + _stringify(jwk.crv));
|
|
273
291
|
// node/OpenSSL import a low-order (e.g. identity) OKP point without complaint and it verifies a
|
|
274
292
|
// forged signature; reject a non-full-order Edwards point through the shared gate, the same check
|
|
275
293
|
// every EdDSA verify path in the toolkit applies.
|
|
@@ -278,7 +296,7 @@ function _assertKeyType(algRow, jwk) {
|
|
|
278
296
|
}
|
|
279
297
|
// An AKP (ML-DSA) JWK carries its parameter set in `alg` (RFC 9964); it MUST match
|
|
280
298
|
// the algorithm's set, or an ML-DSA-44 key could embed an ML-DSA-65 public JWK.
|
|
281
|
-
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " +
|
|
299
|
+
if (algRow.kty === "AKP" && jwk.alg !== algRow.subtle) throw E("jose/bad-alg", "the AKP parameter set " + _stringify(jwk.alg) + " does not match the algorithm " + algRow.subtle);
|
|
282
300
|
}
|
|
283
301
|
|
|
284
302
|
// The exact signature byte length this alg + key must produce.
|
|
@@ -317,10 +335,10 @@ var PRIVATE_JWK_MEMBERS = ["d", "p", "q", "dp", "dq", "qi", "k", "priv"];
|
|
|
317
335
|
* pki.jose.assertPublicJwk({ kty: "EC", crv: "P-256", x: "...", y: "..." });
|
|
318
336
|
*/
|
|
319
337
|
function assertPublicJwk(jwk) {
|
|
320
|
-
if (!jwk || typeof jwk !== "object" ||
|
|
338
|
+
if (!jwk || typeof jwk !== "object" || _isArray(jwk)) throw E("jose/bad-key", "a JWK object is required");
|
|
321
339
|
for (var i = 0; i < PRIVATE_JWK_MEMBERS.length; i++) {
|
|
322
|
-
if (
|
|
323
|
-
throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " +
|
|
340
|
+
if (_hasOwn(jwk, PRIVATE_JWK_MEMBERS[i])) {
|
|
341
|
+
throw E("jose/private-key-material", "a published JWK must be public-only; it carries the private member " + _stringify(PRIVATE_JWK_MEMBERS[i]));
|
|
324
342
|
}
|
|
325
343
|
}
|
|
326
344
|
return jwk;
|
|
@@ -374,9 +392,9 @@ var _VERIFY_KEYS = { key: 1, profile: 1 };
|
|
|
374
392
|
async function verify(jws, opts) {
|
|
375
393
|
opts = opts || {};
|
|
376
394
|
guard.identifier.assertKnownKeys(opts, _VERIFY_KEYS, E, "jose/bad-input", "unknown pki.jose.verify option ");
|
|
377
|
-
if (!jws || typeof jws !== "object" ||
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
395
|
+
if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
|
|
396
|
+
if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
|
|
397
|
+
if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
|
|
380
398
|
if (typeof jws.protected !== "string" || typeof jws.signature !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
|
|
381
399
|
if (typeof jws.payload !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
|
|
382
400
|
var header = parseJson(b64uDecode(jws.protected));
|
|
@@ -393,7 +411,7 @@ async function verify(jws, opts) {
|
|
|
393
411
|
// matters most -- a `key` that came back null from a lookup would verify against whatever the
|
|
394
412
|
// message carried, and the verdict would report the embedded key as though none was named.
|
|
395
413
|
// `undefined` alone means "not supplied", so spreading an options object stays safe.
|
|
396
|
-
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null ||
|
|
414
|
+
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
|
|
397
415
|
throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
|
|
398
416
|
}
|
|
399
417
|
var jwk = header.jwk || opts.key;
|
|
@@ -419,7 +437,7 @@ async function verify(jws, opts) {
|
|
|
419
437
|
var want = _expectedSigBytes(checked.algRow, jwk);
|
|
420
438
|
if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
|
|
421
439
|
// Signing input from the RECEIVED base64url strings verbatim (never a re-serialize).
|
|
422
|
-
var signingInput =
|
|
440
|
+
var signingInput = _bufferFrom(jws.protected + "." + jws.payload, "ascii");
|
|
423
441
|
var key;
|
|
424
442
|
try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
|
|
425
443
|
catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
|
|
@@ -483,23 +501,23 @@ async function sign(opts) {
|
|
|
483
501
|
// those questions separately -- assertPublicJwk sees a public JWK while the bytes that ship carry
|
|
484
502
|
// the private one, and the door sees a jwk-mode header while the signer sees a kid-mode one.
|
|
485
503
|
var headerJson;
|
|
486
|
-
try { headerJson =
|
|
504
|
+
try { headerJson = _stringify(opts.protected); }
|
|
487
505
|
catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
|
|
488
506
|
// Read back through the same bounded reader a received JWS goes through, so a header this side
|
|
489
507
|
// produces is held to the caps and the duplicate-member rule the other side enforces.
|
|
490
|
-
var headerBytes = headerJson === undefined ? null :
|
|
508
|
+
var headerBytes = headerJson === undefined ? null : _bufferFrom(headerJson, "utf8");
|
|
491
509
|
var header = headerBytes === null ? opts.protected : parseJson(headerBytes);
|
|
492
510
|
// Which of the two key sources is in force is known only once the header has been read, so the
|
|
493
511
|
// narrower table runs here. It re-walks the bag, which also catches a name that code added.
|
|
494
512
|
var embedsJwk = !!(header && typeof header === "object" && header.jwk);
|
|
495
513
|
if (embedsJwk) {
|
|
496
514
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS_EMBEDDED_JWK, E, "jose/bad-input", function (k) {
|
|
497
|
-
return "unknown pki.jose.sign option " +
|
|
515
|
+
return "unknown pki.jose.sign option " + _stringify(k) +
|
|
498
516
|
(k === "jwk" ? "; the protected header already embeds the public JWK" : "");
|
|
499
517
|
});
|
|
500
518
|
}
|
|
501
519
|
var payload = opts.payload;
|
|
502
|
-
if (!
|
|
520
|
+
if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
|
|
503
521
|
// Re-view before the length===0 fast-path: a DETACHED payload reads as zero-length
|
|
504
522
|
// and would be silently signed as an empty POST-as-GET body -- fail closed instead.
|
|
505
523
|
payload = guard.bytes.view(payload, JoseError, "jose/bad-input", "payload");
|
|
@@ -523,14 +541,14 @@ async function sign(opts) {
|
|
|
523
541
|
if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
|
|
524
542
|
var protectedB64 = b64uEncode(headerBytes); // the exact bytes every check above read
|
|
525
543
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
526
|
-
var signingInput =
|
|
544
|
+
var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
|
|
527
545
|
var jwk = header.jwk || opts.jwk || {};
|
|
528
546
|
// opts.key is documented as a CryptoKey, so one created outside this engine signs here too: it
|
|
529
547
|
// carries none of the material this engine signs with, and is re-imported through it rather than
|
|
530
548
|
// reaching subtle.sign as a key it cannot read. A key whose material this process cannot reach at
|
|
531
549
|
// all -- non-extractable, or held behind another implementation's interface -- is refused there.
|
|
532
550
|
var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
|
|
533
|
-
var sigBuf =
|
|
551
|
+
var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
|
|
534
552
|
// Pin the produced signature length the same way verify does. A key whose curve /
|
|
535
553
|
// hash / modulus does not match the header alg (a P-384 key under ES256, an
|
|
536
554
|
// HS512 key under HS256) still signs, but emits the wrong length -- caught here
|
|
@@ -590,15 +608,17 @@ var THUMBPRINT_MEMBERS = {
|
|
|
590
608
|
async function thumbprint(jwk) {
|
|
591
609
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
592
610
|
var members = THUMBPRINT_MEMBERS[jwk.kty];
|
|
593
|
-
if (!members) throw E("jose/bad-key", "unsupported JWK key type " +
|
|
611
|
+
if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(jwk.kty));
|
|
594
612
|
var parts = [];
|
|
595
613
|
for (var i = 0; i < members.length; i++) {
|
|
596
614
|
var m = members[i];
|
|
597
|
-
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " +
|
|
598
|
-
|
|
615
|
+
if (typeof jwk[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
|
|
616
|
+
// The canonical form IS the thumbprint preimage (RFC 7638 sec. 3.3), so the serializer that
|
|
617
|
+
// builds it is the captured one: a replacement changes the identity every consumer compares.
|
|
618
|
+
_push(parts, _stringify(m) + ":" + _stringify(jwk[m]));
|
|
599
619
|
}
|
|
600
|
-
var canonical = "{" + parts
|
|
601
|
-
var digest =
|
|
620
|
+
var canonical = "{" + _join(parts, ",") + "}";
|
|
621
|
+
var digest = _bufferFrom(await webcrypto.subtle.digest("SHA-256", _bufferFrom(canonical, "utf8")));
|
|
602
622
|
return b64uEncode(digest);
|
|
603
623
|
}
|
|
604
624
|
|
|
@@ -630,7 +650,7 @@ async function thumbprint(jwk) {
|
|
|
630
650
|
* pss.map(function (r) { return r.alg; }); // -> ["PS256", "PS384", "PS512"]
|
|
631
651
|
*/
|
|
632
652
|
function sigAlgs() {
|
|
633
|
-
return
|
|
653
|
+
return _map(_keys(SIG_ALGS), function (alg) {
|
|
634
654
|
var row = SIG_ALGS[alg];
|
|
635
655
|
var out = { alg: alg, kty: row.kty };
|
|
636
656
|
if (row.crv) out.crv = row.crv;
|
package/lib/key.js
CHANGED
|
@@ -305,6 +305,10 @@ async function import_(input, opts) {
|
|
|
305
305
|
}
|
|
306
306
|
var algorithm = opts.algorithm != null ? opts.algorithm : _inferAlgorithm(detected);
|
|
307
307
|
var isPublic = detected.format === "spki";
|
|
308
|
+
// Checked at this verb's own door rather than left for the engine to discover: a non-list reached
|
|
309
|
+
// the CryptoKey copy, where `Array.prototype.slice` on a number yields an empty list, so the key
|
|
310
|
+
// was minted permitted for NOTHING instead of the call naming the argument that was wrong.
|
|
311
|
+
if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
|
|
308
312
|
var usages = opts.usages || _importUsages(_algName(algorithm), isPublic);
|
|
309
313
|
var extractable = opts.extractable != null ? opts.extractable : false;
|
|
310
314
|
try {
|
|
@@ -344,6 +348,8 @@ async function generate(algorithm, opts) {
|
|
|
344
348
|
guard.identifier.assertKnownKeys(opts, _GENERATE_OPTS, _err, "key/bad-input",
|
|
345
349
|
"pki.key.generate has an unknown option (the WebCrypto spelling is `extractable`): ");
|
|
346
350
|
var extractable = opts.extractable != null ? opts.extractable : true;
|
|
351
|
+
// The same door as import: a non-list is named here rather than becoming an empty usage set.
|
|
352
|
+
if (opts.usages != null && !Array.isArray(opts.usages)) throw _err("key/bad-input", "opts.usages must be an array of key-usage strings");
|
|
347
353
|
var usages = opts.usages || _generateUsages(_algName(algorithm));
|
|
348
354
|
var pair;
|
|
349
355
|
try { pair = await subtle.generateKey(algorithm, extractable, usages); }
|
package/lib/webcrypto.js
CHANGED
|
@@ -42,6 +42,52 @@ 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
|
+
var intrinsic = require("./guard-intrinsic");
|
|
52
|
+
var _isArray = intrinsic.isArray;
|
|
53
|
+
var _hasOwn = intrinsic.hasOwn;
|
|
54
|
+
var _stringify = intrinsic.stringify;
|
|
55
|
+
var _bufferFrom = intrinsic.bufferFrom;
|
|
56
|
+
var _bufToString = intrinsic.uncurry(Buffer.prototype.toString);
|
|
57
|
+
var _bufferConcat = intrinsic.bufferConcat;
|
|
58
|
+
var _bufferAlloc = intrinsic.bufferAlloc;
|
|
59
|
+
var _isBuffer = intrinsic.isBuffer;
|
|
60
|
+
var _keys = intrinsic.keys;
|
|
61
|
+
var _map = intrinsic.map;
|
|
62
|
+
var _forEach = intrinsic.forEach;
|
|
63
|
+
var _indexOf = intrinsic.indexOf;
|
|
64
|
+
var _isInteger = intrinsic.isInteger;
|
|
65
|
+
var _join = intrinsic.join;
|
|
66
|
+
var _filter = intrinsic.filter;
|
|
67
|
+
var _arraySlice = intrinsic.arraySlice;
|
|
68
|
+
var _defineProperty = intrinsic.defineProperty;
|
|
69
|
+
var _max = intrinsic.max;
|
|
70
|
+
var _parseInt = intrinsic.parseInt;
|
|
71
|
+
var _isFinite = intrinsic.isFinite;
|
|
72
|
+
var _String = intrinsic.String;
|
|
73
|
+
var _Number = intrinsic.Number;
|
|
74
|
+
var _BigInt = intrinsic.BigInt;
|
|
75
|
+
var _Uint8Array = intrinsic.Uint8Array;
|
|
76
|
+
var _assign = intrinsic.assign;
|
|
77
|
+
var _create = intrinsic.create;
|
|
78
|
+
var _isView = intrinsic.isView;
|
|
79
|
+
var _toUpperCase = intrinsic.toUpperCase;
|
|
80
|
+
var _toLowerCase = intrinsic.toLowerCase;
|
|
81
|
+
var _stringIndexOf = intrinsic.stringIndexOf;
|
|
82
|
+
var _subarray = intrinsic.subarray;
|
|
83
|
+
var _arrayBufferSlice = intrinsic.arrayBufferSlice;
|
|
84
|
+
var _numberToString = intrinsic.numberToString;
|
|
85
|
+
var _bigIntToString = intrinsic.bigIntToString;
|
|
86
|
+
var _push = intrinsic.push;
|
|
87
|
+
var _concat = intrinsic.concat;
|
|
88
|
+
// Promise.resolve is a constructor method, so it keeps its receiver: `_resolve(Promise, x)`.
|
|
89
|
+
var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
|
|
90
|
+
var _types = intrinsic.types;
|
|
45
91
|
|
|
46
92
|
// Single-owner error class -- co-located with its module (framework-error
|
|
47
93
|
// stays the cross-module home; this is webcrypto-private). withCause: a
|
|
@@ -58,7 +104,7 @@ function _toBuf(data, who) {
|
|
|
58
104
|
}
|
|
59
105
|
|
|
60
106
|
function _toArrayBuffer(buf) {
|
|
61
|
-
return buf.buffer
|
|
107
|
+
return _arrayBufferSlice(buf.buffer, buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
62
108
|
}
|
|
63
109
|
|
|
64
110
|
function _wcErr(code, msg) { return new WebCryptoError(code, msg); }
|
|
@@ -68,7 +114,7 @@ function _wcErr(code, msg) { return new WebCryptoError(code, msg); }
|
|
|
68
114
|
// bogus 6-byte key). Node's own canonical JWK export (_rawPublic) round-trips
|
|
69
115
|
// cleanly, so the strictness is benign there and fail-closed on untrusted import.
|
|
70
116
|
function _b64urlToBuf(s, who) { return guard.encoding.base64url(s, null, _wcErr, "webcrypto/data", who || "base64url value"); }
|
|
71
|
-
function _bufToB64url(buf) { return
|
|
117
|
+
function _bufToB64url(buf) { return _bufToString(_bufferFrom(buf), "base64url"); }
|
|
72
118
|
|
|
73
119
|
function _normalizeAlg(algorithm, who) {
|
|
74
120
|
var alg = (typeof algorithm === "string") ? { name: algorithm } : algorithm;
|
|
@@ -76,8 +122,8 @@ function _normalizeAlg(algorithm, who) {
|
|
|
76
122
|
throw new WebCryptoError("webcrypto/syntax", (who || "operation") + ": algorithm must be a string or an object with a name");
|
|
77
123
|
}
|
|
78
124
|
var out = {};
|
|
79
|
-
for (var k in alg) { if (
|
|
80
|
-
out.name = alg.name
|
|
125
|
+
for (var k in alg) { if (_hasOwn(alg, k)) out[k] = alg[k]; }
|
|
126
|
+
out.name = _toUpperCase(alg.name);
|
|
81
127
|
return out;
|
|
82
128
|
}
|
|
83
129
|
|
|
@@ -119,8 +165,11 @@ var XOF_NODE = {
|
|
|
119
165
|
|
|
120
166
|
function _hashNode(h, who) {
|
|
121
167
|
var name = (typeof h === "string") ? h : (h && h.name);
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
// The case fold is what picks the registry row, so it runs through the captured method: dispatched
|
|
169
|
+
// live, a replaced `toUpperCase` answering "SHA-1" makes a caller who asked for SHA-256 receive a
|
|
170
|
+
// SHA-1 digest, and nothing between here and the hash says the algorithm changed.
|
|
171
|
+
var node = HASH_NODE[_toUpperCase(_String(name))];
|
|
172
|
+
if (!node) throw new WebCryptoError("webcrypto/not-supported", (who || "operation") + ": unsupported hash " + _stringify(name));
|
|
124
173
|
return node;
|
|
125
174
|
}
|
|
126
175
|
|
|
@@ -138,10 +187,10 @@ var HMAC_BLOCK_BITS = {
|
|
|
138
187
|
|
|
139
188
|
function _hmacLengthBits(alg, who) {
|
|
140
189
|
var name = (typeof alg.hash === "string") ? alg.hash : (alg.hash && alg.hash.name);
|
|
141
|
-
var blockBits = HMAC_BLOCK_BITS[
|
|
142
|
-
if (!blockBits) throw new WebCryptoError("webcrypto/not-supported", who + ": unsupported hash " +
|
|
190
|
+
var blockBits = HMAC_BLOCK_BITS[_toUpperCase(_String(name))];
|
|
191
|
+
if (!blockBits) throw new WebCryptoError("webcrypto/not-supported", who + ": unsupported hash " + _stringify(name));
|
|
143
192
|
if (alg.length == null) return blockBits;
|
|
144
|
-
if (typeof alg.length !== "number" || !
|
|
193
|
+
if (typeof alg.length !== "number" || !_isFinite(alg.length) || alg.length <= 0 || alg.length % 8 !== 0) {
|
|
145
194
|
throw new WebCryptoError("webcrypto/syntax", who + ": HMAC length must be a positive multiple of 8 bits");
|
|
146
195
|
}
|
|
147
196
|
return alg.length;
|
|
@@ -161,7 +210,7 @@ var ML_KEM_NODE = { "ML-KEM-512": "ml-kem-512", "ML-KEM-768": "ml-kem-768", "ML-
|
|
|
161
210
|
var SLH_DSA_NODE = {};
|
|
162
211
|
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
163
212
|
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
164
|
-
].forEach(function (s) { SLH_DSA_NODE["SLH-DSA-" + s
|
|
213
|
+
].forEach(function (s) { SLH_DSA_NODE["SLH-DSA-" + _toUpperCase(s)] = "slh-dsa-" + s; });
|
|
165
214
|
|
|
166
215
|
// The algorithm names each keyed operation recognizes. Membership is checked
|
|
167
216
|
// before the algorithm/key name binding so an unrecognized algorithm reports
|
|
@@ -169,7 +218,7 @@ var SLH_DSA_NODE = {};
|
|
|
169
218
|
// InvalidAccessError, matching the W3C error ordering.
|
|
170
219
|
var SIGN_VERIFY_NAMES = {};
|
|
171
220
|
["RSASSA-PKCS1-V1_5", "RSA-PSS", "ECDSA", "ED25519", "ED448", "HMAC"]
|
|
172
|
-
.concat(
|
|
221
|
+
.concat(_keys(ML_DSA_NODE), _keys(SLH_DSA_NODE))
|
|
173
222
|
.forEach(function (n) { SIGN_VERIFY_NAMES[n] = true; });
|
|
174
223
|
var ENCRYPT_DECRYPT_NAMES = { "RSA-OAEP": true, "AES-GCM": true, "AES-CBC": true, "AES-CTR": true };
|
|
175
224
|
var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
|
|
@@ -206,8 +255,16 @@ function CryptoKey(type, extractable, algorithm, usages, handle) {
|
|
|
206
255
|
this.type = type;
|
|
207
256
|
this.extractable = !!extractable;
|
|
208
257
|
this.algorithm = algorithm;
|
|
209
|
-
|
|
210
|
-
|
|
258
|
+
// The copy is what the key will be asked about for the rest of its life, so the method that makes
|
|
259
|
+
// it is the captured one: a replaced `slice` hands back a list the caller still controls. The
|
|
260
|
+
// list-ness is checked rather than left to the copy to discover -- `Array.prototype.slice` on a
|
|
261
|
+
// number yields an empty list, so a non-list would have minted a key permitted for NOTHING
|
|
262
|
+
// instead of naming the bad argument.
|
|
263
|
+
if (usages != null && !_isArray(usages)) {
|
|
264
|
+
throw new WebCryptoError("webcrypto/syntax", "usages must be an array of key-usage strings");
|
|
265
|
+
}
|
|
266
|
+
this.usages = usages ? _arraySlice(usages) : [];
|
|
267
|
+
_defineProperty(this, "_handle", { value: handle, enumerable: false });
|
|
211
268
|
}
|
|
212
269
|
|
|
213
270
|
// This engine reads key material through its own CryptoKey handle, which a key minted by another
|
|
@@ -225,8 +282,11 @@ function _requireOwnKey(key, who) {
|
|
|
225
282
|
|
|
226
283
|
function _requireUsage(key, usage) {
|
|
227
284
|
_requireOwnKey(key, usage);
|
|
228
|
-
|
|
229
|
-
|
|
285
|
+
// The membership test IS the permission check, so it runs through the captured method: dispatched
|
|
286
|
+
// live, an `Array.prototype.indexOf` replaced after load reports every usage present and a key
|
|
287
|
+
// minted to verify signs instead, with this refusal never raised.
|
|
288
|
+
if (_indexOf(key.usages, usage) === -1) {
|
|
289
|
+
throw new WebCryptoError("webcrypto/invalid-access", "key is not permitted for '" + usage + "' (usages: " + _join(key.usages, ",") + ")");
|
|
230
290
|
}
|
|
231
291
|
}
|
|
232
292
|
|
|
@@ -239,8 +299,8 @@ function _requireUsage(key, usage) {
|
|
|
239
299
|
// be satisfied by a key of algorithm Y (algorithm confusion).
|
|
240
300
|
function _requireAlgMatch(alg, key, who) {
|
|
241
301
|
var keyName = key && key.algorithm && key.algorithm.name;
|
|
242
|
-
if (
|
|
243
|
-
throw new WebCryptoError("webcrypto/invalid-access", who + ": algorithm " +
|
|
302
|
+
if (_toUpperCase(_String(keyName)) !== alg.name) {
|
|
303
|
+
throw new WebCryptoError("webcrypto/invalid-access", who + ": algorithm " + _stringify(alg.name) + " does not match the key's algorithm " + _stringify(keyName));
|
|
244
304
|
}
|
|
245
305
|
}
|
|
246
306
|
|
|
@@ -250,7 +310,7 @@ function SubtleCrypto() {}
|
|
|
250
310
|
|
|
251
311
|
SubtleCrypto.prototype.digest = async function digest(algorithm, data) {
|
|
252
312
|
var name = _normalizeAlg(algorithm, "digest").name;
|
|
253
|
-
var xof = XOF_NODE[
|
|
313
|
+
var xof = XOF_NODE[_toUpperCase(_String(name))];
|
|
254
314
|
// An XOF is always constructed with its output length stated. Omitting it
|
|
255
315
|
// makes node emit a deprecation warning and fall back to a 32-byte squeeze,
|
|
256
316
|
// which for SHAKE256 would silently produce half the required digest.
|
|
@@ -263,6 +323,14 @@ SubtleCrypto.prototype.digest = async function digest(algorithm, data) {
|
|
|
263
323
|
|
|
264
324
|
SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extractable, keyUsages) {
|
|
265
325
|
var alg = _normalizeAlg(algorithm, "generateKey");
|
|
326
|
+
// Before the partition below, not only in the CryptoKey constructor: the asymmetric branch splits
|
|
327
|
+
// the usages first, and `Array.prototype.filter` treats a non-list as array-like and yields `[]`,
|
|
328
|
+
// so both halves reached the constructor already valid and the call minted an unusable PAIR
|
|
329
|
+
// instead of naming the argument. The symmetric branches hand `usages` straight to the
|
|
330
|
+
// constructor, which is why its own door caught those and not these.
|
|
331
|
+
if (keyUsages != null && !_isArray(keyUsages)) {
|
|
332
|
+
throw new WebCryptoError("webcrypto/syntax", "generateKey: keyUsages must be an array of key-usage strings");
|
|
333
|
+
}
|
|
266
334
|
var usages = keyUsages || [];
|
|
267
335
|
var name = alg.name;
|
|
268
336
|
|
|
@@ -282,8 +350,10 @@ SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extra
|
|
|
282
350
|
// Asymmetric key pairs.
|
|
283
351
|
var pair = _generateKeyPair(alg);
|
|
284
352
|
var pubAlg = pair.algorithm;
|
|
285
|
-
|
|
286
|
-
|
|
353
|
+
// Which usages each half of the pair is minted with -- the permission set every later operation
|
|
354
|
+
// is checked against -- so the partition runs through the captured method.
|
|
355
|
+
var privUsages = _filter(usages, function (u) { return u === "sign" || u === "deriveKey" || u === "deriveBits" || u === "decrypt" || u === "unwrapKey" || u === "decapsulateKey" || u === "decapsulateBits"; });
|
|
356
|
+
var pubUsages = _filter(usages, function (u) { return u === "verify" || u === "encrypt" || u === "wrapKey" || u === "encapsulateKey" || u === "encapsulateBits"; });
|
|
287
357
|
return {
|
|
288
358
|
privateKey: new CryptoKey("private", extractable, pubAlg, privUsages, pair.privateKey),
|
|
289
359
|
publicKey: new CryptoKey("public", true, pubAlg, pubUsages, pair.publicKey),
|
|
@@ -304,11 +374,11 @@ function _generateKeyPair(alg) {
|
|
|
304
374
|
algorithm = { name: _stdName(name), modulusLength: alg.modulusLength || 2048, publicExponent: alg.publicExponent, hash: rsaHash };
|
|
305
375
|
} else if (name === "ECDSA" || name === "ECDH") {
|
|
306
376
|
var curve = alg.namedCurve;
|
|
307
|
-
if (!CURVE_NODE[curve]) throw new WebCryptoError("webcrypto/not-supported", name + ": unsupported curve " +
|
|
377
|
+
if (!CURVE_NODE[curve]) throw new WebCryptoError("webcrypto/not-supported", name + ": unsupported curve " + _stringify(curve));
|
|
308
378
|
kp = nodeCrypto.generateKeyPairSync("ec", { namedCurve: CURVE_NODE[curve] });
|
|
309
379
|
algorithm = { name: name, namedCurve: curve };
|
|
310
380
|
} else if (name === "ED25519" || name === "ED448" || name === "X25519" || name === "X448") {
|
|
311
|
-
kp = nodeCrypto.generateKeyPairSync(name
|
|
381
|
+
kp = nodeCrypto.generateKeyPairSync(_toLowerCase(name));
|
|
312
382
|
algorithm = { name: _stdName(name) };
|
|
313
383
|
} else if (ML_DSA_NODE[name]) {
|
|
314
384
|
kp = nodeCrypto.generateKeyPairSync(ML_DSA_NODE[name]);
|
|
@@ -320,7 +390,7 @@ function _generateKeyPair(alg) {
|
|
|
320
390
|
kp = nodeCrypto.generateKeyPairSync(SLH_DSA_NODE[name]);
|
|
321
391
|
algorithm = { name: name };
|
|
322
392
|
} else {
|
|
323
|
-
throw new WebCryptoError("webcrypto/not-supported", "generateKey: unsupported algorithm " +
|
|
393
|
+
throw new WebCryptoError("webcrypto/not-supported", "generateKey: unsupported algorithm " + _stringify(name));
|
|
324
394
|
}
|
|
325
395
|
return { publicKey: kp.publicKey, privateKey: kp.privateKey, algorithm: algorithm };
|
|
326
396
|
}
|
|
@@ -354,11 +424,11 @@ function _bufToBigIntNum(exp) {
|
|
|
354
424
|
if (b.length === 0) {
|
|
355
425
|
throw new WebCryptoError("webcrypto/syntax", "publicExponent must be a non-empty BigInteger octet string");
|
|
356
426
|
}
|
|
357
|
-
var v =
|
|
427
|
+
var v = _BigInt("0x" + _bufToString(b, "hex"));
|
|
358
428
|
if (v > 0xffffffffn) {
|
|
359
|
-
throw new WebCryptoError("webcrypto/syntax", "publicExponent " + v
|
|
429
|
+
throw new WebCryptoError("webcrypto/syntax", "publicExponent " + _bigIntToString(v) + " exceeds the 2^32-1 bound");
|
|
360
430
|
}
|
|
361
|
-
return
|
|
431
|
+
return _Number(v);
|
|
362
432
|
}
|
|
363
433
|
|
|
364
434
|
SubtleCrypto.prototype.sign = async function sign(algorithm, key, data) {
|
|
@@ -366,7 +436,7 @@ SubtleCrypto.prototype.sign = async function sign(algorithm, key, data) {
|
|
|
366
436
|
_requireUsage(key, "sign");
|
|
367
437
|
var buf = _toBuf(data, "sign");
|
|
368
438
|
var name = alg.name;
|
|
369
|
-
if (!SIGN_VERIFY_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "sign: unsupported algorithm " +
|
|
439
|
+
if (!SIGN_VERIFY_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "sign: unsupported algorithm " + _stringify(name));
|
|
370
440
|
_requireAlgMatch(alg, key, "sign");
|
|
371
441
|
if (name === "RSASSA-PKCS1-V1_5") {
|
|
372
442
|
return _toArrayBuffer(nodeCrypto.sign(_hashNode(key.algorithm.hash, "sign"), buf, key._handle));
|
|
@@ -394,7 +464,7 @@ SubtleCrypto.prototype.sign = async function sign(algorithm, key, data) {
|
|
|
394
464
|
hm.update(buf);
|
|
395
465
|
return _toArrayBuffer(hm.digest());
|
|
396
466
|
}
|
|
397
|
-
throw new WebCryptoError("webcrypto/not-supported", "sign: unsupported algorithm " +
|
|
467
|
+
throw new WebCryptoError("webcrypto/not-supported", "sign: unsupported algorithm " + _stringify(name));
|
|
398
468
|
};
|
|
399
469
|
|
|
400
470
|
SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature, data) {
|
|
@@ -403,7 +473,7 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
|
|
|
403
473
|
var sig = _toBuf(signature, "verify");
|
|
404
474
|
var buf = _toBuf(data, "verify");
|
|
405
475
|
var name = alg.name;
|
|
406
|
-
if (!SIGN_VERIFY_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " +
|
|
476
|
+
if (!SIGN_VERIFY_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + _stringify(name));
|
|
407
477
|
_requireAlgMatch(alg, key, "verify");
|
|
408
478
|
if (name === "RSASSA-PKCS1-V1_5") {
|
|
409
479
|
return nodeCrypto.verify(_hashNode(key.algorithm.hash, "verify"), buf, key._handle, sig);
|
|
@@ -437,7 +507,7 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
|
|
|
437
507
|
var digest = hm.digest();
|
|
438
508
|
return guard.crypto.constantTimeEqual(digest, sig);
|
|
439
509
|
}
|
|
440
|
-
throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " +
|
|
510
|
+
throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + _stringify(name));
|
|
441
511
|
};
|
|
442
512
|
|
|
443
513
|
// Run a node:crypto AES cipher sequence, converting any raw node fault -- a bad IV length,
|
|
@@ -455,7 +525,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
455
525
|
_requireUsage(key, "encrypt");
|
|
456
526
|
var buf = _toBuf(data, "encrypt");
|
|
457
527
|
var name = alg.name;
|
|
458
|
-
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " +
|
|
528
|
+
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + _stringify(name));
|
|
459
529
|
_requireAlgMatch(alg, key, "encrypt");
|
|
460
530
|
if (name === "RSA-OAEP") {
|
|
461
531
|
return _toArrayBuffer(nodeCrypto.publicEncrypt({
|
|
@@ -472,7 +542,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
472
542
|
var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", kb, iv, { authTagLength: (alg.tagLength || 128) / 8 });
|
|
473
543
|
if (aad) cipher.setAAD(aad);
|
|
474
544
|
var ct = guard.secret.cipherFinish(cipher, buf, WebCryptoError, "webcrypto/operation", "an AES-GCM encrypt intermediate");
|
|
475
|
-
return
|
|
545
|
+
return _bufferConcat([ct, cipher.getAuthTag()]);
|
|
476
546
|
});
|
|
477
547
|
}, "encrypt"));
|
|
478
548
|
}
|
|
@@ -495,7 +565,7 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
495
565
|
});
|
|
496
566
|
}, "encrypt"));
|
|
497
567
|
}
|
|
498
|
-
throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " +
|
|
568
|
+
throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + _stringify(name));
|
|
499
569
|
};
|
|
500
570
|
|
|
501
571
|
SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
@@ -503,7 +573,7 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
503
573
|
_requireUsage(key, "decrypt");
|
|
504
574
|
var buf = _toBuf(data, "decrypt");
|
|
505
575
|
var name = alg.name;
|
|
506
|
-
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " +
|
|
576
|
+
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + _stringify(name));
|
|
507
577
|
_requireAlgMatch(alg, key, "decrypt");
|
|
508
578
|
if (name === "RSA-OAEP") {
|
|
509
579
|
// For a CMS key-transport recipient this plaintext IS the recovered content-encryption key, and
|
|
@@ -521,8 +591,10 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
521
591
|
var tagLen = (alg.tagLength || 128) / 8;
|
|
522
592
|
var iv = _toBuf(alg.iv, "AES-GCM iv");
|
|
523
593
|
if (buf.length < tagLen) throw new WebCryptoError("webcrypto/operation", "decrypt: AES-GCM ciphertext is shorter than its authentication tag");
|
|
524
|
-
|
|
525
|
-
|
|
594
|
+
// The split between ciphertext and tag decides which bytes are authenticated, so the window it
|
|
595
|
+
// is taken with is the captured one: a replaced `subarray` hands the tag check other bytes.
|
|
596
|
+
var ct = _subarray(buf, 0, buf.length - tagLen);
|
|
597
|
+
var tag = _subarray(buf, buf.length - tagLen);
|
|
526
598
|
var gcmAad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
|
|
527
599
|
return _toArrayBuffer(_runCipher(function () {
|
|
528
600
|
return _withSecretBytes(key, function (kb) {
|
|
@@ -554,7 +626,7 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
554
626
|
});
|
|
555
627
|
}, "decrypt"));
|
|
556
628
|
}
|
|
557
|
-
throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " +
|
|
629
|
+
throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + _stringify(name));
|
|
558
630
|
};
|
|
559
631
|
|
|
560
632
|
// _withSecretBytes(key, fn) -- export a secret key's raw material, hand it to fn, and wipe
|
|
@@ -607,7 +679,7 @@ function _requireCtrLength128(alg) {
|
|
|
607
679
|
// the `length / 8` narrowing in the per-algorithm branches below would
|
|
608
680
|
// otherwise silently truncate a fractional byte count.
|
|
609
681
|
function _requireDeriveLength(length, who) {
|
|
610
|
-
if (typeof length !== "number" || !
|
|
682
|
+
if (typeof length !== "number" || !_isFinite(length) || length <= 0 || length % 8 !== 0) {
|
|
611
683
|
throw new WebCryptoError("webcrypto/operation", who + ": length must be a positive multiple of 8 bits");
|
|
612
684
|
}
|
|
613
685
|
}
|
|
@@ -622,7 +694,7 @@ function _requireDeriveLength(length, who) {
|
|
|
622
694
|
// Concurrency is CAPPED so many high-iteration jobs cannot monopolize the whole worker pool and starve
|
|
623
695
|
// unrelated DNS / filesystem / crypto work: at least two pool threads are left free (default pool is four),
|
|
624
696
|
// and derivations beyond the cap queue and run as slots free. The cap tracks UV_THREADPOOL_SIZE when raised.
|
|
625
|
-
var _PBKDF2_MAX_CONCURRENT =
|
|
697
|
+
var _PBKDF2_MAX_CONCURRENT = _max(1, (_parseInt(process.env.UV_THREADPOOL_SIZE, 10) || 4) - 2);
|
|
626
698
|
var _pbkdf2InFlight = 0;
|
|
627
699
|
var _pbkdf2Waiters = [];
|
|
628
700
|
function _pbkdf2Async(pw, salt, iterations, keylen, digest) {
|
|
@@ -663,7 +735,7 @@ async function _deriveBitsRaw(alg, key, length) {
|
|
|
663
735
|
if (length / 8 > secret.length) {
|
|
664
736
|
throw new WebCryptoError("webcrypto/operation", name + ": requested " + length + " bits but the shared secret has " + (secret.length * 8));
|
|
665
737
|
}
|
|
666
|
-
return _toArrayBuffer(secret
|
|
738
|
+
return _toArrayBuffer(_subarray(secret, 0, length / 8));
|
|
667
739
|
} finally {
|
|
668
740
|
guard.secret.zeroize(secret, WebCryptoError, "webcrypto/operation", "the raw key-agreement shared secret");
|
|
669
741
|
}
|
|
@@ -674,8 +746,8 @@ async function _deriveBitsRaw(alg, key, length) {
|
|
|
674
746
|
// itself. A controllable allocation, not one of the runtime-internal copies the best-effort
|
|
675
747
|
// caveat covers, so it is wiped once the derivation has consumed it.
|
|
676
748
|
return _withSecretBytes(key, function (ikm) {
|
|
677
|
-
var derived = nodeCrypto.hkdfSync(_hashNode(alg.hash, "HKDF"), ikm, _toBuf(alg.salt, "HKDF salt"), _toBuf(alg.info ||
|
|
678
|
-
return derived instanceof ArrayBuffer ? derived : _toArrayBuffer(
|
|
749
|
+
var derived = nodeCrypto.hkdfSync(_hashNode(alg.hash, "HKDF"), ikm, _toBuf(alg.salt, "HKDF salt"), _toBuf(alg.info || _bufferAlloc(0), "HKDF info"), length / 8);
|
|
750
|
+
return derived instanceof ArrayBuffer ? derived : _toArrayBuffer(_bufferFrom(derived));
|
|
679
751
|
});
|
|
680
752
|
}
|
|
681
753
|
if (name === "PBKDF2") {
|
|
@@ -700,13 +772,13 @@ async function _deriveBitsRaw(alg, key, length) {
|
|
|
700
772
|
// The base key here HOLDS the ECDH shared secret Z of an RFC 5753 kari, so this export is
|
|
701
773
|
// exactly as sensitive as the raw agreement secret wiped above.
|
|
702
774
|
return _withSecretBytes(key, function (z) {
|
|
703
|
-
var derived = _x963Kdf(_hashNode(alg.hash, "X963KDF"), z, _toBuf(alg.info ||
|
|
775
|
+
var derived = _x963Kdf(_hashNode(alg.hash, "X963KDF"), z, _toBuf(alg.info || _bufferAlloc(0), "X963KDF SharedInfo"), length / 8);
|
|
704
776
|
// Same as PBKDF2 above: the copy is what the caller receives, so the derived buffer is cleared.
|
|
705
777
|
try { return _toArrayBuffer(derived); }
|
|
706
778
|
finally { guard.secret.zeroize(derived, WebCryptoError, "webcrypto/operation", "the X9.63 KDF output"); }
|
|
707
779
|
});
|
|
708
780
|
}
|
|
709
|
-
throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " +
|
|
781
|
+
throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + _stringify(name));
|
|
710
782
|
}
|
|
711
783
|
|
|
712
784
|
// The X9.63 single-step KDF counter loop. Bounded by the 2^32 counter range; every input is a
|
|
@@ -715,16 +787,16 @@ function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
|
|
|
715
787
|
var blocks = [], counter = 1, got = 0;
|
|
716
788
|
while (got < lenBytes) {
|
|
717
789
|
if (counter > 0xffffffff) throw new WebCryptoError("webcrypto/operation", "X963KDF: requested output exceeds the counter range");
|
|
718
|
-
var ctr =
|
|
790
|
+
var ctr = _bufferAlloc(4); ctr.writeUInt32BE(counter, 0);
|
|
719
791
|
var h = nodeCrypto.createHash(hashNode).update(z).update(ctr).update(sharedInfo).digest();
|
|
720
|
-
blocks
|
|
792
|
+
_push(blocks, h); got += h.length; counter += 1;
|
|
721
793
|
}
|
|
722
794
|
// Return an exact-sized buffer the caller wholly owns, not a VIEW over the joined blocks: a
|
|
723
795
|
// caller clearing what it received would otherwise leave the unused tail of the final digest
|
|
724
796
|
// block -- derived from the shared secret -- readable behind it. The accumulator and the
|
|
725
797
|
// per-block digests are cleared here, where they were allocated.
|
|
726
|
-
var joined =
|
|
727
|
-
var exact =
|
|
798
|
+
var joined = _bufferConcat(blocks);
|
|
799
|
+
var exact = _bufferAlloc(lenBytes);
|
|
728
800
|
joined.copy(exact, 0, 0, lenBytes);
|
|
729
801
|
guard.secret.zeroize(joined, WebCryptoError, "webcrypto/operation", "the KDF accumulator");
|
|
730
802
|
for (var bi = 0; bi < blocks.length; bi++) guard.secret.zeroize(blocks[bi], WebCryptoError, "webcrypto/operation", "a KDF block");
|
|
@@ -734,7 +806,7 @@ function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
|
|
|
734
806
|
SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
|
|
735
807
|
var alg = _normalizeAlg(algorithm, "deriveBits");
|
|
736
808
|
_requireUsage(key, "deriveBits");
|
|
737
|
-
if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " +
|
|
809
|
+
if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + _stringify(alg.name));
|
|
738
810
|
_requireAlgMatch(alg, key, "deriveBits");
|
|
739
811
|
return _deriveBitsRaw(alg, key, length);
|
|
740
812
|
};
|
|
@@ -746,11 +818,11 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
|
|
|
746
818
|
// here and route the raw derivation past deriveBits' own usage gate.
|
|
747
819
|
_requireUsage(baseKey, "deriveKey");
|
|
748
820
|
var alg = _normalizeAlg(algorithm, "deriveKey");
|
|
749
|
-
if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveKey: unsupported algorithm " +
|
|
821
|
+
if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveKey: unsupported algorithm " + _stringify(alg.name));
|
|
750
822
|
_requireAlgMatch(alg, baseKey, "deriveKey");
|
|
751
823
|
var dk = _normalizeAlg(derivedKeyType, "deriveKey");
|
|
752
824
|
var bits;
|
|
753
|
-
if (dk.name
|
|
825
|
+
if (_stringIndexOf(dk.name, "AES") === 0) {
|
|
754
826
|
// W3C get-key-length for AES: the derivedKeyType MUST carry a length of
|
|
755
827
|
// 128/192/256. Without this check the derived key would silently take
|
|
756
828
|
// the size of the raw agreement/KDF output (e.g. a 384-bit "AES" key
|
|
@@ -775,7 +847,7 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
|
|
|
775
847
|
// failing import too (a bad derivedKeyType is caller-controlled).
|
|
776
848
|
var raw = await _deriveBitsRaw(alg, baseKey, bits);
|
|
777
849
|
try { return await this.importKey("raw", raw, dk, extractable, keyUsages); }
|
|
778
|
-
finally { guard.secret.zeroize(new
|
|
850
|
+
finally { guard.secret.zeroize(new _Uint8Array(raw), WebCryptoError, "webcrypto/operation", "the derived key material"); }
|
|
779
851
|
};
|
|
780
852
|
|
|
781
853
|
// ML-KEM (FIPS 203) key encapsulation over Node's crypto.encapsulate/decapsulate.
|
|
@@ -791,9 +863,9 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
|
|
|
791
863
|
SubtleCrypto.prototype.encapsulateBits = async function encapsulateBits(algorithm, encapsulationKey) {
|
|
792
864
|
var alg = _normalizeAlg(algorithm, "encapsulateBits");
|
|
793
865
|
_requireUsage(encapsulationKey, "encapsulateBits");
|
|
794
|
-
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "encapsulateBits: unsupported algorithm " +
|
|
866
|
+
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "encapsulateBits: unsupported algorithm " + _stringify(alg.name));
|
|
795
867
|
_requireAlgMatch(alg, encapsulationKey, "encapsulateBits");
|
|
796
|
-
if (encapsulationKey.type !== "public") throw new WebCryptoError("webcrypto/invalid-access", "encapsulateBits requires a public (encapsulation) key, got " +
|
|
868
|
+
if (encapsulationKey.type !== "public") throw new WebCryptoError("webcrypto/invalid-access", "encapsulateBits requires a public (encapsulation) key, got " + _stringify(encapsulationKey.type));
|
|
797
869
|
var r;
|
|
798
870
|
try { r = nodeCrypto.encapsulate(encapsulationKey._handle); }
|
|
799
871
|
catch (e) { throw new WebCryptoError("webcrypto/operation", "encapsulateBits: ML-KEM encapsulation failed", e); }
|
|
@@ -814,9 +886,9 @@ SubtleCrypto.prototype.encapsulateBits = async function encapsulateBits(algorith
|
|
|
814
886
|
SubtleCrypto.prototype.decapsulateBits = async function decapsulateBits(algorithm, decapsulationKey, ciphertext) {
|
|
815
887
|
var alg = _normalizeAlg(algorithm, "decapsulateBits");
|
|
816
888
|
_requireUsage(decapsulationKey, "decapsulateBits");
|
|
817
|
-
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "decapsulateBits: unsupported algorithm " +
|
|
889
|
+
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "decapsulateBits: unsupported algorithm " + _stringify(alg.name));
|
|
818
890
|
_requireAlgMatch(alg, decapsulationKey, "decapsulateBits");
|
|
819
|
-
if (decapsulationKey.type !== "private") throw new WebCryptoError("webcrypto/invalid-access", "decapsulateBits requires a private (decapsulation) key, got " +
|
|
891
|
+
if (decapsulationKey.type !== "private") throw new WebCryptoError("webcrypto/invalid-access", "decapsulateBits requires a private (decapsulation) key, got " + _stringify(decapsulationKey.type));
|
|
820
892
|
var ct = _toBuf(ciphertext, "decapsulateBits ciphertext");
|
|
821
893
|
// FIPS 203 sec. 7.3 makes the ciphertext-length check the ONE per-execution input check a
|
|
822
894
|
// decapsulating party owes, so it belongs here, at the engine boundary, rather than only in the
|
|
@@ -858,25 +930,25 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
|
|
|
858
930
|
// exists to protect. It is this function's allocation and is cleared once the wrap has consumed
|
|
859
931
|
// it, on the delegated branch as well as the AES-KW one.
|
|
860
932
|
var exported = await this.exportKey(format, key);
|
|
861
|
-
var bytes = (format === "jwk") ?
|
|
933
|
+
var bytes = (format === "jwk") ? _bufferFrom(_stringify(exported)) : _bufferFrom(exported);
|
|
862
934
|
try {
|
|
863
935
|
var alg = _normalizeAlg(wrapAlgorithm, "wrapKey");
|
|
864
936
|
_requireUsage(wrappingKey, "wrapKey");
|
|
865
|
-
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " +
|
|
937
|
+
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + _stringify(alg.name));
|
|
866
938
|
_requireAlgMatch(alg, wrappingKey, "wrapKey");
|
|
867
939
|
if (alg.name === "AES-KW") {
|
|
868
940
|
// AES-KW wraps 64-bit blocks (RFC 3394): the serialized key MUST be a multiple of 8
|
|
869
941
|
// bytes and at least 16. A "jwk" serialization (arbitrary-length JSON) or any other
|
|
870
942
|
// non-conforming length is an OperationError -- a typed verdict, never a raw node fault.
|
|
871
943
|
if (bytes.length < 16 || bytes.length % 8 !== 0) {
|
|
872
|
-
throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW requires the serialized key be a multiple of 8 bytes (>= 16); got " + bytes.length + " -- format " +
|
|
944
|
+
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");
|
|
873
945
|
}
|
|
874
946
|
// The mirror of unwrapKey below: in a KEM flow this export is the SENDER's copy of the same
|
|
875
947
|
// key-encryption key, so leaving it unwiped would keep a full copy of the KEK alive for the
|
|
876
948
|
// process lifetime and make the wipes the CMS layer performs pointless in the encrypt direction.
|
|
877
949
|
try {
|
|
878
950
|
return _withSecretBytes(wrappingKey, function (wkBytes) {
|
|
879
|
-
var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", wkBytes,
|
|
951
|
+
var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", wkBytes, _bufferFrom("A6A6A6A6A6A6A6A6", "hex"));
|
|
880
952
|
return _toArrayBuffer(guard.secret.cipherFinish(c, bytes, WebCryptoError, "webcrypto/operation", "an AES-KW wrap intermediate"));
|
|
881
953
|
});
|
|
882
954
|
} catch (e) { throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW key wrap failed", e); }
|
|
@@ -902,7 +974,7 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
902
974
|
// never verify the caller was actually permitted to unwrap (mirrors
|
|
903
975
|
// wrapKey, which checks before both of its paths).
|
|
904
976
|
_requireUsage(unwrappingKey, "unwrapKey");
|
|
905
|
-
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "unwrapKey: unsupported algorithm " +
|
|
977
|
+
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "unwrapKey: unsupported algorithm " + _stringify(alg.name));
|
|
906
978
|
_requireAlgMatch(alg, unwrappingKey, "unwrapKey");
|
|
907
979
|
var bytes;
|
|
908
980
|
if (alg.name === "AES-KW") {
|
|
@@ -921,13 +993,13 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
921
993
|
// would also break the fuzz-harness contract.
|
|
922
994
|
try {
|
|
923
995
|
bytes = _withSecretBytes(unwrappingKey, function (kwBytes) {
|
|
924
|
-
var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", kwBytes,
|
|
996
|
+
var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", kwBytes, _bufferFrom("A6A6A6A6A6A6A6A6", "hex"));
|
|
925
997
|
return guard.secret.cipherFinish(d, wrapped, WebCryptoError, "webcrypto/operation", "the unwrapped key material");
|
|
926
998
|
});
|
|
927
999
|
} catch (e) { throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW key unwrap failed (integrity or length)", e); }
|
|
928
1000
|
} else {
|
|
929
1001
|
var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
|
|
930
|
-
bytes =
|
|
1002
|
+
bytes = _bufferFrom(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
|
|
931
1003
|
}
|
|
932
1004
|
var keyData;
|
|
933
1005
|
if (format === "jwk") {
|
|
@@ -955,7 +1027,7 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
955
1027
|
};
|
|
956
1028
|
|
|
957
1029
|
function _cloneWithUsage(key, usage) {
|
|
958
|
-
var k = new CryptoKey(key.type, key.extractable, key.algorithm, key.usages
|
|
1030
|
+
var k = new CryptoKey(key.type, key.extractable, key.algorithm, _concat(key.usages, [usage]), key._handle);
|
|
959
1031
|
return k;
|
|
960
1032
|
}
|
|
961
1033
|
|
|
@@ -1034,7 +1106,7 @@ function _validateMlKemInner(innerBytes, sizes) {
|
|
|
1034
1106
|
// per-algorithm structural check lives here, at the one import boundary. Only ML-KEM needs it
|
|
1035
1107
|
// today: the engine ACCEPTS the OpenSSL-legacy bare-seed layout RFC 9935 sec. 6 forbids.
|
|
1036
1108
|
var PKCS8_INNER_VALIDATORS = {};
|
|
1037
|
-
|
|
1109
|
+
_forEach(_keys(ML_KEM_INNER), function (o) { PKCS8_INNER_VALIDATORS[o] = function (inner) { _validateMlKemInner(inner, ML_KEM_INNER[o]); }; });
|
|
1038
1110
|
|
|
1039
1111
|
function _preValidatePkcs8(p8, name) {
|
|
1040
1112
|
if (!ML_KEM_NODE[name]) return; // only the ML-KEM CHOICE carries the bare-seed hazard
|
|
@@ -1090,7 +1162,7 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
|
|
|
1090
1162
|
// A JWK is a JSON object; null / undefined / an array / a primitive (incl. the JSON token
|
|
1091
1163
|
// `null` from an unwrapKey over non-authenticating ciphertext) fails closed as a typed
|
|
1092
1164
|
// DataError, never a raw property-access TypeError.
|
|
1093
|
-
if (jwk === null || typeof jwk !== "object" ||
|
|
1165
|
+
if (jwk === null || typeof jwk !== "object" || _isArray(jwk)) {
|
|
1094
1166
|
throw new WebCryptoError("webcrypto/data", "importKey jwk: keyData must be a JsonWebKey object");
|
|
1095
1167
|
}
|
|
1096
1168
|
if (jwk.kty === "oct") {
|
|
@@ -1109,12 +1181,12 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
|
|
|
1109
1181
|
// unrecognized member to be ignored, so a member of that name on an EC or OKP JWK is an
|
|
1110
1182
|
// extension this implementation has no meaning for -- reading it as private material there
|
|
1111
1183
|
// would turn a valid public-key import into a failure.
|
|
1112
|
-
var isPrivate =
|
|
1113
|
-
(jwk.kty === "AKP" &&
|
|
1184
|
+
var isPrivate = _hasOwn(jwk, "d") ||
|
|
1185
|
+
(jwk.kty === "AKP" && _hasOwn(jwk, "priv"));
|
|
1114
1186
|
var ko = _nodeKey(function () { return isPrivate ? nodeCrypto.createPrivateKey({ key: jwk, format: "jwk" }) : nodeCrypto.createPublicKey({ key: jwk, format: "jwk" }); }, "importKey jwk");
|
|
1115
1187
|
return new CryptoKey(isPrivate ? "private" : "public", isPrivate ? extractable : true, _algFromImport(name, alg, ko), usages, ko);
|
|
1116
1188
|
}
|
|
1117
|
-
throw new WebCryptoError("webcrypto/not-supported", "importKey: unsupported format " +
|
|
1189
|
+
throw new WebCryptoError("webcrypto/not-supported", "importKey: unsupported format " + _stringify(format));
|
|
1118
1190
|
};
|
|
1119
1191
|
|
|
1120
1192
|
function _importRawPublic(name, alg, raw, extractable, usages) {
|
|
@@ -1128,11 +1200,11 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
|
|
|
1128
1200
|
if (name === "ECDSA" || name === "ECDH") {
|
|
1129
1201
|
var fb = CURVE_FIELD_BYTES[alg.namedCurve];
|
|
1130
1202
|
if (!fb || raw[0] !== 0x04 || raw.length !== 1 + 2 * fb) throw new WebCryptoError("webcrypto/data", "importKey raw EC: expected an uncompressed point for " + alg.namedCurve);
|
|
1131
|
-
var ecjwk = { kty: "EC", crv: alg.namedCurve, x: _bufToB64url(raw
|
|
1203
|
+
var ecjwk = { kty: "EC", crv: alg.namedCurve, x: _bufToB64url(_subarray(raw, 1, 1 + fb)), y: _bufToB64url(_subarray(raw, 1 + fb)) };
|
|
1132
1204
|
var eck = nodeCrypto.createPublicKey({ key: ecjwk, format: "jwk" });
|
|
1133
1205
|
return new CryptoKey("public", true, { name: name, namedCurve: alg.namedCurve }, usages, eck);
|
|
1134
1206
|
}
|
|
1135
|
-
throw new WebCryptoError("webcrypto/not-supported", "importKey raw: unsupported public-key algorithm " +
|
|
1207
|
+
throw new WebCryptoError("webcrypto/not-supported", "importKey raw: unsupported public-key algorithm " + _stringify(name));
|
|
1136
1208
|
}
|
|
1137
1209
|
|
|
1138
1210
|
// The node asymmetricKeyType(s) each imported asymmetric algorithm requires. The key TYPE
|
|
@@ -1149,7 +1221,7 @@ var IMPORT_KEY_TYPE = {
|
|
|
1149
1221
|
// algorithm name maps to exactly its node asymmetricKeyType, so an RSA (or any) key imported
|
|
1150
1222
|
// under an ML-DSA / ML-KEM / SLH-DSA name is a DataError, not a mislabeled PQC CryptoKey.
|
|
1151
1223
|
[ML_DSA_NODE, ML_KEM_NODE, SLH_DSA_NODE].forEach(function (m) {
|
|
1152
|
-
|
|
1224
|
+
_forEach(_keys(m), function (webName) { var t = {}; t[m[webName]] = 1; IMPORT_KEY_TYPE[webName] = t; });
|
|
1153
1225
|
});
|
|
1154
1226
|
function _algFromImport(name, alg, keyObject) {
|
|
1155
1227
|
var wantType = IMPORT_KEY_TYPE[name];
|
|
@@ -1169,8 +1241,8 @@ function _algFromImport(name, alg, keyObject) {
|
|
|
1169
1241
|
throw new WebCryptoError("webcrypto/not-supported", name + ": imported key uses an unsupported EC curve");
|
|
1170
1242
|
}
|
|
1171
1243
|
if (alg.namedCurve && alg.namedCurve !== actualCurve) {
|
|
1172
|
-
throw new WebCryptoError("webcrypto/data", name + ": importKey namedCurve " +
|
|
1173
|
-
" does not match the imported key's curve " +
|
|
1244
|
+
throw new WebCryptoError("webcrypto/data", name + ": importKey namedCurve " + _stringify(alg.namedCurve) +
|
|
1245
|
+
" does not match the imported key's curve " + _stringify(actualCurve));
|
|
1174
1246
|
}
|
|
1175
1247
|
return { name: name, namedCurve: actualCurve };
|
|
1176
1248
|
}
|
|
@@ -1243,13 +1315,13 @@ SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
|
|
|
1243
1315
|
}
|
|
1244
1316
|
return _toArrayBuffer(_rawPublic(key));
|
|
1245
1317
|
}
|
|
1246
|
-
throw new WebCryptoError("webcrypto/not-supported", "exportKey: unsupported format " +
|
|
1318
|
+
throw new WebCryptoError("webcrypto/not-supported", "exportKey: unsupported format " + _stringify(format));
|
|
1247
1319
|
};
|
|
1248
1320
|
|
|
1249
1321
|
function _rawPublic(key) {
|
|
1250
1322
|
var jwk = key._handle.export({ format: "jwk" });
|
|
1251
1323
|
if (jwk.kty === "OKP") return _b64urlToBuf(jwk.x);
|
|
1252
|
-
if (jwk.kty === "EC") return
|
|
1324
|
+
if (jwk.kty === "EC") return _bufferConcat([_bufferFrom([0x04]), _b64urlToBuf(jwk.x), _b64urlToBuf(jwk.y)]);
|
|
1253
1325
|
throw new WebCryptoError("webcrypto/not-supported", "exportKey raw: unsupported key type " + jwk.kty);
|
|
1254
1326
|
}
|
|
1255
1327
|
|
|
@@ -1275,7 +1347,10 @@ function Crypto() {
|
|
|
1275
1347
|
}
|
|
1276
1348
|
|
|
1277
1349
|
Crypto.prototype.getRandomValues = function getRandomValues(typedArray) {
|
|
1278
|
-
|
|
1350
|
+
// The kinds W3C WebCrypto refuses here are settled from the value's internal slot, not from an
|
|
1351
|
+
// `instanceof` against a replaceable global: replace `Float32Array` after load and the comparison
|
|
1352
|
+
// is against a different constructor, so a real Float32Array passes a check written to reject it.
|
|
1353
|
+
if (!_isView(typedArray) || _types.isFloat32Array(typedArray) || _types.isFloat64Array(typedArray) || _types.isDataView(typedArray)) {
|
|
1279
1354
|
throw new WebCryptoError("webcrypto/data", "getRandomValues: expected an integer TypedArray");
|
|
1280
1355
|
}
|
|
1281
1356
|
// Re-view the (already integer-TypedArray-validated) output buffer through the OUTPUT door --
|
|
@@ -1327,14 +1402,14 @@ function decompressEcPoint(sec1Compressed, nodeCurve, E, code) {
|
|
|
1327
1402
|
var _crypto = new Crypto();
|
|
1328
1403
|
// Null-prototype: is caller-supplied, and an inherited member ("constructor", "toString")
|
|
1329
1404
|
// would otherwise resolve to a truthy non-format and slip past the check below.
|
|
1330
|
-
var _EXPORT_FORMAT =
|
|
1405
|
+
var _EXPORT_FORMAT = _assign(_create(null), { "private": "pkcs8", "public": "spki", "secret": "raw" });
|
|
1331
1406
|
|
|
1332
1407
|
// @internal
|
|
1333
1408
|
// True for any object shaped like a WebCrypto CryptoKey, whichever implementation minted it.
|
|
1334
1409
|
function isCryptoKeyLike(x) {
|
|
1335
1410
|
return !!x && typeof x === "object" && typeof x.type === "string" &&
|
|
1336
1411
|
typeof x.extractable === "boolean" && !!x.algorithm && typeof x.algorithm === "object" &&
|
|
1337
|
-
typeof x.algorithm.name === "string" &&
|
|
1412
|
+
typeof x.algorithm.name === "string" && _isArray(x.usages);
|
|
1338
1413
|
}
|
|
1339
1414
|
|
|
1340
1415
|
// @internal
|
|
@@ -1346,9 +1421,9 @@ function exportAnyKey(key, E, code) {
|
|
|
1346
1421
|
var format = _EXPORT_FORMAT[key && key.type];
|
|
1347
1422
|
if (!format) throw E(code, "a CryptoKey with a private, public, or secret type is required");
|
|
1348
1423
|
if (key instanceof CryptoKey) {
|
|
1349
|
-
return Promise
|
|
1424
|
+
return _resolve(Promise)
|
|
1350
1425
|
.then(function () { return _crypto.subtle.exportKey(format, key); })
|
|
1351
|
-
.then(function (b) { return
|
|
1426
|
+
.then(function (b) { return _bufferFrom(b); });
|
|
1352
1427
|
}
|
|
1353
1428
|
if (!key.extractable) {
|
|
1354
1429
|
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");
|
|
@@ -1360,18 +1435,18 @@ function exportAnyKey(key, E, code) {
|
|
|
1360
1435
|
// key's own extractable promise is this branch's responsibility, not the crypto library's.
|
|
1361
1436
|
var handle = key._handle;
|
|
1362
1437
|
if (handle instanceof nodeCrypto.KeyObject && handle.type === key.type) {
|
|
1363
|
-
return Promise
|
|
1438
|
+
return _resolve(Promise).then(function () {
|
|
1364
1439
|
return format === "raw" ? handle.export() : handle.export({ format: "der", type: format });
|
|
1365
|
-
}).then(function (b) { return
|
|
1440
|
+
}).then(function (b) { return _bufferFrom(b); }, function (e) {
|
|
1366
1441
|
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);
|
|
1367
1442
|
});
|
|
1368
1443
|
}
|
|
1369
1444
|
// Otherwise the only remaining implementation whose keys are reachable from this process is the
|
|
1370
1445
|
// platform's own. A third party's -- a browser's, a userland polyfill's -- keeps its material
|
|
1371
1446
|
// behind its own SubtleCrypto, which nothing here holds a reference to, so it is refused below.
|
|
1372
|
-
return Promise
|
|
1447
|
+
return _resolve(Promise)
|
|
1373
1448
|
.then(function () { return nodeCrypto.webcrypto.subtle.exportKey(format, key); })
|
|
1374
|
-
.then(function (b) { return
|
|
1449
|
+
.then(function (b) { return _bufferFrom(b); }, function (e) {
|
|
1375
1450
|
throw E(code, "the CryptoKey comes from a different WebCrypto implementation and could not be exported for re-import; import it through pki.webcrypto.subtle, or pass the key as DER", e);
|
|
1376
1451
|
});
|
|
1377
1452
|
}
|
|
@@ -1381,19 +1456,19 @@ function exportAnyKey(key, E, code) {
|
|
|
1381
1456
|
// key of its own keeps working -- and a foreign one is re-imported. `importParams` may be null, in
|
|
1382
1457
|
// which case the key's own `algorithm` is used, which is what the key actually is.
|
|
1383
1458
|
function adoptKey(key, importParams, usages, E, code) {
|
|
1384
|
-
if (key instanceof CryptoKey) return Promise
|
|
1459
|
+
if (key instanceof CryptoKey) return _resolve(Promise, key);
|
|
1385
1460
|
// A key's usages are a capability restriction it carries, and re-importing it is the one moment
|
|
1386
1461
|
// that restriction could be widened -- the new key is created with the usages the CALLER wants,
|
|
1387
1462
|
// not the ones the original was created with. Require every one of them up front, so an adopted
|
|
1388
1463
|
// key can do no more than it could where it came from, and a verify-only key is refused here
|
|
1389
1464
|
// exactly as this engine's own verify-only key is refused by _requireUsage.
|
|
1390
|
-
if (!
|
|
1465
|
+
if (!_isArray(key.usages)) throw E(code, "a CryptoKey carrying its permitted usages is required");
|
|
1391
1466
|
for (var i = 0; i < usages.length; i++) {
|
|
1392
|
-
if (key.usages
|
|
1393
|
-
throw E(code, "the CryptoKey is not permitted for '" + usages[i] + "' (usages: " + key.usages
|
|
1467
|
+
if (_indexOf(key.usages, usages[i]) === -1) {
|
|
1468
|
+
throw E(code, "the CryptoKey is not permitted for '" + usages[i] + "' (usages: " + _join(key.usages, ",") + ")");
|
|
1394
1469
|
}
|
|
1395
1470
|
}
|
|
1396
|
-
return Promise
|
|
1471
|
+
return _resolve(Promise)
|
|
1397
1472
|
.then(function () { return exportAnyKey(key, E, code); })
|
|
1398
1473
|
.then(function (der) {
|
|
1399
1474
|
return _crypto.subtle.importKey(_EXPORT_FORMAT[key.type], der, importParams || key.algorithm, false, usages);
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:d0064cbc-a37c-4d4f-ae67-0b9ba6887de2",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-20T21:35:06.293Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.5.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.5.19",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.5.
|
|
25
|
+
"version": "0.5.19",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.5.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.5.19",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.5.
|
|
57
|
+
"ref": "@blamejs/pki@0.5.19",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|