@blamejs/pki 0.6.14 → 0.6.16
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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- 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) {
|
|
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) {
|
|
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) {
|
|
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;
|
|
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) {
|
|
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) {
|