@blamejs/core 0.9.49 → 0.10.2

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +952 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +78 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/cli.js +13 -0
  25. package/lib/compliance.js +176 -8
  26. package/lib/crypto-field.js +114 -14
  27. package/lib/crypto.js +216 -20
  28. package/lib/db.js +1 -0
  29. package/lib/guard-graphql.js +37 -0
  30. package/lib/guard-jmap.js +321 -0
  31. package/lib/guard-managesieve-command.js +566 -0
  32. package/lib/guard-pop3-command.js +317 -0
  33. package/lib/guard-regex.js +138 -1
  34. package/lib/guard-smtp-command.js +58 -3
  35. package/lib/guard-xml.js +39 -1
  36. package/lib/mail-agent.js +20 -7
  37. package/lib/mail-arc-sign.js +12 -8
  38. package/lib/mail-auth.js +323 -34
  39. package/lib/mail-crypto-pgp.js +934 -0
  40. package/lib/mail-crypto-smime.js +340 -0
  41. package/lib/mail-crypto.js +108 -0
  42. package/lib/mail-dav.js +1224 -0
  43. package/lib/mail-deploy.js +492 -0
  44. package/lib/mail-dkim.js +431 -26
  45. package/lib/mail-journal.js +435 -0
  46. package/lib/mail-scan.js +502 -0
  47. package/lib/mail-server-imap.js +64 -26
  48. package/lib/mail-server-jmap.js +488 -0
  49. package/lib/mail-server-managesieve.js +853 -0
  50. package/lib/mail-server-mx.js +40 -30
  51. package/lib/mail-server-pop3.js +836 -0
  52. package/lib/mail-server-rate-limit.js +13 -0
  53. package/lib/mail-server-submission.js +70 -24
  54. package/lib/mail-server-tls.js +445 -0
  55. package/lib/mail-sieve.js +557 -0
  56. package/lib/mail-spam-score.js +284 -0
  57. package/lib/mail.js +99 -0
  58. package/lib/metrics.js +80 -3
  59. package/lib/middleware/dpop.js +58 -3
  60. package/lib/middleware/idempotency-key.js +255 -42
  61. package/lib/middleware/protected-resource-metadata.js +114 -2
  62. package/lib/network-dns-resolver.js +33 -0
  63. package/lib/network-tls.js +46 -0
  64. package/lib/otel-export.js +13 -4
  65. package/lib/outbox.js +62 -12
  66. package/lib/pqc-agent.js +13 -5
  67. package/lib/retry.js +23 -9
  68. package/lib/router.js +23 -1
  69. package/lib/safe-ical.js +634 -0
  70. package/lib/safe-icap.js +502 -0
  71. package/lib/safe-mime.js +15 -0
  72. package/lib/safe-sieve.js +684 -0
  73. package/lib/safe-smtp.js +57 -0
  74. package/lib/safe-url.js +37 -0
  75. package/lib/safe-vcard.js +473 -0
  76. package/lib/self-update-standalone-verifier.js +32 -3
  77. package/lib/self-update.js +153 -33
  78. package/lib/vendor/MANIFEST.json +161 -156
  79. package/lib/vendor-data.js +127 -9
  80. package/lib/vex.js +324 -59
  81. package/package.json +1 -1
  82. package/sbom.cdx.json +6 -6
