@blamejs/pki 0.2.22 → 0.2.23
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 +11 -1
- package/README.md +1 -1
- package/lib/cms-decrypt.js +539 -0
- package/lib/cms-encrypt.js +498 -0
- package/lib/cms-verify.js +63 -1
- package/lib/constants.js +6 -0
- package/lib/oid.js +18 -1
- package/lib/schema-cms.js +9 -0
- package/lib/webcrypto.js +63 -8
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,17 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## v0.2.
|
|
7
|
+
## v0.2.23 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
CMS content encryption arrives as pki.cms.encrypt and pki.cms.decrypt: EnvelopedData, AuthEnvelopedData, and EncryptedData with every RFC 5652 recipient type -- RSA-OAEP, ephemeral-static ECDH, X25519/X448, symmetric key-wrap, password, and post-quantum ML-KEM (RFC 9629/9936) -- and a single, oracle-free decryption verdict.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.cms.encrypt(content, recipients, opts) produces a CMS EnvelopedData, AuthEnvelopedData (AES-GCM, the default), or EncryptedData. Recipients auto-dispatch off the certificate key: RSA -> ktri RSAES-OAEP-SHA256/384/512 (v1.5 never emitted); EC P-256/384/521 -> kari ephemeral-static ECDH with the ANSI-X9.63 KDF; X25519/X448 -> kari with HKDF (RFC 8418); ML-KEM-512/768/1024 -> ori/KEMRecipientInfo (RFC 9629/9936). A { password } recipient uses PBKDF2 + the RFC 3211 PWRI-KEK; a { kek, kekId } recipient uses AES key wrap. EncryptedData takes a raw { cek } or a PBES2 { password }. Content is AES-128/192/256-GCM or -CBC; the same fresh content-encryption key is wrapped for every recipient. Fail-closed with typed CmsError.
|
|
14
|
+
- pki.cms.decrypt(input, keyMaterial, opts) decrypts an EnvelopedData / AuthEnvelopedData / EncryptedData (DER or PEM). It selects the recipient the key material { key, cert } / { password } / { kek } / { cek } targets, acquires the content-encryption key through the matching arm (RSA-OAEP or PKCS#1 v1.5 decrypt-only under the RFC 3218 implicit-rejection countermeasure, ECDH / X25519 / X448, AES key-unwrap, PBKDF2, ML-KEM decapsulation), and decrypts + authenticates the content -- returning { content, contentType, contentTypeName, recipientType, recipientIndex, contentEncryptionAlgorithm, authenticated }. Every secret-dependent failure collapses to one uniform cms/decrypt-failed verdict (Bleichenbacher / EFAIL / password-oracle freedom); a PBKDF2 iteration cap bounds password-based decryption work.
|
|
15
|
+
- The WebCrypto engine gains ML-KEM key encapsulation (SubtleCrypto.encapsulateBits / decapsulateBits over FIPS 203) and the ANSI-X9.63 single-step key-derivation function (the X963KDF derive algorithm), the two primitives the post-quantum and elliptic-curve CMS recipient arms compose.
|
|
16
|
+
|
|
17
|
+
## v0.2.22 — 2026-07-15
|
|
8
18
|
|
|
9
19
|
The RFC 6960 OCSP producer and relying-party surface arrives as pki.ocsp: build and sign OCSP requests and responses, mint unsigned error responses, and verify a response end to end against the same hardened responder-authorization, signature, CertID, currency, and nonce gates the certification-path revocation checker runs.
|
|
10
20
|
|
package/README.md
CHANGED
|
@@ -221,7 +221,7 @@ is callable today; nothing below is a stub.
|
|
|
221
221
|
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
222
222
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
223
223
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
224
|
-
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. Fail-closed with typed `cms/*` errors — `sign`, `verify` |
|
|
224
|
+
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt` |
|
|
225
225
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
226
226
|
| `pki.ocsp` | RFC 6960 Online Certificate Status Protocol — the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs (CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile, or SHA-2; optional RFC 9654 nonce, optional requestor signature). `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER — the issuing CA directly or a delegated responder, any `pki.cms.sign` key including the post-quantum ML-DSA / SLH-DSA sets, with `good` / `revoked` (reason + time) / `unknown` per-certificate status, and `buildErrorResponse(status)` the unsigned §2.3 error (`tryLater` / `unauthorized` / …). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA or a CA-issued delegate bearing id-kp-OCSPSigning **and** id-pkix-ocsp-nocheck, passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency (`thisUpdate`/`nextUpdate`), and the request-nonce echo — returning `{ status: "good" / "revoked" / "unknown", … }`, never a silent accept. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify` |
|
|
227
227
|
| `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct` |
|
|
@@ -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 };
|