@blamejs/blamejs-shop 0.4.31 → 0.4.32
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +392 -278
- package/lib/vendor/blamejs/.github/workflows/ci.yml +34 -3
- package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +21 -4
- package/lib/vendor/blamejs/.gitignore +6 -0
- package/lib/vendor/blamejs/CHANGELOG.md +26 -0
- package/lib/vendor/blamejs/MIGRATING.md +43 -0
- package/lib/vendor/blamejs/README.md +8 -6
- package/lib/vendor/blamejs/SECURITY.md +19 -3
- package/lib/vendor/blamejs/api-snapshot.json +2190 -664
- package/lib/vendor/blamejs/docker/caddy/localstack.Caddyfile +19 -0
- package/lib/vendor/blamejs/docker/init/generate-certs.sh +1 -1
- package/lib/vendor/blamejs/docker/otel/config.yaml +42 -0
- package/lib/vendor/blamejs/docker/otel/export/.gitkeep +0 -0
- package/lib/vendor/blamejs/docker/postgres/initdb/10-replication.sh +15 -0
- package/lib/vendor/blamejs/docker/postgres/replica-entrypoint.sh +38 -0
- package/lib/vendor/blamejs/docker/toxiproxy/toxiproxy.json +14 -0
- package/lib/vendor/blamejs/docker-compose.test.yml +209 -0
- package/lib/vendor/blamejs/examples/wiki/lib/page-generator.js +132 -0
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +221 -61
- package/lib/vendor/blamejs/examples/wiki/lib/source-doc-parser.js +144 -9
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +99 -0
- package/lib/vendor/blamejs/fuzz/guard-sql.fuzz.js +36 -0
- package/lib/vendor/blamejs/index.js +4 -0
- package/lib/vendor/blamejs/lib/agent-envelope-mac.js +104 -0
- package/lib/vendor/blamejs/lib/agent-event-bus.js +105 -4
- package/lib/vendor/blamejs/lib/agent-posture-chain.js +8 -42
- package/lib/vendor/blamejs/lib/ai-content-detect.js +9 -10
- package/lib/vendor/blamejs/lib/api-key.js +158 -77
- package/lib/vendor/blamejs/lib/atomic-file.js +62 -4
- package/lib/vendor/blamejs/lib/audit-chain.js +47 -11
- package/lib/vendor/blamejs/lib/audit-sign.js +77 -2
- package/lib/vendor/blamejs/lib/audit-tools.js +79 -51
- package/lib/vendor/blamejs/lib/audit.js +259 -123
- package/lib/vendor/blamejs/lib/auth/oauth.js +53 -9
- package/lib/vendor/blamejs/lib/auth/openid-federation.js +108 -47
- package/lib/vendor/blamejs/lib/auth/saml.js +6 -8
- package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +31 -5
- package/lib/vendor/blamejs/lib/backup/index.js +45 -10
- package/lib/vendor/blamejs/lib/break-glass.js +355 -147
- package/lib/vendor/blamejs/lib/cache.js +174 -105
- package/lib/vendor/blamejs/lib/chain-writer.js +38 -16
- package/lib/vendor/blamejs/lib/cli.js +19 -14
- package/lib/vendor/blamejs/lib/cluster-provider-db.js +130 -104
- package/lib/vendor/blamejs/lib/cluster-storage.js +119 -22
- package/lib/vendor/blamejs/lib/cluster.js +119 -71
- package/lib/vendor/blamejs/lib/codepoint-class.js +23 -0
- package/lib/vendor/blamejs/lib/compliance.js +206 -4
- package/lib/vendor/blamejs/lib/consent.js +82 -29
- package/lib/vendor/blamejs/lib/constants.js +27 -11
- package/lib/vendor/blamejs/lib/crypto-field.js +916 -156
- package/lib/vendor/blamejs/lib/db-declare-row-policy.js +35 -22
- package/lib/vendor/blamejs/lib/db-file-lifecycle.js +3 -2
- package/lib/vendor/blamejs/lib/db-query.js +882 -260
- package/lib/vendor/blamejs/lib/db-schema.js +228 -44
- package/lib/vendor/blamejs/lib/db.js +249 -99
- package/lib/vendor/blamejs/lib/dsr.js +385 -55
- package/lib/vendor/blamejs/lib/error-page.js +14 -1
- package/lib/vendor/blamejs/lib/external-db-migrate.js +239 -137
- package/lib/vendor/blamejs/lib/external-db.js +549 -34
- package/lib/vendor/blamejs/lib/file-upload.js +52 -7
- package/lib/vendor/blamejs/lib/framework-error.js +20 -1
- package/lib/vendor/blamejs/lib/framework-files.js +73 -0
- package/lib/vendor/blamejs/lib/framework-schema.js +695 -394
- package/lib/vendor/blamejs/lib/gate-contract.js +659 -1
- package/lib/vendor/blamejs/lib/guard-agent-registry.js +26 -44
- package/lib/vendor/blamejs/lib/guard-all.js +1 -0
- package/lib/vendor/blamejs/lib/guard-auth.js +42 -112
- package/lib/vendor/blamejs/lib/guard-cidr.js +33 -154
- package/lib/vendor/blamejs/lib/guard-csv.js +46 -113
- package/lib/vendor/blamejs/lib/guard-domain.js +34 -157
- package/lib/vendor/blamejs/lib/guard-dsn.js +27 -43
- package/lib/vendor/blamejs/lib/guard-email.js +47 -69
- package/lib/vendor/blamejs/lib/guard-envelope.js +19 -32
- package/lib/vendor/blamejs/lib/guard-event-bus-payload.js +24 -42
- package/lib/vendor/blamejs/lib/guard-event-bus-topic.js +25 -43
- package/lib/vendor/blamejs/lib/guard-filename.js +42 -106
- package/lib/vendor/blamejs/lib/guard-graphql.js +42 -123
- package/lib/vendor/blamejs/lib/guard-html.js +53 -108
- package/lib/vendor/blamejs/lib/guard-idempotency-key.js +24 -42
- package/lib/vendor/blamejs/lib/guard-image.js +46 -103
- package/lib/vendor/blamejs/lib/guard-imap-command.js +18 -32
- package/lib/vendor/blamejs/lib/guard-jmap.js +16 -30
- package/lib/vendor/blamejs/lib/guard-json.js +38 -108
- package/lib/vendor/blamejs/lib/guard-jsonpath.js +38 -171
- package/lib/vendor/blamejs/lib/guard-jwt.js +49 -179
- package/lib/vendor/blamejs/lib/guard-list-id.js +25 -41
- package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +27 -43
- package/lib/vendor/blamejs/lib/guard-mail-compose.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-move.js +26 -44
- package/lib/vendor/blamejs/lib/guard-mail-query.js +28 -46
- package/lib/vendor/blamejs/lib/guard-mail-reply.js +24 -42
- package/lib/vendor/blamejs/lib/guard-mail-sieve.js +24 -42
- package/lib/vendor/blamejs/lib/guard-managesieve-command.js +17 -31
- package/lib/vendor/blamejs/lib/guard-markdown.js +37 -104
- package/lib/vendor/blamejs/lib/guard-message-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-mime.js +39 -151
- package/lib/vendor/blamejs/lib/guard-oauth.js +54 -135
- package/lib/vendor/blamejs/lib/guard-pdf.js +45 -101
- package/lib/vendor/blamejs/lib/guard-pop3-command.js +21 -31
- package/lib/vendor/blamejs/lib/guard-posture-chain.js +24 -42
- package/lib/vendor/blamejs/lib/guard-regex.js +33 -107
- package/lib/vendor/blamejs/lib/guard-saga-config.js +24 -42
- package/lib/vendor/blamejs/lib/guard-shell.js +42 -172
- package/lib/vendor/blamejs/lib/guard-smtp-command.js +48 -54
- package/lib/vendor/blamejs/lib/guard-snapshot-envelope.js +24 -42
- package/lib/vendor/blamejs/lib/guard-sql.js +1491 -0
- package/lib/vendor/blamejs/lib/guard-stream-args.js +24 -43
- package/lib/vendor/blamejs/lib/guard-svg.js +47 -65
- package/lib/vendor/blamejs/lib/guard-template.js +35 -172
- package/lib/vendor/blamejs/lib/guard-tenant-id.js +26 -45
- package/lib/vendor/blamejs/lib/guard-time.js +32 -154
- package/lib/vendor/blamejs/lib/guard-trace-context.js +25 -44
- package/lib/vendor/blamejs/lib/guard-uuid.js +32 -153
- package/lib/vendor/blamejs/lib/guard-xml.js +38 -113
- package/lib/vendor/blamejs/lib/guard-yaml.js +51 -163
- package/lib/vendor/blamejs/lib/http-client.js +37 -9
- package/lib/vendor/blamejs/lib/inbox.js +120 -107
- package/lib/vendor/blamejs/lib/legal-hold.js +121 -50
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +47 -31
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +32 -18
- package/lib/vendor/blamejs/lib/mail-auth.js +236 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +2 -6
- package/lib/vendor/blamejs/lib/mail-dkim.js +1 -0
- package/lib/vendor/blamejs/lib/mail-greylist.js +2 -6
- package/lib/vendor/blamejs/lib/mail-helo.js +2 -6
- package/lib/vendor/blamejs/lib/mail-journal.js +85 -64
- package/lib/vendor/blamejs/lib/mail-rbl.js +2 -6
- package/lib/vendor/blamejs/lib/mail-scan.js +2 -6
- package/lib/vendor/blamejs/lib/mail-server-jmap.js +117 -12
- package/lib/vendor/blamejs/lib/mail-server-mx.js +276 -7
- package/lib/vendor/blamejs/lib/mail-spam-score.js +2 -6
- package/lib/vendor/blamejs/lib/mail-store.js +293 -154
- package/lib/vendor/blamejs/lib/mail.js +8 -4
- package/lib/vendor/blamejs/lib/middleware/body-parser.js +71 -25
- package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +19 -8
- package/lib/vendor/blamejs/lib/middleware/dpop.js +10 -1
- package/lib/vendor/blamejs/lib/middleware/fetch-metadata.js +17 -7
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +75 -51
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +102 -32
- package/lib/vendor/blamejs/lib/middleware/security-headers.js +21 -5
- package/lib/vendor/blamejs/lib/migrations.js +108 -66
- package/lib/vendor/blamejs/lib/network-heartbeat.js +7 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +24 -1
- package/lib/vendor/blamejs/lib/nonce-store.js +31 -9
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/vendor/blamejs/lib/object-store/azure-blob.js +57 -3
- package/lib/vendor/blamejs/lib/object-store/gcs.js +4 -1
- package/lib/vendor/blamejs/lib/object-store/sigv4-bucket-ops.js +5 -2
- package/lib/vendor/blamejs/lib/object-store/sigv4.js +38 -6
- package/lib/vendor/blamejs/lib/observability-otlp-exporter.js +9 -1
- package/lib/vendor/blamejs/lib/observability.js +124 -0
- package/lib/vendor/blamejs/lib/otel-export.js +12 -3
- package/lib/vendor/blamejs/lib/outbox.js +184 -83
- package/lib/vendor/blamejs/lib/parsers/safe-xml.js +47 -7
- package/lib/vendor/blamejs/lib/pqc-agent.js +44 -0
- package/lib/vendor/blamejs/lib/pubsub-cluster.js +42 -20
- package/lib/vendor/blamejs/lib/queue-local.js +225 -140
- package/lib/vendor/blamejs/lib/queue-redis.js +9 -1
- package/lib/vendor/blamejs/lib/queue-sqs.js +6 -0
- package/lib/vendor/blamejs/lib/queue.js +7 -0
- package/lib/vendor/blamejs/lib/redact.js +68 -11
- package/lib/vendor/blamejs/lib/redis-client.js +160 -31
- package/lib/vendor/blamejs/lib/request-helpers.js +7 -0
- package/lib/vendor/blamejs/lib/retention.js +101 -40
- package/lib/vendor/blamejs/lib/router.js +212 -5
- package/lib/vendor/blamejs/lib/safe-dns.js +29 -45
- package/lib/vendor/blamejs/lib/safe-ical.js +18 -33
- package/lib/vendor/blamejs/lib/safe-icap.js +27 -43
- package/lib/vendor/blamejs/lib/safe-sieve.js +21 -40
- package/lib/vendor/blamejs/lib/safe-sql.js +212 -3
- package/lib/vendor/blamejs/lib/safe-url.js +170 -3
- package/lib/vendor/blamejs/lib/safe-vcard.js +18 -33
- package/lib/vendor/blamejs/lib/scheduler.js +35 -12
- package/lib/vendor/blamejs/lib/seeders.js +122 -74
- package/lib/vendor/blamejs/lib/session-stores.js +42 -14
- package/lib/vendor/blamejs/lib/session.js +175 -77
- package/lib/vendor/blamejs/lib/sql.js +3842 -0
- package/lib/vendor/blamejs/lib/sse.js +26 -0
- package/lib/vendor/blamejs/lib/ssrf-guard.js +151 -4
- package/lib/vendor/blamejs/lib/static.js +177 -34
- package/lib/vendor/blamejs/lib/subject.js +96 -49
- package/lib/vendor/blamejs/lib/vault/index.js +3 -2
- package/lib/vendor/blamejs/lib/vault/passphrase-ops.js +3 -2
- package/lib/vendor/blamejs/lib/vault/rotate.js +168 -108
- package/lib/vendor/blamejs/lib/vault-aad.js +6 -0
- package/lib/vendor/blamejs/lib/vendor-data.js +2 -0
- package/lib/vendor/blamejs/lib/websocket.js +35 -5
- package/lib/vendor/blamejs/lib/worker-pool.js +11 -0
- package/lib/vendor/blamejs/package.json +2 -2
- package/lib/vendor/blamejs/release-notes/v0.14.x.json +1503 -0
- package/lib/vendor/blamejs/release-notes/v0.15.0.json +77 -0
- package/lib/vendor/blamejs/release-notes/v0.15.1.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.2.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.3.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.4.json +39 -0
- package/lib/vendor/blamejs/release-notes/v0.15.5.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.15.6.json +59 -0
- package/lib/vendor/blamejs/scripts/check-services.js +21 -0
- package/lib/vendor/blamejs/scripts/gen-migrating.js +51 -0
- package/lib/vendor/blamejs/scripts/release.js +398 -38
- package/lib/vendor/blamejs/test/00-primitives.js +117 -0
- package/lib/vendor/blamejs/test/10-state.js +140 -14
- package/lib/vendor/blamejs/test/20-db.js +65 -2
- package/lib/vendor/blamejs/test/helpers/db.js +9 -0
- package/lib/vendor/blamejs/test/helpers/drivers.js +27 -15
- package/lib/vendor/blamejs/test/helpers/services.js +21 -0
- package/lib/vendor/blamejs/test/integration/audit-actor-binding-pg.test.js +246 -0
- package/lib/vendor/blamejs/test/integration/audit-chain-external-db.test.js +517 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-mysql.test.js +639 -0
- package/lib/vendor/blamejs/test/integration/audit-stack-postgres.test.js +832 -0
- package/lib/vendor/blamejs/test/integration/backup-restore-objectstore.test.js +453 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-mysql.test.js +649 -0
- package/lib/vendor/blamejs/test/integration/data-layer-cluster-pg.test.js +770 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql-privacy.test.js +630 -0
- package/lib/vendor/blamejs/test/integration/data-layer-mysql.test.js +610 -0
- package/lib/vendor/blamejs/test/integration/data-layer-pg.test.js +577 -0
- package/lib/vendor/blamejs/test/integration/data-layer-postgres.test.js +771 -0
- package/lib/vendor/blamejs/test/integration/db-layer-mysql.test.js +549 -0
- package/lib/vendor/blamejs/test/integration/db-layer-postgres.test.js +598 -0
- package/lib/vendor/blamejs/test/integration/distributed-scheduler-fencing-pg.test.js +602 -0
- package/lib/vendor/blamejs/test/integration/external-db-postgres.test.js +576 -0
- package/lib/vendor/blamejs/test/integration/framework-schema-mysql.test.js +353 -0
- package/lib/vendor/blamejs/test/integration/log-stream-cloudwatch.test.js +224 -0
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +142 -17
- package/lib/vendor/blamejs/test/integration/network-heartbeat.test.js +25 -10
- package/lib/vendor/blamejs/test/integration/object-store-azure.test.js +101 -0
- package/lib/vendor/blamejs/test/integration/object-store-gcs.test.js +239 -0
- package/lib/vendor/blamejs/test/integration/object-store-sigv4.test.js +35 -16
- package/lib/vendor/blamejs/test/integration/object-store-worm-lock.test.js +291 -0
- package/lib/vendor/blamejs/test/integration/pubsub.test.js +14 -0
- package/lib/vendor/blamejs/test/integration/queue-sqs.test.js +322 -0
- package/lib/vendor/blamejs/test/integration/redis-reconnect-toxiproxy.test.js +300 -0
- package/lib/vendor/blamejs/test/integration/sql-fts5-catalog-sqlite.test.js +154 -0
- package/lib/vendor/blamejs/test/integration/tls-classical-downgrade-audit.test.js +71 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-event-bus.test.js +175 -12
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-exclusive-temp.test.js +216 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-checkpoint-false-rollback.test.js +203 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-query-self-log.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-safeemit-redacts-secrets.test.js +196 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-signing-key-rotation.test.js +197 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/audit-verifybundle-tamper.test.js +209 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/azure-blob-key-encoding.test.js +121 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-residency-posture.test.js +168 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-scheduletest-drill.test.js +318 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/break-glass.test.js +233 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +1120 -14
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance.test.js +229 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +24 -7
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-dual-read-migrate.test.js +165 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-per-row-key.test.js +350 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-unseal-rate-cap.test.js +27 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-upgrade-dialect.test.js +76 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-interop-oracles.test.js +392 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/csrf-protect.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +180 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-cross-schema.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/db-query-sealed-field-in.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-raw-residency-gate.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-drift.test.js +38 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/db-schema-reconcile-emittable.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-stream-and-payload-shape.test.js +267 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/db-worm.test.js +150 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +46 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dsr.test.js +218 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/erase-posture-vacuum.test.js +210 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +4 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-migrate.test.js +48 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +237 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/fetch-metadata.test.js +20 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/file-upload-content-safety-skip-audit.test.js +193 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/guard-csv.test.js +90 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/http-client-stream.test.js +85 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/idempotency-key.test.js +10 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/inbox.test.js +15 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/legal-hold.test.js +146 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth.test.js +189 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-journal.test.js +3 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-jmap.test.js +123 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +207 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-store.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/oauth-callback.test.js +43 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otel-export.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +101 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/outbox-inflight-reaper.test.js +136 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/parsers-standalone.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/passkey-real-vectors.test.js +429 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/pqc-agent-curve.test.js +21 -11
- package/lib/vendor/blamejs/test/layer-0-primitives/queue-byo-db.test.js +40 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redact-dlp.test.js +83 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/redis-client.test.js +113 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/retention-dryrun-no-vacuum.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/router-use-path-scope.test.js +255 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-url-canonicalize.test.js +309 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-xml.test.js +143 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notonorafter.test.js +287 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc-ecdsa-p1363.test.js +79 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sd-jwt-vc.test.js +50 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/security-headers.test.js +31 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/session-extensions.test.js +45 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sigv4-bucket-ops.test.js +49 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +595 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sse-backpressure.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ssrf-guard.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/static.test.js +194 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/websocket-extension-header.test.js +88 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/worker-pool-recycle-race.test.js +66 -0
- package/lib/vendor/blamejs/test/layer-1-state/api-key.test.js +84 -0
- package/lib/vendor/blamejs/test/layer-5-integration/external-db-residency.test.js +638 -0
- package/lib/vendor/blamejs/test/layer-5-integration/guard-host-integration.test.js +21 -0
- package/lib/vendor/blamejs/test/smoke.js +79 -21
- package/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.0.json +0 -43
- package/lib/vendor/blamejs/release-notes/v0.14.1.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.10.json +0 -54
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +0 -72
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +0 -95
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +0 -52
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +0 -31
- package/lib/vendor/blamejs/release-notes/v0.14.16.json +0 -45
- package/lib/vendor/blamejs/release-notes/v0.14.17.json +0 -57
- package/lib/vendor/blamejs/release-notes/v0.14.18.json +0 -127
- package/lib/vendor/blamejs/release-notes/v0.14.19.json +0 -61
- package/lib/vendor/blamejs/release-notes/v0.14.2.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.20.json +0 -73
- package/lib/vendor/blamejs/release-notes/v0.14.21.json +0 -98
- package/lib/vendor/blamejs/release-notes/v0.14.22.json +0 -91
- package/lib/vendor/blamejs/release-notes/v0.14.3.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.4.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.5.json +0 -18
- package/lib/vendor/blamejs/release-notes/v0.14.6.json +0 -60
- package/lib/vendor/blamejs/release-notes/v0.14.7.json +0 -77
- package/lib/vendor/blamejs/release-notes/v0.14.8.json +0 -27
- package/lib/vendor/blamejs/release-notes/v0.14.9.json +0 -40
|
@@ -15,21 +15,34 @@
|
|
|
15
15
|
* random nonce, so two seals of the same plaintext never collide.
|
|
16
16
|
*
|
|
17
17
|
* Per-row key (K_row) derivation is opt-in via `declarePerRowKey`.
|
|
18
|
-
* Tables that opt in get a fresh K_row per INSERT
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* K_row
|
|
25
|
-
*
|
|
18
|
+
* Tables that opt in get a fresh K_row per INSERT: the framework
|
|
19
|
+
* generates a 32-byte CSPRNG row-secret, derives
|
|
20
|
+
* `K_row = SHAKE256(rowSecret || ":" || table || ":" || rowId || ":"
|
|
21
|
+
* || info)`, and stores the SECRET (never K_row) AAD-sealed in
|
|
22
|
+
* `_blamejs_per_row_keys.wrappedKey`. Because the secret is random —
|
|
23
|
+
* not a function of any on-disk salt — an attacker with full disk
|
|
24
|
+
* access cannot re-derive K_row once the wrapped secret is gone. The
|
|
25
|
+
* AAD on the wrap binds (table, rowId, column, schemaVersion):
|
|
26
|
+
* copying a wrapped secret from one row to another fails Poly1305
|
|
27
|
+
* verification, so a DB-write attacker cannot move it between rows to
|
|
28
|
+
* bypass row-scoped erasure. Sealed columns on a keyed row carry the
|
|
29
|
+
* `vault.row:` prefix and are XChaCha20-Poly1305 ciphertext under
|
|
30
|
+
* K_row, AEAD-bound to the same (table, rowId, column) tuple. This is
|
|
31
|
+
* the crypto-shred substrate for `b.subject.eraseHard` /
|
|
32
|
+
* `b.retention`: destroying the wrapped secret leaves WAL / replica
|
|
33
|
+
* residual ciphertext mathematically undecryptable — even with the
|
|
34
|
+
* vault root key — because K_row is gone everywhere it ever lived.
|
|
26
35
|
*
|
|
27
36
|
* Derived hashes (`derivedHashes`) provide indexed lookup for sealed
|
|
28
|
-
* columns
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
37
|
+
* columns. The default digest is a keyed MAC
|
|
38
|
+
* (`hmac-shake256`: SHAKE256 under the vault's per-deployment MAC key) +
|
|
39
|
+
* a per-field namespace, so an attacker who recovers the salt alone
|
|
40
|
+
* cannot correlate low-entropy plaintexts across fields or across
|
|
41
|
+
* deployments. Operators keeping byte-compatibility with an existing
|
|
42
|
+
* salted index opt out per-table (`derivedHashMode: "salted-sha3"`) or
|
|
43
|
+
* per-column (`derivedHashes.<col>.mode`). Sealed columns without a
|
|
44
|
+
* derived hash are unindexable — queries on them silently return zero
|
|
45
|
+
* rows.
|
|
33
46
|
*
|
|
34
47
|
* Per-column residency (`declareColumnResidency`) declares EU / US /
|
|
35
48
|
* global tags; the storage-write gate (`assertColumnResidency`)
|
|
@@ -47,9 +60,12 @@ var vault = require("./vault");
|
|
|
47
60
|
var vaultAad = require("./vault-aad");
|
|
48
61
|
var validateOpts = require("./validate-opts");
|
|
49
62
|
var numericBounds = require("./numeric-bounds");
|
|
63
|
+
var safeJson = require("./safe-json");
|
|
64
|
+
var frameworkSchema = require("./framework-schema");
|
|
65
|
+
var sql = require("./sql");
|
|
50
66
|
var { defineClass } = require("./framework-error");
|
|
51
|
-
var { sha3Hash, kdf } = require("./crypto");
|
|
52
|
-
var { HASH_PREFIX, VAULT_PREFIX, TIME } = require("./constants");
|
|
67
|
+
var { sha3Hash, kdf, generateBytes, encryptPacked, decryptPacked, generateToken } = require("./crypto");
|
|
68
|
+
var { HASH_PREFIX, VAULT_PREFIX, ROW_PREFIX, TIME } = require("./constants");
|
|
53
69
|
|
|
54
70
|
// Typed refusal raised when a (actor, table, column) tuple exceeds the
|
|
55
71
|
// opt-in unseal-failure rate cap and is in cooldown. alwaysPermanent —
|
|
@@ -58,6 +74,38 @@ var { HASH_PREFIX, VAULT_PREFIX, TIME } = require("./constants");
|
|
|
58
74
|
var CryptoFieldRateError = defineClass("CryptoFieldRateError", { alwaysPermanent: true });
|
|
59
75
|
var CryptoFieldError = defineClass("CryptoFieldError", { alwaysPermanent: true });
|
|
60
76
|
|
|
77
|
+
// Typed-value codec for sealed columns. Sealing previously String()-coerced
|
|
78
|
+
// every value before encryption, which silently corrupts a Buffer (lossy
|
|
79
|
+
// UTF-8 round-trip) or an object ("[object Object]"). This codec preserves
|
|
80
|
+
// byte/type fidelity through a sealed column so unseal restores the original
|
|
81
|
+
// type. Backward-compatible: a plain string is stored VERBATIM (pre-codec
|
|
82
|
+
// cells decode unchanged) - only a non-string value, or the rare string that
|
|
83
|
+
// itself begins with the sentinel, is wrapped. The NUL-led sentinel never
|
|
84
|
+
// occurs at the start of a normal stored string. number / boolean / bigint
|
|
85
|
+
// keep the existing String() contract (they round-trip as strings as before).
|
|
86
|
+
var TYPED_SENTINEL = String.fromCharCode(0) + "bjsv1:";
|
|
87
|
+
|
|
88
|
+
function _encodeTyped(value) {
|
|
89
|
+
if (typeof value === "string") {
|
|
90
|
+
return value.indexOf(TYPED_SENTINEL) === 0 ? TYPED_SENTINEL + "S:" + value : value;
|
|
91
|
+
}
|
|
92
|
+
if (Buffer.isBuffer(value)) return TYPED_SENTINEL + "B:" + value.toString("base64");
|
|
93
|
+
if (value instanceof Uint8Array) return TYPED_SENTINEL + "B:" + Buffer.from(value).toString("base64");
|
|
94
|
+
if (typeof value === "object" && value !== null) return TYPED_SENTINEL + "J:" + JSON.stringify(value);
|
|
95
|
+
return String(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _decodeTyped(str) {
|
|
99
|
+
if (typeof str !== "string" || str.indexOf(TYPED_SENTINEL) !== 0) return str;
|
|
100
|
+
var body = str.slice(TYPED_SENTINEL.length);
|
|
101
|
+
var tag = body.slice(0, 2);
|
|
102
|
+
var payload = body.slice(2);
|
|
103
|
+
if (tag === "B:") return Buffer.from(payload, "base64");
|
|
104
|
+
if (tag === "J:") return safeJson.parse(payload); // plaintext is AEAD-verified; safeJson blocks proto-pollution defensively
|
|
105
|
+
if (tag === "S:") return payload;
|
|
106
|
+
return str; // unknown tag - return the raw decrypted string defensively
|
|
107
|
+
}
|
|
108
|
+
|
|
61
109
|
var compliance = lazyRequire(function () { return require("./compliance"); });
|
|
62
110
|
var db = lazyRequire(function () { return require("./db"); });
|
|
63
111
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -132,15 +180,117 @@ var schemas = Object.create(null);
|
|
|
132
180
|
//
|
|
133
181
|
// { tableName: { columnName: "eu" | "us" | "global" | <tag> } }
|
|
134
182
|
var columnResidency = Object.create(null);
|
|
183
|
+
// Per-ROW residency registry — table → { residencyColumn, allowedTags }.
|
|
184
|
+
// The row-level sibling of columnResidency: one plaintext column on each
|
|
185
|
+
// row carries that row's residency tag; write gates refuse a tagged row
|
|
186
|
+
// landing on an incompatible backend.
|
|
187
|
+
var perRowResidency = Object.create(null);
|
|
135
188
|
|
|
136
189
|
// Per-row key declaration registry. For tables that opt
|
|
137
|
-
// into per-row keying, b.subject.eraseHard
|
|
138
|
-
// from _blamejs_per_row_keys, leaving WAL/replica
|
|
139
|
-
// undecryptable.
|
|
190
|
+
// into per-row keying, b.subject.eraseHard / b.retention destroy the
|
|
191
|
+
// wrapped row-secret from _blamejs_per_row_keys, leaving WAL/replica
|
|
192
|
+
// residual ciphertext undecryptable.
|
|
140
193
|
//
|
|
141
|
-
// { tableName: { keySize, info
|
|
194
|
+
// { tableName: { keySize, info } }
|
|
142
195
|
var perRowKeyTables = Object.create(null);
|
|
143
196
|
|
|
197
|
+
// Seal-envelope strength ranking. A regulated posture can declare a
|
|
198
|
+
// sealEnvelopeFloor in b.compliance POSTURE_DEFAULTS; registerTable
|
|
199
|
+
// refuses a table that seals columns under a weaker envelope than the
|
|
200
|
+
// floor when that posture is the globally-pinned one. Higher rank =
|
|
201
|
+
// stronger binding:
|
|
202
|
+
// plain — vault.seal: XChaCha20-Poly1305 under the vault root,
|
|
203
|
+
// no AAD; a DB-write attacker can copy a cell to another
|
|
204
|
+
// row undetected (CWE-311 / CWE-326).
|
|
205
|
+
// aad — vault.aad.seal: AEAD-bound to (table,row,column,
|
|
206
|
+
// schemaVersion); a relocated cell fails Poly1305.
|
|
207
|
+
// per-row-key — K_row crypto-shred: aad binding PLUS a per-row key,
|
|
208
|
+
// so destroying the row-secret renders residue
|
|
209
|
+
// mathematically undecryptable.
|
|
210
|
+
var SEAL_ENVELOPE_RANK = Object.freeze({
|
|
211
|
+
"plain": 0,
|
|
212
|
+
"aad": 1,
|
|
213
|
+
"per-row-key": 2,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// The framework registry table that holds each row's AAD-sealed
|
|
217
|
+
// row-secret. Named once so the seal-side AAD (materializePerRowKey),
|
|
218
|
+
// the read-side AAD (unsealRow's K_row fetch), and rotate's reseal all
|
|
219
|
+
// quote the byte-identical (table, rowId, column, schemaVersion) tuple.
|
|
220
|
+
// Canonical LOGICAL name for the per-row-key registry. It is the AAD-tuple
|
|
221
|
+
// table component (so seal / unseal / rotate quote a byte-identical tuple)
|
|
222
|
+
// and the frameworkSchema.tableName key the local-handle SQL resolves
|
|
223
|
+
// through. allow:hand-rolled-sql — canonical logical-name declaration.
|
|
224
|
+
var PER_ROW_KEYS_TABLE = "_blamejs_per_row_keys"; // allow:hand-rolled-sql
|
|
225
|
+
var PER_ROW_KEYS_COLUMN = "wrappedKey";
|
|
226
|
+
var PER_ROW_KEYS_SCHEMA_VERSION = "1";
|
|
227
|
+
|
|
228
|
+
// The per-row-key registry is read/written against the LOCAL db() / dbHandle
|
|
229
|
+
// handle directly (not clusterStorage), so SQL composed for it uses the
|
|
230
|
+
// RESOLVED name (prefix-aware via frameworkSchema.tableName) and quoteName so
|
|
231
|
+
// b.sql emits the quoted identifier the single-node path expects — the same
|
|
232
|
+
// shape db-query.js's _sqlOpts and db.js's own local-handle b.sql calls use.
|
|
233
|
+
var _PER_ROW_SQL_OPTS = { dialect: "sqlite", quoteName: true };
|
|
234
|
+
function _perRowKeysTableName() {
|
|
235
|
+
return frameworkSchema.tableName(PER_ROW_KEYS_TABLE);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build the canonical AAD parts for a row-secret wrap in
|
|
239
|
+
// _blamejs_per_row_keys. One source of truth so seal / unseal / rotate
|
|
240
|
+
// never drift. `rowId` is the app row's _id (the same value
|
|
241
|
+
// destroyPerRowKey + subject.eraseHard delete on).
|
|
242
|
+
function _wrappedKeyAad(rowId) {
|
|
243
|
+
return vaultAad.buildColumnAad({
|
|
244
|
+
table: PER_ROW_KEYS_TABLE,
|
|
245
|
+
rowId: rowId,
|
|
246
|
+
column: PER_ROW_KEYS_COLUMN,
|
|
247
|
+
schemaVersion: PER_ROW_KEYS_SCHEMA_VERSION,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Build the canonical AAD parts for a K_row-sealed data cell. Binds the
|
|
252
|
+
// ciphertext to (table, rowId, column, schemaVersion) under K_row so a
|
|
253
|
+
// cell pasted into a different row / column fails Poly1305 — the same
|
|
254
|
+
// copy-protection the AAD-bound vault.aad: path gives, but keyed by the
|
|
255
|
+
// row-scoped K_row rather than the vault root.
|
|
256
|
+
function _rowCellAad(schema, table, column, rowId) {
|
|
257
|
+
return vaultAad.buildColumnAad({
|
|
258
|
+
table: table,
|
|
259
|
+
rowId: rowId,
|
|
260
|
+
column: column,
|
|
261
|
+
schemaVersion: (schema && schema.schemaVersion) || "1",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Encode a buildColumnAad parts object into the byte form
|
|
266
|
+
// encryptPacked / decryptPacked thread into the AEAD tag. The vault.aad
|
|
267
|
+
// canonicalizer (length-prefixed, sorted-keys) is the one encoder so a
|
|
268
|
+
// K_row cell sealed here and a wrapped-secret sealed via vaultAad.seal
|
|
269
|
+
// agree byte-for-byte on the same logical AAD.
|
|
270
|
+
function _aadBytes(parts) {
|
|
271
|
+
return vaultAad.canonicalizeAad(parts);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @primitive b.cryptoField.isRowSealed
|
|
276
|
+
* @signature b.cryptoField.isRowSealed(value)
|
|
277
|
+
* @since 0.14.25
|
|
278
|
+
* @related b.cryptoField.sealRow, b.cryptoField.unsealRow
|
|
279
|
+
*
|
|
280
|
+
* Returns `true` when `value` is a string carrying the per-row-key
|
|
281
|
+
* sealed-cell prefix (`vault.row:`), `false` otherwise. The row-keyed
|
|
282
|
+
* sibling of `b.vault.aad.isAadSealed` — the read path uses it to route
|
|
283
|
+
* a cell to its K_row decrypt instead of the vault-root unseal.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* b.cryptoField.isRowSealed("vault.row:AAAA"); // → true
|
|
287
|
+
* b.cryptoField.isRowSealed("vault:AAAA"); // → false
|
|
288
|
+
* b.cryptoField.isRowSealed(null); // → false
|
|
289
|
+
*/
|
|
290
|
+
function isRowSealed(value) {
|
|
291
|
+
return typeof value === "string" && value.indexOf(ROW_PREFIX) === 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
144
294
|
/**
|
|
145
295
|
* @primitive b.cryptoField.registerTable
|
|
146
296
|
* @signature b.cryptoField.registerTable(name, opts)
|
|
@@ -154,6 +304,14 @@ var perRowKeyTables = Object.create(null);
|
|
|
154
304
|
* hash namespaces. Subsequent `sealRow` / `unsealRow` / `eraseRow`
|
|
155
305
|
* calls dispatch through this registry.
|
|
156
306
|
*
|
|
307
|
+
* Seal-envelope floor: when a compliance posture that declares a
|
|
308
|
+
* `sealEnvelopeFloor` is globally pinned (`b.compliance.set` — today
|
|
309
|
+
* `hipaa` / `pci-dss` require at least an AAD-bound envelope), a table
|
|
310
|
+
* that seals columns under a weaker envelope throws
|
|
311
|
+
* `crypto-field/seal-envelope-below-floor` here at registration so the
|
|
312
|
+
* operator catches the under-protected schema at boot. Unpinned and
|
|
313
|
+
* non-regulated deployments register unchanged.
|
|
314
|
+
*
|
|
157
315
|
* @opts
|
|
158
316
|
* sealedFields: string[], // column names sealed via vault.seal
|
|
159
317
|
* derivedHashes: { [hashCol]: { from: string, normalize?: fn } },
|
|
@@ -195,11 +353,20 @@ function registerTable(name, opts) {
|
|
|
195
353
|
var rowIdField = typeof opts.rowIdField === "string" && opts.rowIdField.length > 0
|
|
196
354
|
? opts.rowIdField : "id";
|
|
197
355
|
var schemaVersion = opts.schemaVersion != null ? String(opts.schemaVersion) : "1";
|
|
198
|
-
|
|
356
|
+
// Derived-hash mode default-on flip (v0.15.0): the per-table default is
|
|
357
|
+
// the keyed MAC "hmac-shake256" (SHAKE256 under vault.getDerivedHashMacKey),
|
|
358
|
+
// so an attacker who recovers the per-deployment salt alone cannot
|
|
359
|
+
// correlate two low-entropy plaintexts across the indexed-lookup column.
|
|
360
|
+
// Operators who need the deterministic-per-deployment salted digest (e.g.
|
|
361
|
+
// to keep byte-compatibility with an existing salted-sha3 index) opt out
|
|
362
|
+
// explicitly with registerTable({ derivedHashMode: "salted-sha3" }), or
|
|
363
|
+
// per-column via derivedHashes.<col>.mode. GDPR Art. 4(5) pseudonymisation;
|
|
364
|
+
// HIPAA 45 CFR 164.514(b); FIPS 202; NIST SP 800-185.
|
|
365
|
+
var derivedHashMode = opts.derivedHashMode || "hmac-shake256";
|
|
199
366
|
if (derivedHashMode !== "salted-sha3" && derivedHashMode !== "hmac-shake256") {
|
|
200
367
|
throw new CryptoFieldError("crypto-field/bad-derived-hash-mode",
|
|
201
|
-
"registerTable: derivedHashMode must be '
|
|
202
|
-
"'
|
|
368
|
+
"registerTable: derivedHashMode must be 'hmac-shake256' (default) or " +
|
|
369
|
+
"'salted-sha3', got " + JSON.stringify(derivedHashMode));
|
|
203
370
|
}
|
|
204
371
|
var derivedHashes = Object.assign({}, opts.derivedHashes || {});
|
|
205
372
|
for (var col in derivedHashes) {
|
|
@@ -211,8 +378,25 @@ function registerTable(name, opts) {
|
|
|
211
378
|
"'salted-sha3' or 'hmac-shake256', got " + JSON.stringify(colMode));
|
|
212
379
|
}
|
|
213
380
|
}
|
|
381
|
+
var sealedFields = Array.isArray(opts.sealedFields) ? opts.sealedFields.slice() : [];
|
|
382
|
+
// Seal-envelope floor gate. Only fires when ALL hold:
|
|
383
|
+
// (1) a posture is globally pinned (b.compliance.set) — read via
|
|
384
|
+
// compliance().current(), the same source the residency write
|
|
385
|
+
// gates read; an UNPINNED deployment is untouched (back-compat),
|
|
386
|
+
// (2) that posture declares a sealEnvelopeFloor in POSTURE_DEFAULTS
|
|
387
|
+
// (only regulated regimes do — hipaa / pci-dss), and
|
|
388
|
+
// (3) the table actually seals columns under an envelope WEAKER than
|
|
389
|
+
// the floor.
|
|
390
|
+
// A non-sealing table, an unpinned deployment, or a posture without a
|
|
391
|
+
// floor all pass through exactly as before. Config-time / entry-point
|
|
392
|
+
// tier: THROW so the operator catches the under-protected schema at
|
|
393
|
+
// boot rather than shipping PHI/PCI under a relocatable plain seal
|
|
394
|
+
// (CWE-311 / CWE-326).
|
|
395
|
+
if (sealedFields.length > 0) {
|
|
396
|
+
_assertSealEnvelopeFloor(name, aadOn);
|
|
397
|
+
}
|
|
214
398
|
schemas[name] = {
|
|
215
|
-
sealedFields:
|
|
399
|
+
sealedFields: sealedFields,
|
|
216
400
|
derivedHashes: derivedHashes,
|
|
217
401
|
hashNamespaces: Object.assign({}, opts.hashNamespaces || {}),
|
|
218
402
|
aad: aadOn,
|
|
@@ -222,28 +406,94 @@ function registerTable(name, opts) {
|
|
|
222
406
|
};
|
|
223
407
|
}
|
|
224
408
|
|
|
409
|
+
// _assertSealEnvelopeFloor — config-time guard for registerTable. Reads
|
|
410
|
+
// the globally-pinned posture (compliance().current()) and its declared
|
|
411
|
+
// sealEnvelopeFloor; throws when `table` seals columns under a weaker
|
|
412
|
+
// envelope. No-op when no posture is pinned, the posture declares no
|
|
413
|
+
// floor, or compliance isn't loaded — so unpinned/non-regulated
|
|
414
|
+
// deployments register exactly as before.
|
|
415
|
+
function _assertSealEnvelopeFloor(table, aadOn) {
|
|
416
|
+
var posture;
|
|
417
|
+
var floor;
|
|
418
|
+
try {
|
|
419
|
+
var c = compliance();
|
|
420
|
+
posture = c.current();
|
|
421
|
+
if (typeof posture !== "string" || posture.length === 0) return;
|
|
422
|
+
floor = c.postureDefault(posture, "sealEnvelopeFloor");
|
|
423
|
+
} catch (_e) {
|
|
424
|
+
// compliance not loaded / unavailable — record nothing, gate nothing.
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (typeof floor !== "string" || !Object.prototype.hasOwnProperty.call(SEAL_ENVELOPE_RANK, floor)) {
|
|
428
|
+
return; // posture pins no recognised floor → back-compat pass-through
|
|
429
|
+
}
|
|
430
|
+
// Declared envelope for this table: per-row-key beats aad beats plain.
|
|
431
|
+
// declarePerRowKey may run before or after registerTable; honour it
|
|
432
|
+
// when it ran first.
|
|
433
|
+
var declared = perRowKeyTables[table] ? "per-row-key" : (aadOn ? "aad" : "plain");
|
|
434
|
+
if (SEAL_ENVELOPE_RANK[declared] < SEAL_ENVELOPE_RANK[floor]) {
|
|
435
|
+
throw new CryptoFieldError("crypto-field/seal-envelope-below-floor",
|
|
436
|
+
"registerTable: table '" + table + "' seals columns under the '" +
|
|
437
|
+
declared + "' envelope, but the pinned compliance posture '" +
|
|
438
|
+
posture + "' requires at least '" + floor + "'. " +
|
|
439
|
+
(floor === "aad"
|
|
440
|
+
? "Pass registerTable({ aad: true, rowIdField: <pk> }) so each " +
|
|
441
|
+
"cell is AEAD-bound to (table, row, column) and cannot be " +
|
|
442
|
+
"relocated between rows"
|
|
443
|
+
: "Call b.cryptoField.declarePerRowKey('" + table + "', ...) " +
|
|
444
|
+
"before registerTable so each row gets a crypto-shred K_row") +
|
|
445
|
+
" (CWE-311 / CWE-326). Unpinned or non-regulated deployments are " +
|
|
446
|
+
"unaffected; this gate fires only under a posture that declares a " +
|
|
447
|
+
"sealEnvelopeFloor.");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
225
451
|
// Derived-hash digest width for the keyed (hmac-shake256) mode: 32
|
|
226
452
|
// bytes -> 64 hex chars.
|
|
227
453
|
var DERIVED_HASH_BYTES = 32;
|
|
228
454
|
|
|
229
455
|
// Compute the indexed-lookup digest for a derived-hash column.
|
|
230
|
-
// - "
|
|
231
|
-
// + value
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
456
|
+
// - "hmac-shake256" (registerTable default since v0.15.0):
|
|
457
|
+
// SHAKE256(<vault-sealed MAC key> || ns + value) truncated to 32 bytes
|
|
458
|
+
// (64 hex). The key is a vault-derived secret, NOT a static salt, so an
|
|
459
|
+
// attacker who recovers the salt alone can't correlate two low-entropy
|
|
460
|
+
// plaintexts; the sponge has no length-extension weakness.
|
|
461
|
+
// (b.crypto.hmacSha3 (HMAC-SHA3-512) was considered; SHAKE256(key||msg)
|
|
462
|
+
// is chosen for the fixed-width keyed digest with the same MAC-grade
|
|
463
|
+
// guarantee.) FIPS 202; NIST SP 800-185; GDPR Art. 4(5)
|
|
464
|
+
// pseudonymisation; HIPAA 45 CFR 164.514(b).
|
|
465
|
+
// - "salted-sha3" (opt-out / pre-v0.15.0 legacy index): SHA3-512 over
|
|
466
|
+
// <per-deployment salt> + ns + value (128 hex). Deterministic per
|
|
467
|
+
// deployment, byte-compatible with the legacy index.
|
|
468
|
+
// The bare-fallback (`|| "salted-sha3"`) applies only when NEITHER the
|
|
469
|
+
// per-column spec.mode NOR a table mode is supplied — an ad-hoc caller that
|
|
470
|
+
// named no mode; registerTable always records a derivedHashMode, so a
|
|
471
|
+
// registered table is never bare-fallthrough.
|
|
240
472
|
function _computeDerivedHash(spec, tableMode, ns, normalized) {
|
|
241
|
-
var mode = (spec
|
|
473
|
+
var mode = _resolveDerivedHashMode(spec, tableMode);
|
|
242
474
|
if (mode === "hmac-shake256") {
|
|
243
475
|
var macKey = vault.getDerivedHashMacKey();
|
|
244
476
|
return kdf(Buffer.concat([macKey, Buffer.from(ns + normalized, "utf8")]),
|
|
245
477
|
DERIVED_HASH_BYTES).toString("hex");
|
|
246
478
|
}
|
|
479
|
+
return _legacyDerivedHash(ns, normalized);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Resolve the effective derived-hash mode for a (spec, tableMode) pair —
|
|
483
|
+
// per-column override beats the table mode beats the bare salted-sha3
|
|
484
|
+
// fallback (the ad-hoc-no-mode case; see _computeDerivedHash).
|
|
485
|
+
function _resolveDerivedHashMode(spec, tableMode) {
|
|
486
|
+
return (spec && spec.mode) || tableMode || "salted-sha3";
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// The legacy (pre-v0.15.0 default) salted-sha3 digest — SHA3-512 over the
|
|
490
|
+
// per-deployment salt + namespace + normalized value (128 hex). Factored out
|
|
491
|
+
// so the dual-read LOOKUP path and the upgrade-on-read auto-migrate can
|
|
492
|
+
// recompute the OLD-default hash for a (ns, value) regardless of the table's
|
|
493
|
+
// current keyed-MAC mode: a row written before the default flipped still
|
|
494
|
+
// carries this digest in its derived-hash column, and a lookup that only
|
|
495
|
+
// computed the keyed-MAC would miss it.
|
|
496
|
+
function _legacyDerivedHash(ns, normalized) {
|
|
247
497
|
return sha3Hash(vault.getDerivedHashSalt().toString("hex") + ns + normalized);
|
|
248
498
|
}
|
|
249
499
|
|
|
@@ -420,6 +670,33 @@ function namespaceFor(table, field, registered) {
|
|
|
420
670
|
*
|
|
421
671
|
* b.cryptoField.computeDerived("users", "email", null); // → null
|
|
422
672
|
*/
|
|
673
|
+
// Build the derived-hash result for a (schema, derivedField, spec,
|
|
674
|
+
// sourceField, value) tuple — the single source of truth for both
|
|
675
|
+
// computeDerived and lookupHash. Returns `{ field, value, legacyValue? }`.
|
|
676
|
+
//
|
|
677
|
+
// value — the digest under the column's ACTIVE mode (keyed-MAC for a
|
|
678
|
+
// v0.15.0-default table; salted-sha3 when opted out). New
|
|
679
|
+
// writes index under this, so it stays the primary equality
|
|
680
|
+
// value every existing caller already reads.
|
|
681
|
+
// legacyValue — present ONLY when the active mode is the keyed MAC: the
|
|
682
|
+
// byte-form a row written under the PRE-v0.15.0 salted-sha3
|
|
683
|
+
// default would carry. A dual-read lookup matches EITHER
|
|
684
|
+
// value so the keyed-default flip doesn't silently lose
|
|
685
|
+
// pre-flip rows; the upgrade-on-read auto-migrate in
|
|
686
|
+
// unsealRow re-hashes a row found via the legacy digest.
|
|
687
|
+
function _derivedHashResult(s, table, derivedField, spec, sourceField, value) {
|
|
688
|
+
var ns = namespaceFor(table, sourceField, s.hashNamespaces);
|
|
689
|
+
var normalized = spec.normalize ? spec.normalize(value) : String(value);
|
|
690
|
+
var mode = _resolveDerivedHashMode(spec, s.derivedHashMode);
|
|
691
|
+
var primary = _computeDerivedHash(spec, s.derivedHashMode, ns, normalized);
|
|
692
|
+
var out = { field: derivedField, value: primary };
|
|
693
|
+
if (mode === "hmac-shake256") {
|
|
694
|
+
var legacy = _legacyDerivedHash(ns, normalized);
|
|
695
|
+
if (legacy !== primary) out.legacyValue = legacy;
|
|
696
|
+
}
|
|
697
|
+
return out;
|
|
698
|
+
}
|
|
699
|
+
|
|
423
700
|
function computeDerived(table, sourceField, sourceValue) {
|
|
424
701
|
if (sourceValue === undefined || sourceValue === null) return null;
|
|
425
702
|
var s = schemas[table];
|
|
@@ -428,9 +705,7 @@ function computeDerived(table, sourceField, sourceValue) {
|
|
|
428
705
|
for (var derivedField in s.derivedHashes) {
|
|
429
706
|
var spec = s.derivedHashes[derivedField];
|
|
430
707
|
if (spec.from === sourceField) {
|
|
431
|
-
|
|
432
|
-
var normalized = spec.normalize ? spec.normalize(sourceValue) : String(sourceValue);
|
|
433
|
-
return { field: derivedField, value: _computeDerivedHash(spec, s.derivedHashMode, ns, normalized) };
|
|
708
|
+
return _derivedHashResult(s, table, derivedField, spec, sourceField, sourceValue);
|
|
434
709
|
}
|
|
435
710
|
}
|
|
436
711
|
return null;
|
|
@@ -450,18 +725,50 @@ function computeDerived(table, sourceField, sourceValue) {
|
|
|
450
725
|
// tuple are refused for `cooldownMs` with a typed CryptoFieldRateError and
|
|
451
726
|
// a distinct system.crypto.unseal_rate_exceeded audit row.
|
|
452
727
|
//
|
|
453
|
-
// Default
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
728
|
+
// Default-ON (v0.15.0) — the cap is armed at module load with the
|
|
729
|
+
// DEFAULT_RATE_CAP below, so a forged-ciphertext unseal-oracle is bounded
|
|
730
|
+
// out of the box. Operators who want the prior audit-only behaviour opt
|
|
731
|
+
// out explicitly with configureUnsealRateCap(null) / { disabled: true }.
|
|
732
|
+
// Composes the same timestamp-array sliding-window shape used by
|
|
733
|
+
// b.mail.server.rateLimit (_pruneWindow): count-based, lazily pruned on
|
|
734
|
+
// read, no background timer.
|
|
457
735
|
//
|
|
458
736
|
// CWE-307 (Improper Restriction of Excessive Authentication Attempts —
|
|
459
737
|
// generalized here to excessive decryption-oracle attempts); OWASP ASVS
|
|
460
738
|
// v5 §2.2.1 (anti-automation); NIST SP 800-63B §5.2.2 (rate limiting).
|
|
461
|
-
|
|
739
|
+
//
|
|
740
|
+
// DEFAULT_RATE_CAP — the secure baseline the cap arms with at module load.
|
|
741
|
+
// 10 forged-ciphertext failures for one (actor, table, column) inside a
|
|
742
|
+
// 1-minute window trip a 5-minute cooldown. Generous enough that no
|
|
743
|
+
// legitimate read pattern hits it (a real ciphertext never fails the
|
|
744
|
+
// AEAD), tight enough that an oracle-hammering attacker is shut off fast.
|
|
745
|
+
var DEFAULT_RATE_CAP_THRESHOLD = 10;
|
|
746
|
+
var DEFAULT_RATE_CAP_WINDOW_MS = TIME.minutes(1);
|
|
747
|
+
var DEFAULT_RATE_CAP_COOLDOWN_MS = TIME.minutes(5);
|
|
748
|
+
var _rateCap = null; // installed by _installDefaultRateCap() below
|
|
462
749
|
var _rateFailWindows = new Map(); // "actor\x00table\x00column" → [tsMs, ...]
|
|
463
750
|
var _rateCooldowns = new Map(); // same key → cooldownUntilMs
|
|
464
751
|
|
|
752
|
+
// Build the default cap record (Date.now clock, framework-audit sink).
|
|
753
|
+
// Separated so module-load and clearRateCapForTest install the identical
|
|
754
|
+
// secure baseline.
|
|
755
|
+
function _defaultRateCapRecord() {
|
|
756
|
+
return {
|
|
757
|
+
threshold: DEFAULT_RATE_CAP_THRESHOLD,
|
|
758
|
+
windowMs: DEFAULT_RATE_CAP_WINDOW_MS,
|
|
759
|
+
cooldownMs: DEFAULT_RATE_CAP_COOLDOWN_MS,
|
|
760
|
+
now: function () { return Date.now(); },
|
|
761
|
+
onAudit: null,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function _installDefaultRateCap() {
|
|
765
|
+
_rateCap = _defaultRateCapRecord();
|
|
766
|
+
_rateFailWindows.clear();
|
|
767
|
+
_rateCooldowns.clear();
|
|
768
|
+
}
|
|
769
|
+
// Arm the secure default at module load (security-on, not opt-in).
|
|
770
|
+
_installDefaultRateCap();
|
|
771
|
+
|
|
465
772
|
// Tuple key. \x00 is not a legal column / table identifier byte and is
|
|
466
773
|
// vanishingly unlikely in an actor id, so the join is unambiguous; the
|
|
467
774
|
// composite is only ever a Map key (never an object property), so no
|
|
@@ -477,23 +784,25 @@ function _rateKey(actor, table, column) {
|
|
|
477
784
|
* @compliance hipaa, gdpr, pci-dss
|
|
478
785
|
* @related b.cryptoField.unsealRow, b.cryptoField.clearRateCapForTest
|
|
479
786
|
*
|
|
480
|
-
*
|
|
481
|
-
* FAILURES.
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* every subsequent `unsealRow` touching that tuple is REFUSED for
|
|
488
|
-
* `cooldownMs` with a `CryptoFieldRateError` and a distinct
|
|
787
|
+
* Tune the per-(actor, table, column) cap on sealed-column unseal
|
|
788
|
+
* FAILURES. The cap is ON BY DEFAULT (default-on, v0.15.0): the framework
|
|
789
|
+
* arms it at module load (threshold 10 / 1-minute window / 5-minute
|
|
790
|
+
* cooldown) so a forged-ciphertext oracle is bounded with no operator
|
|
791
|
+
* action. Once a single tuple accrues `threshold` failures inside
|
|
792
|
+
* `windowMs`, every subsequent `unsealRow` touching that tuple is REFUSED
|
|
793
|
+
* for `cooldownMs` with a `CryptoFieldRateError` and a distinct
|
|
489
794
|
* `system.crypto.unseal_rate_exceeded` audit row, bounding the oracle.
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
* `
|
|
496
|
-
*
|
|
795
|
+
* Without the cap, an attacker who can write `vault:<crafted>` payloads
|
|
796
|
+
* can hammer the KEM-decapsulation / AEAD-verify oracle indefinitely and
|
|
797
|
+
* only an off-band operator alert rule catches the burst.
|
|
798
|
+
*
|
|
799
|
+
* Pass an opts object to RAISE/lower the thresholds. Pass `null` (or
|
|
800
|
+
* `{ disabled: true }`) to turn the cap off entirely and fall back to
|
|
801
|
+
* audit-only (the pre-v0.15.0 behaviour) — the documented opt-out for the
|
|
802
|
+
* rare deployment that needs an unbounded read path. Validation is
|
|
803
|
+
* config-time / entry-point tier — bad `threshold` / `windowMs` /
|
|
804
|
+
* `cooldownMs` THROW so an operator catches the typo at boot rather than
|
|
805
|
+
* silently mis-configuring the cap.
|
|
497
806
|
*
|
|
498
807
|
* CWE-307 (excessive-attempt restriction); OWASP ASVS v5 §2.2.1;
|
|
499
808
|
* NIST SP 800-63B §5.2.2.
|
|
@@ -616,27 +925,26 @@ function _rateNoteFailure(actor, table, column) {
|
|
|
616
925
|
* @status experimental
|
|
617
926
|
* @related b.cryptoField.configureUnsealRateCap
|
|
618
927
|
*
|
|
619
|
-
* Test-only helper.
|
|
620
|
-
* in-flight sliding-window + cooldown entry so a fixture
|
|
621
|
-
* the cap between cases
|
|
622
|
-
*
|
|
928
|
+
* Test-only helper. Restores the secure DEFAULT cap (default-on baseline)
|
|
929
|
+
* and drops every in-flight sliding-window + cooldown entry so a fixture
|
|
930
|
+
* can re-configure the cap between cases from a known-good starting point.
|
|
931
|
+
* Operator code never calls this — production deployments inherit the
|
|
932
|
+
* default cap at boot and tune or disable it via configureUnsealRateCap.
|
|
623
933
|
*
|
|
624
934
|
* @example
|
|
625
935
|
* b.cryptoField.configureUnsealRateCap({ threshold: 3 });
|
|
626
936
|
* b.cryptoField.clearRateCapForTest();
|
|
627
|
-
* // cap is
|
|
937
|
+
* // cap is back at the secure default; windows + cooldowns cleared
|
|
628
938
|
*/
|
|
629
939
|
function clearRateCapForTest() {
|
|
630
|
-
|
|
631
|
-
_rateFailWindows.clear();
|
|
632
|
-
_rateCooldowns.clear();
|
|
940
|
+
_installDefaultRateCap();
|
|
633
941
|
}
|
|
634
942
|
|
|
635
943
|
// ---- Row sealing / unsealing ----
|
|
636
944
|
|
|
637
945
|
/**
|
|
638
946
|
* @primitive b.cryptoField.sealRow
|
|
639
|
-
* @signature b.cryptoField.sealRow(table, row)
|
|
947
|
+
* @signature b.cryptoField.sealRow(table, row, opts?)
|
|
640
948
|
* @since 0.4.0
|
|
641
949
|
* @compliance hipaa, gdpr, pci-dss
|
|
642
950
|
* @related b.cryptoField.unsealRow, b.cryptoField.eraseRow, b.vault.seal
|
|
@@ -649,6 +957,20 @@ function clearRateCapForTest() {
|
|
|
649
957
|
* computed BEFORE sealing the source so the indexed lookup column
|
|
650
958
|
* captures the plaintext digest.
|
|
651
959
|
*
|
|
960
|
+
* When `opts.kRow` (a row-scoped key Buffer from
|
|
961
|
+
* `materializePerRowKey`) is supplied — wired automatically by the
|
|
962
|
+
* db-query write boundary for `declarePerRowKey` tables — sealed
|
|
963
|
+
* columns are instead XChaCha20-Poly1305-encrypted under K_row and
|
|
964
|
+
* emitted with the `vault.row:` prefix, AEAD-bound to (table, rowId,
|
|
965
|
+
* column, schemaVersion). The residency-tag column (when the table
|
|
966
|
+
* declares per-row residency) is NEVER K_row-sealed: the write gate
|
|
967
|
+
* and reads must see it in plaintext.
|
|
968
|
+
*
|
|
969
|
+
* @opts
|
|
970
|
+
* kRow: Buffer, // row-scoped key from materializePerRowKey; when present,
|
|
971
|
+
* // sealed columns emit vault.row: cells under K_row
|
|
972
|
+
* rowId: string, // the row's _id; required when kRow is present (AAD term)
|
|
973
|
+
*
|
|
652
974
|
* @example
|
|
653
975
|
* b.cryptoField.registerTable("patients", {
|
|
654
976
|
* sealedFields: ["ssn"],
|
|
@@ -660,11 +982,29 @@ function clearRateCapForTest() {
|
|
|
660
982
|
* typeof sealed.ssnHash; // → "string"
|
|
661
983
|
* row.ssn; // → "123-45-6789" (input untouched)
|
|
662
984
|
*/
|
|
663
|
-
function sealRow(table, row) {
|
|
985
|
+
function sealRow(table, row, opts) {
|
|
664
986
|
if (!row) return row;
|
|
665
987
|
var s = schemas[table];
|
|
666
988
|
if (!s) return row;
|
|
667
989
|
var out = Object.assign({}, row);
|
|
990
|
+
opts = opts || {};
|
|
991
|
+
var kRow = Buffer.isBuffer(opts.kRow) ? opts.kRow : null;
|
|
992
|
+
// The per-row-key path needs the row identity for the cell AAD. Prefer
|
|
993
|
+
// the explicit opts.rowId; fall back to the row's _id. A K_row with no
|
|
994
|
+
// rowId can't build a stable AAD, so refuse rather than seal under a
|
|
995
|
+
// placeholder that no later unseal could open.
|
|
996
|
+
var kRowId = kRow
|
|
997
|
+
? String(opts.rowId != null ? opts.rowId : (out._id != null ? out._id : ""))
|
|
998
|
+
: null;
|
|
999
|
+
if (kRow && kRowId.length === 0) {
|
|
1000
|
+
throw new CryptoFieldError("crypto-field/seal-row-krow-rowid-missing",
|
|
1001
|
+
"cryptoField.sealRow: opts.kRow supplied but no rowId (set opts.rowId " +
|
|
1002
|
+
"or row._id) — the K_row cell AAD binds (table, rowId, column)");
|
|
1003
|
+
}
|
|
1004
|
+
// Residency tag column must stay plaintext even under a K_row seal —
|
|
1005
|
+
// the write gate reads it before sealRow and reads surface it verbatim.
|
|
1006
|
+
var residencySpec = perRowResidency[table];
|
|
1007
|
+
var residencyCol = residencySpec ? residencySpec.residencyColumn : null;
|
|
668
1008
|
|
|
669
1009
|
// Compute derived hashes from plaintext source values BEFORE sealing those
|
|
670
1010
|
// sources. If a source value arrives already sealed (e.g. from an internal
|
|
@@ -700,29 +1040,44 @@ function sealRow(table, row) {
|
|
|
700
1040
|
"' is AAD-bound (registerTable({aad:true})); the row's identity " +
|
|
701
1041
|
"column '" + s.rowIdField + "' must be populated BEFORE sealRow. " +
|
|
702
1042
|
"Generate the primary key first (e.g. uuid / sequence INSERT … RETURNING), " +
|
|
703
|
-
"set row." + s.rowIdField + ", then sealRow.");
|
|
1043
|
+
"set row." + s.rowIdField + ", then sealRow."); // allow:hand-rolled-sql — error-message prose, not SQL
|
|
704
1044
|
}
|
|
705
1045
|
}
|
|
706
1046
|
|
|
707
|
-
// Seal fields.
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
1047
|
+
// Seal fields. Three shapes:
|
|
1048
|
+
// - K_row (opts.kRow present): XChaCha20-Poly1305 under the row-
|
|
1049
|
+
// scoped key, vault.row: prefix, AEAD-bound (table, rowId, column,
|
|
1050
|
+
// schemaVersion). Crypto-shred: destroying the wrapped row-secret
|
|
1051
|
+
// leaves these cells undecryptable.
|
|
1052
|
+
// - AAD mode (registerTable({aad:true})): vault.aad.seal binds the
|
|
1053
|
+
// tag to (table, rowId, column, schemaVersion) under the vault root.
|
|
1054
|
+
// - plain mode: vault.seal (idempotent — already-sealed pass through).
|
|
711
1055
|
for (var i = 0; i < s.sealedFields.length; i++) {
|
|
712
1056
|
var field = s.sealedFields[i];
|
|
713
|
-
if (out[field]
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1057
|
+
if (out[field] === undefined || out[field] === null) continue;
|
|
1058
|
+
if (kRow && field === residencyCol) continue; // residency tag stays plaintext
|
|
1059
|
+
if (kRow) {
|
|
1060
|
+
// Idempotent: an already-K_row-sealed value passes through.
|
|
1061
|
+
if (isRowSealed(out[field])) continue;
|
|
1062
|
+
var cellAad = _aadBytes(_rowCellAad(s, table, field, kRowId));
|
|
1063
|
+
// Encode the value type-faithfully (Buffer / object preserved, not
|
|
1064
|
+
// String()-mangled), then UTF-8 to bytes for the AEAD. The typed
|
|
1065
|
+
// encoding of a string / base64 / JSON is pure ASCII-or-UTF8, so the
|
|
1066
|
+
// Buffer.from(str, "utf8") round-trips losslessly.
|
|
1067
|
+
var plainStr = _encodeTyped(out[field]);
|
|
1068
|
+
out[field] = ROW_PREFIX +
|
|
1069
|
+
encryptPacked(Buffer.from(plainStr, "utf8"), kRow, cellAad).toString("base64");
|
|
1070
|
+
} else if (s.aad) {
|
|
1071
|
+
// Idempotent: already-AAD-sealed values pass through unchanged.
|
|
1072
|
+
if (typeof out[field] === "string" && vaultAad.isAadSealed(out[field])) {
|
|
1073
|
+
continue;
|
|
725
1074
|
}
|
|
1075
|
+
out[field] = vaultAad.seal(_encodeTyped(out[field]),
|
|
1076
|
+
_aadParts(s, table, field, out));
|
|
1077
|
+
} else {
|
|
1078
|
+
// allow:seal-without-aad — plain-mode legacy table; operator
|
|
1079
|
+
// opts into AAD via registerTable({aad:true})
|
|
1080
|
+
out[field] = vault.seal(_encodeTyped(out[field]));
|
|
726
1081
|
}
|
|
727
1082
|
}
|
|
728
1083
|
|
|
@@ -744,7 +1099,7 @@ function _aadParts(schema, table, column, row) {
|
|
|
744
1099
|
|
|
745
1100
|
/**
|
|
746
1101
|
* @primitive b.cryptoField.unsealRow
|
|
747
|
-
* @signature b.cryptoField.unsealRow(table, row, actor?)
|
|
1102
|
+
* @signature b.cryptoField.unsealRow(table, row, actor?, dbHandle?)
|
|
748
1103
|
* @since 0.4.0
|
|
749
1104
|
* @compliance hipaa, gdpr, pci-dss
|
|
750
1105
|
* @related b.cryptoField.sealRow, b.vault.unseal, b.cryptoField.configureUnsealRateCap
|
|
@@ -758,15 +1113,29 @@ function _aadParts(schema, table, column, row) {
|
|
|
758
1113
|
* so downstream code sees "no value" instead of crashing the request.
|
|
759
1114
|
* The input row is never mutated.
|
|
760
1115
|
*
|
|
761
|
-
*
|
|
762
|
-
* `
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
1116
|
+
* `vault.row:`-prefixed cells (per-row-key tables, `declarePerRowKey`)
|
|
1117
|
+
* are decrypted under the row's K_row: a `dbHandle` (the db-query layer
|
|
1118
|
+
* passes `this._db`) is used to fetch the row's wrapped secret from
|
|
1119
|
+
* `_blamejs_per_row_keys`, unwrap it, and derive K_row once per call.
|
|
1120
|
+
* When a caller passes no `dbHandle` (e.g. `b.breakGlass.unsealRow`,
|
|
1121
|
+
* which reads the row via clusterStorage), the framework's local db is
|
|
1122
|
+
* resolved automatically — the wrapped secret always lives in the local
|
|
1123
|
+
* `_blamejs_per_row_keys`, so keyed reads work on every path.
|
|
1124
|
+
* A missing wrapped row (crypto-shredded by `eraseHard` / `retention`)
|
|
1125
|
+
* makes the unwrap throw → the field nulls + `system.crypto.unseal_failed`
|
|
1126
|
+
* fires, which is correct: shredded data reads as absent.
|
|
1127
|
+
*
|
|
1128
|
+
* The unseal-failure rate cap is ON BY DEFAULT (default-on, v0.15.0):
|
|
1129
|
+
* repeated forged-ciphertext failures for a single `(actor, table,
|
|
1130
|
+
* column)` tuple trip a cooldown (threshold 10 / 1-minute window /
|
|
1131
|
+
* 5-minute cooldown out of the box; tune or disable via
|
|
1132
|
+
* `configureUnsealRateCap`). Once tripped, this call THROWS
|
|
1133
|
+
* `CryptoFieldRateError` and emits a distinct
|
|
1134
|
+
* `system.crypto.unseal_rate_exceeded` audit instead of exercising the
|
|
1135
|
+
* decryption oracle again (CWE-307). `actor` identifies the caller for
|
|
1136
|
+
* that tuple (e.g. session subject / API key id); it defaults to an
|
|
1137
|
+
* anonymous bucket when omitted, and is ignored entirely when the cap is
|
|
1138
|
+
* disabled (full back-compat for the 2-arg call).
|
|
770
1139
|
*
|
|
771
1140
|
* @example
|
|
772
1141
|
* b.cryptoField.registerTable("patients", { sealedFields: ["ssn"] });
|
|
@@ -774,7 +1143,7 @@ function _aadParts(schema, table, column, row) {
|
|
|
774
1143
|
* var clear = b.cryptoField.unsealRow("patients", sealed);
|
|
775
1144
|
* clear.ssn; // → "123-45-6789"
|
|
776
1145
|
*/
|
|
777
|
-
function unsealRow(table, row, actor) {
|
|
1146
|
+
function unsealRow(table, row, actor, dbHandle) {
|
|
778
1147
|
if (!row) return row;
|
|
779
1148
|
var s = schemas[table];
|
|
780
1149
|
if (!s || s.sealedFields.length === 0) return row;
|
|
@@ -782,15 +1151,65 @@ function unsealRow(table, row, actor) {
|
|
|
782
1151
|
var capActor = (actor === undefined || actor === null || String(actor).length === 0)
|
|
783
1152
|
? "_anon" : String(actor);
|
|
784
1153
|
|
|
1154
|
+
// Lazy K_row: derive at most once per unsealRow call, only if a cell
|
|
1155
|
+
// actually carries the vault.row: prefix. Cached across fields (and
|
|
1156
|
+
// the failure case is cached too, so a shredded row doesn't re-query
|
|
1157
|
+
// _blamejs_per_row_keys for every sealed column). The row identity for
|
|
1158
|
+
// both the cell AAD and the wrapped-secret lookup is the row's _id —
|
|
1159
|
+
// the same value the seal side (write boundary) passed as rowId and
|
|
1160
|
+
// that destroyPerRowKey / eraseHard delete on.
|
|
1161
|
+
var kRowId = out._id != null ? String(out._id) : "";
|
|
1162
|
+
var keyedTable = hasPerRowKey(table);
|
|
1163
|
+
var _kRowCache; // undefined = not yet derived; null = derive failed
|
|
1164
|
+
function _kRowOnce() {
|
|
1165
|
+
if (_kRowCache !== undefined) return _kRowCache;
|
|
1166
|
+
_kRowCache = null;
|
|
1167
|
+
if (!keyedTable || kRowId.length === 0) return null;
|
|
1168
|
+
// Resolve a prepared-statement source for the wrapped-secret lookup.
|
|
1169
|
+
// Prefer the caller's dbHandle (the db-query read layer threads it on
|
|
1170
|
+
// first()/all()/stream()); otherwise resolve the framework's local
|
|
1171
|
+
// db ourselves. A DIRECT caller — e.g. b.breakGlass.unsealRow, which
|
|
1172
|
+
// fetches the target row via clusterStorage and calls unsealRow with
|
|
1173
|
+
// no handle — would otherwise null every K_row cell on a keyed table
|
|
1174
|
+
// even though the wrapped secret still exists. The secret always
|
|
1175
|
+
// lives in the local _blamejs_per_row_keys, so keyed reads must work
|
|
1176
|
+
// on every path, not only db-query's. Any failure (db not yet
|
|
1177
|
+
// initialized, unusable handle) → null, and the field reads as absent
|
|
1178
|
+
// exactly as a shredded row would (the caller audits it).
|
|
1179
|
+
var spec = perRowKeyTables[table];
|
|
1180
|
+
var wrap;
|
|
1181
|
+
try {
|
|
1182
|
+
var prep = (dbHandle && typeof dbHandle.prepare === "function")
|
|
1183
|
+
? dbHandle.prepare.bind(dbHandle)
|
|
1184
|
+
: db().prepare;
|
|
1185
|
+
var wrapSelBuilt = sql.select(_perRowKeysTableName(), _PER_ROW_SQL_OPTS)
|
|
1186
|
+
.columns(["wrappedKey"])
|
|
1187
|
+
.where("tableName", table)
|
|
1188
|
+
.where("rowId", kRowId)
|
|
1189
|
+
.toSql();
|
|
1190
|
+
var wrapStmt = prep(wrapSelBuilt.sql);
|
|
1191
|
+
wrap = wrapStmt.get.apply(wrapStmt, wrapSelBuilt.params);
|
|
1192
|
+
} catch (_e) {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
if (!wrap || wrap.wrappedKey == null) return null; // shredded / never materialized
|
|
1196
|
+
_kRowCache = _deriveKRow(_unwrapRowSecret(wrap.wrappedKey, kRowId), table, kRowId, spec);
|
|
1197
|
+
return _kRowCache;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
785
1200
|
for (var i = 0; i < s.sealedFields.length; i++) {
|
|
786
1201
|
var field = s.sealedFields[i];
|
|
787
1202
|
if (out[field]) {
|
|
788
|
-
//
|
|
1203
|
+
// Per-cell envelope shape for audit metadata (operators write alert
|
|
1204
|
+
// rules off it): "row" = K_row cell, "aad" = vault.aad: cell on an
|
|
1205
|
+
// AAD table, "plain" otherwise.
|
|
1206
|
+
var shape = isRowSealed(out[field]) ? "row" : (s.aad ? "aad" : "plain");
|
|
1207
|
+
// Default-on cap: if this (actor, table, column) tuple is in cooldown
|
|
789
1208
|
// from prior forged-ciphertext failures, refuse before touching the
|
|
790
1209
|
// decryption oracle again (CWE-307). No-op when the cap is disabled.
|
|
791
1210
|
if (_rateInCooldown(capActor, table, field)) {
|
|
792
1211
|
_emitRateAudit({
|
|
793
|
-
table: table, field: field, actor: capActor, shape:
|
|
1212
|
+
table: table, field: field, actor: capActor, shape: shape,
|
|
794
1213
|
threshold: _rateCap.threshold, windowMs: _rateCap.windowMs, cooldownMs: _rateCap.cooldownMs,
|
|
795
1214
|
});
|
|
796
1215
|
throw new CryptoFieldRateError("crypto-field/unseal-rate-exceeded",
|
|
@@ -802,11 +1221,26 @@ function unsealRow(table, row, actor) {
|
|
|
802
1221
|
// Auto-detect the envelope shape so an AAD-bound table that
|
|
803
1222
|
// contains pre-migration plain-vault rows still reads. Read-
|
|
804
1223
|
// side migration is lazy; the next sealRow re-emits AAD-bound.
|
|
805
|
-
if (typeof out[field] === "string" &&
|
|
806
|
-
|
|
807
|
-
|
|
1224
|
+
if (typeof out[field] === "string" && isRowSealed(out[field])) {
|
|
1225
|
+
// Per-row-key cell: derive K_row (lazy, once), then decrypt
|
|
1226
|
+
// under it with the (table, rowId, column, schemaVersion) AAD.
|
|
1227
|
+
// A null K_row means the wrapped secret is gone (shredded) or
|
|
1228
|
+
// unreadable — throw so the catch nulls the field + audits.
|
|
1229
|
+
var kRow = _kRowOnce();
|
|
1230
|
+
if (!kRow) {
|
|
1231
|
+
throw new CryptoFieldError("crypto-field/row-key-unavailable",
|
|
1232
|
+
"unsealRow: per-row key for '" + table + "' row '" + kRowId +
|
|
1233
|
+
"' is unavailable (shredded or never materialized)");
|
|
1234
|
+
}
|
|
1235
|
+
var cellAad = _aadBytes(_rowCellAad(s, table, field, kRowId));
|
|
1236
|
+
unsealed = _decodeTyped(decryptPacked(
|
|
1237
|
+
Buffer.from(out[field].slice(ROW_PREFIX.length), "base64"), kRow, cellAad
|
|
1238
|
+
).toString("utf8"));
|
|
1239
|
+
} else if (typeof out[field] === "string" && vaultAad.isAadSealed(out[field])) {
|
|
1240
|
+
unsealed = _decodeTyped(vaultAad.unseal(out[field],
|
|
1241
|
+
_aadParts(s, table, field, out)));
|
|
808
1242
|
} else if (typeof out[field] === "string" && out[field].startsWith(VAULT_PREFIX)) {
|
|
809
|
-
unsealed = vault.unseal(out[field]);
|
|
1243
|
+
unsealed = _decodeTyped(vault.unseal(out[field]));
|
|
810
1244
|
} else {
|
|
811
1245
|
// Not a sealed value — pass through.
|
|
812
1246
|
unsealed = out[field];
|
|
@@ -829,18 +1263,18 @@ function unsealRow(table, row, actor) {
|
|
|
829
1263
|
table: table,
|
|
830
1264
|
field: field,
|
|
831
1265
|
rowId: out[s.rowIdField] || out._id || null,
|
|
832
|
-
shape:
|
|
1266
|
+
shape: shape,
|
|
833
1267
|
reason: (e && e.message) || String(e),
|
|
834
1268
|
},
|
|
835
1269
|
});
|
|
836
1270
|
} catch (_e) { /* drop-silent */ }
|
|
837
|
-
//
|
|
1271
|
+
// Default-on rate cap: account this failure against the (actor,
|
|
838
1272
|
// table, column) tuple. When it trips the threshold, arm the
|
|
839
1273
|
// cooldown + emit the distinct rate-exceeded audit once on the
|
|
840
1274
|
// transition. No-op when the cap is disabled.
|
|
841
1275
|
if (_rateNoteFailure(capActor, table, field)) {
|
|
842
1276
|
_emitRateAudit({
|
|
843
|
-
table: table, field: field, actor: capActor, shape:
|
|
1277
|
+
table: table, field: field, actor: capActor, shape: shape,
|
|
844
1278
|
threshold: _rateCap.threshold, windowMs: _rateCap.windowMs, cooldownMs: _rateCap.cooldownMs,
|
|
845
1279
|
});
|
|
846
1280
|
}
|
|
@@ -857,9 +1291,100 @@ function unsealRow(table, row, actor) {
|
|
|
857
1291
|
}
|
|
858
1292
|
}
|
|
859
1293
|
|
|
1294
|
+
// Upgrade-on-read auto-migrate for the keyed-MAC derived-hash default
|
|
1295
|
+
// flip (v0.15.0). A row written BEFORE the default moved from salted-sha3
|
|
1296
|
+
// to hmac-shake256 carries the legacy salted digest in its derived-hash
|
|
1297
|
+
// column; a keyed-only lookup would miss it (the dual-read in
|
|
1298
|
+
// lookupHashCandidates is what FINDS it). When such a row is unsealed and
|
|
1299
|
+
// we now hold the source plaintext, recompute the keyed-MAC digest and, if
|
|
1300
|
+
// the stored column still holds the legacy salted-sha3 value, re-write that
|
|
1301
|
+
// column to the keyed form so the row is keyed-indexed from now on and the
|
|
1302
|
+
// candidate set collapses back to a single value over time. Best-effort:
|
|
1303
|
+
// the returned row always carries the upgraded hash; the durable rewrite
|
|
1304
|
+
// happens only when a writable dbHandle is available + the row has an _id.
|
|
1305
|
+
_upgradeDerivedHashesOnRead(s, table, out, dbHandle);
|
|
1306
|
+
|
|
860
1307
|
return out;
|
|
861
1308
|
}
|
|
862
1309
|
|
|
1310
|
+
// Re-hash any legacy-salted derived-hash columns on a just-unsealed row to
|
|
1311
|
+
// the active keyed-MAC form. Pure-detect + in-place upgrade on the returned
|
|
1312
|
+
// `out` object; when `dbHandle` exposes a writable .prepare(), the upgrade is
|
|
1313
|
+
// also persisted with one UPDATE per row keyed on `_id`. Never throws — a
|
|
1314
|
+
// failed durable rewrite leaves the row matchable via the legacy digest (the
|
|
1315
|
+
// dual-read still finds it next time).
|
|
1316
|
+
function _upgradeDerivedHashesOnRead(s, table, out, dbHandle) {
|
|
1317
|
+
if (!s.derivedHashes) return;
|
|
1318
|
+
var rowId = out._id != null ? String(out._id) : "";
|
|
1319
|
+
var upgrades = null; // { derivedField: keyedValue } to persist
|
|
1320
|
+
for (var derivedField in s.derivedHashes) {
|
|
1321
|
+
if (!Object.prototype.hasOwnProperty.call(s.derivedHashes, derivedField)) continue;
|
|
1322
|
+
var spec = s.derivedHashes[derivedField];
|
|
1323
|
+
// Only the keyed-MAC mode has a distinct legacy form to migrate from.
|
|
1324
|
+
if (_resolveDerivedHashMode(spec, s.derivedHashMode) !== "hmac-shake256") continue;
|
|
1325
|
+
var stored = out[derivedField];
|
|
1326
|
+
if (typeof stored !== "string" || stored.length === 0) continue;
|
|
1327
|
+
var plain = out[spec.from];
|
|
1328
|
+
if (plain === undefined || plain === null) continue; // source erased / absent — nothing to re-hash
|
|
1329
|
+
var ns = namespaceFor(table, spec.from, s.hashNamespaces);
|
|
1330
|
+
var normalized = spec.normalize ? spec.normalize(plain) : String(plain);
|
|
1331
|
+
var keyed = _computeDerivedHash(spec, s.derivedHashMode, ns, normalized);
|
|
1332
|
+
if (stored === keyed) continue; // already keyed-indexed
|
|
1333
|
+
var legacy = _legacyDerivedHash(ns, normalized);
|
|
1334
|
+
if (stored !== legacy) continue; // not the legacy digest — leave untouched
|
|
1335
|
+
// Found a legacy-indexed row: surface the keyed hash on the returned row
|
|
1336
|
+
// and queue the durable rewrite.
|
|
1337
|
+
out[derivedField] = keyed;
|
|
1338
|
+
if (!upgrades) upgrades = {};
|
|
1339
|
+
upgrades[derivedField] = keyed;
|
|
1340
|
+
}
|
|
1341
|
+
if (!upgrades) return;
|
|
1342
|
+
// Persist when we can resolve a writable local handle + have a row identity.
|
|
1343
|
+
// The derived-hash columns + the app table live on the LOCAL db (the same
|
|
1344
|
+
// handle the per-row-key registry uses); the rewrite is a plain UPDATE.
|
|
1345
|
+
if (rowId.length === 0) return;
|
|
1346
|
+
var handle = (dbHandle && typeof dbHandle.prepare === "function")
|
|
1347
|
+
? dbHandle
|
|
1348
|
+
: _resolveLocalDbHandle();
|
|
1349
|
+
if (!handle) return;
|
|
1350
|
+
try {
|
|
1351
|
+
// The rewrite runs on whatever handle resolved the read. The local b.db is
|
|
1352
|
+
// node:sqlite; a caller-supplied external handle declares its dialect on
|
|
1353
|
+
// handle.dialect ("postgres" | "mysql"), so the UPDATE must quote
|
|
1354
|
+
// identifiers for THAT dialect — a sqlite-quoted UPDATE ("users") is parsed
|
|
1355
|
+
// as a string literal by MySQL (which expects `users`) and the durable
|
|
1356
|
+
// re-hash silently no-ops. Resolve the dialect the way db-query._dialect()
|
|
1357
|
+
// does (validated set, sqlite default).
|
|
1358
|
+
var handleDialect = (handle.dialect === "postgres" || handle.dialect === "mysql" ||
|
|
1359
|
+
handle.dialect === "sqlite") ? handle.dialect : "sqlite";
|
|
1360
|
+
var updBuilt = sql.update(table, { dialect: handleDialect, quoteName: true })
|
|
1361
|
+
.set(upgrades)
|
|
1362
|
+
.where("_id", rowId)
|
|
1363
|
+
.toSql();
|
|
1364
|
+
var stmt = handle.prepare(updBuilt.sql);
|
|
1365
|
+
stmt.run.apply(stmt, updBuilt.params);
|
|
1366
|
+
} catch (_e) {
|
|
1367
|
+
// Best-effort — DB not initialized, read-only handle, or the app table
|
|
1368
|
+
// isn't on this handle (cluster mode where the row came from the external
|
|
1369
|
+
// backend). The returned row still carries the upgraded hash; the legacy
|
|
1370
|
+
// digest stays matchable via lookupHashCandidates until a writable read
|
|
1371
|
+
// path re-hashes it.
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Resolve the framework's local db handle for the upgrade-on-read rewrite.
|
|
1376
|
+
// Mirrors the K_row read path's fallback: prefer an explicit dbHandle, else
|
|
1377
|
+
// the framework's own db(). Returns null when no .prepare()-capable handle
|
|
1378
|
+
// is reachable (db not initialized yet) so the caller skips the durable write.
|
|
1379
|
+
function _resolveLocalDbHandle() {
|
|
1380
|
+
try {
|
|
1381
|
+
var inst = db();
|
|
1382
|
+
return (inst && typeof inst.prepare === "function") ? inst : null;
|
|
1383
|
+
} catch (_e) {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
863
1388
|
// ---- Erasure (GDPR Art. 17 / "right to be forgotten") ----
|
|
864
1389
|
//
|
|
865
1390
|
// eraseRow(table, row) returns a tombstoned copy of the row: every
|
|
@@ -994,6 +1519,16 @@ function eraseRow(table, row) {
|
|
|
994
1519
|
* every encryption uses a fresh random nonce, so the ciphertext alone
|
|
995
1520
|
* cannot anchor a query.
|
|
996
1521
|
*
|
|
1522
|
+
* `value` is the digest under the column's ACTIVE mode (keyed-MAC by
|
|
1523
|
+
* default since v0.15.0; salted-sha3 when opted out), so existing callers
|
|
1524
|
+
* that emit `where(result.field, result.value)` are unchanged. When the
|
|
1525
|
+
* active mode is the keyed MAC, the result ALSO carries `legacyValue` — the
|
|
1526
|
+
* byte-form a row written under the pre-v0.15.0 salted-sha3 default would
|
|
1527
|
+
* hold. Callers that can issue a match-EITHER query (or that prefer the
|
|
1528
|
+
* ready-made candidate list) use `b.cryptoField.lookupHashCandidates`; the
|
|
1529
|
+
* upgrade-on-read auto-migrate in `unsealRow` re-hashes any row found via
|
|
1530
|
+
* the legacy digest to the keyed-MAC form.
|
|
1531
|
+
*
|
|
997
1532
|
* @example
|
|
998
1533
|
* b.cryptoField.registerTable("users", {
|
|
999
1534
|
* sealedFields: ["email"],
|
|
@@ -1011,14 +1546,52 @@ function lookupHash(table, field, value) {
|
|
|
1011
1546
|
for (var derivedField in s.derivedHashes) {
|
|
1012
1547
|
var spec = s.derivedHashes[derivedField];
|
|
1013
1548
|
if (spec.from === field) {
|
|
1014
|
-
|
|
1015
|
-
var normalized = spec.normalize ? spec.normalize(value) : String(value);
|
|
1016
|
-
return { field: derivedField, value: _computeDerivedHash(spec, s.derivedHashMode, ns, normalized) };
|
|
1549
|
+
return _derivedHashResult(s, table, derivedField, spec, field, value);
|
|
1017
1550
|
}
|
|
1018
1551
|
}
|
|
1019
1552
|
return null;
|
|
1020
1553
|
}
|
|
1021
1554
|
|
|
1555
|
+
/**
|
|
1556
|
+
* @primitive b.cryptoField.lookupHashCandidates
|
|
1557
|
+
* @signature b.cryptoField.lookupHashCandidates(table, field, value)
|
|
1558
|
+
* @since 0.15.0
|
|
1559
|
+
* @compliance gdpr, hipaa
|
|
1560
|
+
* @related b.cryptoField.lookupHash, b.cryptoField.unsealRow
|
|
1561
|
+
*
|
|
1562
|
+
* Dual-read sibling of `lookupHash`. Returns `{ field, values }` where
|
|
1563
|
+
* `values` is the list of derived-hash digests that should ALL be treated
|
|
1564
|
+
* as a match for `value` — the digest under the column's active mode FIRST,
|
|
1565
|
+
* plus (when the active mode is the keyed MAC) the pre-v0.15.0 salted-sha3
|
|
1566
|
+
* digest a row written under the old default would carry. A caller that can
|
|
1567
|
+
* issue an `IN (…)` / `OR` equality over `field` finds both the new
|
|
1568
|
+
* keyed-indexed rows and the legacy salted-indexed rows in one query, so the
|
|
1569
|
+
* keyed-MAC default flip never silently drops pre-flip rows. Returns null
|
|
1570
|
+
* when no derived hash is declared for `field`.
|
|
1571
|
+
*
|
|
1572
|
+
* Pair it with the upgrade-on-read auto-migrate: `unsealRow` re-hashes any
|
|
1573
|
+
* row whose stored derived-hash matches the legacy digest to the keyed-MAC
|
|
1574
|
+
* form, so the candidate list shrinks back to a single value as rows are
|
|
1575
|
+
* read over time.
|
|
1576
|
+
*
|
|
1577
|
+
* @example
|
|
1578
|
+
* b.cryptoField.registerTable("users", {
|
|
1579
|
+
* sealedFields: ["email"],
|
|
1580
|
+
* derivedHashes: { emailHash: { from: "email" } },
|
|
1581
|
+
* });
|
|
1582
|
+
* var c = b.cryptoField.lookupHashCandidates("users", "email", "alice@example.com");
|
|
1583
|
+
* c.field; // → "emailHash"
|
|
1584
|
+
* c.values.length; // → 2 (keyed-MAC + legacy salted-sha3)
|
|
1585
|
+
* // → b.db.from("users").where(c.field, "IN", c.values)
|
|
1586
|
+
*/
|
|
1587
|
+
function lookupHashCandidates(table, field, value) {
|
|
1588
|
+
var r = lookupHash(table, field, value);
|
|
1589
|
+
if (!r) return null;
|
|
1590
|
+
var values = [r.value];
|
|
1591
|
+
if (r.legacyValue && r.legacyValue !== r.value) values.push(r.legacyValue);
|
|
1592
|
+
return { field: r.field, values: values };
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1022
1595
|
/**
|
|
1023
1596
|
* @primitive b.cryptoField.declareColumnResidency
|
|
1024
1597
|
* @signature b.cryptoField.declareColumnResidency(table, opts)
|
|
@@ -1105,7 +1678,7 @@ function getColumnResidency(table) {
|
|
|
1105
1678
|
* @signature b.cryptoField.assertColumnResidency(table, row, args)
|
|
1106
1679
|
* @since 0.7.27
|
|
1107
1680
|
* @compliance gdpr
|
|
1108
|
-
* @related b.cryptoField.declareColumnResidency
|
|
1681
|
+
* @related b.cryptoField.declareColumnResidency, b.cryptoField.declarePerRowResidency
|
|
1109
1682
|
*
|
|
1110
1683
|
* Storage-write gate. Storage backends call this with the proposed
|
|
1111
1684
|
* row before the SQL hits the wire; refusal under regulated postures
|
|
@@ -1161,6 +1734,137 @@ function assertColumnResidency(table, row, args) {
|
|
|
1161
1734
|
return null;
|
|
1162
1735
|
}
|
|
1163
1736
|
|
|
1737
|
+
/**
|
|
1738
|
+
* @primitive b.cryptoField.declarePerRowResidency
|
|
1739
|
+
* @signature b.cryptoField.declarePerRowResidency(table, opts)
|
|
1740
|
+
* @since 0.14.24
|
|
1741
|
+
* @compliance gdpr
|
|
1742
|
+
* @related b.cryptoField.getPerRowResidency, b.cryptoField.declareColumnResidency
|
|
1743
|
+
*
|
|
1744
|
+
* Declares per-ROW data residency for `table`: one plaintext column on
|
|
1745
|
+
* each row carries that row's residency tag, and the write gates
|
|
1746
|
+
* refuse a tagged row landing on an incompatible backend. The sibling
|
|
1747
|
+
* of `declareColumnResidency` — columns answer "which fields are
|
|
1748
|
+
* region-bound", rows answer "which region does THIS record belong
|
|
1749
|
+
* to" (an EU user's row next to a US user's row in the same table).
|
|
1750
|
+
* Local writes (`b.db.from(...).insertOne` / `.update`) enforce the
|
|
1751
|
+
* tag against the deployment's `dataResidency` region set under
|
|
1752
|
+
* cross-border regulated postures; external writes
|
|
1753
|
+
* (`b.externalDb.query`) take the tag per call via
|
|
1754
|
+
* `opts.rowResidencyTag` because raw SQL carries no row object. Rows
|
|
1755
|
+
* tagged "global" or "unrestricted" pass any backend. Throws on bad
|
|
1756
|
+
* input (config-time fail-loud).
|
|
1757
|
+
*
|
|
1758
|
+
* @opts
|
|
1759
|
+
* residencyColumn: string, // plaintext column carrying the row's tag
|
|
1760
|
+
* allowedTags: string[], // whitelist of valid tag values ("eu", "us", "global", region names)
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* b.cryptoField.declarePerRowResidency("users", {
|
|
1764
|
+
* residencyColumn: "dataRegion",
|
|
1765
|
+
* allowedTags: ["eu-west-1", "us-east-1", "global"],
|
|
1766
|
+
* });
|
|
1767
|
+
* var spec = b.cryptoField.getPerRowResidency("users");
|
|
1768
|
+
* spec.residencyColumn; // → "dataRegion"
|
|
1769
|
+
*/
|
|
1770
|
+
function declarePerRowResidency(table, opts) {
|
|
1771
|
+
validateOpts.requireNonEmptyString(table, "declarePerRowResidency: table",
|
|
1772
|
+
CryptoFieldError, "crypto-field/per-row-residency-table-empty");
|
|
1773
|
+
validateOpts.requireObject(opts, "declarePerRowResidency",
|
|
1774
|
+
CryptoFieldError, "crypto-field/per-row-residency-opts-not-object");
|
|
1775
|
+
validateOpts(opts, ["residencyColumn", "allowedTags"], "cryptoField.declarePerRowResidency");
|
|
1776
|
+
validateOpts.requireNonEmptyString(opts.residencyColumn,
|
|
1777
|
+
"declarePerRowResidency: opts.residencyColumn",
|
|
1778
|
+
CryptoFieldError, "crypto-field/per-row-residency-column-invalid");
|
|
1779
|
+
if (!Array.isArray(opts.allowedTags) || opts.allowedTags.length === 0) {
|
|
1780
|
+
throw new CryptoFieldError("crypto-field/per-row-residency-tags-invalid",
|
|
1781
|
+
"declarePerRowResidency: opts.allowedTags must be a non-empty array of tag strings");
|
|
1782
|
+
}
|
|
1783
|
+
validateOpts.optionalNonEmptyStringArray(opts.allowedTags,
|
|
1784
|
+
"declarePerRowResidency: opts.allowedTags",
|
|
1785
|
+
CryptoFieldError, "crypto-field/per-row-residency-tag-empty");
|
|
1786
|
+
// The residency tag column MUST stay plaintext — the write gate reads
|
|
1787
|
+
// it on every INSERT / UPDATE before sealRow, and reads return it
|
|
1788
|
+
// verbatim. A sealed residency column would be ciphertext the gate
|
|
1789
|
+
// can't compare and reads can't surface. Refuse the misconfiguration
|
|
1790
|
+
// at declaration time when the table's sealed-field set is already
|
|
1791
|
+
// known (registration order permitting).
|
|
1792
|
+
var sealed = getSealedFields(table);
|
|
1793
|
+
if (Array.isArray(sealed) && sealed.indexOf(opts.residencyColumn) !== -1) {
|
|
1794
|
+
throw new CryptoFieldError("crypto-field/per-row-residency-sealed-conflict",
|
|
1795
|
+
"declarePerRowResidency: residencyColumn '" + opts.residencyColumn +
|
|
1796
|
+
"' is a sealed field on table '" + table + "' — the residency tag must " +
|
|
1797
|
+
"stay plaintext so the write gate can read it. Choose a non-sealed column");
|
|
1798
|
+
}
|
|
1799
|
+
perRowResidency[table] = {
|
|
1800
|
+
residencyColumn: opts.residencyColumn,
|
|
1801
|
+
allowedTags: opts.allowedTags.slice(),
|
|
1802
|
+
};
|
|
1803
|
+
return {
|
|
1804
|
+
table: table,
|
|
1805
|
+
residencyColumn: opts.residencyColumn,
|
|
1806
|
+
allowedTags: opts.allowedTags.slice(),
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/**
|
|
1811
|
+
* @primitive b.cryptoField.getPerRowResidency
|
|
1812
|
+
* @signature b.cryptoField.getPerRowResidency(table)
|
|
1813
|
+
* @since 0.14.24
|
|
1814
|
+
* @related b.cryptoField.declarePerRowResidency
|
|
1815
|
+
*
|
|
1816
|
+
* Returns the per-row residency spec declared for `table`
|
|
1817
|
+
* (`{ residencyColumn, allowedTags }`), or null when the table has no
|
|
1818
|
+
* declaration. Read-only — storage backends call this at the write
|
|
1819
|
+
* boundary to decide whether the row-residency gate applies.
|
|
1820
|
+
*
|
|
1821
|
+
* @example
|
|
1822
|
+
* b.cryptoField.declarePerRowResidency("users", {
|
|
1823
|
+
* residencyColumn: "dataRegion",
|
|
1824
|
+
* allowedTags: ["eu-west-1", "global"],
|
|
1825
|
+
* });
|
|
1826
|
+
* b.cryptoField.getPerRowResidency("users").allowedTags; // → ["eu-west-1", "global"]
|
|
1827
|
+
* b.cryptoField.getPerRowResidency("unknown"); // → null
|
|
1828
|
+
*/
|
|
1829
|
+
function getPerRowResidency(table) {
|
|
1830
|
+
var spec = perRowResidency[table];
|
|
1831
|
+
if (!spec) return null;
|
|
1832
|
+
return { residencyColumn: spec.residencyColumn, allowedTags: spec.allowedTags.slice() };
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* @primitive b.cryptoField.listPerRowResidency
|
|
1837
|
+
* @signature b.cryptoField.listPerRowResidency()
|
|
1838
|
+
* @since 0.15.4
|
|
1839
|
+
* @related b.cryptoField.getPerRowResidency, b.cryptoField.declarePerRowResidency
|
|
1840
|
+
*
|
|
1841
|
+
* Enumerate every table opted into per-row residency. Returns one entry per
|
|
1842
|
+
* declared table — `{ table, residencyColumn, allowedTags }` — where
|
|
1843
|
+
* `allowedTags` lists the regions that table's rows may be tagged to.
|
|
1844
|
+
* Read-only. Consumers that must reason about residency across the whole
|
|
1845
|
+
* deployment rather than one table use this: `b.backup.create` enumerates it
|
|
1846
|
+
* to surface the per-row cross-border regions a deployment-level region
|
|
1847
|
+
* compare is blind to.
|
|
1848
|
+
*
|
|
1849
|
+
* @example
|
|
1850
|
+
* b.cryptoField.declarePerRowResidency("residents", {
|
|
1851
|
+
* residencyColumn: "region",
|
|
1852
|
+
* allowedTags: ["eu-west-1", "us-east-1"],
|
|
1853
|
+
* });
|
|
1854
|
+
* b.cryptoField.listPerRowResidency();
|
|
1855
|
+
* // → [ { table: "residents", residencyColumn: "region",
|
|
1856
|
+
* // allowedTags: ["eu-west-1", "us-east-1"] } ]
|
|
1857
|
+
*/
|
|
1858
|
+
function listPerRowResidency() {
|
|
1859
|
+
return Object.keys(perRowResidency).map(function (t) {
|
|
1860
|
+
return {
|
|
1861
|
+
table: t,
|
|
1862
|
+
residencyColumn: perRowResidency[t].residencyColumn,
|
|
1863
|
+
allowedTags: perRowResidency[t].allowedTags.slice(),
|
|
1864
|
+
};
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1164
1868
|
/**
|
|
1165
1869
|
* @primitive b.cryptoField.declarePerRowKey
|
|
1166
1870
|
* @signature b.cryptoField.declarePerRowKey(table, opts)
|
|
@@ -1169,13 +1873,16 @@ function assertColumnResidency(table, row, args) {
|
|
|
1169
1873
|
* @related b.cryptoField.materializePerRowKey, b.cryptoField.destroyPerRowKey, b.subject.eraseHard
|
|
1170
1874
|
*
|
|
1171
1875
|
* Opts a table into per-row keying (K_row crypto-shred substrate).
|
|
1172
|
-
* After registration, every INSERT generates a fresh
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1876
|
+
* After registration, every INSERT generates a fresh 32-byte CSPRNG
|
|
1877
|
+
* row-secret, derives K_row from it, and stores the SECRET (never
|
|
1878
|
+
* K_row) AAD-sealed in `_blamejs_per_row_keys (tableName, rowId,
|
|
1879
|
+
* wrappedKey)`. AAD on the wrap binds (table, rowId, column,
|
|
1880
|
+
* schemaVersion) — a wrapped secret copied to a different row fails
|
|
1881
|
+
* Poly1305 verification. `b.subject.eraseHard(subjectId)` /
|
|
1882
|
+
* `b.retention` destroy the per-row entries for the subject's rows; WAL
|
|
1883
|
+
* / replica residual ciphertext becomes mathematically undecryptable
|
|
1884
|
+
* because the random row-secret — the only seed for K_row — is gone
|
|
1885
|
+
* everywhere it ever lived. Throws on bad input (config-time
|
|
1179
1886
|
* fail-loud).
|
|
1180
1887
|
*
|
|
1181
1888
|
* @opts
|
|
@@ -1237,15 +1944,22 @@ function hasPerRowKey(table) {
|
|
|
1237
1944
|
* @compliance gdpr, hipaa
|
|
1238
1945
|
* @related b.cryptoField.declarePerRowKey, b.cryptoField.destroyPerRowKey
|
|
1239
1946
|
*
|
|
1240
|
-
* Derive-and-store: called by the storage backend on INSERT
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
*
|
|
1947
|
+
* Derive-and-store: called by the storage backend on INSERT (the
|
|
1948
|
+
* db-query write boundary, gated on `hasPerRowKey`). Generates a fresh
|
|
1949
|
+
* 32-byte CSPRNG row-secret, derives
|
|
1950
|
+
* `K_row = SHAKE256(rowSecret || ":" || table || ":" || rowId || ":"
|
|
1951
|
+
* || info, keySize)`, AAD-seals the SECRET (base64) into
|
|
1952
|
+
* `_blamejs_per_row_keys.wrappedKey` via `b.vault.aad.seal`, and
|
|
1953
|
+
* returns the unwrapped K_row Buffer for the caller to encrypt sealed
|
|
1954
|
+
* columns under the row-scoped key. The secret is random — never a
|
|
1955
|
+
* function of any on-disk salt — so destroying the wrapped secret
|
|
1956
|
+
* makes K_row unrecoverable even with full disk + vault-root access.
|
|
1957
|
+
* Idempotent on UPSERT — if a secret already exists for (table,
|
|
1958
|
+
* rowId), unwraps it and re-derives the same K_row. The AAD-bound wrap
|
|
1959
|
+
* rejects copy-row attacks: a wrapped secret pasted under a different
|
|
1960
|
+
* rowId fails Poly1305 verification at unseal time. `dbHandle` is a
|
|
1961
|
+
* b.db handle (`.prepare`); rowId MUST be the row's `_id` (the value
|
|
1962
|
+
* `destroyPerRowKey` / `b.subject.eraseHard` delete on).
|
|
1249
1963
|
*
|
|
1250
1964
|
* @example
|
|
1251
1965
|
* b.cryptoField.declarePerRowKey("orders", { keySize: 32 });
|
|
@@ -1265,30 +1979,66 @@ function materializePerRowKey(table, rowId, dbHandle) {
|
|
|
1265
1979
|
throw new CryptoFieldError("crypto-field/materialize-per-row-key-no-db",
|
|
1266
1980
|
"materializePerRowKey: dbHandle (b.db) is required");
|
|
1267
1981
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1982
|
+
var ridStr = String(rowId);
|
|
1983
|
+
// Existing secret? Unwrap + re-derive to support idempotent UPSERTs.
|
|
1984
|
+
var existingSelBuilt = sql.select(_perRowKeysTableName(), _PER_ROW_SQL_OPTS)
|
|
1985
|
+
.columns(["wrappedKey"])
|
|
1986
|
+
.where("tableName", table)
|
|
1987
|
+
.where("rowId", ridStr)
|
|
1988
|
+
.toSql();
|
|
1989
|
+
var existingStmt = dbHandle.prepare(existingSelBuilt.sql);
|
|
1990
|
+
var existing = existingStmt.get.apply(existingStmt, existingSelBuilt.params);
|
|
1272
1991
|
if (existing) {
|
|
1273
|
-
return
|
|
1992
|
+
return _deriveKRow(_unwrapRowSecret(existing.wrappedKey, ridStr), table, ridStr, spec);
|
|
1274
1993
|
}
|
|
1275
|
-
//
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
var
|
|
1281
|
-
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
var sealed =
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1994
|
+
// Fresh random row-secret. CRITICAL: this is CSPRNG, not a function
|
|
1995
|
+
// of any on-disk value (the pre-v0.14.25 design derived K_row from
|
|
1996
|
+
// the plaintext-on-disk derivedHash salt, so an attacker with disk
|
|
1997
|
+
// access re-derived it and deleting the wrap shred nothing). With a
|
|
1998
|
+
// random secret, K_row is unrecoverable once the wrap is destroyed.
|
|
1999
|
+
var rowSecret = generateBytes(32);
|
|
2000
|
+
var kRow = _deriveKRow(rowSecret, table, ridStr, spec);
|
|
2001
|
+
// Store the SECRET (never K_row), AAD-sealed under the vault root so a
|
|
2002
|
+
// wrapped secret copied to a different (table, rowId) fails Poly1305.
|
|
2003
|
+
var sealed = vaultAad.seal(rowSecret.toString("base64"), _wrappedKeyAad(ridStr));
|
|
2004
|
+
// _id is the rotation pipeline's pagination/UPDATE key (the natural
|
|
2005
|
+
// identity is the composite (tableName, rowId)). A fresh token keeps
|
|
2006
|
+
// it unique per registry row.
|
|
2007
|
+
var insBuilt = sql.insert(_perRowKeysTableName(), _PER_ROW_SQL_OPTS)
|
|
2008
|
+
.values({
|
|
2009
|
+
_id: generateToken(16),
|
|
2010
|
+
tableName: table,
|
|
2011
|
+
rowId: ridStr,
|
|
2012
|
+
wrappedKey: sealed,
|
|
2013
|
+
createdAt: Date.now(),
|
|
2014
|
+
})
|
|
2015
|
+
.toSql();
|
|
2016
|
+
var insStmt = dbHandle.prepare(insBuilt.sql);
|
|
2017
|
+
insStmt.run.apply(insStmt, insBuilt.params);
|
|
1289
2018
|
return kRow;
|
|
1290
2019
|
}
|
|
1291
2020
|
|
|
2021
|
+
// Derive the row-scoped key from the random row-secret. SHAKE256 expand
|
|
2022
|
+
// (HKDF-shaped, matches the framework's PQC-first kdf) over
|
|
2023
|
+
// rowSecret || ":" || table || ":" || rowId || ":" || info — the
|
|
2024
|
+
// non-secret context terms domain-separate two rows that (astronomically
|
|
2025
|
+
// improbably) drew the same secret; the secret is the entropy source.
|
|
2026
|
+
function _deriveKRow(rowSecret, table, rowId, spec) {
|
|
2027
|
+
var ikm = Buffer.concat([
|
|
2028
|
+
rowSecret,
|
|
2029
|
+
Buffer.from(":" + table + ":" + rowId + ":" + spec.info, "utf8"),
|
|
2030
|
+
]);
|
|
2031
|
+
return kdf(ikm, spec.keySize);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Unwrap a stored row-secret back to its 32 raw bytes. The wrap is
|
|
2035
|
+
// AAD-bound to (PER_ROW_KEYS_TABLE, rowId, wrappedKey, schemaVersion);
|
|
2036
|
+
// a tampered / copied wrap throws here, which the read path surfaces as
|
|
2037
|
+
// system.crypto.unseal_failed (shredded data reads as absent).
|
|
2038
|
+
function _unwrapRowSecret(wrapped, rowId) {
|
|
2039
|
+
return Buffer.from(vaultAad.unseal(wrapped, _wrappedKeyAad(rowId)), "base64");
|
|
2040
|
+
}
|
|
2041
|
+
|
|
1292
2042
|
/**
|
|
1293
2043
|
* @primitive b.cryptoField.destroyPerRowKey
|
|
1294
2044
|
* @signature b.cryptoField.destroyPerRowKey(table, rowId, dbHandle)
|
|
@@ -1296,13 +2046,14 @@ function materializePerRowKey(table, rowId, dbHandle) {
|
|
|
1296
2046
|
* @compliance gdpr, hipaa
|
|
1297
2047
|
* @related b.cryptoField.materializePerRowKey, b.subject.eraseHard
|
|
1298
2048
|
*
|
|
1299
|
-
* Crypto-shred: drops the
|
|
1300
|
-
* `_blamejs_per_row_keys`. Called by `b.subject.eraseHard`
|
|
1301
|
-
* row mapped to the erased subject. Returns
|
|
2049
|
+
* Crypto-shred: drops the row's wrapped row-secret from
|
|
2050
|
+
* `_blamejs_per_row_keys`. Called by `b.subject.eraseHard` and
|
|
2051
|
+
* `b.retention` for each row mapped to the erased subject. Returns
|
|
1302
2052
|
* `{ destroyed: <rowsAffected> }`. After destruction, any WAL /
|
|
1303
2053
|
* replica residual ciphertext for the row is mathematically
|
|
1304
|
-
* undecryptable — even with the vault root key — because
|
|
1305
|
-
*
|
|
2054
|
+
* undecryptable — even with the vault root key — because the random
|
|
2055
|
+
* row-secret (the only seed for K_row) is gone everywhere it ever
|
|
2056
|
+
* lived. `rowId` MUST be the row's `_id`. No-op when the table is not
|
|
1306
2057
|
* registered for per-row keying.
|
|
1307
2058
|
*
|
|
1308
2059
|
* @example
|
|
@@ -1322,9 +2073,12 @@ function destroyPerRowKey(table, rowId, dbHandle) {
|
|
|
1322
2073
|
throw new CryptoFieldError("crypto-field/destroy-per-row-key-no-db",
|
|
1323
2074
|
"destroyPerRowKey: dbHandle (b.db) is required");
|
|
1324
2075
|
}
|
|
1325
|
-
var
|
|
1326
|
-
|
|
1327
|
-
|
|
2076
|
+
var delBuilt = sql.delete(_perRowKeysTableName(), _PER_ROW_SQL_OPTS)
|
|
2077
|
+
.where("tableName", table)
|
|
2078
|
+
.where("rowId", String(rowId))
|
|
2079
|
+
.toSql();
|
|
2080
|
+
var delStmt = dbHandle.prepare(delBuilt.sql);
|
|
2081
|
+
var result = delStmt.run.apply(delStmt, delBuilt.params);
|
|
1328
2082
|
return { destroyed: (result && result.changes) || 0 };
|
|
1329
2083
|
}
|
|
1330
2084
|
|
|
@@ -1336,10 +2090,10 @@ function destroyPerRowKey(table, rowId, dbHandle) {
|
|
|
1336
2090
|
* @related b.cryptoField.declareColumnResidency, b.cryptoField.declarePerRowKey
|
|
1337
2091
|
*
|
|
1338
2092
|
* Test-only helper. Drops every entry from the per-column residency
|
|
1339
|
-
* registry
|
|
1340
|
-
* re-declare
|
|
1341
|
-
*
|
|
1342
|
-
* boot.
|
|
2093
|
+
* registry, the per-row residency registry, and the per-row-key
|
|
2094
|
+
* registry so a test fixture can re-declare them between cases.
|
|
2095
|
+
* Operator code never calls this — production declarations come from
|
|
2096
|
+
* `b.db.init({ schema })` once at boot.
|
|
1343
2097
|
*
|
|
1344
2098
|
* @example
|
|
1345
2099
|
* b.cryptoField.declareColumnResidency("users", {
|
|
@@ -1351,6 +2105,7 @@ function destroyPerRowKey(table, rowId, dbHandle) {
|
|
|
1351
2105
|
function clearResidencyForTest() {
|
|
1352
2106
|
for (var t in columnResidency) delete columnResidency[t];
|
|
1353
2107
|
for (var u in perRowKeyTables) delete perRowKeyTables[u];
|
|
2108
|
+
for (var v in perRowResidency) delete perRowResidency[v];
|
|
1354
2109
|
}
|
|
1355
2110
|
|
|
1356
2111
|
module.exports = {
|
|
@@ -1359,6 +2114,7 @@ module.exports = {
|
|
|
1359
2114
|
getSealedFields: getSealedFields,
|
|
1360
2115
|
sealRow: sealRow,
|
|
1361
2116
|
unsealRow: unsealRow,
|
|
2117
|
+
isRowSealed: isRowSealed,
|
|
1362
2118
|
configureUnsealRateCap: configureUnsealRateCap,
|
|
1363
2119
|
clearRateCapForTest: clearRateCapForTest,
|
|
1364
2120
|
CryptoFieldRateError: CryptoFieldRateError,
|
|
@@ -1378,10 +2134,14 @@ module.exports = {
|
|
|
1378
2134
|
computeDerived: computeDerived,
|
|
1379
2135
|
computeNamespacedHash: computeNamespacedHash,
|
|
1380
2136
|
lookupHash: lookupHash,
|
|
2137
|
+
lookupHashCandidates: lookupHashCandidates,
|
|
1381
2138
|
clearForTest: clearForTest,
|
|
1382
2139
|
declareColumnResidency: declareColumnResidency,
|
|
1383
2140
|
getColumnResidency: getColumnResidency,
|
|
1384
2141
|
assertColumnResidency: assertColumnResidency,
|
|
2142
|
+
declarePerRowResidency: declarePerRowResidency,
|
|
2143
|
+
getPerRowResidency: getPerRowResidency,
|
|
2144
|
+
listPerRowResidency: listPerRowResidency,
|
|
1385
2145
|
declarePerRowKey: declarePerRowKey,
|
|
1386
2146
|
hasPerRowKey: hasPerRowKey,
|
|
1387
2147
|
materializePerRowKey: materializePerRowKey,
|