@blamejs/core 0.14.24 → 0.14.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 +4 -0
- package/lib/auth/oauth.js +25 -5
- package/lib/auth/sd-jwt-vc.js +16 -3
- package/lib/break-glass.js +153 -3
- package/lib/constants.js +11 -0
- package/lib/crypto-field.js +307 -78
- package/lib/db-query.js +65 -5
- package/lib/db.js +17 -3
- package/lib/dsr.js +378 -52
- package/lib/middleware/idempotency-key.js +21 -13
- package/lib/queue-local.js +23 -1
- package/lib/queue.js +7 -0
- package/lib/request-helpers.js +7 -0
- package/lib/retention.js +11 -1
- package/lib/vault/rotate.js +64 -44
- package/lib/vault-aad.js +6 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/db-query.js
CHANGED
|
@@ -509,7 +509,9 @@ class Query {
|
|
|
509
509
|
this._whereClause() + this._orderLimitOffset() + " LIMIT 1";
|
|
510
510
|
var stmt = this._db.prepare(sql);
|
|
511
511
|
var row = stmt.get.apply(stmt, this._whereParams);
|
|
512
|
-
|
|
512
|
+
// 4th arg (dbHandle) lets unsealRow fetch + unwrap the row-scoped
|
|
513
|
+
// K_row for vault.row: cells (declarePerRowKey tables).
|
|
514
|
+
return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row, undefined, this._db) : null;
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
all() {
|
|
@@ -519,8 +521,9 @@ class Query {
|
|
|
519
521
|
var rows = stmt.all.apply(stmt, this._whereParams);
|
|
520
522
|
var out = new Array(rows.length);
|
|
521
523
|
var key = this._cryptoFieldKey();
|
|
524
|
+
var dbHandle = this._db;
|
|
522
525
|
for (var i = 0; i < rows.length; i++) {
|
|
523
|
-
out[i] = cryptoField.unsealRow(key, rows[i]);
|
|
526
|
+
out[i] = cryptoField.unsealRow(key, rows[i], undefined, dbHandle);
|
|
524
527
|
}
|
|
525
528
|
return out;
|
|
526
529
|
}
|
|
@@ -549,6 +552,7 @@ class Query {
|
|
|
549
552
|
}
|
|
550
553
|
var stmt = this._db.prepare(sql);
|
|
551
554
|
var key = this._cryptoFieldKey();
|
|
555
|
+
var dbHandle = this._db;
|
|
552
556
|
var iter;
|
|
553
557
|
try { iter = stmt.iterate.apply(stmt, this._whereParams); }
|
|
554
558
|
catch (e) {
|
|
@@ -570,7 +574,7 @@ class Query {
|
|
|
570
574
|
var step = iter.next();
|
|
571
575
|
if (step.done) { this.push(null); return; }
|
|
572
576
|
emitted += 1;
|
|
573
|
-
this.push(cryptoField.unsealRow(key, step.value));
|
|
577
|
+
this.push(cryptoField.unsealRow(key, step.value, undefined, dbHandle));
|
|
574
578
|
} catch (e) {
|
|
575
579
|
this.destroy(e);
|
|
576
580
|
}
|
|
@@ -596,7 +600,19 @@ class Query {
|
|
|
596
600
|
// Residency gates read the PLAINTEXT row (the tag column must be
|
|
597
601
|
// inspectable even when sibling columns seal below).
|
|
598
602
|
_assertLocalResidency(this._cryptoFieldKey(), withId, "insert");
|
|
599
|
-
|
|
603
|
+
// Per-row-key tables (declarePerRowKey): materialize a fresh K_row
|
|
604
|
+
// BEFORE sealRow so sealed columns encrypt under the row-scoped key
|
|
605
|
+
// (vault.row: cells). rowId MUST be withId._id — the same value
|
|
606
|
+
// b.subject.eraseHard / b.retention destroy on, so a later shred
|
|
607
|
+
// makes these cells undecryptable. Materialize stores the random
|
|
608
|
+
// row-secret AAD-sealed in _blamejs_per_row_keys.
|
|
609
|
+
var sealOpts;
|
|
610
|
+
var cfKey = this._cryptoFieldKey();
|
|
611
|
+
if (cryptoField.hasPerRowKey(cfKey)) {
|
|
612
|
+
var kRow = cryptoField.materializePerRowKey(cfKey, withId._id, this._db);
|
|
613
|
+
sealOpts = { kRow: kRow, rowId: withId._id };
|
|
614
|
+
}
|
|
615
|
+
var sealed = cryptoField.sealRow(cfKey, withId, sealOpts);
|
|
600
616
|
var cols = Object.keys(sealed);
|
|
601
617
|
var placeholders = cols.map(function () { return "?"; }).join(", ");
|
|
602
618
|
var quotedCols = cols.map(function (c) { return '"' + c + '"'; }).join(", ");
|
|
@@ -637,7 +653,16 @@ class Query {
|
|
|
637
653
|
// touches the residency tag (or a region-bound column) is a
|
|
638
654
|
// transfer and goes through the same refusal matrix as INSERT.
|
|
639
655
|
_assertLocalResidency(this._cryptoFieldKey(), changes, "update");
|
|
640
|
-
var
|
|
656
|
+
var cfKey = this._cryptoFieldKey();
|
|
657
|
+
// Per-row-key tables: sealed columns must re-encrypt under EACH
|
|
658
|
+
// affected row's own K_row, so a single set-based UPDATE can't seal
|
|
659
|
+
// one value across rows. Resolve the affected _id set, then seal +
|
|
660
|
+
// write each row under its row-scoped key. Idempotent materialize
|
|
661
|
+
// re-derives the existing K_row (created on INSERT).
|
|
662
|
+
if (cryptoField.hasPerRowKey(cfKey)) {
|
|
663
|
+
return this._updatePerRowKey(cfKey, changes, single);
|
|
664
|
+
}
|
|
665
|
+
var sealed = cryptoField.sealRow(cfKey, changes);
|
|
641
666
|
var setKeys = Object.keys(sealed);
|
|
642
667
|
if (setKeys.length === 0) {
|
|
643
668
|
throw new Error("update changes object is empty");
|
|
@@ -666,6 +691,41 @@ class Query {
|
|
|
666
691
|
return info.changes;
|
|
667
692
|
}
|
|
668
693
|
|
|
694
|
+
// Per-row-key UPDATE. Sealed columns on a declarePerRowKey table are
|
|
695
|
+
// K_row cells (vault.row:), so each affected row must be re-sealed
|
|
696
|
+
// under its OWN K_row — a single set-based UPDATE can't carry per-row
|
|
697
|
+
// ciphertext. Resolve the affected _id set via the WHERE, then for
|
|
698
|
+
// each row: materialize (idempotent) its K_row, seal the change set
|
|
699
|
+
// under it (derived hashes computed from plaintext as usual), and
|
|
700
|
+
// UPDATE that single row by _id. `single` stops after the first row.
|
|
701
|
+
_updatePerRowKey(cfKey, changes, single) {
|
|
702
|
+
var whereSql = this._where.join(" AND ");
|
|
703
|
+
var qt = this._quotedTable();
|
|
704
|
+
var idStmt = this._db.prepare(
|
|
705
|
+
"SELECT _id FROM " + qt + " WHERE " + whereSql + (single ? " LIMIT 1" : ""));
|
|
706
|
+
var idRows = idStmt.all.apply(idStmt, this._whereParams);
|
|
707
|
+
var changed = 0;
|
|
708
|
+
for (var r = 0; r < idRows.length; r++) {
|
|
709
|
+
var rowId = idRows[r]._id;
|
|
710
|
+
if (rowId === undefined || rowId === null) continue;
|
|
711
|
+
var kRow = cryptoField.materializePerRowKey(cfKey, rowId, this._db);
|
|
712
|
+
var sealed = cryptoField.sealRow(cfKey, changes, { kRow: kRow, rowId: rowId });
|
|
713
|
+
var setKeys = Object.keys(sealed);
|
|
714
|
+
if (setKeys.length === 0) {
|
|
715
|
+
throw new Error("update changes object is empty");
|
|
716
|
+
}
|
|
717
|
+
setKeys.forEach(_validateField);
|
|
718
|
+
var selfUpd = this;
|
|
719
|
+
setKeys.forEach(function (k) { selfUpd._assertColumnMember(k, "update"); });
|
|
720
|
+
var setClause = setKeys.map(function (k) { return '"' + k + '" = ?'; }).join(", ");
|
|
721
|
+
var setValues = setKeys.map(function (k) { return sealed[k]; });
|
|
722
|
+
var updStmt = this._db.prepare("UPDATE " + qt + " SET " + setClause + " WHERE _id = ?");
|
|
723
|
+
var info = updStmt.run.apply(updStmt, setValues.concat([rowId]));
|
|
724
|
+
changed += (info && info.changes) || 0;
|
|
725
|
+
}
|
|
726
|
+
return changed;
|
|
727
|
+
}
|
|
728
|
+
|
|
669
729
|
deleteOne() {
|
|
670
730
|
return this._delete(true) > 0;
|
|
671
731
|
}
|
package/lib/db.js
CHANGED
|
@@ -295,11 +295,21 @@ var FRAMEWORK_SCHEMA = [
|
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
// Per-row crypto-erasure key registry — per-row keys.
|
|
298
|
-
// Each entry holds
|
|
299
|
-
// rowId)
|
|
300
|
-
//
|
|
298
|
+
// Each entry holds the AAD-sealed random row-secret keyed by
|
|
299
|
+
// (tableName, rowId); the row-scoped K_row is derived from it.
|
|
300
|
+
// b.subject.eraseHard / b.retention destroy the entry, leaving WAL /
|
|
301
|
+
// replica residuals undecryptable. wrappedKey is registered as an
|
|
302
|
+
// AAD-bound sealed field (aad:true, rowIdField:"rowId") so a vault
|
|
303
|
+
// keypair rotation auto-reseals it old-root -> new-root via
|
|
304
|
+
// rotate._rotateColumn — without this a rotation would orphan every
|
|
305
|
+
// wrapped secret and brick every keyed row.
|
|
301
306
|
name: "_blamejs_per_row_keys",
|
|
302
307
|
columns: {
|
|
308
|
+
// _id is the rotation pipeline's keyset-pagination + UPDATE key
|
|
309
|
+
// (rotate._rotateColumn SELECTs _id and orders by it); the natural
|
|
310
|
+
// identity stays the composite (tableName, rowId). materialize
|
|
311
|
+
// populates _id with a fresh token.
|
|
312
|
+
_id: "TEXT",
|
|
303
313
|
tableName: "TEXT NOT NULL",
|
|
304
314
|
rowId: "TEXT NOT NULL",
|
|
305
315
|
wrappedKey: "BLOB NOT NULL",
|
|
@@ -307,6 +317,10 @@ var FRAMEWORK_SCHEMA = [
|
|
|
307
317
|
},
|
|
308
318
|
primaryKey: ["tableName", "rowId"],
|
|
309
319
|
indexes: [],
|
|
320
|
+
sealedFields: ["wrappedKey"],
|
|
321
|
+
aad: true,
|
|
322
|
+
rowIdField: "rowId",
|
|
323
|
+
schemaVersion: "1",
|
|
310
324
|
},
|
|
311
325
|
{
|
|
312
326
|
// Operator-declared WORM (write-once-read-many) registry. Each
|