@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/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
- return row ? cryptoField.unsealRow(this._cryptoFieldKey(), row) : null;
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
- var sealed = cryptoField.sealRow(this._cryptoFieldKey(), withId);
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 sealed = cryptoField.sealRow(this._cryptoFieldKey(), changes);
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 a sealed wrapped K_row keyed by (table,
299
- // rowId). b.subject.eraseHard deletes the entry, leaving WAL /
300
- // replica residuals undecryptable.
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