feat(kv-store): encryption-at-rest for SQLite-OPFS + PXE opaque-keys (DO NOT MERGE, PROOF OF CONCEPT)#22683
Open
mverzilli wants to merge 5 commits intomerge-train/fairiesfrom
Open
Conversation
…opt-ins Adds value-level encryption to the SQLite-OPFS backend and opts the PXE's sensitive containers into the new opaque-keys mode. Disabled by default — stores without a cipher behave exactly as before, and non-SQLite-OPFS backends (LMDB, IndexedDB) silently ignore the new flag. kv-store changes ---------------- - New cipher module: `ValueCipher` interface + `IdentityCipher` (null object, passthrough) + `AesGcmCipher` (AES-GCM-256 values, HMAC-SHA-256 digests, HKDF-derived sub-keys from a master) + `RawKeyProvider` (32-byte seed). - Values, digests, and optionally keys flow through the store's cipher. The map/multi_map/set containers support `opaqueKeys: true`: keys are HMAC'd on disk, the encrypted value blob embeds the original key for iteration, and range queries with start/end/reverse throw (HMAC destroys ordering). - AES-GCM AAD = slot binds each ciphertext to its row. Row-rebinding attacks (moving a value blob to a different slot via disk write) break the auth tag on decrypt. Covered by integration tests. - Shared interfaces (`AztecKVStore`, `AztecAsyncKVStore`) gain an optional `OpenContainerOptions` arg on `openMap`/`openMultiMap`/`openSet`. - Three backends (IDB, LMDB, LMDB-v2) accept-and-ignore the new option. PXE opaqueKeys opt-ins ---------------------- Thirteen audit-flagged containers now request opaqueKeys so keys don't leak on disk when encryption is enabled: - note_store: notes, note_nullifiers_by_contract, note_block_number_to_nullifier - private_event_store: private_event_logs, events_by_contract_selector, events_by_block_number - address_store: complete_address_index - tagging_store: pending_indexes, last_finalized_indexes, highest_aged_index, highest_finalized_index, address_book Wallet DB refactor ------------------ wallet_db previously packed accounts + aliases into two shared maps with `type:ADDR`, `senders:ALIAS`, `accounts:ALIAS` prefixes and iterated them via prefix ranges. HMAC'd keys break prefix ranges, so the DB is split into three namespace-per-map containers (`accounts`, `account_aliases`, `sender_aliases`), each with opaqueKeys. Public API unchanged; 17 existing tests still pass. Tests ----- - 23 cipher unit tests (encrypt/decrypt/digest/keyDigest + AAD round-trip + tampering/wrong-key/wrong-AAD rejections) - 19 opaque-keys semantics tests (validation, at-rest observation proving the key column is HMAC'd and the value column starts with 0x01 + no plaintext, row-rebinding attack detection, unordered iteration) - Each container's shared test suite runs twice — once with IdentityCipher, once with AesGcmCipher — 171 kv-store tests total. Format break note ----------------- Phase-1 SQLite-OPFS DBs stored the multi-map `hash` column as ohash-of-value; this PR changes it to a bytes-level digest (SHA-256 in identity mode, HMAC in cipher mode). Pre-existing phase-1 rows will no longer dedup against fresh inserts of the same (key, value) pair — one extra row instead of dedup. Only dev DBs are affected in practice. Security considerations (F1–F7 audit findings) are documented inline at the top of `cipher.ts`. F1 (row-rebinding) is fixed; F2 (equivalence-class leak), F3 (no forward secrecy), F4 (KeyProvider entropy discipline), F5 (undetected deletions), F6 (accepted metadata leaks), and F7 (format break) are documented. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arrel External consumers (e.g., gregoswap wiring) need AesGcmCipher / RawKeyProvider / IdentityCipher / ValueCipher / KeyProvider to construct and pass ciphers into AztecSQLiteOPFSStore.open. Re-export from the package entrypoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…use hazard msgpackr's Encoder.pack() returns a Uint8Array that is a view into a reusable internal buffer — subsequent pack calls can overwrite the backing bytes. The map's set() path fires cipher.encrypt and cipher.digest under Promise.all on the same pack output; if any concurrent set() runs before those resolve, the underlying bytes can mutate, leading to silently corrupt encrypted rows. Copy the view into an owned Uint8Array before handing it to the cipher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: the SQLite-OPFS store threw at openMap/openSet/openMultiMap time if opaqueKeys: true was requested on a store without an encryption cipher. Problem: PXE storage classes hard-code opaqueKeys: true on their sensitive containers at the source level. The store-level decision of "with or without cipher" is made much further out (application wiring). With the strict guard in place, running PXE code without encryption — e.g. for isolation testing, unit tests, or phase-out of the feature — was a hard error that no consumer could paper over. The LMDB and IndexedDB sibling backends already silently ignore the flag (they have no cipher). Matching that behavior in SQLite-OPFS restores correctness-preserving fall-back: if the store is unencrypted, keys are stored in the clear regardless of the hint. Also updates the corresponding test block from "throws when no cipher" to "falls back to plaintext when no cipher" and adds a sanity check that the key column carries ordered-binary (not HMAC) bytes in that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors src/bench/sqlite-opfs/map_bench.test.ts but opens the store with a real AesGcmCipher (RawKeyProvider with fresh seed per run). The delta vs the plaintext bench is the direct cost of cipher.encrypt + cipher.digest on writes and cipher.decrypt on reads. Gated by VITE_BENCH=1, consistent with the existing IDB + plaintext SQLite-OPFS variants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ValueCipher(null-objectIdentityCipherfor off,AesGcmCipherfor on) — disabled by default, bytes-identical to non-encrypted version when no cipher is passed.What's in the box
kv-store — new
cipher.tsmodule (AES-GCM-256 for values, HMAC-SHA-256 for deterministic digests, HKDF-derived sub-keys, row-binding via AES-GCM AAD = slot). Shared interface gets an optionalOpenContainerOptionsarg onopenMap/openMultiMap/openSet; LMDB/LMDB-v2/IDB accept-and-ignore it silently.PXE sweep —
note_store,private_event_store,address_store, both tagging stores, and the address book flipopaqueKeys: trueon 13 audit-flagged containers.Wallet DB refactor — previous prefix-range pattern (
type:ADDR/accounts:ALIAS/senders:ALIASin two shared maps) is incompatible with HMAC'd keys, so the DB is split into three namespace-per-map containers with opaqueKeys. Public API unchanged; all 17 existing tests pass.Tests
Test plan
yarn workspace @aztec/kv-store test:browser src/sqlite-opfs/— 171 passedyarn workspace @aztec/pxe test src/storage/— 137 passedyarn workspace @aztec/wallets test src/embedded/wallet_db.test.ts— 17 passedyarn buildclean on the touched packagesyarn lintclean on kv-store, pxe, walletsOut of scope / follow-ups
IndexedDBKeyProvider— currentRawKeyProvider(32 bytes)is for tests / programmatic use. Next up: a provider that persists an unextractable HKDF master to IDB so there's a zero-UX key source for browsers.🤖 Generated with Claude Code