@blamejs/blamejs-shop 0.3.39 → 0.3.41
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 +4 -0
- package/lib/admin.js +331 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +244 -4
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +8 -0
- package/lib/vendor/blamejs/README.md +8 -5
- package/lib/vendor/blamejs/SECURITY.md +7 -0
- package/lib/vendor/blamejs/api-snapshot.json +266 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +7 -1
- package/lib/vendor/blamejs/lib/agent-idempotency.js +113 -0
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +108 -0
- package/lib/vendor/blamejs/lib/agent-snapshot.js +137 -0
- package/lib/vendor/blamejs/lib/agent-tenant.js +193 -17
- package/lib/vendor/blamejs/lib/ai-input.js +167 -3
- package/lib/vendor/blamejs/lib/ai-output.js +463 -0
- package/lib/vendor/blamejs/lib/ai-prompt.js +304 -0
- package/lib/vendor/blamejs/lib/archive-wrap.js +234 -1
- package/lib/vendor/blamejs/lib/archive.js +1 -0
- package/lib/vendor/blamejs/lib/audit.js +3 -0
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +47 -28
- package/lib/vendor/blamejs/lib/cluster.js +186 -14
- package/lib/vendor/blamejs/lib/codepoint-class.js +18 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act.js +446 -0
- package/lib/vendor/blamejs/lib/consent.js +104 -8
- package/lib/vendor/blamejs/lib/content-credentials.js +851 -41
- package/lib/vendor/blamejs/lib/crypto-field.js +5 -0
- package/lib/vendor/blamejs/lib/db.js +15 -0
- package/lib/vendor/blamejs/lib/framework-error.js +21 -0
- package/lib/vendor/blamejs/lib/mail-srs.js +122 -19
- package/lib/vendor/blamejs/lib/privacy.js +168 -0
- package/lib/vendor/blamejs/lib/safe-archive.js +196 -136
- package/lib/vendor/blamejs/lib/validate-opts.js +24 -0
- package/lib/vendor/blamejs/lib/vault/rotate.js +175 -15
- package/lib/vendor/blamejs/lib/vault-aad.js +84 -33
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +72 -0
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +95 -0
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +52 -0
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +31 -0
- package/lib/vendor/blamejs/test/00-primitives.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +103 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +186 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +140 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-output.test.js +125 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-prompt.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +6 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-vault-rotation.test.js +243 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js +70 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +289 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/privacy-vendor-review.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate-aad.test.js +158 -0
- package/package.json +1 -1
|
@@ -58,6 +58,8 @@ var { defineClass } = require("./framework-error");
|
|
|
58
58
|
var guardAgentRegistry = require("./guard-agent-registry");
|
|
59
59
|
var bCrypto = require("./crypto");
|
|
60
60
|
var agentAudit = require("./agent-audit");
|
|
61
|
+
var vaultAad = require("./vault-aad");
|
|
62
|
+
var validateOpts = require("./validate-opts");
|
|
61
63
|
|
|
62
64
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
63
65
|
var cluster = lazyRequire(function () { return require("./cluster"); });
|
|
@@ -796,13 +798,119 @@ function _safeAudit(ctx, action, actor, metadata) {
|
|
|
796
798
|
agentAudit.safeAudit(ctx.audit, action, actor, metadata);
|
|
797
799
|
}
|
|
798
800
|
|
|
801
|
+
// ---- Vault-key rotation: out-of-band reseal hook -------------------------
|
|
802
|
+
//
|
|
803
|
+
// Registry rows are AAD-sealed on an OPERATOR-SUPPLIED backend (opts.backend /
|
|
804
|
+
// the in-memory default), NOT in the framework's db.enc. The vault-key
|
|
805
|
+
// rotation pipeline (b.vaultRotate.rotate) only walks tables inside db.enc,
|
|
806
|
+
// so it cannot reach this backend — sealed tenantId + metadata cells would be
|
|
807
|
+
// ORPHANED under the old vault root after a rotation (CWE-320 cryptographic-
|
|
808
|
+
// key-management failure: ciphertext stranded under a retired key, then
|
|
809
|
+
// unreadable once the old keypair is destroyed). This reseal hook rotates the
|
|
810
|
+
// backend out-of-band, composing the SAME explicit-root primitive the in-tree
|
|
811
|
+
// pipeline uses (vaultAad.resealRoot) and the SAME AAD builder the seal path
|
|
812
|
+
// used (cryptoField._aadParts) so the re-sealed AAD tuple is byte-identical —
|
|
813
|
+
// one source of truth, no drift.
|
|
814
|
+
//
|
|
815
|
+
// Reseal store contract: the durable backend the operator wired for
|
|
816
|
+
// opts.backend already exposes list() (enumerate every row) + set(name, row)
|
|
817
|
+
// (write by name). The row identity column `name` is the AAD anchor and is
|
|
818
|
+
// never sealed, so it is always present in plaintext for the write-back.
|
|
819
|
+
// `tenantId` is a plain sealed string; `metadata` is a sealed JSON string —
|
|
820
|
+
// both are AAD-sealed cells, so each is re-sealed in place under the same AAD
|
|
821
|
+
// without unwrapping the metadata JSON.
|
|
822
|
+
/**
|
|
823
|
+
* @primitive b.agent.orchestrator.reseal
|
|
824
|
+
* @signature b.agent.orchestrator.reseal(opts)
|
|
825
|
+
* @since 0.14.12
|
|
826
|
+
* @status stable
|
|
827
|
+
* @compliance gdpr, soc2
|
|
828
|
+
* @related b.vault.getKeysJson, b.cryptoField.sealRow
|
|
829
|
+
*
|
|
830
|
+
* Re-seals every AAD-bound registry cell (tenantId / metadata) on an
|
|
831
|
+
* operator-supplied backend from the OLD vault keypair to the NEW one,
|
|
832
|
+
* out-of-band. The in-tree vault-key rotation pipeline only walks tables
|
|
833
|
+
* inside `db.enc`, so an operator-supplied orchestrator backend is
|
|
834
|
+
* unreachable to it — after a keypair rotation its cells would otherwise be
|
|
835
|
+
* orphaned under the retired root (CWE-320). Rebuilds each cell's AAD from
|
|
836
|
+
* the registered schema (one source of truth); only AAD-sealed cells are
|
|
837
|
+
* touched. The `name` row-identity column is the AAD anchor and is never
|
|
838
|
+
* sealed, so it is always present for the write-back.
|
|
839
|
+
*
|
|
840
|
+
* @opts
|
|
841
|
+
* store: Object, // { list(): rows[], set(name, row) } (the create() backend contract)
|
|
842
|
+
* oldRootJson: string, // b.vault.getKeysJson() of the retired keypair
|
|
843
|
+
* newRootJson: string, // b.vault.getKeysJson() of the new keypair
|
|
844
|
+
*
|
|
845
|
+
* @example
|
|
846
|
+
* await b.agent.orchestrator.reseal({ store: backend, oldRootJson: oldKeys, newRootJson: newKeys });
|
|
847
|
+
* // → { table: "agent_orchestrator_registry", resealed: 4 }
|
|
848
|
+
*/
|
|
849
|
+
function reseal(args) {
|
|
850
|
+
args = args || {};
|
|
851
|
+
validateOpts.requireNonEmptyString(args.oldRootJson,
|
|
852
|
+
"reseal: oldRootJson (b.vault.getKeysJson() of the OLD keypair)",
|
|
853
|
+
AgentOrchestratorError, "agent-orchestrator/bad-root");
|
|
854
|
+
validateOpts.requireNonEmptyString(args.newRootJson,
|
|
855
|
+
"reseal: newRootJson (b.vault.getKeysJson() of the NEW keypair)",
|
|
856
|
+
AgentOrchestratorError, "agent-orchestrator/bad-root");
|
|
857
|
+
var store = args.store;
|
|
858
|
+
validateOpts.requireMethods(store, ["list", "set"],
|
|
859
|
+
"reseal: operator store (same backend contract as create({ backend }))",
|
|
860
|
+
AgentOrchestratorError, "agent-orchestrator/bad-reseal-store");
|
|
861
|
+
_ensureSealTable();
|
|
862
|
+
var schema = cryptoField().getSchema(SEAL_TABLE);
|
|
863
|
+
return Promise.resolve(store.list()).then(function (rows) {
|
|
864
|
+
if (!Array.isArray(rows)) {
|
|
865
|
+
throw new AgentOrchestratorError("agent-orchestrator/bad-reseal-store",
|
|
866
|
+
"reseal: store.list() must resolve to an array of rows");
|
|
867
|
+
}
|
|
868
|
+
var chain = Promise.resolve();
|
|
869
|
+
var resealed = 0;
|
|
870
|
+
rows.forEach(function (row) {
|
|
871
|
+
if (!row || typeof row !== "object") return;
|
|
872
|
+
var changed = false;
|
|
873
|
+
for (var f = 0; f < schema.sealedFields.length; f += 1) {
|
|
874
|
+
var column = schema.sealedFields[f];
|
|
875
|
+
var value = row[column];
|
|
876
|
+
// Only AAD-sealed cells need rotating. Vault-less / pre-sealing rows
|
|
877
|
+
// carry plain values (sealRow leaves them untouched when vault-less);
|
|
878
|
+
// resealRoot would throw not-sealed on a plain value, so skip.
|
|
879
|
+
if (typeof value !== "string" || !vaultAad.isAadSealed(value)) continue;
|
|
880
|
+
var aadParts = cryptoField()._aadParts(schema, SEAL_TABLE, column, row);
|
|
881
|
+
row[column] = vaultAad.resealRoot(value, aadParts, args.oldRootJson, args.newRootJson);
|
|
882
|
+
changed = true;
|
|
883
|
+
}
|
|
884
|
+
if (changed) {
|
|
885
|
+
resealed += 1;
|
|
886
|
+
chain = chain.then(function () { return store.set(row.name, row); });
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
return chain.then(function () { return { table: SEAL_TABLE, resealed: resealed }; });
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
799
893
|
module.exports = {
|
|
800
894
|
create: create,
|
|
801
895
|
shardFor: shardFor,
|
|
896
|
+
reseal: reseal,
|
|
802
897
|
AgentOrchestratorError: AgentOrchestratorError,
|
|
803
898
|
guards: {
|
|
804
899
|
registry: guardAgentRegistry,
|
|
805
900
|
},
|
|
901
|
+
// AAD_ROTATION — the vault-key rotation descriptor every framework module
|
|
902
|
+
// that seals an {aad:true} table on an OPERATOR-SUPPLIED backend (outside
|
|
903
|
+
// db.enc) exports, so an operator can register it with a rotation eager-
|
|
904
|
+
// sweep and the codebase-patterns detect-and-refuse gate can confirm no
|
|
905
|
+
// such external-store table is silently orphaned. `backend: "external"`
|
|
906
|
+
// flags that the in-tree b.vaultRotate.rotate pipeline cannot reach it.
|
|
907
|
+
AAD_ROTATION: {
|
|
908
|
+
table: SEAL_TABLE,
|
|
909
|
+
rowIdField: "name",
|
|
910
|
+
schemaVersion: "1",
|
|
911
|
+
backend: "external",
|
|
912
|
+
reseal: reseal,
|
|
913
|
+
},
|
|
806
914
|
// Test-only — flush the salted FNV basis cache so a vault reset
|
|
807
915
|
// between tests forces re-derivation.
|
|
808
916
|
_resetForTest: function () { _saltedFnvBasisCache = null; },
|
|
@@ -53,6 +53,8 @@ var bCrypto = require("./crypto");
|
|
|
53
53
|
var guardSnapshotEnvelope = require("./guard-snapshot-envelope");
|
|
54
54
|
var agentAudit = require("./agent-audit");
|
|
55
55
|
var safeJson = require("./safe-json");
|
|
56
|
+
var vaultAad = require("./vault-aad");
|
|
57
|
+
var validateOpts = require("./validate-opts");
|
|
56
58
|
|
|
57
59
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
58
60
|
var auditSign = lazyRequire(function () { return require("./audit-sign"); });
|
|
@@ -163,6 +165,117 @@ function create(opts) {
|
|
|
163
165
|
};
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
// ---- Wrapped-AAD root re-seal (vault-key rotation pipeline) ----------------
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @primitive b.agent.snapshot.reseal
|
|
172
|
+
* @signature b.agent.snapshot.reseal(opts)
|
|
173
|
+
* @since 0.14.12
|
|
174
|
+
* @status stable
|
|
175
|
+
* @related b.agent.snapshot.create, b.vault.getKeysJson
|
|
176
|
+
*
|
|
177
|
+
* Re-seal every persisted snapshot envelope from the OLD vault root to
|
|
178
|
+
* the NEW vault root under the SAME column-shaped AAD, for a vault-key
|
|
179
|
+
* rotation. The snapshot seal is a `vault.aad:` ciphertext hidden behind
|
|
180
|
+
* the `snap-sealed-v1:` wrapper prefix and written to an operator
|
|
181
|
+
* backend, so a `db.enc` scan for the bare `vault.aad:` prefix can
|
|
182
|
+
* neither detect nor reach it — the rotation pipeline drives the re-key
|
|
183
|
+
* through this explicit backend walk. Each row is unsealed under the old
|
|
184
|
+
* root and re-sealed under the new root in memory (composing
|
|
185
|
+
* `b.vault.aad.resealRoot`); the plaintext envelope is never written to
|
|
186
|
+
* operator-readable storage. The decorative wrapper fields the backend's
|
|
187
|
+
* `list()` filters on (`snapshotId` / `takenAt` / `tenantId`) are
|
|
188
|
+
* preserved, so the index is untouched.
|
|
189
|
+
*
|
|
190
|
+
* `allowPlaintext` envelopes (no `sealed` wrapper) carry no AAD-sealed
|
|
191
|
+
* blob to re-key and are skipped; the returned `resealed` count reflects
|
|
192
|
+
* only re-sealed rows. A row sealed by a non-default KMS sealer (the
|
|
193
|
+
* inner blob is not a `vault.aad:` value) is refused — re-key it through
|
|
194
|
+
* the operator's own KMS, not this path.
|
|
195
|
+
*
|
|
196
|
+
* @opts
|
|
197
|
+
* backend: { put, get, list }, // the same backend create() was wired with
|
|
198
|
+
* oldRootJson: string, // b.vault.getKeysJson() of the OLD keypair
|
|
199
|
+
* newRootJson: string, // b.vault.getKeysJson() of the NEW keypair
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* var result = await b.agent.snapshot.reseal({
|
|
203
|
+
* backend: operatorBackend,
|
|
204
|
+
* oldRootJson: oldKeysJson,
|
|
205
|
+
* newRootJson: newKeysJson,
|
|
206
|
+
* });
|
|
207
|
+
* result.table; // → "agent.snapshot"
|
|
208
|
+
* result.resealed; // → <count of re-keyed snapshots>
|
|
209
|
+
*/
|
|
210
|
+
async function reseal(opts) {
|
|
211
|
+
opts = opts || {};
|
|
212
|
+
var backend = opts.backend;
|
|
213
|
+
validateOpts.requireMethods(backend, ["put", "get", "list"],
|
|
214
|
+
"reseal: opts.backend (same backend create() was wired with)",
|
|
215
|
+
AgentSnapshotError, "agent-snapshot/bad-backend");
|
|
216
|
+
validateOpts.requireNonEmptyString(opts.oldRootJson,
|
|
217
|
+
"reseal: opts.oldRootJson (b.vault.getKeysJson() of the OLD keypair)",
|
|
218
|
+
AgentSnapshotError, "agent-snapshot/bad-root");
|
|
219
|
+
validateOpts.requireNonEmptyString(opts.newRootJson,
|
|
220
|
+
"reseal: opts.newRootJson (b.vault.getKeysJson() of the NEW keypair)",
|
|
221
|
+
AgentSnapshotError, "agent-snapshot/bad-root");
|
|
222
|
+
|
|
223
|
+
var entries = await backend.list();
|
|
224
|
+
if (!Array.isArray(entries)) return { table: SNAPSHOT_TABLE, resealed: 0 };
|
|
225
|
+
var resealed = 0;
|
|
226
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
227
|
+
var snapshotId = entries[i] && entries[i].snapshotId;
|
|
228
|
+
if (typeof snapshotId !== "string" || snapshotId.length === 0) continue;
|
|
229
|
+
var raw = await backend.get(snapshotId);
|
|
230
|
+
if (!raw) continue;
|
|
231
|
+
// Only the sealed-wrapper shape carries a re-keyable blob. The
|
|
232
|
+
// allowPlaintext path stores the bare envelope (no `sealed`) — skip.
|
|
233
|
+
if (!raw.sealed || typeof raw.sealed !== "string" ||
|
|
234
|
+
raw.sealed.indexOf(SEALED_PREFIX) !== 0) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
var innerBlob = raw.sealed.slice(SEALED_PREFIX.length);
|
|
238
|
+
// The inner blob is a vault.aad: ciphertext (when sealed by the
|
|
239
|
+
// default b.vault.aad sealer — the only sealer resealRoot can
|
|
240
|
+
// re-key). A custom KMS sealer's blob isn't a vault.aad: value, so
|
|
241
|
+
// refuse rather than silently no-op: the operator must drive the
|
|
242
|
+
// re-key through their own KMS.
|
|
243
|
+
if (!vaultAad.isAadSealed(innerBlob)) {
|
|
244
|
+
throw new AgentSnapshotError("agent-snapshot/not-vault-sealed",
|
|
245
|
+
"reseal: snapshot " + snapshotId + " was sealed by a non-vault sealer " +
|
|
246
|
+
"(no " + JSON.stringify(vaultAad.AAD_PREFIX) + " prefix on the inner blob); " +
|
|
247
|
+
"re-key it through the KMS the operator wired as opts.sealer at create() time");
|
|
248
|
+
}
|
|
249
|
+
// Rebuild the EXACT AAD the envelope was sealed under via the
|
|
250
|
+
// module's own _snapshotAad builder — single source of truth with
|
|
251
|
+
// the seal (_persist) + unseal (_unwrapAndVerify) paths. The wrapper
|
|
252
|
+
// carries snapshotId; schemaVersion mirrors the unseal path's
|
|
253
|
+
// `raw.schemaVersion || SCHEMA_VERSION` fallback so an envelope
|
|
254
|
+
// written under an older SCHEMA_VERSION re-keys under its original
|
|
255
|
+
// AAD, not the current one.
|
|
256
|
+
var aad = _snapshotAad({
|
|
257
|
+
snapshotId: snapshotId,
|
|
258
|
+
schemaVersion: raw.schemaVersion != null ? raw.schemaVersion : SCHEMA_VERSION,
|
|
259
|
+
});
|
|
260
|
+
var rekeyed;
|
|
261
|
+
try {
|
|
262
|
+
rekeyed = vaultAad.resealRoot(innerBlob, aad, opts.oldRootJson, opts.newRootJson);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
throw new AgentSnapshotError("agent-snapshot/reseal-failed",
|
|
265
|
+
"reseal: snapshot " + snapshotId + " failed to re-key — the value may not have " +
|
|
266
|
+
"been sealed under oldRootJson + this AAD, or the bytes are tampered (" +
|
|
267
|
+
((e && e.message) || String(e)) + ")");
|
|
268
|
+
}
|
|
269
|
+
// Re-apply the prefix + preserve every decorative wrapper field
|
|
270
|
+
// (snapshotId / takenAt / tenantId the backend's list() filters on)
|
|
271
|
+
// so the rotation leaves the index untouched.
|
|
272
|
+
var rewritten = Object.assign({}, raw, { sealed: SEALED_PREFIX + rekeyed });
|
|
273
|
+
await backend.put(snapshotId, rewritten);
|
|
274
|
+
resealed += 1;
|
|
275
|
+
}
|
|
276
|
+
return { table: SNAPSHOT_TABLE, resealed: resealed };
|
|
277
|
+
}
|
|
278
|
+
|
|
166
279
|
// ---- Signer + sealer resolution -------------------------------------------
|
|
167
280
|
|
|
168
281
|
function _resolveSigner(ctx) {
|
|
@@ -666,11 +779,35 @@ function _frameworkVersion() {
|
|
|
666
779
|
catch (_e) { return "unknown"; }
|
|
667
780
|
}
|
|
668
781
|
|
|
782
|
+
// AAD_ROTATION — the eager-register descriptor the vault-key rotation
|
|
783
|
+
// pipeline consumes. backend "external" because snapshot envelopes live
|
|
784
|
+
// in an operator-supplied backend (not the framework db.enc store), so a
|
|
785
|
+
// `db.enc` scan for the bare "vault.aad:" prefix can't reach them — the
|
|
786
|
+
// pipeline drives the re-key through `reseal` against the same backend.
|
|
787
|
+
// The descriptor's `reseal({ store, oldRootJson, newRootJson })` maps the
|
|
788
|
+
// pipeline's generic `store` term onto this module's `backend` (the
|
|
789
|
+
// snapshot backing store), then defers to the module's own reseal.
|
|
669
790
|
module.exports = {
|
|
670
791
|
create: create,
|
|
792
|
+
reseal: reseal,
|
|
671
793
|
SCHEMA_VERSION: SCHEMA_VERSION,
|
|
794
|
+
SEALED_PREFIX: SEALED_PREFIX,
|
|
672
795
|
AgentSnapshotError: AgentSnapshotError,
|
|
673
796
|
guards: {
|
|
674
797
|
envelope: guardSnapshotEnvelope,
|
|
675
798
|
},
|
|
799
|
+
AAD_ROTATION: {
|
|
800
|
+
table: SNAPSHOT_TABLE,
|
|
801
|
+
rowIdField: "snapshotId",
|
|
802
|
+
schemaVersion: String(SCHEMA_VERSION),
|
|
803
|
+
backend: "external",
|
|
804
|
+
reseal: function (rotationOpts) {
|
|
805
|
+
rotationOpts = rotationOpts || {};
|
|
806
|
+
return reseal({
|
|
807
|
+
backend: rotationOpts.store || rotationOpts.backend,
|
|
808
|
+
oldRootJson: rotationOpts.oldRootJson,
|
|
809
|
+
newRootJson: rotationOpts.newRootJson,
|
|
810
|
+
});
|
|
811
|
+
},
|
|
812
|
+
},
|
|
676
813
|
};
|
|
@@ -58,6 +58,8 @@ var guardTenantId = require("./guard-tenant-id");
|
|
|
58
58
|
var bCrypto = require("./crypto");
|
|
59
59
|
var agentAudit = require("./agent-audit");
|
|
60
60
|
var safeJson = require("./safe-json");
|
|
61
|
+
var vaultAad = require("./vault-aad");
|
|
62
|
+
var validateOpts = require("./validate-opts");
|
|
61
63
|
|
|
62
64
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
63
65
|
var cryptoField = lazyRequire(function () { return require("./crypto-field"); });
|
|
@@ -115,8 +117,12 @@ var CROSS_TENANT_ADMIN_SCOPE = "framework-cross-tenant-admin";
|
|
|
115
117
|
|
|
116
118
|
// Per-tenant key derivation domain separators. NIST SP 800-108 r1 §5.1
|
|
117
119
|
// KDF-in-Counter shape — fixed "label" + tenantId-as-salt + purpose-as-
|
|
118
|
-
// info.
|
|
119
|
-
//
|
|
120
|
+
// info. The root secret is the vault master keypair (SHA3-512 of
|
|
121
|
+
// b.vault.getKeysJson()); rotating the vault keypair changes the root,
|
|
122
|
+
// so every tnt-v1: cell sealed under the old root must be re-sealed
|
|
123
|
+
// under the new root. That migration is NOT automatic — it runs via the
|
|
124
|
+
// AAD_ROTATION reseal hook (see `reseal` below), which the vault-key
|
|
125
|
+
// rotation pipeline composes per the explicit-root primitive contract.
|
|
120
126
|
var TENANT_KDF_LABEL = "blamejs.agent.tenant/v1";
|
|
121
127
|
// 32 bytes — XChaCha20-Poly1305 key length. Distinct from the audit
|
|
122
128
|
// truncation buffer so future key-length bumps don't have to chase a
|
|
@@ -391,20 +397,32 @@ function _check(ctx, actor, agentTenantId) {
|
|
|
391
397
|
// rootKey is SHA3-512(vault.getKeysJson()). Same derivation `b.vault.aad`
|
|
392
398
|
// uses internally — the vault's master keypair PEM is the secret KDK.
|
|
393
399
|
// Rotating the vault passphrase / keypair (b.vaultRotate.rotate)
|
|
394
|
-
// changes rootKey, which changes every derived tenant key —
|
|
395
|
-
//
|
|
400
|
+
// changes rootKey, which changes every derived tenant key — so every
|
|
401
|
+
// prior tnt-v1: cell must be re-sealed old-root -> new-root. The
|
|
402
|
+
// `reseal` hook below (eager-registered via AAD_ROTATION) performs that
|
|
403
|
+
// migration; it uses the explicit-root variant of the tenant-key
|
|
404
|
+
// derivation (rootKeysJson arg) to decrypt under the old root and
|
|
405
|
+
// re-encrypt under the new one within one process.
|
|
396
406
|
//
|
|
397
407
|
// `derivedKey` returns a hex-encoded 32-byte key (64 chars) to keep
|
|
398
408
|
// the wire shape compatible with prior callers. Internal callers that
|
|
399
409
|
// need the raw key use `_tenantFieldKey` directly.
|
|
400
410
|
|
|
401
|
-
function _vaultRootBytes() {
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
411
|
+
function _vaultRootBytes(rootKeysJson) {
|
|
412
|
+
// rootKeysJson lets the vault-key rotation pipeline derive the per-
|
|
413
|
+
// tenant key under a SPECIFIC vault root (old or new keypair) within
|
|
414
|
+
// one process — mirrors vault-aad._deriveKey's explicit-root arg. When
|
|
415
|
+
// omitted it reads the live singleton via vault().getKeysJson().
|
|
416
|
+
//
|
|
417
|
+
// getKeysJson() throws when the vault hasn't been init'd. That is the
|
|
418
|
+
// right secure-by-default posture: tenant-derived keys cannot be
|
|
419
|
+
// produced before the operator has bootstrapped the vault. The error
|
|
420
|
+
// reaches the caller (sealField / register) so an operator mis-ordering
|
|
421
|
+
// boot (start agents before vault.init) sees a clear refusal rather
|
|
422
|
+
// than getting weakened-but-deterministic keys.
|
|
423
|
+
if (typeof rootKeysJson === "string" && rootKeysJson.length > 0) {
|
|
424
|
+
return Buffer.from(bCrypto.sha3Hash(rootKeysJson), "hex");
|
|
425
|
+
}
|
|
408
426
|
var keysJson;
|
|
409
427
|
try { keysJson = vault().getKeysJson(); }
|
|
410
428
|
catch (e) {
|
|
@@ -415,7 +433,7 @@ function _vaultRootBytes() {
|
|
|
415
433
|
return Buffer.from(bCrypto.sha3Hash(keysJson), "hex");
|
|
416
434
|
}
|
|
417
435
|
|
|
418
|
-
function _deriveTenantKeyBytes(tenantId, purpose) {
|
|
436
|
+
function _deriveTenantKeyBytes(tenantId, purpose, rootKeysJson) {
|
|
419
437
|
guardTenantId.validate(tenantId);
|
|
420
438
|
if (typeof purpose !== "string" || purpose.length === 0) {
|
|
421
439
|
throw new AgentTenantError("agent-tenant/bad-purpose",
|
|
@@ -424,8 +442,9 @@ function _deriveTenantKeyBytes(tenantId, purpose) {
|
|
|
424
442
|
// Domain-separated KDF input. NUL separators between fields prevent
|
|
425
443
|
// (label, tenantId="x\0y", purpose="z") colliding with
|
|
426
444
|
// (label, tenantId="x", purpose="y\0z") — same byte concatenation,
|
|
427
|
-
// different logical context.
|
|
428
|
-
|
|
445
|
+
// different logical context. rootKeysJson (optional) pins the vault
|
|
446
|
+
// root for the rotation reseal path; default is the live singleton.
|
|
447
|
+
var rootBytes = _vaultRootBytes(rootKeysJson);
|
|
429
448
|
var input = Buffer.concat([
|
|
430
449
|
Buffer.from(TENANT_KDF_LABEL, "utf8"),
|
|
431
450
|
Buffer.from([0x00]),
|
|
@@ -455,7 +474,11 @@ function _deriveTenantKeyBytes(tenantId, purpose) {
|
|
|
455
474
|
* `derivedKey(t, "archive-wrap")` is recoverable later from the same
|
|
456
475
|
* tenant + purpose with no key escrow. Rotating the vault
|
|
457
476
|
* (`b.vaultRotate.rotate`) changes the root and therefore every
|
|
458
|
-
* derived key
|
|
477
|
+
* derived key, so every cell sealed under the old root must be
|
|
478
|
+
* re-sealed under the new one. That migration runs through the
|
|
479
|
+
* module's `reseal` hook (eager-registered via `AAD_ROTATION`), not
|
|
480
|
+
* silently on the next read — a value sealed under the old root does
|
|
481
|
+
* not decrypt under the new root until the rotation pipeline walks it.
|
|
459
482
|
*
|
|
460
483
|
* Throws if the vault has not been initialized (keys cannot be derived
|
|
461
484
|
* before bootstrap) or if `purpose` is empty. This is the same
|
|
@@ -552,12 +575,14 @@ function _auditFor(ctx, tenantId) {
|
|
|
552
575
|
|
|
553
576
|
var TENANT_FIELD_PREFIX = "tnt-v1:";
|
|
554
577
|
|
|
555
|
-
function _tenantFieldKey(tenantId, table) {
|
|
578
|
+
function _tenantFieldKey(tenantId, table, rootKeysJson) {
|
|
556
579
|
// 32-byte symmetric key for XChaCha20-Poly1305. _deriveTenantKeyBytes
|
|
557
580
|
// returns the raw key bound to the vault master + tenantId + purpose
|
|
558
581
|
// — see the commentary above _derivedKey for the threat
|
|
559
582
|
// model that drove this away from public-input-only derivation.
|
|
560
|
-
|
|
583
|
+
// rootKeysJson (optional) pins a specific vault root for the rotation
|
|
584
|
+
// reseal path; default is the live singleton.
|
|
585
|
+
return _deriveTenantKeyBytes(tenantId, "cryptoField:" + table, rootKeysJson);
|
|
561
586
|
}
|
|
562
587
|
|
|
563
588
|
function _tenantFieldAad(tenantId, table, field) {
|
|
@@ -602,6 +627,25 @@ function _unsealField(tenantId, table, field, ciphertext) {
|
|
|
602
627
|
return plain.toString("utf8");
|
|
603
628
|
}
|
|
604
629
|
|
|
630
|
+
// Explicit-root re-seal of a single tnt-v1: cell, old root -> new root,
|
|
631
|
+
// under the SAME (tenantId, table, field) AAD. Decrypts with the key
|
|
632
|
+
// derived from oldRootJson and re-encrypts under newRootJson — the
|
|
633
|
+
// XChaCha20-Poly1305 tag refuses any cell that wasn't sealed under the
|
|
634
|
+
// old root + this AAD (CWE-345 / CWE-441). Values without the prefix
|
|
635
|
+
// (plaintext columns, already-rotated cells) pass through untouched.
|
|
636
|
+
function _resealTenantCell(tenantId, table, field, ciphertext, oldRootJson, newRootJson) {
|
|
637
|
+
if (typeof ciphertext !== "string" || ciphertext.indexOf(TENANT_FIELD_PREFIX) !== 0) {
|
|
638
|
+
return ciphertext;
|
|
639
|
+
}
|
|
640
|
+
var packed = Buffer.from(ciphertext.slice(TENANT_FIELD_PREFIX.length), "base64");
|
|
641
|
+
var aad = _tenantFieldAad(tenantId, table, field);
|
|
642
|
+
var oldKey = _tenantFieldKey(tenantId, table, oldRootJson);
|
|
643
|
+
var plain = bCrypto.decryptPacked(packed, oldKey, aad);
|
|
644
|
+
var newKey = _tenantFieldKey(tenantId, table, newRootJson);
|
|
645
|
+
var reSealed = bCrypto.encryptPacked(plain, newKey, aad);
|
|
646
|
+
return TENANT_FIELD_PREFIX + reSealed.toString("base64");
|
|
647
|
+
}
|
|
648
|
+
|
|
605
649
|
function _sealRowForTenant(tenantId, table, row) {
|
|
606
650
|
// Adopts the existing b.cryptoField table schema (sealedFields) but
|
|
607
651
|
// routes each field through the per-tenant AEAD instead of the
|
|
@@ -663,6 +707,113 @@ function _unsealRowForTenant(ctx, tenantId, table, row) {
|
|
|
663
707
|
return out;
|
|
664
708
|
}
|
|
665
709
|
|
|
710
|
+
// ---- Vault-key rotation: re-seal hook -------------------------------------
|
|
711
|
+
//
|
|
712
|
+
// Two root-derived families live in this module, both keyed off the vault
|
|
713
|
+
// master keypair (SHA3-512 of b.vault.getKeysJson()):
|
|
714
|
+
//
|
|
715
|
+
// 1. The registry table "agent_tenant_registry" — a b.cryptoField
|
|
716
|
+
// {aad:true} table whose `metadata` column holds vault.aad: cells.
|
|
717
|
+
// Re-sealed old-root -> new-root via vaultAad.resealRoot, with the
|
|
718
|
+
// AAD tuple rebuilt by cryptoField._aadParts (the SAME builder the
|
|
719
|
+
// seal side uses — single source of truth, no drift).
|
|
720
|
+
//
|
|
721
|
+
// 2. The tnt-v1: per-tenant sealed cells written by sealField /
|
|
722
|
+
// sealRowForTenant. Re-sealed via _resealTenantCell, which derives
|
|
723
|
+
// the per-tenant XChaCha20-Poly1305 key under each root explicitly
|
|
724
|
+
// and re-binds the (tenantId|table|field) AAD.
|
|
725
|
+
//
|
|
726
|
+
// Both descriptors export a `reseal({ store, oldRootJson, newRootJson })`
|
|
727
|
+
// hook. The vault-key rotation pipeline eager-registers every module's
|
|
728
|
+
// AAD_ROTATION descriptor(s) and calls each reseal with the operator-
|
|
729
|
+
// supplied backing store; without this hook, a keypair rotation would
|
|
730
|
+
// orphan every prior cell (decryptable under neither root). oldRootJson /
|
|
731
|
+
// newRootJson are b.vault.getKeysJson() output for the two keypairs.
|
|
732
|
+
|
|
733
|
+
var REGISTRY_SCHEMA_VERSION = "1";
|
|
734
|
+
|
|
735
|
+
// Rebuild the registry's AAD tuple via the cryptoField seal-side builder
|
|
736
|
+
// so the rotate side can never drift from the seal side. The schema
|
|
737
|
+
// shape cryptoField._aadParts reads is { rowIdField, schemaVersion }.
|
|
738
|
+
function _registryAadFor(row) {
|
|
739
|
+
return cryptoField()._aadParts(
|
|
740
|
+
{ rowIdField: "tenantId", schemaVersion: REGISTRY_SCHEMA_VERSION },
|
|
741
|
+
SEAL_TABLE, "metadata", row);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Re-seal the registry table's vault.aad: metadata cells. `store` is the
|
|
745
|
+
// operator's backing store for SEAL_TABLE, exposing list() ->
|
|
746
|
+
// [{ tenantId, metadata, ... }] and set(tenantId, row). Each metadata
|
|
747
|
+
// cell that is vault.aad-sealed is re-sealed old-root -> new-root under
|
|
748
|
+
// the SAME (table, tenantId, "metadata", schemaVersion) AAD.
|
|
749
|
+
function _resealRegistry(args) {
|
|
750
|
+
var store = args && args.store;
|
|
751
|
+
validateOpts.requireMethods(store, ["list", "set"],
|
|
752
|
+
"reseal: store for the '" + SEAL_TABLE + "' table",
|
|
753
|
+
AgentTenantError, "agent-tenant/bad-reseal-store");
|
|
754
|
+
validateOpts.requireNonEmptyString(args.oldRootJson,
|
|
755
|
+
"reseal: oldRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
|
|
756
|
+
validateOpts.requireNonEmptyString(args.newRootJson,
|
|
757
|
+
"reseal: newRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
|
|
758
|
+
return Promise.resolve(store.list()).then(function (rows) {
|
|
759
|
+
rows = Array.isArray(rows) ? rows : [];
|
|
760
|
+
var resealed = 0;
|
|
761
|
+
var chain = Promise.resolve();
|
|
762
|
+
rows.forEach(function (row) {
|
|
763
|
+
if (!row || row.tenantId == null) return;
|
|
764
|
+
var cell = row.metadata;
|
|
765
|
+
if (!vaultAad.isAadSealed(cell)) return; // plaintext / already-migrated
|
|
766
|
+
var aad = _registryAadFor(row);
|
|
767
|
+
var next = vaultAad.resealRoot(cell, aad, args.oldRootJson, args.newRootJson);
|
|
768
|
+
var updated = Object.assign({}, row, { metadata: next });
|
|
769
|
+
resealed += 1;
|
|
770
|
+
chain = chain.then(function () { return store.set(row.tenantId, updated); });
|
|
771
|
+
});
|
|
772
|
+
return chain.then(function () {
|
|
773
|
+
return { table: SEAL_TABLE, resealed: resealed };
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Re-seal the tnt-v1: per-tenant sealed cells. `store` is the operator's
|
|
779
|
+
// backing store for every table that carries tnt-v1: columns, exposing
|
|
780
|
+
// list() -> [{ tenantId, table, field, value, _id? }] (one entry per
|
|
781
|
+
// sealed cell, carrying the context needed to rebuild AAD + key) and
|
|
782
|
+
// write(cell, newValue) to persist the re-sealed value. Plaintext /
|
|
783
|
+
// already-rotated values pass through untouched.
|
|
784
|
+
function _resealTenantCells(args) {
|
|
785
|
+
var store = args && args.store;
|
|
786
|
+
validateOpts.requireMethods(store, ["list", "write"],
|
|
787
|
+
"reseal: store for tnt-v1: cells",
|
|
788
|
+
AgentTenantError, "agent-tenant/bad-reseal-store");
|
|
789
|
+
validateOpts.requireNonEmptyString(args.oldRootJson,
|
|
790
|
+
"reseal: oldRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
|
|
791
|
+
validateOpts.requireNonEmptyString(args.newRootJson,
|
|
792
|
+
"reseal: newRootJson (b.vault.getKeysJson output)", AgentTenantError, "agent-tenant/bad-reseal-root");
|
|
793
|
+
return Promise.resolve(store.list()).then(function (cells) {
|
|
794
|
+
cells = Array.isArray(cells) ? cells : [];
|
|
795
|
+
var resealed = 0;
|
|
796
|
+
var chain = Promise.resolve();
|
|
797
|
+
cells.forEach(function (cell) {
|
|
798
|
+
if (!cell || cell.tenantId == null ||
|
|
799
|
+
typeof cell.table !== "string" || typeof cell.field !== "string") {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
var value = cell.value;
|
|
803
|
+
if (typeof value !== "string" || value.indexOf(TENANT_FIELD_PREFIX) !== 0) {
|
|
804
|
+
return; // not a tnt-v1: cell — leave untouched
|
|
805
|
+
}
|
|
806
|
+
var next = _resealTenantCell(cell.tenantId, cell.table, cell.field,
|
|
807
|
+
value, args.oldRootJson, args.newRootJson);
|
|
808
|
+
resealed += 1;
|
|
809
|
+
chain = chain.then(function () { return store.write(cell, next); });
|
|
810
|
+
});
|
|
811
|
+
return chain.then(function () {
|
|
812
|
+
return { table: TENANT_FIELD_PREFIX, resealed: resealed };
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
666
817
|
// ---- Destroy preconditions ------------------------------------------------
|
|
667
818
|
|
|
668
819
|
function _checkDestroyPreconditions(args, tenantId) {
|
|
@@ -703,11 +854,36 @@ function _inMemoryBackend() {
|
|
|
703
854
|
};
|
|
704
855
|
}
|
|
705
856
|
|
|
857
|
+
// Vault-key rotation descriptors — the rotation pipeline eager-registers
|
|
858
|
+
// these and calls each reseal({ store, oldRootJson, newRootJson }) to
|
|
859
|
+
// re-seal this module's two root-derived families old-root -> new-root.
|
|
860
|
+
// backend: "external" — the cells live in the operator's backing store
|
|
861
|
+
// (the registry backend + the operator's tenant data tables), not in the
|
|
862
|
+
// framework's at-rest SQLite, so the rotation pipeline cannot walk them
|
|
863
|
+
// from the data directory alone; the operator supplies the store.
|
|
864
|
+
var AAD_ROTATION = [
|
|
865
|
+
{
|
|
866
|
+
table: SEAL_TABLE,
|
|
867
|
+
rowIdField: "tenantId",
|
|
868
|
+
schemaVersion: REGISTRY_SCHEMA_VERSION,
|
|
869
|
+
backend: "external",
|
|
870
|
+
reseal: _resealRegistry,
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
table: TENANT_FIELD_PREFIX, // prefix family, not a single SQL table
|
|
874
|
+
rowIdField: "tenantId",
|
|
875
|
+
schemaVersion: "v1", // tnt-v1: ciphertext shape version
|
|
876
|
+
backend: "external",
|
|
877
|
+
reseal: _resealTenantCells,
|
|
878
|
+
},
|
|
879
|
+
];
|
|
880
|
+
|
|
706
881
|
module.exports = {
|
|
707
882
|
create: create,
|
|
708
883
|
derivedKey: _derivedKey,
|
|
709
884
|
CROSS_TENANT_ADMIN_SCOPE: CROSS_TENANT_ADMIN_SCOPE,
|
|
710
885
|
AgentTenantError: AgentTenantError,
|
|
886
|
+
AAD_ROTATION: AAD_ROTATION,
|
|
711
887
|
guards: {
|
|
712
888
|
tenantId: guardTenantId,
|
|
713
889
|
},
|