@blamejs/pki 0.2.22 → 0.2.24

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.
@@ -0,0 +1,539 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the pki.cms.decrypt implementation. The operator-facing @module pki.cms + the
6
+ // @primitive pki.cms.decrypt block live in cms-verify.js, which re-exports this function.
7
+ //
8
+ // CMS EnvelopedData / AuthEnvelopedData / EncryptedData decryption (RFC 5652/5083/5084/3560/5753/
9
+ // 8418/9629/9936/3211/8018), the consuming side of pki.cms.encrypt. Three stages behind ONE
10
+ // oracle-free choke point: (1) SELECT the recipient (identifier matching -- typed + distinct),
11
+ // (2) ACQUIRE the CEK (per-arm unwrap/decap/derive), (3) OPEN the content (AEAD verify / CBC
12
+ // decrypt+unpad). Every secret-dependent failure in stages 2-3 collapses to the SINGLE uniform
13
+ // verdict `cms/decrypt-failed` (Bleichenbacher / EFAIL / PWRI-check-byte oracle freedom); the
14
+ // ktri v1.5 arm substitutes a random CEK on any decode fault (RFC 3218 implicit rejection) so its
15
+ // failure emerges at stage 3 identically to every other bad-key path.
16
+
17
+ var nodeCrypto = require("crypto");
18
+ var asn1 = require("./asn1-der");
19
+ var oid = require("./oid");
20
+ var x509 = require("./schema-x509");
21
+ var pkcs8 = require("./schema-pkcs8");
22
+ var schemaCms = require("./schema-cms");
23
+ var webcrypto = require("./webcrypto");
24
+ var frameworkError = require("./framework-error");
25
+ var guard = require("./guard-all");
26
+ var C = require("./constants");
27
+ var b = asn1.build;
28
+ var subtle = webcrypto.webcrypto.subtle;
29
+ var CmsError = frameworkError.CmsError;
30
+ var WRAP_KEK_LENGTHS = schemaCms.WRAP_KEK_LENGTHS;
31
+ var KEM_CT_LENGTHS = schemaCms.KEM_CT_LENGTHS;
32
+
33
+ function O(n) { return oid.byName(n); }
34
+ function _err(code, message, cause) { return new CmsError(code, message, cause); }
35
+ // The ONE uniform secret-dependent-failure verdict. No cause chaining that distinguishes the site.
36
+ function _fail() { return new CmsError("cms/decrypt-failed", "the CMS content could not be decrypted (uniform by design -- padding / integrity / key-unwrap failures are indistinguishable to defeat oracles)"); }
37
+
38
+ var CONTENT_KEYBITS = {}; // content-encryption OID -> key bits
39
+ [["aes128-CBC", 128], ["aes192-CBC", 192], ["aes256-CBC", 256], ["aes128-GCM", 128], ["aes192-GCM", 192], ["aes256-GCM", 256]].forEach(function (r) { CONTENT_KEYBITS[O(r[0])] = r[1]; });
40
+
41
+ // ---- entry -----------------------------------------------------------------
42
+ async function decrypt(input, keyMaterial, opts) {
43
+ opts = opts || {};
44
+ if (keyMaterial == null || typeof keyMaterial !== "object") throw _err("cms/bad-input", "decrypt requires a key-material object");
45
+ var parsed = _parse(input);
46
+ var ct = parsed.contentTypeName;
47
+ if (ct === "encryptedData") return _decryptEncryptedData(parsed, keyMaterial, opts);
48
+ if (ct !== "envelopedData" && ct !== "authEnvelopedData") throw _err("cms/bad-input", "input is not an EnvelopedData / AuthEnvelopedData / EncryptedData (got " + ct + ")");
49
+
50
+ var recips = parsed.recipientInfos || [];
51
+ var candidates = _selectCandidates(recips, keyMaterial, opts); // stage 1 (typed, distinct)
52
+ var eci = parsed.encryptedContentInfo;
53
+ // Password- and KEK-based recipients carry no rid, so several may match the same key material; try
54
+ // each until one yields a CEK that opens the content. A single candidate propagates its exact
55
+ // verdict; multiple ambiguous candidates collapse to the uniform verdict so nothing is leaked.
56
+ for (var ci = 0; ci < candidates.length; ci++) {
57
+ try {
58
+ _assertSupported(candidates[ci].ri, keyMaterial); // distinct-code reject (MQV / non-KEM ori)
59
+ var cek = await _acquireCek(candidates[ci].ri, keyMaterial, opts); // stage 2 (uniform)
60
+ var content = await _openContent(parsed, eci, cek, ct); // stage 3 (uniform)
61
+ return {
62
+ content: content,
63
+ contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType,
64
+ recipientType: candidates[ci].ri.type, recipientIndex: candidates[ci].index,
65
+ contentEncryptionAlgorithm: eci.contentEncryptionAlgorithm.name || eci.contentEncryptionAlgorithm.oid,
66
+ authenticated: ct === "authEnvelopedData",
67
+ };
68
+ } catch (e) {
69
+ if (candidates.length === 1) throw e; // one candidate: preserve its exact (distinct or uniform) verdict
70
+ }
71
+ }
72
+ throw _fail(); // every ambiguous candidate failed -> uniform, leaking nothing about which
73
+ }
74
+
75
+ function _parse(input) {
76
+ // Always drive the strict parser over caller-supplied bytes -- an arbitrary object that merely
77
+ // carries a contentTypeName is not a trusted parse result and must not bypass DER validation.
78
+ return schemaCms.parse(_toDer(input));
79
+ }
80
+ function _toDer(input) {
81
+ if (Buffer.isBuffer(input)) return input;
82
+ if (input instanceof Uint8Array) return Buffer.from(input);
83
+ if (typeof input === "string") { try { return schemaCms.pemDecode(input); } catch (e) { throw _err("cms/bad-input", "the CMS PEM could not be decoded", e); } }
84
+ throw _err("cms/bad-input", "input must be a DER Buffer, Uint8Array, or PEM string");
85
+ }
86
+
87
+ // ---- stage 1: recipient selection (NOT secret-dependent) -------------------
88
+ // Returns every recipient the key material may target. An explicit recipientIndex or an rid match
89
+ // yields exactly one; unlabelled pwri / kekri recipients yield all matches for the caller to try.
90
+ function _selectCandidates(recips, km, opts) {
91
+ if (!recips.length) throw _err("cms/no-matching-recipient", "the message carries no RecipientInfos");
92
+ if (opts.recipientIndex != null) {
93
+ var i = opts.recipientIndex;
94
+ if (typeof i !== "number" || !isFinite(i) || i < 0 || i >= recips.length || Math.floor(i) !== i) throw _err("cms/bad-input", "recipientIndex must be an in-range integer");
95
+ return [{ ri: recips[i], index: i }];
96
+ }
97
+ var want = _riKindForKey(km);
98
+ var cert = km.cert != null ? x509.parse(_normCertDer(km.cert)) : null;
99
+ var out = [];
100
+ for (var k = 0; k < recips.length; k++) {
101
+ var r = recips[k];
102
+ if (want === "asym" && (r.type === "ktri" || r.type === "kari" || r.type === "ori")) {
103
+ if (cert && _ridMatches(r, cert)) out.push({ ri: r, index: k }); // a cert may match several recipients
104
+ } else if (want === "pwri" && r.type === "pwri") { out.push({ ri: r, index: k }); }
105
+ else if (want === "kekri" && r.type === "kekri") {
106
+ if (km.kekId == null || _kekIdMatches(r, km.kekId)) out.push({ ri: r, index: k });
107
+ }
108
+ }
109
+ if (!out.length) throw _err("cms/no-matching-recipient", "no recipient matches the supplied key material");
110
+ return out;
111
+ }
112
+ function _riKindForKey(km) {
113
+ if (km.password != null) return "pwri";
114
+ if (km.kek != null) return "kekri";
115
+ if (km.key != null) return "asym";
116
+ throw _err("cms/bad-input", "key material needs { key, cert }, { password }, { kek }, or { cek }");
117
+ }
118
+ function _assertSupported(ri, km) {
119
+ if (ri.type === "kari" && ri.keyEncryptionAlgorithm && /mqv/i.test(ri.keyEncryptionAlgorithm.name || "")) throw _err("cms/unsupported-algorithm", "ECMQV kari is not supported");
120
+ if (ri.type === "ori") {
121
+ if (ri.oriType !== O("kem")) throw _err("cms/unsupported-recipient-type", "unsupported OtherRecipientInfo type " + ri.oriType);
122
+ if (ri.kemri && ri.kemri.kem && ri.kemri.kem.oid === O("id-kem-rsa")) throw _err("cms/unsupported-algorithm", "RSA-KEM is not supported");
123
+ }
124
+ void km;
125
+ }
126
+
127
+ function _ridMatches(ri, cert) {
128
+ // A kari carries a RecipientEncryptedKey per agreeing recipient (RFC 5652 sec. 6.2.2); this
129
+ // recipient may be at ANY position, so match against every rek, not just element 0.
130
+ if (ri.type === "kari") return !!_kariRekFor(ri, cert);
131
+ var rid = ri.rid || (ri.kemri && ri.kemri.rid);
132
+ if (!rid) return false;
133
+ return _ridEq(rid, cert);
134
+ }
135
+ function _kariRekFor(ri, cert) {
136
+ var reks = ri.recipientEncryptedKeys || [];
137
+ for (var i = 0; i < reks.length; i++) if (reks[i].rid && _ridEq(reks[i].rid, cert)) return reks[i];
138
+ return null;
139
+ }
140
+ function _ridEq(rid, cert) {
141
+ if (rid.issuer && rid.serialNumber != null) {
142
+ try { return guard.name.dnEqual(cert.issuer.rdns, rid.issuer.rdns, _err, "cms/bad-input", "recipient issuer") && cert.serialNumber === rid.serialNumber; }
143
+ catch (_e) { return false; }
144
+ }
145
+ if (rid.subjectKeyIdentifier) { var ski = _skiOf(cert); return !!ski && Buffer.compare(ski, rid.subjectKeyIdentifier) === 0; }
146
+ return false; // coverage residual: unreachable -- a parsed rid is always issuerAndSerialNumber or a SKI form
147
+ }
148
+ function _skiOf(cert) {
149
+ var exts = cert.extensions || [];
150
+ for (var i = 0; i < exts.length; i++) if (exts[i].name === "subjectKeyIdentifier" && exts[i].value != null) {
151
+ try { return asn1.read.octetString(asn1.decode(exts[i].value)); } catch (e) { throw _err("cms/bad-input", "the certificate's subjectKeyIdentifier extension is malformed", e); }
152
+ }
153
+ return null;
154
+ }
155
+ function _kekIdMatches(ri, kekId) {
156
+ var id = ri.kekid && ri.kekid.keyIdentifier;
157
+ if (!id) return false;
158
+ return Buffer.compare(id, guard.bytes.view(kekId, CmsError, "cms/bad-input", "kekId")) === 0;
159
+ }
160
+
161
+ // Codes that name a structural / resource / config fault and MUST NOT be masked by the uniform
162
+ // secret-dependent verdict (they leak nothing about the key or plaintext).
163
+ var _passThrough = { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1 };
164
+
165
+ // ---- stage 2: acquire the CEK (uniform failure) ----------------------------
166
+ async function _acquireCek(ri, km, opts) {
167
+ try {
168
+ if (ri.type === "ktri") return await _ktriCek(ri, km);
169
+ if (ri.type === "kari") return await _kariCek(ri, km);
170
+ if (ri.type === "kekri") return await _kekriCek(ri, km);
171
+ if (ri.type === "pwri") return await _pwriCek(ri, km, opts);
172
+ if (ri.type === "ori") return await _kemriCek(ri, km);
173
+ } catch (e) {
174
+ // Structural / resource / config faults keep their own code -- only SECRET-DEPENDENT failures
175
+ // (a bad unwrap, a wrong key, a padding fault) collapse to the uniform verdict.
176
+ if (e instanceof CmsError && _passThrough[e.code]) throw e;
177
+ throw _fail();
178
+ }
179
+ // Coverage residual: unreachable -- _selectCandidates only returns one of the five handled types.
180
+ throw _err("cms/unsupported-recipient-type", "unsupported recipient type " + ri.type);
181
+ }
182
+
183
+ // ktri: OAEP or PKCS#1 v1.5 (v1.5 = decrypt-only + RFC 3218 implicit rejection).
184
+ async function _ktriCek(ri, km) {
185
+ var kea = ri.keyEncryptionAlgorithm;
186
+ var keyDer = _normKeyDer(km.key);
187
+ if (kea.oid === O("rsaesOaep")) {
188
+ var hash = _oaepHashFromParams(kea.parameters);
189
+ var pub = await subtle.importKey("pkcs8", keyDer, { name: "RSA-OAEP", hash: hash }, false, ["decrypt"]);
190
+ return Buffer.from(await subtle.decrypt({ name: "RSA-OAEP" }, pub, ri.encryptedKey));
191
+ }
192
+ if (kea.oid === O("rsaEncryption")) {
193
+ // RFC 3218 sec. 2.3.2 implicit rejection: NEVER surface a v1.5 failure here. Any decode fault
194
+ // yields a fresh random CEK of the content-alg length; the mismatch emerges at stage 3.
195
+ var keyObj = nodeCrypto.createPrivateKey({ key: keyDer, format: "der", type: "pkcs8" });
196
+ try { return nodeCrypto.privateDecrypt({ key: keyObj, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }, ri.encryptedKey); }
197
+ catch (_e) { return null; } // signal: use a random CEK (length decided at open time)
198
+ }
199
+ // Coverage residual: reachable only from a hostile message (our encrypt emits only OAEP; OpenSSL
200
+ // emits OAEP or rsaEncryption) -- a fail-closed reject the fuzz harness exercises.
201
+ throw _err("cms/unsupported-algorithm", "unsupported ktri keyEncryptionAlgorithm " + kea.oid);
202
+ }
203
+
204
+ // kari: reconstruct Z from the originatorKey + recipient private key, KDF -> KEK, AES-KW unwrap.
205
+ async function _kariCek(ri, km) {
206
+ var keyDer = _normKeyDer(km.key);
207
+ var kea = ri.keyEncryptionAlgorithm;
208
+ var wrapAlg = _kariWrap(kea);
209
+ var scheme = kea.oid;
210
+ var origSpki = _originatorSpki(ri.originator);
211
+ // Unwrap THIS recipient's RecipientEncryptedKey (matched by rid), not element 0 -- a kari may list
212
+ // several recipients under one ephemeral key.
213
+ var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
214
+ var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
215
+ if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
216
+ var ukm = ri.ukm || null;
217
+ var kek;
218
+ if (_isMont(origSpki)) {
219
+ var mont = _montName(origSpki);
220
+ var recipPriv = await subtle.importKey("pkcs8", keyDer, { name: mont.name }, false, ["deriveBits"]);
221
+ var origPub = await subtle.importKey("spki", origSpki, { name: mont.name }, false, []);
222
+ var mz = Buffer.from(await subtle.deriveBits({ name: mont.name, public: origPub }, recipPriv, null));
223
+ if (mz.every(function (x) { return x === 0; })) throw _fail();
224
+ var mzKey = await subtle.importKey("raw", mz, { name: "HKDF" }, false, ["deriveBits"]);
225
+ // RFC 8418 sec. 2.2: a present ukm is used BOTH as the HKDF salt AND as the ECC-CMS-SharedInfo
226
+ // entityUInfo -- mirror the producer so both sides derive the same KEK as a conformant peer.
227
+ kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: mont.hkdf, salt: ukm || Buffer.alloc(0), info: _eccSharedInfo(wrapAlg.name, ukm, kekBytes) }, mzKey, kekBytes * 8));
228
+ } else {
229
+ // RFC 5753 sec. 7.1 permits the originator EC key to omit its curve parameters, inheriting the
230
+ // curve from the recipient's certificate; resolve the curve from the recipient (authoritative)
231
+ // and rebuild the originator SPKI with explicit parameters so importKey can consume it.
232
+ var origAlg = asn1.decode(origSpki).children[0];
233
+ var origHasParams = origAlg.children.length > 1;
234
+ var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) || (origHasParams ? asn1.read.oid(origAlg.children[1]) : null);
235
+ var curve = curveOid ? CURVE[curveOid] : null;
236
+ if (!curve) throw _err("cms/unsupported-algorithm", "unsupported or missing originator EC curve");
237
+ var origSpkiFull = origHasParams ? origSpki : _withEcCurveParams(origSpki, curveOid);
238
+ var recipEc = await subtle.importKey("pkcs8", keyDer, { name: "ECDH", namedCurve: curve.curve }, false, ["deriveBits"]);
239
+ var origEc = await subtle.importKey("spki", origSpkiFull, { name: "ECDH", namedCurve: curve.curve }, false, []);
240
+ var z = Buffer.from(await subtle.deriveBits({ name: "ECDH", public: origEc }, recipEc, null));
241
+ var zKey = await subtle.importKey("raw", z, { name: "X963KDF" }, false, ["deriveBits"]);
242
+ kek = Buffer.from(await subtle.deriveBits({ name: "X963KDF", hash: _x963Hash(scheme), info: _eccSharedInfo(wrapAlg.name, ukm, kekBytes) }, zKey, kekBytes * 8));
243
+ }
244
+ return await _aesKwUnwrap(kek, rek.encryptedKey);
245
+ }
246
+
247
+ // kekri: AES-KW unwrap under the caller-supplied KEK.
248
+ async function _kekriCek(ri, km) {
249
+ var kek = guard.bytes.view(km.kek, CmsError, "cms/bad-input", "kek");
250
+ return await _aesKwUnwrap(kek, ri.encryptedKey);
251
+ }
252
+
253
+ // pwri: PBKDF2 -> KEK, RFC 3211 double-CBC unwrap.
254
+ async function _pwriCek(ri, km, opts) {
255
+ var kdf = ri.keyDerivationAlgorithm;
256
+ if (!kdf) throw _err("cms/missing-key-derivation", "the pwri recipient has no keyDerivationAlgorithm (externally-supplied KEK is not supported)");
257
+ if (kdf.oid !== O("pbkdf2")) throw _err("cms/unsupported-algorithm", "unsupported pwri key-derivation " + kdf.oid);
258
+ var pb = _pbkdf2Params(kdf.parameters, opts);
259
+ var kea = ri.keyEncryptionAlgorithm;
260
+ if (kea.oid !== O("id-alg-PWRI-KEK")) throw _err("cms/unsupported-algorithm", "unsupported pwri key-encryption " + kea.oid);
261
+ var inner = asn1.decode(kea.parameters); // inner AES-CBC AlgorithmIdentifier
262
+ var innerOid = asn1.read.oid(inner.children[0]);
263
+ var innerBits = CONTENT_KEYBITS[innerOid];
264
+ if (!innerBits || !/CBC/.test(oid.name(innerOid) || "")) throw _err("cms/unsupported-algorithm", "unsupported pwri inner cipher");
265
+ var iv = asn1.read.octetString(inner.children[1]);
266
+ var kek = nodeCrypto.pbkdf2Sync(_passwordBytes(km.password), pb.salt, pb.iterations, innerBits / 8, pb.prfNode);
267
+ return _pwriUnwrap(kek, ri.encryptedKey, iv, innerBits);
268
+ }
269
+
270
+ // kemri (ML-KEM ori): decapsulate -> ss, HKDF(CMSORIforKEMOtherInfo) -> KEK, AES-KW unwrap.
271
+ async function _kemriCek(ri, km) {
272
+ var k = ri.kemri;
273
+ var wcName = _mlkemName(k.kem.oid);
274
+ if (!wcName) throw _err("cms/unsupported-algorithm", "unsupported KEM " + k.kem.oid);
275
+ // Honor the declared key-derivation function: we implement HKDF-SHA256 (RFC 9629), so a message
276
+ // naming any other KDF is rejected rather than silently derived with the wrong one.
277
+ if (k.kdf.oid !== O("hkdfWithSha256")) throw _err("cms/unsupported-algorithm", "unsupported KEM key-derivation " + k.kdf.oid);
278
+ var wantCt = KEM_CT_LENGTHS[k.kem.oid];
279
+ var kemct = k.kemct;
280
+ // Coverage residual (both _fail re-checks): unreachable via a parsed message -- the strict parser
281
+ // already rejects a wrong kemct length (cms/bad-kem-ciphertext) and a kekLength != wrap size
282
+ // (cms/kek-length-mismatch). These are defense-in-depth on the consumer path (M32 / M29).
283
+ if (wantCt && kemct.length !== wantCt) throw _fail(); // M32: exact ct length BEFORE decap
284
+ var kekBytes = Number(k.kekLength);
285
+ var wrapAlg = k.wrap;
286
+ if (WRAP_KEK_LENGTHS[wrapAlg.oid] !== kekBytes) throw _fail(); // M29 re-check on the consumer path
287
+ var priv = await subtle.importKey("pkcs8", _normKeyDer(km.key), { name: wcName }, false, ["decapsulateBits"]);
288
+ var ss = Buffer.from(await subtle.decapsulateBits({ name: wcName }, priv, kemct));
289
+ var ssKey = await subtle.importKey("raw", ss, { name: "HKDF" }, false, ["deriveBits"]);
290
+ var kek = Buffer.from(await subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: Buffer.alloc(0), info: _kemOtherInfo(wrapAlg.name, kekBytes, k.ukm || null) }, ssKey, kekBytes * 8));
291
+ return await _aesKwUnwrap(kek, k.encryptedKey);
292
+ }
293
+
294
+ // ---- stage 3: open the content (uniform failure) ---------------------------
295
+ async function _openContent(parsed, eci, cek, ct) {
296
+ var alg = eci.contentEncryptionAlgorithm;
297
+ var keyBits = CONTENT_KEYBITS[alg.oid];
298
+ if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported contentEncryptionAlgorithm " + alg.oid);
299
+ if (eci.encryptedContent == null) throw _err("cms/no-encrypted-content", "the message has no encryptedContent (detached; supply it out of band)");
300
+ // A null CEK (v1.5 implicit rejection) or a wrong-length CEK -> a fresh random key of the right
301
+ // length, so the failure surfaces here as the uniform verdict, never earlier.
302
+ if (cek == null || cek.length !== keyBits / 8) cek = nodeCrypto.randomBytes(keyBits / 8);
303
+ try {
304
+ if (ct === "authEnvelopedData") {
305
+ var aad = parsed.authAttrsBytes != null ? _explicitSetOf(parsed.authAttrsBytes) : Buffer.alloc(0);
306
+ return _gcmOpen(cek, parsed.aead.nonce, eci.encryptedContent, parsed.mac, aad, keyBits, parsed.aead.icvLen);
307
+ }
308
+ var iv = asn1.read.octetString(asn1.decode(alg.parameters));
309
+ if (iv.length !== 16) throw _fail();
310
+ return _cbcOpen(cek, iv, eci.encryptedContent, keyBits);
311
+ } catch (e) {
312
+ if (e instanceof CmsError && e.code !== "cms/decrypt-failed") throw e;
313
+ throw _fail();
314
+ }
315
+ }
316
+ function _gcmOpen(cek, nonce, ct, tag, aad, keyBits, icvLen) {
317
+ // Coverage residual: unreachable via a parsed message -- the strict parser rejects any
318
+ // AuthEnvelopedData whose mac length != aes-ICVlen (cms/mac-length-mismatch) before we get here.
319
+ if (!tag || tag.length !== icvLen) throw _fail();
320
+ var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-gcm", cek, nonce, { authTagLength: icvLen });
321
+ d.setAuthTag(tag);
322
+ if (aad && aad.length) d.setAAD(aad);
323
+ return Buffer.concat([d.update(ct), d.final()]); // final() throws on tag mismatch -> uniform
324
+ }
325
+ function _cbcOpen(cek, iv, ct, keyBits) {
326
+ var d = nodeCrypto.createDecipheriv("aes-" + keyBits + "-cbc", cek, iv);
327
+ return Buffer.concat([d.update(ct), d.final()]); // final() throws on bad pad -> uniform
328
+ }
329
+
330
+ // ---- EncryptedData (sec. 8) ------------------------------------------------
331
+ async function _decryptEncryptedData(parsed, km, opts) {
332
+ var eci = parsed.encryptedContentInfo;
333
+ var alg = eci.contentEncryptionAlgorithm;
334
+ if (eci.encryptedContent == null) throw _err("cms/no-encrypted-content", "the EncryptedData has no encryptedContent");
335
+ if (alg.oid === O("pbes2")) return _decryptPbes2(parsed, eci, km, opts);
336
+ var keyBits = CONTENT_KEYBITS[alg.oid];
337
+ if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported EncryptedData content algorithm " + alg.oid);
338
+ if (km.cek == null) throw _err("cms/bad-input", "this EncryptedData needs a raw { cek }");
339
+ var cek = guard.bytes.view(km.cek, CmsError, "cms/bad-input", "cek");
340
+ if (cek.length !== keyBits / 8) throw _err("cms/bad-input", "the supplied cek length does not match the content algorithm");
341
+ var iv = asn1.read.octetString(asn1.decode(alg.parameters));
342
+ try { return { content: _cbcOpen(cek, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "cek", recipientIndex: -1, contentEncryptionAlgorithm: alg.name || alg.oid, authenticated: false }; }
343
+ catch (_e) { throw _fail(); }
344
+ }
345
+ async function _decryptPbes2(parsed, eci, km, opts) {
346
+ if (km.password == null) throw _err("cms/bad-input", "this EncryptedData needs a { password }");
347
+ // The PBES2 parameters are attacker-controlled structure the strict parser surfaces raw (PBES2 is
348
+ // not an AEAD it validates): parse them behind a structural guard so a malformed shape is a typed
349
+ // cms/bad-input, never a raw dereference fault (the PBES2 structure is public -- not a decrypt oracle).
350
+ var kdf, encOid, iv, pb;
351
+ try {
352
+ var params = _seqChildren(eci.contentEncryptionAlgorithm.parameters, 2, "PBES2 parameters");
353
+ kdf = _requireChildren(params[0], 2, "PBES2 keyDerivationFunc");
354
+ var encScheme = _requireChildren(params[1], 2, "PBES2 encryptionScheme");
355
+ if (asn1.read.oid(kdf[0]) !== O("pbkdf2")) throw _err("cms/unsupported-algorithm", "PBES2 keyDerivationFunc must be PBKDF2");
356
+ pb = _pbkdf2Params(kdf[1].bytes, opts);
357
+ encOid = asn1.read.oid(encScheme[0]);
358
+ iv = asn1.read.octetString(encScheme[1]);
359
+ } catch (e) {
360
+ if (e instanceof CmsError) throw e;
361
+ throw _err("cms/bad-input", "malformed PBES2 parameters", e);
362
+ }
363
+ var keyBits = CONTENT_KEYBITS[encOid];
364
+ if (!keyBits) throw _err("cms/unsupported-algorithm", "unsupported PBES2 content cipher " + encOid);
365
+ var key = nodeCrypto.pbkdf2Sync(_passwordBytes(km.password), pb.salt, pb.iterations, keyBits / 8, pb.prfNode);
366
+ try { return { content: _cbcOpen(key, iv, eci.encryptedContent, keyBits), contentType: eci.contentType, contentTypeName: oid.name(eci.contentType) || eci.contentType, recipientType: "password", recipientIndex: -1, contentEncryptionAlgorithm: oid.name(encOid) || encOid, authenticated: false }; }
367
+ catch (_e) { throw _fail(); }
368
+ }
369
+
370
+ // ---- shared helpers (mirror cms-encrypt's builders) ------------------------
371
+ async function _aesKwUnwrap(kek, wrapped) {
372
+ var kekKey = await subtle.importKey("raw", kek, { name: "AES-KW" }, false, ["unwrapKey"]);
373
+ var cekKey = await subtle.unwrapKey("raw", wrapped, kekKey, { name: "AES-KW" }, { name: "AES-CBC" }, true, ["decrypt"]);
374
+ return Buffer.from(await subtle.exportKey("raw", cekKey));
375
+ }
376
+ function _eccSharedInfo(wrapName, ukm, kekBytes) {
377
+ var kids = [b.sequence([b.oid(O(wrapName))])];
378
+ if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
379
+ var supp = Buffer.alloc(4); supp.writeUInt32BE(kekBytes * 8, 0);
380
+ kids.push(b.explicit(2, b.octetString(supp)));
381
+ return b.sequence(kids);
382
+ }
383
+ function _kemOtherInfo(wrapName, kekBytes, ukm) {
384
+ var kids = [b.sequence([b.oid(O(wrapName))]), b.integer(BigInt(kekBytes))];
385
+ if (ukm) kids.push(b.explicit(0, b.octetString(ukm)));
386
+ return b.sequence(kids);
387
+ }
388
+ // RFC 3211 double-CBC unwrap (M26): decrypt pass 2 (IV = last block of pass 1), then pass 1
389
+ // (IV = first block), validate the count + complement check bytes -- uniform failure on mismatch.
390
+ function _pwriUnwrap(kek, wrapped, iv, keyBits) {
391
+ var blk = 16, alg = "aes-" + keyBits + "-cbc";
392
+ if (wrapped.length < 2 * blk || wrapped.length % blk !== 0) throw _fail();
393
+ var n = wrapped.length;
394
+ // pass2 = CBC(kek, iv2 = pass1[last block], pass1). Recover pass1's last block by decrypting the
395
+ // last ciphertext block (ECB) and XORing the previous ciphertext block -- a standard CBC last-
396
+ // block decrypt that needs no IV -- then CBC-decrypt pass2 under that recovered iv2 to get pass1.
397
+ var ecb = nodeCrypto.createDecipheriv("aes-" + keyBits + "-ecb", kek, Buffer.alloc(0)); ecb.setAutoPadding(false);
398
+ var lastDec = Buffer.concat([ecb.update(wrapped.subarray(n - blk)), ecb.final()]);
399
+ var iv2 = Buffer.alloc(blk);
400
+ for (var i = 0; i < blk; i++) iv2[i] = lastDec[i] ^ wrapped[n - 2 * blk + i];
401
+ var d1 = nodeCrypto.createDecipheriv(alg, kek, iv2); d1.setAutoPadding(false);
402
+ var pass1 = Buffer.concat([d1.update(wrapped), d1.final()]);
403
+ var d2 = nodeCrypto.createDecipheriv(alg, kek, iv); d2.setAutoPadding(false);
404
+ var body = Buffer.concat([d2.update(pass1), d2.final()]);
405
+ var count = body[0];
406
+ if (count < 1 || count + 4 > body.length) throw _fail();
407
+ var cek = body.subarray(4, 4 + count);
408
+ var bad = 0;
409
+ for (var j = 0; j < 3; j++) bad |= (body[1 + j] ^ 0xff) ^ cek[j]; // complement check bytes
410
+ if (bad !== 0) throw _fail();
411
+ return Buffer.from(cek);
412
+ }
413
+ // SEQUENCE structural guards -- a malformed (primitive / too-short) attacker-controlled parameter
414
+ // SEQUENCE is a typed cms/bad-input, never a raw `.children` dereference fault.
415
+ function _requireChildren(node, minLen, what) {
416
+ if (!node || !node.children || node.children.length < minLen) throw _err("cms/bad-input", "malformed " + what);
417
+ return node.children;
418
+ }
419
+ function _seqChildren(paramsDer, minLen, what) {
420
+ if (paramsDer == null) throw _err("cms/bad-input", "missing " + what);
421
+ return _requireChildren(asn1.decode(paramsDer), minLen, what);
422
+ }
423
+ function _pbkdf2Params(paramsDer, opts) {
424
+ var node = Buffer.isBuffer(paramsDer) ? asn1.decode(paramsDer) : paramsDer;
425
+ var kids = _requireChildren(node, 2, "PBKDF2 parameters");
426
+ var salt = asn1.read.octetString(kids[0]);
427
+ if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("cms/bad-input", "PBKDF2 salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
428
+ var iterations = guard.range.positiveInt31(asn1.read.integer(kids[1]), _err, "cms/bad-input", "PBKDF2 iterationCount");
429
+ // A caller-supplied maxIterations must be a positive integer -- a NaN / non-number would make
430
+ // Math.min return NaN and silently disable the DoS cap (iterations > NaN is always false).
431
+ var cap = C.LIMITS.PBKDF2_MAX_ITERATIONS;
432
+ if (opts.maxIterations != null) {
433
+ if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw _err("cms/bad-input", "maxIterations must be a positive integer");
434
+ cap = Math.min(opts.maxIterations, cap);
435
+ }
436
+ if (iterations > cap) throw _err("cms/iteration-limit", "PBKDF2 iterationCount " + iterations + " exceeds the cap " + cap);
437
+ var prfNode = "sha1";
438
+ for (var i = 2; i < node.children.length; i++) {
439
+ var ch = node.children[i];
440
+ if (ch.tagClass === "universal" && ch.tagNumber === asn1.TAGS.SEQUENCE) prfNode = _prfNode(asn1.read.oid(ch.children[0]));
441
+ }
442
+ return { salt: salt, iterations: iterations, prfNode: prfNode };
443
+ }
444
+ // Coverage residual (the unsupported-algorithm throw arm of each lookup below -- _prfNode, _hashW3c,
445
+ // _x963Hash, and _originatorSpki that follow): reachable only from a fully well-formed recipient that
446
+ // names an inner algorithm we do not implement (a non-registry prf / OAEP hash / kari scheme /
447
+ // originator form). The producer never emits one; the fuzz harness (fuzz/cms-decrypt.fuzz.js) drives
448
+ // these arms behaviorally under the PkiError-only contract.
449
+ var PRF_NODE = {}; PRF_NODE[O("hmacWithSHA1")] = "sha1"; PRF_NODE[O("hmacWithSHA256")] = "sha256"; PRF_NODE[O("hmacWithSHA384")] = "sha384"; PRF_NODE[O("hmacWithSHA512")] = "sha512";
450
+ function _prfNode(oidStr) { if (!PRF_NODE[oidStr]) throw _err("cms/unsupported-algorithm", "unsupported PBKDF2 prf " + oidStr); return PRF_NODE[oidStr]; }
451
+
452
+ function _oaepHashFromParams(paramsBytes) {
453
+ if (paramsBytes == null) return "SHA-1"; // absent = the RFC 4055 defaults (accept floor)
454
+ var node = asn1.decode(paramsBytes);
455
+ var hashName = "SHA-1", mgfHash = null, label = null;
456
+ (node.children || []).forEach(function (ch) {
457
+ if (ch.tagClass !== "context") return;
458
+ if (ch.tagNumber === 0) { hashName = _hashW3c(asn1.read.oid(ch.children[0].children[0])); } // hashAlgorithm [0]
459
+ else if (ch.tagNumber === 1) { // maskGenAlgorithm [1]
460
+ var mg = ch.children[0];
461
+ if (asn1.read.oid(mg.children[0]) !== O("mgf1")) throw _err("cms/unsupported-algorithm", "unsupported OAEP mask generation function");
462
+ mgfHash = _hashW3c(asn1.read.oid(mg.children[1].children[0]));
463
+ } else if (ch.tagNumber === 2) { label = asn1.read.octetString(ch.children[0].children[1]); } // pSourceAlgorithm [2]
464
+ });
465
+ // WebCrypto RSA-OAEP ties the MGF1 hash to the OAEP hash and supports only an empty label -- reject,
466
+ // rather than silently ignore, any parameter set we cannot faithfully honor.
467
+ if (mgfHash != null && mgfHash !== hashName) throw _err("cms/unsupported-algorithm", "the OAEP MGF1 hash must equal the OAEP hash");
468
+ if (label != null && label.length > 0) throw _err("cms/unsupported-algorithm", "a non-empty OAEP label is not supported");
469
+ return hashName;
470
+ }
471
+ var HASH_W3C = {}; HASH_W3C[O("sha1")] = "SHA-1"; HASH_W3C[O("sha256")] = "SHA-256"; HASH_W3C[O("sha384")] = "SHA-384"; HASH_W3C[O("sha512")] = "SHA-512";
472
+ function _hashW3c(o) { if (!HASH_W3C[o]) throw _err("cms/unsupported-algorithm", "unsupported OAEP hash " + o); return HASH_W3C[o]; }
473
+
474
+ var X963_HASH = {}; // kari stdDH scheme OID -> W3C hash
475
+ [["dhSinglePass-stdDH-sha1kdf-scheme", "SHA-1"], ["dhSinglePass-stdDH-sha224kdf-scheme", "SHA-224"], ["dhSinglePass-stdDH-sha256kdf-scheme", "SHA-256"], ["dhSinglePass-stdDH-sha384kdf-scheme", "SHA-384"], ["dhSinglePass-stdDH-sha512kdf-scheme", "SHA-512"],
476
+ ["dhSinglePass-cofactorDH-sha1kdf-scheme", "SHA-1"], ["dhSinglePass-cofactorDH-sha224kdf-scheme", "SHA-224"], ["dhSinglePass-cofactorDH-sha256kdf-scheme", "SHA-256"], ["dhSinglePass-cofactorDH-sha384kdf-scheme", "SHA-384"], ["dhSinglePass-cofactorDH-sha512kdf-scheme", "SHA-512"]
477
+ ].forEach(function (r) { X963_HASH[O(r[0])] = r[1]; });
478
+ function _x963Hash(scheme) { if (!X963_HASH[scheme]) throw _err("cms/unsupported-algorithm", "unsupported kari key-agreement scheme " + scheme); return X963_HASH[scheme]; }
479
+ function _kariWrap(kea) {
480
+ var params = asn1.decode(kea.parameters);
481
+ var wrapOid = asn1.read.oid(params.children[0]);
482
+ return { oid: wrapOid, name: oid.name(wrapOid) };
483
+ }
484
+ function _originatorSpki(originator) {
485
+ // originator OriginatorIdentifierOrKey CHOICE; we require originatorKey [1] (RFC 5753 MUST for
486
+ // ephemeral-static). The parser surfaces { form, value:{ algorithm, publicKey } } -- reconstruct
487
+ // the SPKI (SEQUENCE { algorithm, subjectPublicKey BIT STRING }) for importKey.
488
+ if (!originator || originator.form !== "originatorKey") throw _err("cms/unsupported-algorithm", "kari requires an originatorKey (ephemeral-static ECDH)");
489
+ var v = originator.value;
490
+ var algKids = [b.oid(v.algorithm.oid)];
491
+ if (v.algorithm.parameters != null) algKids.push(b.raw(v.algorithm.parameters));
492
+ return b.sequence([b.sequence(algKids), b.bitString(v.publicKey.bytes, v.publicKey.unusedBits)]);
493
+ }
494
+ var MONT = {}; MONT[O("X25519")] = { name: "X25519", hkdf: "SHA-256" }; MONT[O("X448")] = { name: "X448", hkdf: "SHA-512" };
495
+ function _isMont(spki) { var o = asn1.read.oid(asn1.decode(spki).children[0].children[0]); return !!MONT[o]; }
496
+ function _montName(spki) { return MONT[asn1.read.oid(asn1.decode(spki).children[0].children[0])]; }
497
+ var CURVE = {}; CURVE[O("prime256v1")] = { curve: "P-256" }; CURVE[O("secp384r1")] = { curve: "P-384" }; CURVE[O("secp521r1")] = { curve: "P-521" };
498
+ // The recipient certificate's EC curve OID -- authoritative for the kari agreement (both keys share it).
499
+ function _ecCurveFromCert(cert) {
500
+ var spki = x509.parse(_normCertDer(cert)).subjectPublicKeyInfo;
501
+ if (spki.algorithm.oid !== O("ecPublicKey") || spki.algorithm.parameters == null) return null;
502
+ return asn1.read.oid(asn1.decode(spki.algorithm.parameters));
503
+ }
504
+ // Rebuild an EC SPKI with explicit namedCurve parameters (for an originator key that omitted them).
505
+ function _withEcCurveParams(spki, curveOid) {
506
+ var node = asn1.decode(spki);
507
+ var alg = node.children[0];
508
+ return b.sequence([b.sequence([b.raw(alg.children[0].bytes), b.oid(curveOid)]), b.raw(node.children[1].bytes)]);
509
+ }
510
+ var MLKEM = {}; MLKEM[O("id-ml-kem-512")] = "ML-KEM-512"; MLKEM[O("id-ml-kem-768")] = "ML-KEM-768"; MLKEM[O("id-ml-kem-1024")] = "ML-KEM-1024";
511
+ function _mlkemName(o) { return MLKEM[o]; }
512
+
513
+ function _explicitSetOf(implicitBytes) {
514
+ // The AAD is the authAttrs re-encoded under the EXPLICIT SET OF tag (RFC 5083 sec. 2.2). The
515
+ // parser surfaces authAttrsBytes as the transmitted [1] IMPLICIT form; retag 0xA1 -> 0x31.
516
+ var out = Buffer.from(implicitBytes);
517
+ out[0] = 0x31;
518
+ return out;
519
+ }
520
+ function _normKeyDer(key) {
521
+ if (Buffer.isBuffer(key)) return key;
522
+ if (key instanceof Uint8Array) return Buffer.from(key);
523
+ if (typeof key === "string") { try { return pkcs8.pemDecode(key); } catch (e) { throw _err("cms/bad-input", "the recipient private-key PEM could not be decoded", e); } }
524
+ throw _err("cms/bad-input", "the recipient private key must be a PKCS#8 DER Buffer or PEM string");
525
+ }
526
+ function _normCertDer(cert) {
527
+ if (Buffer.isBuffer(cert)) return cert;
528
+ if (cert instanceof Uint8Array) return Buffer.from(cert);
529
+ if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("cms/bad-input", "the recipient certificate PEM could not be decoded", e); } }
530
+ throw _err("cms/bad-input", "the recipient certificate must be a DER Buffer or PEM string");
531
+ }
532
+ function _passwordBytes(p) {
533
+ if (Buffer.isBuffer(p)) return p;
534
+ if (p instanceof Uint8Array) return Buffer.from(p);
535
+ if (typeof p === "string") return Buffer.from(p, "utf8");
536
+ throw _err("cms/bad-input", "a password must be a string, Buffer, or Uint8Array");
537
+ }
538
+
539
+ module.exports = { decrypt: decrypt };