@@ -0,0 +1,340 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.crypto.smime
4
+ * @nav Communication
5
+ * @title Mail S/MIME
6
+ * @order 121
7
+ * @slug mail-crypto-smime
8
+ *
9
+ * @card
10
+ * S/MIME 4.0 signature verification per RFC 8551 + RFC 5652 CMS
11
+ * SignedData. v1 surface is cert preflight; sign/verify deferred.
12
+ *
13
+ * @intro
14
+ * S/MIME 4.0 (RFC 8551, replacing RFC 5751) `multipart/signed;
15
+ * protocol="application/pkcs7-signature"` signature verification
16
+ * for inbound mail. CMS SignedData (RFC 5652) carries the actual
17
+ * signature; the signed payload travels in the first MIME part of
18
+ * the multipart/signed wrapper with the SignedData attached to the
19
+ * second part as base64-encoded DER.
20
+ *
21
+ * Posture (when the surface lights up):
22
+ * - Refuses SHA-1 as the signature hash (CVE-2017-9006-class —
23
+ * PKCS#7 collision attacks against legacy S/MIME) and as the
24
+ * certificate signature algorithm.
25
+ * - Refuses RSA keys < 2048 bits (RFC 8301 §3.1 — same posture
26
+ * as the rest of the mail surface).
27
+ * - Refuses MD5 anywhere (the historical S/MIME-v2 default; long
28
+ * broken).
29
+ * - Validates the signer certificate's chain against an operator-
30
+ * supplied trust anchor set; never falls back to a system root
31
+ * store implicitly (the system store binds operator trust to
32
+ * whatever the host happens to ship with).
33
+ * - Refuses certificate algorithms outside the modern set
34
+ * (RSA-PKCS1-v1_5 with SHA-256 / SHA-384 / SHA-512, ECDSA over
35
+ * P-256 / P-384 with SHA-256 / SHA-384, Ed25519). RFC 8551 §2.5
36
+ * mandates SHA-256 as the MUST-support floor.
37
+ *
38
+ * Threat model:
39
+ * - EFAIL (CVE-2017-17688 / CVE-2017-17689) — the S/MIME variant
40
+ * attacks decrypt+render pipelines. Same gate as PGP: when
41
+ * encrypt/decrypt lights up, decrypted HTML routes through
42
+ * `b.guardHtml` strict profile, remote-content fetches in
43
+ * encrypted parts are refused, and the MIME-part tree at
44
+ * decrypt time is compared byte-for-byte against the tree at
45
+ * render time.
46
+ * - PKCS#7 / CMS parser confusion — only the SignedData
47
+ * (ContentType 1.2.840.113549.1.7.2) ContentInfo shape is
48
+ * accepted; degenerate, certs-only-bag, AuthEnvelopedData, and
49
+ * encrypted-content variants are refused at parse time.
50
+ *
51
+ * v1 status — DEFERRED with documented conditions:
52
+ *
53
+ * Both sign() and verify() throw `MailCryptoError("mail-crypto/
54
+ * smime/deferred", ...)` in v1. The CMS SignedData ASN.1
55
+ * structure (RFC 5652 §5.1) is a five-field SEQUENCE with nested
56
+ * SET-OF / OPTIONAL / IMPLICIT-tagged fields, a DER content
57
+ * octet-string with constructed indefinite-length variants seen
58
+ * in the wild, and signed-attributes / unsigned-attributes
59
+ * ordering rules (§5.4 — DER set-of attributes MUST be sorted by
60
+ * encoded value for the signature to verify). node:crypto does
61
+ * not expose a CMS codec, and a hand-rolled ASN.1 BER/DER parser
62
+ * of the depth required to round-trip every fielded S/MIME
63
+ * signer's output is comparable in surface to the OpenPGP
64
+ * packet decoder shipped in `b.mail.crypto.pgp` — but with
65
+ * dramatically more shape variation across implementations.
66
+ *
67
+ * Defer condition: no operator demand has surfaced for in-process
68
+ * S/MIME verification; operators receiving S/MIME-signed mail
69
+ * today either (a) trust the gateway's authentication-results
70
+ * header (composed via `b.mail.authResults`) and treat S/MIME as
71
+ * a downstream concern, or (b) run S/MIME verification in their
72
+ * own consumer code with a vetted CMS library. Reopen this
73
+ * surface when ALL of the following hold:
74
+ * 1. At least one operator surfaces concrete demand for
75
+ * in-process S/MIME verify (use case + sample message
76
+ * shape).
77
+ * 2. A vendorable ASN.1 BER/DER decoder lands in `lib/vendor/`
78
+ * under the framework's vendoring discipline (MANIFEST.json
79
+ * + sha256 pin + no transitive deps), OR an operator
80
+ * provides a tested decoder we can fold in directly.
81
+ * 3. RFC 8551 §2.5 + RFC 5652 §11 conformance test vectors are
82
+ * available to drive the implementation. (NIST PKITS-style
83
+ * test vectors exist for X.509 chain validation; equivalent
84
+ * coverage for CMS SignedData is sparser.)
85
+ *
86
+ * Cheap escape hatch: operators wire `node-forge` / `pkijs` /
87
+ * openssl(1) (via child_process) in their own consumer code,
88
+ * extract the signed payload + signature components, and compose
89
+ * with `b.mail.authResults` to record the verification outcome
90
+ * for downstream policy. The S/MIME wire-format constants
91
+ * (Content-Type protocol parameter, micalg mapping, base64 DER
92
+ * framing) are stable and operator-side code interoperates with
93
+ * any inbound S/MIME-signed message regardless of this module's
94
+ * state.
95
+ *
96
+ * v2 reopen tag: the next minor (v0.9.60+) once the conditions
97
+ * above are met. The deferred surface lights up sign+verify
98
+ * together so operators never see a half-implementation.
99
+ *
100
+ * RFC citations:
101
+ * - RFC 8551 (S/MIME 4.0 Message Specification, April 2019;
102
+ * obsoletes RFC 5751)
103
+ * - RFC 5652 (Cryptographic Message Syntax — CMS)
104
+ * - RFC 8550 (S/MIME 4.0 Certificate Handling)
105
+ * - RFC 5280 (X.509 PKI)
106
+ * - RFC 8301 (RSA bit floor — reused as cross-mail-surface RSA posture)
107
+ *
108
+ * CVE citations:
109
+ * - CVE-2017-17688 / CVE-2017-17689 (EFAIL — S/MIME variant; informs
110
+ * the encrypt+decrypt deferral when that surface lights up)
111
+ * - CVE-2017-9006 (PKCS#7 / S/MIME signature-validation bypass
112
+ * class — informs the SHA-1 refusal posture)
113
+ * - CVE-2018-5407 (PortSmash — informs the side-channel hardening
114
+ * posture when private operations land in v2)
115
+ */
116
+ var lazyRequire = require("./lazy-require");
117
+ var audit = lazyRequire(function () { return require("./audit"); });
118
+ var nodeCrypto = require("node:crypto");
119
+ var validateOpts = require("./validate-opts");
120
+ var { defineClass } = require("./framework-error");
121
+
122
+ var MailCryptoError = defineClass("MailCryptoError", { alwaysPermanent: true });
123
+
124
+ // Constant posture values exported so operators reading this module
125
+ // from configuration code can pin to them by reference rather than
126
+ // hand-copying strings. These reflect RFC 8551 §2.5 + RFC 8301 floors.
127
+ var RSA_MIN_BITS = 2048; // allow:raw-byte-literal — RFC 8301 §3.1
128
+ var ALLOWED_HASHES = ["sha256", "sha384", "sha512"];
129
+ var REFUSED_HASHES = ["md5", "sha1"]; // allow:raw-byte-literal — CVE-2017-9006-class
130
+
131
+ // PROFILES + COMPLIANCE_POSTURES — the framework's standard cross-
132
+ // primitive contract. v1 only emits the metadata; the deferred sign/
133
+ // verify methods read them when they light up.
134
+ var PROFILES = ["strict", "balanced", "permissive"];
135
+ var COMPLIANCE_POSTURES = {
136
+ hipaa: "strict",
137
+ "pci-dss": "strict",
138
+ gdpr: "strict",
139
+ soc2: "strict",
140
+ };
141
+
142
+ var DEFERRAL_MESSAGE =
143
+ "b.mail.crypto.smime is deferred in v1. See the @intro comment block " +
144
+ "in lib/mail-crypto-smime.js for the deferral conditions and the " +
145
+ "documented escape hatch (operator-side CMS via a vetted third-party " +
146
+ "library or openssl(1)). Lights up in v0.9.60+ once a vendorable " +
147
+ "ASN.1 BER/DER decoder is folded in under lib/vendor/.";
148
+
149
+ // ---- Public surface (deferred) ----
150
+
151
+ /**
152
+ * @primitive b.mail.crypto.smime.sign
153
+ * @signature b.mail.crypto.smime.sign(opts)
154
+ * @since 0.9.58
155
+ * @status experimental
156
+ * @compliance hipaa, pci-dss, gdpr, soc2
157
+ *
158
+ * Deferred entry point. v1 surface is recognition + posture-only;
159
+ * actual CMS emission lights up in v0.9.60+ once a vendorable
160
+ * ASN.1 BER/DER codec is folded in. Throws
161
+ * `mail-crypto/smime/deferred` with a documented escape-hatch path
162
+ * (operator-side CMS via openssl(1) or a vetted library).
163
+ *
164
+ * @example
165
+ * try {
166
+ * b.mail.crypto.smime.sign({ message: m, certPem: c, privateKeyPem: k });
167
+ * } catch (e) {
168
+ * // e.code === "mail-crypto/smime/deferred"
169
+ * }
170
+ */
171
+ function sign(opts) {
172
+ opts = opts || {};
173
+ validateOpts(opts, ["message", "certPem", "privateKeyPem", "passphrase", "audit"],
174
+ "mail.crypto.smime.sign");
175
+ _audit(opts.audit, "mail.crypto.smime.sign", "denied", { reason: "deferred" });
176
+ throw new MailCryptoError("mail-crypto/smime/deferred", DEFERRAL_MESSAGE);
177
+ }
178
+
179
+ /**
180
+ * @primitive b.mail.crypto.smime.verify
181
+ * @signature b.mail.crypto.smime.verify(opts)
182
+ * @since 0.9.58
183
+ * @status experimental
184
+ * @compliance hipaa, pci-dss, gdpr, soc2
185
+ *
186
+ * Deferred entry point — same posture as sign. v1 throws
187
+ * `mail-crypto/smime/deferred`; v0.9.60+ verifies a CMS SignedData
188
+ * blob against `opts.trustedCertsPem`.
189
+ *
190
+ * @example
191
+ * try {
192
+ * b.mail.crypto.smime.verify({ message: m, armored: a, trustedCertsPem: t });
193
+ * } catch (e) {
194
+ * // e.code === "mail-crypto/smime/deferred"
195
+ * }
196
+ */
197
+ function verify(opts) {
198
+ opts = opts || {};
199
+ validateOpts(opts, ["message", "armored", "trustedCertsPem", "audit"],
200
+ "mail.crypto.smime.verify");
201
+ _audit(opts.audit, "mail.crypto.smime.verify_fail", "denied", { reason: "deferred" });
202
+ throw new MailCryptoError("mail-crypto/smime/deferred", DEFERRAL_MESSAGE);
203
+ }
204
+
205
+ // ---- Cert-shape preflight (operator-supplied trust roots) ----
206
+ //
207
+ // This *is* implemented in v1 — even before sign/verify light up,
208
+ // operators wiring an `b.mail.crypto.smime.checkCert({ certPem })`
209
+ // call against a candidate signing cert at boot get the SHA-1 / weak-
210
+ // RSA refusal posture surfaced as a config-time error rather than
211
+ // discovering it post-deploy. Reuses node:crypto's X509Certificate
212
+ // (cf. lib/mtls-ca.js).
213
+
214
+ /**
215
+ * @primitive b.mail.crypto.smime.checkCert
216
+ * @signature b.mail.crypto.smime.checkCert(opts)
217
+ * @since 0.9.58
218
+ * @status stable
219
+ * @compliance hipaa, pci-dss, gdpr, soc2
220
+ *
221
+ * Operator-side cert preflight that lights up at boot: refuses
222
+ * SHA-1 / MD5 signatures, RSA keys < 2048 bits, MD2 / MD5 / SHA-1
223
+ * as the certificate-signature algorithm. Returns the parsed cert
224
+ * shape (subject CN, issuer CN, validFrom / validTo, key algorithm
225
+ * + size, signature algorithm). Throws `mail-crypto/smime/bad-cert`
226
+ * on any of the above; throws `mail-crypto/smime/expired-cert` if
227
+ * the cert is outside its validity window.
228
+ *
229
+ * @example
230
+ * var info = b.mail.crypto.smime.checkCert({ certPem: pem });
231
+ * // → { subjectCN, issuerCN, validFrom, validTo, keyAlg, keyBits, sigAlg }
232
+ */
233
+ function checkCert(opts) {
234
+ opts = validateOpts.requireObject(opts, "mail.crypto.smime.checkCert",
235
+ MailCryptoError, "mail-crypto/smime/bad-opts");
236
+ validateOpts(opts, ["certPem"], "mail.crypto.smime.checkCert");
237
+ validateOpts.requireNonEmptyString(opts.certPem, "certPem",
238
+ MailCryptoError, "mail-crypto/smime/bad-cert");
239
+
240
+ var cert;
241
+ try {
242
+ cert = new nodeCrypto.X509Certificate(opts.certPem);
243
+ } catch (e) {
244
+ throw new MailCryptoError("mail-crypto/smime/bad-cert",
245
+ "certPem could not be parsed as X.509: " + ((e && e.message) || String(e)));
246
+ }
247
+
248
+ // Cert signature algorithm refusal — node:crypto X509Certificate
249
+ // exposes `signatureAlgorithm` (OpenSSL long name like
250
+ // "sha256WithRSAEncryption", "ecdsa-with-SHA384", "ED25519") and
251
+ // `signatureAlgorithmOid` (the canonical OID). We screen on the
252
+ // lowercase long name so SHA-1 / MD5 substrings catch every
253
+ // fielded variant. The OID is reported in the returned shape so
254
+ // operators with stricter posture can pin on it.
255
+ var sigAlgName = cert.signatureAlgorithm || cert.sigAlgName || "";
256
+ var sigAlg = String(sigAlgName).toLowerCase();
257
+ for (var i = 0; i < REFUSED_HASHES.length; i += 1) {
258
+ if (sigAlg.indexOf(REFUSED_HASHES[i]) !== -1) {
259
+ throw new MailCryptoError("mail-crypto/smime/refused-hash",
260
+ "cert signature algorithm '" + sigAlgName +
261
+ "' refused — SHA-1 / MD5 in cert signatures is forbidden " +
262
+ "(CVE-2017-9006-class). Acceptable hashes: " + ALLOWED_HASHES.join(", "));
263
+ }
264
+ }
265
+
266
+ // RSA bit floor — when the public key is RSA, refuse < RSA_MIN_BITS.
267
+ // The X509Certificate exposes the public key via .publicKey
268
+ // (node 17+) which is a KeyObject we can inspect.
269
+ var pub = cert.publicKey;
270
+ if (pub && pub.asymmetricKeyType === "rsa") {
271
+ var jwk = pub.export({ format: "jwk" });
272
+ var nBytes = Buffer.from(jwk.n, "base64url");
273
+ var bits = nBytes.length * 8; // allow:raw-byte-literal — bits-per-byte conversion // allow:raw-time-literal — RFC 5280 in comment, not seconds
274
+ if (bits < RSA_MIN_BITS) {
275
+ throw new MailCryptoError("mail-crypto/smime/rsa-too-small",
276
+ "cert public key is " + bits + " RSA bits; minimum is " + RSA_MIN_BITS +
277
+ " (RFC 8301 §3.1)");
278
+ }
279
+ }
280
+
281
+ // Validity window — refuse certs outside their notBefore / notAfter
282
+ // window. Codex P1: checkCert's docstring promises this throws
283
+ // `mail-crypto/smime/expired-cert` but the impl was missing, letting
284
+ // expired or not-yet-valid signing certs pass boot-time preflight
285
+ // and fail interop later when peers verify signatures against the
286
+ // RFC 5280 §4.1.2.5 validity field.
287
+ var nowMs = Date.now();
288
+ var notBeforeMs = Date.parse(cert.validFrom);
289
+ var notAfterMs = Date.parse(cert.validTo);
290
+ if (isFinite(notBeforeMs) && nowMs < notBeforeMs) {
291
+ throw new MailCryptoError("mail-crypto/smime/expired-cert",
292
+ "cert is not yet valid (notBefore=" + cert.validFrom + ", now=" +
293
+ new Date(nowMs).toISOString() + ")");
294
+ }
295
+ if (isFinite(notAfterMs) && nowMs > notAfterMs) {
296
+ throw new MailCryptoError("mail-crypto/smime/expired-cert",
297
+ "cert is expired (notAfter=" + cert.validTo + ", now=" +
298
+ new Date(nowMs).toISOString() + ")");
299
+ }
300
+
301
+ return {
302
+ subject: cert.subject,
303
+ issuer: cert.issuer,
304
+ validFrom: cert.validFrom,
305
+ validTo: cert.validTo,
306
+ sigAlgName: sigAlgName,
307
+ sigAlgOid: cert.signatureAlgorithmOid || null,
308
+ keyType: pub && pub.asymmetricKeyType,
309
+ fingerprint256: cert.fingerprint256,
310
+ };
311
+ }
312
+
313
+ // ---- Audit (drop-silent) ----
314
+
315
+ function _audit(auditHandle, action, outcome, metadata) {
316
+ try {
317
+ var a = auditHandle || audit();
318
+ if (a && typeof a.safeEmit === "function") {
319
+ a.safeEmit({
320
+ action: action,
321
+ outcome: outcome,
322
+ actor: {},
323
+ metadata: metadata,
324
+ });
325
+ }
326
+ } catch (_e) { /* drop-silent — audit failures must not crash callers */ }
327
+ }
328
+
329
+ module.exports = {
330
+ sign: sign,
331
+ verify: verify,
332
+ checkCert: checkCert,
333
+ MailCryptoError: MailCryptoError,
334
+ PROFILES: PROFILES,
335
+ COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
336
+ ALLOWED_HASHES: ALLOWED_HASHES,
337
+ REFUSED_HASHES: REFUSED_HASHES,
338
+ RSA_MIN_BITS: RSA_MIN_BITS,
339
+ DEFERRAL_MESSAGE: DEFERRAL_MESSAGE,
340
+ };
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.crypto
4
+ * @featured false
5
+ * @nav Communication
6
+ * @title Mail crypto (PGP + S/MIME)
7
+ * @order 119
8
+ * @slug mail-crypto
9
+ *
10
+ * @card
11
+ * End-to-end mail signing (OpenPGP per RFC 9580) + S/MIME 4.0
12
+ * posture (RFC 8551 / RFC 5652 CMS). Sub-namespaces: pgp, smime.
13
+ *
14
+ * @intro
15
+ * End-to-end mail signing + verification, organized into two sub-
16
+ * namespaces by wire format:
17
+ *
18
+ * - `b.mail.crypto.pgp` — OpenPGP per RFC 9580 (November 2024),
19
+ * wrapped in `multipart/signed; protocol="application/pgp-
20
+ * signature"` per RFC 3156. v1 surface: sign() + verify() with
21
+ * v4 detached signatures over Ed25519 (pub-alg 22) and RSA
22
+ * (pub-alg 1, EMSA-PKCS1-v1_5 + SHA-256, 2048-bit floor per
23
+ * RFC 8301).
24
+ * - `b.mail.crypto.smime` — S/MIME 4.0 per RFC 8551 with CMS
25
+ * SignedData per RFC 5652. v1 surface: checkCert() — the
26
+ * operator-side preflight that refuses SHA-1 / MD5 / < 2048-bit
27
+ * RSA certs at boot. sign() + verify() are DEFERRED in v1; see
28
+ * the @intro block in lib/mail-crypto-smime.js for the deferral
29
+ * conditions and the operator escape hatch.
30
+ *
31
+ * Both sub-namespaces share `MailCryptoError` (FrameworkError
32
+ * subclass via defineClass with alwaysPermanent: true) so operator
33
+ * error handling can `catch (e) { if (e instanceof
34
+ * b.mail.crypto.MailCryptoError) ... }` once and cover both
35
+ * protocols.
36
+ *
37
+ * Composition with the rest of the mail surface:
38
+ * - DKIM-Signature (b.mail.dkim) signs at the SMTP-message
39
+ * transport boundary; PGP / S/MIME sign at the user-visible
40
+ * payload boundary. The two are complementary — a message can
41
+ * carry BOTH a DKIM-Signature header (proving the sending
42
+ * domain) AND a PGP / S/MIME signature (proving the human
43
+ * sender's key). Operators wiring both wire DKIM via
44
+ * `opts.dkimSigner` on the smtp transport and call
45
+ * `b.mail.crypto.pgp.sign()` over the multipart body before
46
+ * handing it to the transport.
47
+ * - When the EFAIL-class encrypt/decrypt surface lights up (see
48
+ * per-sub-namespace deferral conditions), rendered HTML routes
49
+ * through `b.guardHtml` strict profile and the MIME-part tree
50
+ * is captured at decrypt time + diffed against the tree at
51
+ * render time.
52
+ *
53
+ * This top-level module is a thin re-export — the actual surface
54
+ * lives in lib/mail-crypto-pgp.js and lib/mail-crypto-smime.js.
55
+ *
56
+ * RFC citations:
57
+ * - RFC 9580 (OpenPGP, Nov 2024; obsoletes RFC 4880)
58
+ * - RFC 3156 (MIME Security with OpenPGP)
59
+ * - RFC 8551 (S/MIME 4.0 Message Specification; obsoletes RFC 5751)
60
+ * - RFC 5652 (Cryptographic Message Syntax)
61
+ * - RFC 8550 (S/MIME 4.0 Certificate Handling)
62
+ *
63
+ * CVE citations:
64
+ * - CVE-2017-17688 / CVE-2017-17689 (EFAIL)
65
+ * - CVE-2017-9006 (PKCS#7 / S/MIME signature-validation bypass class)
66
+ */
67
+
68
+ var pgp = require("./mail-crypto-pgp");
69
+ var smime = require("./mail-crypto-smime");
70
+
71
+ // Both sub-modules define `MailCryptoError` independently (each via
72
+ // `defineClass("MailCryptoError", { alwaysPermanent: true })`) — at
73
+ // runtime they are distinct classes. The facade re-exports the PGP
74
+ // one and provides `isMailCryptoError(e)` for the cross-protocol
75
+ // shape check that doesn't depend on class identity. Both classes
76
+ // extend FrameworkError and both set the `isMailCryptoError = true`
77
+ // flag, so the duck-type check is reliable across the boundary.
78
+ var MailCryptoError = pgp.MailCryptoError;
79
+
80
+ /**
81
+ * @primitive b.mail.crypto.isMailCryptoError
82
+ * @signature b.mail.crypto.isMailCryptoError(err)
83
+ * @since 0.9.58
84
+ * @status stable
85
+ *
86
+ * Duck-type check that returns true for any `MailCryptoError` raised
87
+ * by either sub-namespace. Each sub-module defines its own
88
+ * `MailCryptoError` class so `instanceof` doesn't span them; this
89
+ * helper checks the `isMailCryptoError === true` flag both classes
90
+ * set, giving operators one cross-protocol catch-all.
91
+ *
92
+ * @example
93
+ * try {
94
+ * b.mail.crypto.pgp.verify(opts);
95
+ * } catch (e) {
96
+ * if (b.mail.crypto.isMailCryptoError(e)) { handle(e); }
97
+ * }
98
+ */
99
+ function isMailCryptoError(e) {
100
+ return !!(e && e.isMailCryptoError === true);
101
+ }
102
+
103
+ module.exports = {
104
+ pgp: pgp,
105
+ smime: smime,
106
+ MailCryptoError: MailCryptoError,
107
+ isMailCryptoError: isMailCryptoError,
108
+ };