@blamejs/pki 0.2.25 → 0.2.26
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 +9 -0
- package/README.md +1 -1
- package/lib/smime.js +139 -12
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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.26 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
S/MIME encryption arrives as pki.smime.encrypt / pki.smime.decrypt: envelope and open RFC 8551 encrypted messages over the CMS layer, with AES-GCM authenticated enveloping as the default and bidirectional OpenSSL interoperability.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.smime.encrypt(content, recipients, opts) envelopes a MIME entity as an encrypted RFC 8551 S/MIME message (opaque application/pkcs7-mime). The default AES-256-GCM content encryption yields an authEnveloped-data message (confidentiality and integrity, RFC 8551 sec. 3.4); opts.contentEncryptionAlgorithm can select AES-CBC for an enveloped-data message (confidentiality only, RFC 8551 sec. 3.3). recipients is the pki.cms.encrypt recipient array (RSA-OAEP key transport, ECDH / X25519 / X448 key agreement, AES key wrap, PBKDF2 password, and post-quantum ML-KEM), and a single descriptor is accepted and normalized to a one-element array. content is wrapped as a text/plain entity by default or taken verbatim with opts.entity. Fail-closed with typed SmimeError.
|
|
14
|
+
- pki.smime.decrypt(message, keyMaterial, opts) opens an encrypted S/MIME message, returning the recovered inner MIME entity plus smimeType, authenticated (true only for an AuthEnvelopedData -- a CBC enveloped-data message reports false, the RFC 8551 sec. 3.3 no-integrity caveat), recipientType, recipientIndex, and contentEncryptionAlgorithm. The smime-type is derived from the CMS body, not the header; opts.strictSmimeType additionally rejects a header smime-type that disagrees with the body. keyMaterial is the pki.cms.decrypt key material ({ key, cert }, { password }, or { kek, kekId }). Decryption is fail-closed and oracle-free. OpenSSL's legacy application/x-pkcs7-mime and a missing smime-type are both accepted. A recovered content that is itself a signed S/MIME message is returned for the caller to feed back to pki.smime.verify. Bidirectionally interoperable with openssl cms -encrypt / -decrypt.
|
|
15
|
+
|
|
7
16
|
## v0.2.25 — 2026-07-16
|
|
8
17
|
|
|
9
18
|
S/MIME signed-message assembly and verification arrive as pki.smime: sign and verify RFC 8551 multipart/signed and application/pkcs7-mime messages over the CMS layer, bidirectionally interoperable with OpenSSL.
|
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ is callable today; nothing below is a stub.
|
|
|
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
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
|
-
| `pki.smime` | RFC 8551 S/MIME message assembly
|
|
225
|
+
| `pki.smime` | RFC 8551 S/MIME message assembly, verification, and encryption over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt` |
|
|
226
226
|
| `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` |
|
|
227
227
|
| `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` |
|
|
228
228
|
| `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. The producing side: `encodeSctList` builds the extension value byte-for-byte (the exact inverse of `parseSctList`) and `signSct` performs a log's signing step (rebuilding the same signed-data preimage the verifier hashes and signing it with the log's ECDSA-P-256 / RSA key). Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct`, `encodeSctList`, `signSct` |
|
package/lib/smime.js
CHANGED
|
@@ -9,22 +9,27 @@
|
|
|
9
9
|
* @slug smime
|
|
10
10
|
*
|
|
11
11
|
* @intro
|
|
12
|
-
* RFC 8551 S/MIME assembly + verification over the shipped CMS
|
|
13
|
-
* `sign` wraps a MIME entity as a signed S/MIME message and `verify`
|
|
14
|
-
* and verifies one, in both forms: `multipart/signed` (clear-signed --
|
|
15
|
-
* content stays readable, a detached CMS signature rides alongside) and
|
|
12
|
+
* RFC 8551 S/MIME assembly + verification + encryption over the shipped CMS
|
|
13
|
+
* layer. `sign` wraps a MIME entity as a signed S/MIME message and `verify`
|
|
14
|
+
* unwraps and verifies one, in both forms: `multipart/signed` (clear-signed --
|
|
15
|
+
* the content stays readable, a detached CMS signature rides alongside) and
|
|
16
16
|
* `application/pkcs7-mime` (opaque -- the whole entity is a base64 CMS
|
|
17
|
-
* SignedData).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* SignedData). `encrypt` envelopes a MIME entity as an opaque
|
|
18
|
+
* `application/pkcs7-mime` message and `decrypt` opens one -- `authEnveloped-data`
|
|
19
|
+
* (AES-GCM, confidentiality AND integrity, the default) or `enveloped-data`
|
|
20
|
+
* (AES-CBC, confidentiality only, so `decrypt` surfaces `authenticated:false`,
|
|
21
|
+
* the EFAIL / RFC 8551 sec. 3.3 no-integrity caveat). The crypto is entirely
|
|
22
|
+
* `pki.cms.sign` / `verify` / `encrypt` / `decrypt`; the new work is the MIME
|
|
23
|
+
* frame and the RFC 8551 sec. 3.1.1 canonicalization signer and verifier share
|
|
24
|
+
* one canonicalizer (`lib/mime.js`) so their digests cannot diverge. Like
|
|
21
25
|
* `cms.verify`, `verify` returns the per-signer cryptographic verdict; chaining
|
|
22
26
|
* a signer to a trust anchor is the caller's `pki.path.validate` step.
|
|
23
27
|
*
|
|
24
28
|
* @card
|
|
25
|
-
* Assemble
|
|
26
|
-
* application/pkcs7-mime
|
|
27
|
-
* fail-closed,
|
|
29
|
+
* Assemble, verify, and encrypt RFC 8551 S/MIME messages (signed:
|
|
30
|
+
* multipart/signed + application/pkcs7-mime; encrypted: enveloped-data +
|
|
31
|
+
* authEnveloped-data) over any CMS signer / recipient -- fail-closed,
|
|
32
|
+
* algorithm-agnostic.
|
|
28
33
|
*/
|
|
29
34
|
|
|
30
35
|
var frameworkError = require("./framework-error.js");
|
|
@@ -235,6 +240,128 @@ async function verify(message, opts) {
|
|
|
235
240
|
throw _err("smime/unsupported-type", "not a signed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
236
241
|
}
|
|
237
242
|
|
|
243
|
+
// The application/pkcs7-mime header for an enveloped S/MIME message (RFC 8551 sec. 3.2 / sec. 3.2.2).
|
|
244
|
+
function _pkcs7MimeHead(smimeType) {
|
|
245
|
+
return Buffer.from("Content-Type: application/pkcs7-mime; smime-type=" + smimeType + "; name=smime.p7m\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=smime.p7m\r\n\r\n", "latin1");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The smime-type for a CMS enveloped body, DERIVED from its OUTER content type (RFC 8551 sec. 3.2.2) --
|
|
249
|
+
// the single source of truth, never an opts guess: AuthEnvelopedData -> "authEnveloped-data" (AEAD,
|
|
250
|
+
// integrity), EnvelopedData -> "enveloped-data" (no integrity). Anything else is not an S/MIME enveloped
|
|
251
|
+
// message. Returns { smimeType, name } (name is the parsed contentTypeName). Throws smime/unsupported-type
|
|
252
|
+
// for a non-enveloped body (EncryptedData / SignedData carries no smime-type mapping here).
|
|
253
|
+
function _envelopedTypeOf(der, badParseCode) {
|
|
254
|
+
var name;
|
|
255
|
+
try { name = schemaCms.parse(guard.bytes.view(der, SmimeError, "smime/bad-mime", "the CMS body")).contentTypeName; }
|
|
256
|
+
catch (e) { throw _err("smime/bad-mime", "the pkcs7-mime body is not a parseable CMS structure", e); }
|
|
257
|
+
if (name === "authEnvelopedData") return "authEnveloped-data";
|
|
258
|
+
if (name === "envelopedData") return "enveloped-data";
|
|
259
|
+
throw _err(badParseCode, "the pkcs7-mime body is a " + name + ", not an EnvelopedData / AuthEnvelopedData");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Map smime opts to cms.encrypt opts. The S/MIME layer is recipient-agnostic: it forwards the content
|
|
263
|
+
// -encryption choice + every recipient-shaping opt unchanged, and never emits PEM (the CMS body is base64
|
|
264
|
+
// inside a MIME frame). opts.contentType is the MIME media type of the INNER entity (handled by
|
|
265
|
+
// _entityBytes), NOT the CMS encapsulated content type -- it is deliberately not forwarded here.
|
|
266
|
+
function _cmsEncryptOpts(opts) {
|
|
267
|
+
var o = {};
|
|
268
|
+
if (opts.contentEncryptionAlgorithm !== undefined) o.contentEncryptionAlgorithm = opts.contentEncryptionAlgorithm;
|
|
269
|
+
if (opts.oaepHash !== undefined) o.oaepHash = opts.oaepHash;
|
|
270
|
+
if (opts.keyIdentifier !== undefined) o.keyIdentifier = opts.keyIdentifier;
|
|
271
|
+
if (opts.ukm !== undefined) o.ukm = opts.ukm;
|
|
272
|
+
return o;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @primitive pki.smime.encrypt
|
|
277
|
+
* @signature pki.smime.encrypt(content, recipients, opts?) -> Promise<Buffer>
|
|
278
|
+
* @since 0.2.26
|
|
279
|
+
* @status experimental
|
|
280
|
+
* @spec RFC 8551, RFC 5652, RFC 5083
|
|
281
|
+
* @related pki.smime.decrypt, pki.cms.encrypt
|
|
282
|
+
*
|
|
283
|
+
* Envelope a MIME entity as an encrypted S/MIME message (RFC 8551 sec. 3.3 / sec. 3.4). `content` is the
|
|
284
|
+
* payload -- a raw body wrapped as a `text/plain` entity by default, or the caller's own complete MIME
|
|
285
|
+
* entity when `opts.entity` is set; `recipients` is the `pki.cms.encrypt` recipient array (any RSA-OAEP
|
|
286
|
+
* ktri / EC or X25519/X448 kari / ML-KEM ori-KEM / password pwri / kek kekri -- the S/MIME layer is
|
|
287
|
+
* recipient-agnostic; a single descriptor is accepted and normalized to a one-element array). Enveloping
|
|
288
|
+
* has ONE form -- opaque `application/pkcs7-mime` with the whole entity base64-encoded. The `smime-type`
|
|
289
|
+
* is derived from the produced CMS: AES-GCM (the default) yields an AuthEnvelopedData with
|
|
290
|
+
* `smime-type=authEnveloped-data` (confidentiality AND integrity); a CBC choice yields an EnvelopedData
|
|
291
|
+
* with `smime-type=enveloped-data` (confidentiality only -- no integrity, RFC 8551 sec. 3.3). Returns the
|
|
292
|
+
* assembled message bytes. Fail-closed with `SmimeError`.
|
|
293
|
+
*
|
|
294
|
+
* @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
|
|
295
|
+
* @opts contentType the wrapped entity's MIME Content-Type (default `text/plain; charset=utf-8`).
|
|
296
|
+
* @opts contentEncryptionAlgorithm forwarded to cms.encrypt: `"aes-256-gcm"` (default) / `"aes-128-gcm"` / `"aes-256-cbc"` / `"aes-128-cbc"`.
|
|
297
|
+
* @opts oaepHash forwarded: the RSAES-OAEP hash for ktri recipients.
|
|
298
|
+
* @opts keyIdentifier forwarded: `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
299
|
+
* @opts ukm forwarded: user keying material for kari / kemri recipients.
|
|
300
|
+
* @example
|
|
301
|
+
* var enc = await pki.smime.encrypt(Buffer.from("secret"), [{ cert: recipientCertDer }]);
|
|
302
|
+
*/
|
|
303
|
+
async function encrypt(content, recipients, opts) {
|
|
304
|
+
opts = opts || {};
|
|
305
|
+
var entity = _entityBytes(content, opts);
|
|
306
|
+
// Normalize a single descriptor to an array so cms.encrypt always takes the ENVELOPED path (an array of
|
|
307
|
+
// RecipientInfos), never bare EncryptedData -- which is not an S/MIME construct (no smime-type maps to it).
|
|
308
|
+
var recips = Array.isArray(recipients) ? recipients : [recipients];
|
|
309
|
+
var der = _toBuf(await cms.encrypt(entity, recips, _cmsEncryptOpts(opts)));
|
|
310
|
+
var head = _pkcs7MimeHead(_envelopedTypeOf(der, "smime/bad-mime"));
|
|
311
|
+
return _capped(Buffer.concat([head, _base64Body(der), mime.CRLF]));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @primitive pki.smime.decrypt
|
|
316
|
+
* @signature pki.smime.decrypt(message, keyMaterial, opts?) -> Promise<{ content, smimeType, authenticated, recipientType, recipientIndex, contentEncryptionAlgorithm }>
|
|
317
|
+
* @since 0.2.26
|
|
318
|
+
* @status experimental
|
|
319
|
+
* @spec RFC 8551, RFC 5652, RFC 5083
|
|
320
|
+
* @related pki.smime.encrypt, pki.cms.decrypt, pki.smime.verify
|
|
321
|
+
*
|
|
322
|
+
* Open an encrypted S/MIME message (RFC 8551 sec. 3.3 / sec. 3.4) -- an `application/pkcs7-mime` entity
|
|
323
|
+
* whose base64 body is a CMS EnvelopedData or AuthEnvelopedData. `keyMaterial` is the `pki.cms.decrypt`
|
|
324
|
+
* key material (`{ key, cert }`, `{ password }`, or `{ kek, kekId? }`). Returns the recovered inner MIME
|
|
325
|
+
* entity as `content`, the `smimeType`, `authenticated` (true only for AuthEnvelopedData -- a CBC
|
|
326
|
+
* `enveloped-data` message reports `false`, the RFC 8551 sec. 3.3 / EFAIL no-integrity caveat), and the
|
|
327
|
+
* `recipientType` / `recipientIndex` / `contentEncryptionAlgorithm` from the CMS layer. Fail-closed and
|
|
328
|
+
* oracle-free: every secret-dependent failure collapses to the uniform `cms/decrypt-failed` the CMS layer
|
|
329
|
+
* emits (this layer only propagates it). A recovered `content` that is itself a signed S/MIME message is
|
|
330
|
+
* returned as-is for the caller to feed back to `pki.smime.verify` (no auto-recursion). Accepts OpenSSL's
|
|
331
|
+
* legacy `application/x-pkcs7-mime` and a missing `smime-type`.
|
|
332
|
+
*
|
|
333
|
+
* @opts recipientIndex forwarded to cms.decrypt: explicitly select the recipient by index.
|
|
334
|
+
* @opts maxIterations forwarded to cms.decrypt: lower the PBKDF2 iteration cap (downward only).
|
|
335
|
+
* @opts strictSmimeType reject a header `smime-type` that disagrees with the CMS body (`smime/smime-type-mismatch`).
|
|
336
|
+
* @example
|
|
337
|
+
* var res = await pki.smime.decrypt(smimeMessageBytes, { key: recipientKeyPkcs8, cert: recipientCertDer });
|
|
338
|
+
* res.content; // the recovered inner MIME entity
|
|
339
|
+
* res.authenticated; // false for an enveloped-only (CBC) message -- no integrity
|
|
340
|
+
*/
|
|
341
|
+
async function decrypt(message, keyMaterial, opts) {
|
|
342
|
+
opts = opts || {};
|
|
343
|
+
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
344
|
+
var ct = ent.contentType;
|
|
345
|
+
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
346
|
+
// The smime-type header is advisory (the CMS body is authoritative), so compare it case-insensitively --
|
|
347
|
+
// liberal on accept, strict on emit -- rather than rejecting a legitimate message over its parameter casing.
|
|
348
|
+
var st = ct.params["smime-type"];
|
|
349
|
+
var stLower = st ? st.toLowerCase() : st;
|
|
350
|
+
if (st && stLower !== "enveloped-data" && stLower !== "authenveloped-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only enveloped-data / authEnveloped-data)");
|
|
351
|
+
var der = _decodeCms(ent);
|
|
352
|
+
var smimeType = _envelopedTypeOf(der, "smime/unsupported-type");
|
|
353
|
+
if (opts.strictSmimeType && st && stLower !== smimeType.toLowerCase()) throw _err("smime/smime-type-mismatch", "the header smime-type " + JSON.stringify(st) + " disagrees with the CMS body (" + smimeType + ")");
|
|
354
|
+
var cmsOpts = {};
|
|
355
|
+
if (opts.recipientIndex !== undefined) cmsOpts.recipientIndex = opts.recipientIndex;
|
|
356
|
+
if (opts.maxIterations !== undefined) cmsOpts.maxIterations = opts.maxIterations;
|
|
357
|
+
var res = await cms.decrypt(der, keyMaterial, cmsOpts);
|
|
358
|
+
return {
|
|
359
|
+
content: res.content, smimeType: smimeType, authenticated: res.authenticated,
|
|
360
|
+
recipientType: res.recipientType, recipientIndex: res.recipientIndex,
|
|
361
|
+
contentEncryptionAlgorithm: res.contentEncryptionAlgorithm,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
238
365
|
// Decode a CMS body per its Content-Transfer-Encoding (base64 is the S/MIME norm; 7bit/8bit/binary pass through).
|
|
239
366
|
function _decodeCms(ent) {
|
|
240
367
|
// guard.encoding.base64 is strict (no whitespace) and rejects via a (code,msg) FACTORY -- strip the
|
|
@@ -245,4 +372,4 @@ function _decodeCms(ent) {
|
|
|
245
372
|
|
|
246
373
|
function _toBuf(v) { return Buffer.isBuffer(v) ? v : Buffer.from(v); }
|
|
247
374
|
|
|
248
|
-
module.exports = { sign: sign, verify: verify };
|
|
375
|
+
module.exports = { sign: sign, verify: verify, encrypt: encrypt, decrypt: decrypt };
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f346b9f1-fcfc-449a-bd4f-4e1dfeb3209c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T12:38:31.773Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.26",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.26",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.26",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|