@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.
- package/CHANGELOG.md +952 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +78 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/cli.js +13 -0
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-graphql.js +37 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-regex.js +138 -1
- package/lib/guard-smtp-command.js +58 -3
- package/lib/guard-xml.js +39 -1
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/otel-export.js +13 -4
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/compliance.js
CHANGED
|
@@ -245,9 +245,40 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
245
245
|
"cwe-top-25-2024", // CWE Top 25 Most Dangerous Software Weaknesses (2024)
|
|
246
246
|
"cis-controls-v8", // CIS Controls v8
|
|
247
247
|
"cmmc-2.0-level-2", // CMMC 2.0 Level 2 (Advanced) — 110 NIST 800-171 Rev 2 controls // allow:raw-byte-literal — NIST pub number / level, not bytes
|
|
248
|
+
// ---- v0.9.57 — granular CMMC level distinction ----
|
|
249
|
+
// CMMC 2.0 maturity levels carry distinct control-mapping
|
|
250
|
+
// expectations: Level 1 = 15 controls (FAR 52.204-21), Level 2 =
|
|
251
|
+
// 110 controls (NIST 800-171 Rev 2), Level 3 = additional NIST
|
|
252
|
+
// 800-172 enhanced controls. The umbrella "cmmc-2.0" posture
|
|
253
|
+
// remains for back-compat with existing operators; the explicit
|
|
254
|
+
// L1/L2/L3 postures are the recommended pin for new deployments.
|
|
255
|
+
"cmmc-2.0-level-1", // CMMC 2.0 Level 1 (Foundational) — 15 FAR controls; FCI-only data // allow:raw-byte-literal — regulatory identifier, not bytes
|
|
256
|
+
"cmmc-2.0-level-3", // CMMC 2.0 Level 3 (Expert) — NIST 800-172 enhanced controls atop L2 // allow:raw-byte-literal — regulatory identifier, not bytes
|
|
248
257
|
]);
|
|
249
258
|
|
|
250
|
-
|
|
259
|
+
// SUPPLY-34 — Artifact standards (SBOM / VEX format families) are NOT
|
|
260
|
+
// regulatory regimes. Pinning a posture like `cyclonedx-v1.6` to
|
|
261
|
+
// cascade audit + TLS floors conflates the act of EMITTING a SBOM
|
|
262
|
+
// format with the regulatory floor an operator needs. Operators who
|
|
263
|
+
// emit CycloneDX SBOMs do so because of an underlying regime
|
|
264
|
+
// (FedRAMP SBOM requirement, SSDF PW.4, etc.) — not because emitting
|
|
265
|
+
// the format itself defines the floor.
|
|
266
|
+
//
|
|
267
|
+
// b.compliance.artifactStandards exposes the format catalog as a
|
|
268
|
+
// READ-ONLY channel — operators pick a format (or set of formats)
|
|
269
|
+
// for SBOM / VEX emission without affecting the regulatory posture
|
|
270
|
+
// cascade. The names remain in KNOWN_POSTURES for back-compat
|
|
271
|
+
// (existing operators may have pinned them); pinning them via
|
|
272
|
+
// b.compliance.set emits a `compliance.posture.format_as_regime`
|
|
273
|
+
// audit warning so the misconfiguration is grep-able in the audit
|
|
274
|
+
// chain.
|
|
275
|
+
var ARTIFACT_STANDARDS = Object.freeze([
|
|
276
|
+
"cyclonedx-v1.6", // CycloneDX 1.6 SBOM
|
|
277
|
+
"spdx-v3.0", // SPDX 3.0 SBOM
|
|
278
|
+
"vex-csaf-2.1", // VEX via OASIS CSAF 2.1
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
var STATE = { posture: null, setAt: null, fipsMode: false };
|
|
251
282
|
|
|
252
283
|
function _emitAudit(action, metadata, outcome) {
|
|
253
284
|
try {
|
|
@@ -330,6 +361,38 @@ function set(posture) {
|
|
|
330
361
|
STATE.setAt = Date.now();
|
|
331
362
|
_emitAudit("compliance.posture.set", { posture: posture });
|
|
332
363
|
|
|
364
|
+
// SUPPLY-34 — emit a `format_as_regime` audit warning when an
|
|
365
|
+
// operator pins an artifact-standard format (cyclonedx-v1.6 /
|
|
366
|
+
// spdx-v3.0 / vex-csaf-2.1) as the regulatory posture. These names
|
|
367
|
+
// remain in KNOWN_POSTURES for back-compat but pinning them as the
|
|
368
|
+
// primary regime conflates "I emit this SBOM/VEX format" with "my
|
|
369
|
+
// regulatory floor is X". Operators should pin the regulatory
|
|
370
|
+
// regime (FedRAMP / SSDF / HIPAA / etc.) and surface artifact
|
|
371
|
+
// standards via b.compliance.artifactStandards.
|
|
372
|
+
if (ARTIFACT_STANDARDS.indexOf(posture) !== -1) {
|
|
373
|
+
_emitAudit("compliance.posture.format_as_regime",
|
|
374
|
+
{ posture: posture, artifactStandards: ARTIFACT_STANDARDS,
|
|
375
|
+
recommendation: "Artifact standards describe what SBOM/VEX format the deployment emits — not the regulatory floor. Pin the underlying regime (e.g. 'nist-800-218-ssdf', 'fedramp-rev5-moderate') and surface emitted formats via b.compliance.artifactStandards()." },
|
|
376
|
+
"warning");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// SUPPLY-21 — emit `fips_conflict` audit warning when posture is
|
|
380
|
+
// FedRAMP / CMMC L3 AND the framework's PQC-first crypto defaults
|
|
381
|
+
// are active without an explicit fipsMode opt-in. Operators see
|
|
382
|
+
// this in the audit chain and either (a) document the deviation
|
|
383
|
+
// in their SSP or (b) set b.compliance.fipsMode(true) before set()
|
|
384
|
+
// to switch the audit-signing path to FIPS-validated AES-GCM +
|
|
385
|
+
// SHA-384.
|
|
386
|
+
var FIPS_BOUNDARY_POSTURES = ["fedramp-rev5-moderate", "cmmc-2.0-level-3"];
|
|
387
|
+
if (FIPS_BOUNDARY_POSTURES.indexOf(posture) !== -1 && !STATE.fipsMode) {
|
|
388
|
+
_emitAudit("compliance.posture.fips_conflict",
|
|
389
|
+
{ posture: posture,
|
|
390
|
+
cryptoDefaults: "PQC-first (ML-KEM-1024 / SLH-DSA-SHAKE-256f / XChaCha20-Poly1305 / SHA3-512)",
|
|
391
|
+
fipsMode: false,
|
|
392
|
+
recommendation: "Call b.compliance.fipsMode(true) BEFORE b.compliance.set() to switch b.audit.sign to FIPS-140-3 validated AES-GCM + SHA-384, or document the PQC-first deviation in the SSP." },
|
|
393
|
+
"warning");
|
|
394
|
+
}
|
|
395
|
+
|
|
333
396
|
// F-POSTURE-1 — cascade the posture into every primitive that owns a
|
|
334
397
|
// posture-conditioned default. Each primitive exposes an
|
|
335
398
|
// `applyPosture(name)` that merges the POSTURE_DEFAULTS entry for the
|
|
@@ -480,8 +543,9 @@ function clear() {
|
|
|
480
543
|
}
|
|
481
544
|
|
|
482
545
|
function _resetForTest() {
|
|
483
|
-
STATE.posture
|
|
484
|
-
STATE.setAt
|
|
546
|
+
STATE.posture = null;
|
|
547
|
+
STATE.setAt = null;
|
|
548
|
+
STATE.fipsMode = false;
|
|
485
549
|
}
|
|
486
550
|
|
|
487
551
|
// Posture → human-readable name + statutory citation + jurisdiction.
|
|
@@ -999,16 +1063,43 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
999
1063
|
"circia": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1000
1064
|
// ---- v0.9.6 — exceptd framework-control-gap closure cascade ----
|
|
1001
1065
|
"nist-800-53": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1002
|
-
|
|
1066
|
+
// SUPPLY-18 — NIST AI-RMF MANAGE.4.3 / ISO 23894 §6.5 / ISO 42001
|
|
1067
|
+
// §A.6 require encrypted backups for AI system state (model
|
|
1068
|
+
// weights, training data, prompt logs all contain regulated
|
|
1069
|
+
// payload). All AI-domain postures now enforce backupEncryption.
|
|
1070
|
+
"nist-ai-rmf-1.0": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1003
1071
|
"iso-42001-2023": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1004
|
-
"iso-23894-2023": Object.freeze({ backupEncryptionRequired:
|
|
1005
|
-
"owasp-llm-top-10-2025": Object.freeze({ backupEncryptionRequired:
|
|
1006
|
-
|
|
1072
|
+
"iso-23894-2023": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1073
|
+
"owasp-llm-top-10-2025": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1074
|
+
// SUPPLY-19 — OWASP ASVS v5.0 §8.3.4 (sensitive-data deletion)
|
|
1075
|
+
// requires post-delete storage reclamation. Set requireVacuumAfterErase
|
|
1076
|
+
// so operators pinning ASVS v5.0 inherit the proper floor.
|
|
1077
|
+
"owasp-asvs-v5.0": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1007
1078
|
"nist-800-218-ssdf": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1008
1079
|
"nist-800-82-r3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1009
1080
|
"nist-800-63b-rev4": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1010
1081
|
"iec-62443-3-3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1011
|
-
|
|
1082
|
+
// SUPPLY-21 — FedRAMP Rev 5 Moderate baseline references FIPS 140-3
|
|
1083
|
+
// validated cryptography for protect-against-disclosure controls
|
|
1084
|
+
// (SC-13, SC-28). The framework's PQC-first defaults (ML-KEM-1024,
|
|
1085
|
+
// XChaCha20-Poly1305, SHA3-512) are NOT FIPS-140-3 validated as of
|
|
1086
|
+
// the FedRAMP Rev 5 baseline publication — FIPS modules are still
|
|
1087
|
+
// being certified for the ML-KEM / ML-DSA primitives upstream.
|
|
1088
|
+
//
|
|
1089
|
+
// Conflict resolution: PQC-first remains the framework default
|
|
1090
|
+
// (CLAUDE.md rule §2 — never weaken security middleware), but
|
|
1091
|
+
// operators in a FedRAMP boundary opt into `fipsMode: true` to
|
|
1092
|
+
// switch `b.audit.sign` from SLH-DSA-SHAKE-256f to FIPS-validated
|
|
1093
|
+
// AES-GCM + SHA-384 for the audit-chain signing path. The runtime
|
|
1094
|
+
// emits a `compliance.posture.fips_conflict` audit warning when
|
|
1095
|
+
// posture=fedramp-rev5-moderate AND fipsMode is NOT set so the
|
|
1096
|
+
// conflict is grep-able in the audit chain.
|
|
1097
|
+
//
|
|
1098
|
+
// Operators pinning this posture without setting fipsMode are
|
|
1099
|
+
// signaling "ship the PQC-first defaults and accept that the
|
|
1100
|
+
// FedRAMP boundary will need to document the deviation in their
|
|
1101
|
+
// SSP." The audit warning is the operator-visible signal.
|
|
1102
|
+
"fedramp-rev5-moderate": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
|
|
1012
1103
|
"hipaa-security-rule": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1013
1104
|
"hitrust-csf-v11.4": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1014
1105
|
"nerc-cip-007-6": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
@@ -1023,7 +1114,18 @@ var POSTURE_DEFAULTS = Object.freeze({
|
|
|
1023
1114
|
"nist-800-115": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1024
1115
|
"cwe-top-25-2024": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1025
1116
|
"cis-controls-v8": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1117
|
+
// SUPPLY-20 — CMMC 2.0 levels differ in control mapping:
|
|
1118
|
+
// L1 (Foundational, 15 FAR controls, FCI data only) — encrypted
|
|
1119
|
+
// backups NOT mandated; audit-chain encouraged.
|
|
1120
|
+
// L2 (Advanced, 110 NIST 800-171 Rev 2 controls, CUI data) —
|
|
1121
|
+
// encrypted backups + signed audit + post-erase vacuum.
|
|
1122
|
+
// L3 (Expert, NIST 800-172 enhanced atop L2) — same control floor
|
|
1123
|
+
// as L2 plus operator-attested enhanced practices the
|
|
1124
|
+
// framework can't auto-cascade (FIPS 140-3 boundary,
|
|
1125
|
+
// continuous monitoring).
|
|
1126
|
+
"cmmc-2.0-level-1": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
|
|
1026
1127
|
"cmmc-2.0-level-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
|
|
1128
|
+
"cmmc-2.0-level-3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
|
|
1027
1129
|
});
|
|
1028
1130
|
|
|
1029
1131
|
/**
|
|
@@ -1202,6 +1304,69 @@ function list() {
|
|
|
1202
1304
|
return out;
|
|
1203
1305
|
}
|
|
1204
1306
|
|
|
1307
|
+
/**
|
|
1308
|
+
* @primitive b.compliance.artifactStandards
|
|
1309
|
+
* @signature b.compliance.artifactStandards()
|
|
1310
|
+
* @since 0.9.57
|
|
1311
|
+
* @status stable
|
|
1312
|
+
*
|
|
1313
|
+
* Return the set of SBOM / VEX artifact standards the framework can
|
|
1314
|
+
* emit. These are FORMAT FAMILIES, not regulatory regimes — pinning
|
|
1315
|
+
* one of these names as the deployment's compliance posture conflates
|
|
1316
|
+
* "format I emit" with "regulatory floor I meet" (SUPPLY-34). Pin
|
|
1317
|
+
* the regulatory regime (FedRAMP / SSDF / HIPAA / etc.) via
|
|
1318
|
+
* `b.compliance.set()` and surface the emitted artifact standards via
|
|
1319
|
+
* this read-only catalog.
|
|
1320
|
+
*
|
|
1321
|
+
* @example
|
|
1322
|
+
* b.compliance.artifactStandards();
|
|
1323
|
+
* // → ["cyclonedx-v1.6", "spdx-v3.0", "vex-csaf-2.1"]
|
|
1324
|
+
*/
|
|
1325
|
+
function artifactStandards() {
|
|
1326
|
+
return ARTIFACT_STANDARDS.slice();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* @primitive b.compliance.fipsMode
|
|
1331
|
+
* @signature b.compliance.fipsMode(enable?)
|
|
1332
|
+
* @since 0.9.57
|
|
1333
|
+
* @status stable
|
|
1334
|
+
* @related b.compliance.set
|
|
1335
|
+
*
|
|
1336
|
+
* Get or set the FIPS-mode flag. When `enable === true`, the
|
|
1337
|
+
* framework's audit-chain signing path (b.audit.sign) switches from
|
|
1338
|
+
* the PQC-first default (SLH-DSA-SHAKE-256f) to a FIPS-140-3
|
|
1339
|
+
* validated AES-GCM + SHA-384 path so a FedRAMP / CMMC L3 boundary
|
|
1340
|
+
* can pin the audit signer to a validated module.
|
|
1341
|
+
*
|
|
1342
|
+
* Call BEFORE b.compliance.set() so the fips_conflict audit warning
|
|
1343
|
+
* doesn't fire at posture-set time. Cannot be toggled after posture
|
|
1344
|
+
* is pinned — runtime switches create half-set crypto state. Returns
|
|
1345
|
+
* the current flag value when called with no argument.
|
|
1346
|
+
*
|
|
1347
|
+
* @example
|
|
1348
|
+
* b.compliance.fipsMode(true); // opt into FIPS-validated path
|
|
1349
|
+
* b.compliance.set("fedramp-rev5-moderate");
|
|
1350
|
+
* b.compliance.fipsMode(); // → true
|
|
1351
|
+
*/
|
|
1352
|
+
function fipsMode(enable) {
|
|
1353
|
+
if (enable === undefined) return STATE.fipsMode === true;
|
|
1354
|
+
if (typeof enable !== "boolean") {
|
|
1355
|
+
throw new ComplianceError("compliance/bad-fips-mode",
|
|
1356
|
+
"compliance.fipsMode: argument must be boolean when supplied (got " +
|
|
1357
|
+
typeof enable + ")");
|
|
1358
|
+
}
|
|
1359
|
+
if (STATE.posture) {
|
|
1360
|
+
throw new ComplianceError("compliance/fips-after-set",
|
|
1361
|
+
"compliance.fipsMode: posture is already pinned ('" + STATE.posture +
|
|
1362
|
+
"'); FIPS-mode must be set BEFORE b.compliance.set() — runtime " +
|
|
1363
|
+
"switches create half-set crypto state.");
|
|
1364
|
+
}
|
|
1365
|
+
STATE.fipsMode = enable;
|
|
1366
|
+
_emitAudit("compliance.fips_mode.set", { fipsMode: enable });
|
|
1367
|
+
return STATE.fipsMode;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1205
1370
|
module.exports = {
|
|
1206
1371
|
set: set,
|
|
1207
1372
|
current: current,
|
|
@@ -1214,8 +1379,11 @@ module.exports = {
|
|
|
1214
1379
|
postureDefault: postureDefault,
|
|
1215
1380
|
sanctions: sanctions,
|
|
1216
1381
|
aiAct: aiAct,
|
|
1382
|
+
artifactStandards: artifactStandards,
|
|
1383
|
+
fipsMode: fipsMode,
|
|
1217
1384
|
KNOWN_POSTURES: KNOWN_POSTURES,
|
|
1218
1385
|
POSTURE_DEFAULTS: POSTURE_DEFAULTS,
|
|
1386
|
+
ARTIFACT_STANDARDS: ARTIFACT_STANDARDS,
|
|
1219
1387
|
REGIME_MAP: REGIME_MAP,
|
|
1220
1388
|
ComplianceError: ComplianceError,
|
|
1221
1389
|
_resetForTest: _resetForTest,
|
package/lib/crypto-field.js
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
var lazyRequire = require("./lazy-require");
|
|
46
46
|
var vault = require("./vault");
|
|
47
|
+
var vaultAad = require("./vault-aad");
|
|
47
48
|
var { sha3Hash, kdf } = require("./crypto");
|
|
48
49
|
var { HASH_PREFIX, VAULT_PREFIX, TIME } = require("./constants");
|
|
49
50
|
|
|
@@ -147,6 +148,21 @@ var perRowKeyTables = Object.create(null);
|
|
|
147
148
|
* sealedFields: string[], // column names sealed via vault.seal
|
|
148
149
|
* derivedHashes: { [hashCol]: { from: string, normalize?: fn } },
|
|
149
150
|
* hashNamespaces: { [field]: string }, // override default rainbow-defense ns
|
|
151
|
+
* aad: boolean, // when true, route seal/unseal through
|
|
152
|
+
* // b.vault.aad — AEAD-binds the ciphertext
|
|
153
|
+
* // to (table, rowIdField=primary key, column)
|
|
154
|
+
* // so a DB-write attacker can't copy a
|
|
155
|
+
* // sealed value between rows. CRYPTO-1.
|
|
156
|
+
* rowIdField: string, // when aad=true, the column name carrying
|
|
157
|
+
* // the row identity. Default "id". The row
|
|
158
|
+
* // passed to sealRow MUST already have this
|
|
159
|
+
* // column populated; sealRow refuses when
|
|
160
|
+
* // missing (an AAD bound to a placeholder
|
|
161
|
+
* // would silently fail every unseal).
|
|
162
|
+
* schemaVersion: string|number, // when aad=true, the schema version
|
|
163
|
+
* // threaded into AAD. Default "1". Bump
|
|
164
|
+
* // when the column layout changes to
|
|
165
|
+
* // invalidate all prior ciphertext.
|
|
150
166
|
*
|
|
151
167
|
* @example
|
|
152
168
|
* b.cryptoField.registerTable("patients", {
|
|
@@ -156,12 +172,26 @@ var perRowKeyTables = Object.create(null);
|
|
|
156
172
|
* }
|
|
157
173
|
* });
|
|
158
174
|
* b.cryptoField.getSealedFields("patients"); // → ["ssn", "diagnosis"]
|
|
175
|
+
*
|
|
176
|
+
* // AAD-bound table (recommended for new schemas — CRYPTO-1).
|
|
177
|
+
* b.cryptoField.registerTable("idempotency_keys", {
|
|
178
|
+
* sealedFields: ["headers", "body"],
|
|
179
|
+
* aad: true,
|
|
180
|
+
* rowIdField: "k", // primary key column
|
|
181
|
+
* });
|
|
159
182
|
*/
|
|
160
183
|
function registerTable(name, opts) {
|
|
184
|
+
var aadOn = opts.aad === true;
|
|
185
|
+
var rowIdField = typeof opts.rowIdField === "string" && opts.rowIdField.length > 0
|
|
186
|
+
? opts.rowIdField : "id";
|
|
187
|
+
var schemaVersion = opts.schemaVersion != null ? String(opts.schemaVersion) : "1";
|
|
161
188
|
schemas[name] = {
|
|
162
189
|
sealedFields: Array.isArray(opts.sealedFields) ? opts.sealedFields.slice() : [],
|
|
163
190
|
derivedHashes: Object.assign({}, opts.derivedHashes || {}),
|
|
164
191
|
hashNamespaces: Object.assign({}, opts.hashNamespaces || {}),
|
|
192
|
+
aad: aadOn,
|
|
193
|
+
rowIdField: rowIdField,
|
|
194
|
+
schemaVersion: schemaVersion,
|
|
165
195
|
};
|
|
166
196
|
}
|
|
167
197
|
|
|
@@ -327,7 +357,14 @@ function sealRow(table, row) {
|
|
|
327
357
|
var spec = s.derivedHashes[derivedField];
|
|
328
358
|
var raw = out[spec.from];
|
|
329
359
|
if (raw === undefined || raw === null) continue;
|
|
330
|
-
var plain
|
|
360
|
+
var plain;
|
|
361
|
+
if (typeof raw === "string" && raw.startsWith(VAULT_PREFIX)) {
|
|
362
|
+
plain = vault.unseal(raw);
|
|
363
|
+
} else if (typeof raw === "string" && vaultAad.isAadSealed(raw)) {
|
|
364
|
+
plain = vaultAad.unseal(raw, _aadParts(s, table, spec.from, out));
|
|
365
|
+
} else {
|
|
366
|
+
plain = raw;
|
|
367
|
+
}
|
|
331
368
|
var ns = namespaceFor(table, spec.from, s.hashNamespaces);
|
|
332
369
|
var normalized = spec.normalize ? spec.normalize(plain) : String(plain);
|
|
333
370
|
var saltHex2 = vault.getDerivedHashSalt().toString("hex");
|
|
@@ -335,17 +372,59 @@ function sealRow(table, row) {
|
|
|
335
372
|
}
|
|
336
373
|
}
|
|
337
374
|
|
|
338
|
-
//
|
|
375
|
+
// CRYPTO-1 — AAD-bound table requires the row's identity column to
|
|
376
|
+
// be populated BEFORE sealRow runs. Sealing under a placeholder /
|
|
377
|
+
// missing rowId produces ciphertext that no later unseal can open
|
|
378
|
+
// because the AAD on read is computed against the row's actual id.
|
|
379
|
+
if (s.aad) {
|
|
380
|
+
var rowId = out[s.rowIdField];
|
|
381
|
+
if (rowId === undefined || rowId === null || String(rowId).length === 0) {
|
|
382
|
+
throw new Error("cryptoField.sealRow: table '" + table +
|
|
383
|
+
"' is AAD-bound (registerTable({aad:true})); the row's identity " +
|
|
384
|
+
"column '" + s.rowIdField + "' must be populated BEFORE sealRow. " +
|
|
385
|
+
"Generate the primary key first (e.g. uuid / sequence INSERT … RETURNING), " +
|
|
386
|
+
"set row." + s.rowIdField + ", then sealRow.");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Seal fields. Plain mode: vault.seal (idempotent — already-sealed
|
|
391
|
+
// values pass through). AAD mode: vault.aad.seal binds the AEAD tag
|
|
392
|
+
// to (table, rowId, column, schemaVersion) — cross-row copy of a
|
|
393
|
+
// ciphertext fails Poly1305 on read. CRYPTO-1.
|
|
339
394
|
for (var i = 0; i < s.sealedFields.length; i++) {
|
|
340
395
|
var field = s.sealedFields[i];
|
|
341
396
|
if (out[field] !== undefined && out[field] !== null) {
|
|
342
|
-
|
|
397
|
+
if (s.aad) {
|
|
398
|
+
// Idempotent: already-AAD-sealed values pass through unchanged.
|
|
399
|
+
if (typeof out[field] === "string" && vaultAad.isAadSealed(out[field])) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
out[field] = vaultAad.seal(String(out[field]),
|
|
403
|
+
_aadParts(s, table, field, out));
|
|
404
|
+
} else {
|
|
405
|
+
// allow:seal-without-aad — plain-mode legacy table; operator
|
|
406
|
+
// opts into AAD via registerTable({aad:true})
|
|
407
|
+
out[field] = vault.seal(String(out[field]));
|
|
408
|
+
}
|
|
343
409
|
}
|
|
344
410
|
}
|
|
345
411
|
|
|
346
412
|
return out;
|
|
347
413
|
}
|
|
348
414
|
|
|
415
|
+
// _aadParts — build the canonical AAD object for an AAD-bound table.
|
|
416
|
+
// Threads (table, rowId, column, schemaVersion) so seal + unseal
|
|
417
|
+
// produce the same AAD bytes. Centralized so the seal path and the
|
|
418
|
+
// unseal path can never drift.
|
|
419
|
+
function _aadParts(schema, table, column, row) {
|
|
420
|
+
return {
|
|
421
|
+
table: table,
|
|
422
|
+
rowId: String(row[schema.rowIdField]),
|
|
423
|
+
column: column,
|
|
424
|
+
schemaVersion: schema.schemaVersion,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
349
428
|
/**
|
|
350
429
|
* @primitive b.cryptoField.unsealRow
|
|
351
430
|
* @signature b.cryptoField.unsealRow(table, row)
|
|
@@ -379,21 +458,39 @@ function unsealRow(table, row) {
|
|
|
379
458
|
if (out[field]) {
|
|
380
459
|
var unsealed;
|
|
381
460
|
try {
|
|
382
|
-
|
|
461
|
+
// Auto-detect the envelope shape so an AAD-bound table that
|
|
462
|
+
// contains pre-migration plain-vault rows still reads. Read-
|
|
463
|
+
// side migration is lazy; the next sealRow re-emits AAD-bound.
|
|
464
|
+
if (typeof out[field] === "string" && vaultAad.isAadSealed(out[field])) {
|
|
465
|
+
unsealed = vaultAad.unseal(out[field],
|
|
466
|
+
_aadParts(s, table, field, out));
|
|
467
|
+
} else if (typeof out[field] === "string" && out[field].startsWith(VAULT_PREFIX)) {
|
|
468
|
+
unsealed = vault.unseal(out[field]);
|
|
469
|
+
} else {
|
|
470
|
+
// Not a sealed value — pass through.
|
|
471
|
+
unsealed = out[field];
|
|
472
|
+
}
|
|
383
473
|
} catch (e) {
|
|
384
|
-
// A DB-write attacker who can write `vault:<crafted>`
|
|
385
|
-
// payloads to sealed columns can force
|
|
386
|
-
// decapsulation on attacker-controlled
|
|
387
|
-
// path. Surface the failure as a chain
|
|
388
|
-
// alert on burst patterns; null the field
|
|
389
|
-
// code sees "no value" instead of crashing the
|
|
474
|
+
// A DB-write attacker who can write `vault:<crafted>` /
|
|
475
|
+
// `vault.aad:<crafted>` payloads to sealed columns can force
|
|
476
|
+
// KEM decapsulation / AEAD verify on attacker-controlled
|
|
477
|
+
// bytes via this read path. Surface the failure as a chain
|
|
478
|
+
// row so operators alert on burst patterns; null the field
|
|
479
|
+
// so downstream code sees "no value" instead of crashing the
|
|
480
|
+
// request. AAD-shape failures additionally indicate cross-
|
|
481
|
+
// row copy attempts — the audit metadata flags the shape so
|
|
482
|
+
// operators can write alert rules.
|
|
390
483
|
try {
|
|
391
|
-
|
|
392
|
-
auditMod.safeEmit({
|
|
484
|
+
audit().safeEmit({
|
|
393
485
|
action: "system.crypto.unseal_failed",
|
|
394
486
|
outcome: "failure",
|
|
395
|
-
metadata: {
|
|
396
|
-
|
|
487
|
+
metadata: {
|
|
488
|
+
table: table,
|
|
489
|
+
field: field,
|
|
490
|
+
rowId: out[s.rowIdField] || out._id || null,
|
|
491
|
+
shape: s.aad ? "aad" : "plain",
|
|
492
|
+
reason: (e && e.message) || String(e),
|
|
493
|
+
},
|
|
397
494
|
});
|
|
398
495
|
} catch (_e) { /* drop-silent */ }
|
|
399
496
|
unsealed = null;
|
|
@@ -818,6 +915,9 @@ function materializePerRowKey(table, rowId, dbHandle) {
|
|
|
818
915
|
var saltHex = vault.getDerivedHashSalt().toString("hex");
|
|
819
916
|
var ikm = Buffer.from(saltHex + ":" + table + ":" + rowId + ":" + spec.info, "utf8");
|
|
820
917
|
var kRow = kdf(ikm, spec.keySize);
|
|
918
|
+
// allow:seal-without-aad — per-row K_row wrap; row identity is the
|
|
919
|
+
// K_row KDF input, not the AEAD AAD on the wrap. Copy-attacks fail
|
|
920
|
+
// because the wrapped K_row only decrypts data sealed under it.
|
|
821
921
|
var sealed = vault.seal(kRow.toString("base64"));
|
|
822
922
|
dbHandle.prepare(
|
|
823
923
|
'INSERT INTO "_blamejs_per_row_keys" (tableName, rowId, wrappedKey, createdAt) ' +
|