docs(adr): record the sealed-wallet at-rest encryption design as ADR 0012 - #2496
Draft
zancas wants to merge 1 commit into
Draft
docs(adr): record the sealed-wallet at-rest encryption design as ADR 0012#2496zancas wants to merge 1 commit into
zancas wants to merge 1 commit into
Conversation
…0012 A grilling session on 2026-07-21 resolved the design for encrypting all on-disk wallet data. The library owns a sealed wallet-file format opened by a caller-supplied raw 32-byte Unlock Key; biometrics, keystores, and passphrases stay in the consumers. Envelope encryption over ChaCha20-Poly1305 makes rekeying cheap, one seal covers the whole file with the background-sync trade-off delegated to the consumer's keystore policy, sealing is opt-in and sticky with a deliberate unseal, and the CLI derives its key with in-tree PBKDF2 behind a KDF identifier. Nym state stays ephemeral by pinned invariant, and the log is starved of secrets rather than encrypted. The scheme adds no dependencies. This commit records the decisions only: ADR 0012, five glossary terms in the zingolib CONTEXT (Sealed Wallet, Unlock Key, Unlock, Seal / Unseal, Rekey), and the .agent-plans stream declaration. Implementation is deliberately deferred and tracked in issue #2494. The ADR numbering leaves 0009-0011 to the in-flight nym stream, which this ADR references. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zancas
added a commit
that referenced
this pull request
Jul 23, 2026
The Pool Activation ADR declares a second pool-to-upgrade mapping anywhere in the workspace a defect, and add_shard_ranges still carried one: a three-arm ShieldedPool ladder over raw activation_height calls. It and the sapling-era boundary lookup in final_tree_sizes now derive through PoolActivation::of, which removes the last NetworkUpgrade reference from the sync state module entirely. The ADR also moves from 0012 to 0014, with its two code references updated: 0010 through 0013 are claimed by in-flight branches (the OP_RETURN pipeline, the typed-error arc, the sealed-wallet draft in PR #2496, and the viewmodel extraction), so keeping 0012 would collide with whichever merges next. Co-Authored-By: Claude Fable 5 <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.
Closes nothing yet — this is the documentation half of #2494, which tracks the implementation. This PR preserves the complete sealed-wallet design so the plan survives independent of any session: ADR 0012, five glossary terms, and the
.agent-plansstream declaration. Implementation is deliberately deferred.The design being recorded
The goal is to protect all on-disk wallet data from any reader of the device's storage, with unlock delegated to the consumer (biometrics on mobile, a passphrase on the CLI). A grilling session on 2026-07-21 resolved every branch of the decision tree:
Trust boundary. zingolib defines the sealed wallet-file format and performs all encryption; its entire key interface is a raw 32-byte Unlock Key presented at open time. zingo-mobile guards that key with the platform keystore behind a biometric prompt; zingo-cli derives it from a passphrase. The library never sees a passphrase, a biometric, or a keystore. (Rejected: biometric APIs in the library; trusting OS file encryption alone.)
Envelope encryption. A random inner data-encryption key encrypts the wallet bytes with ChaCha20-Poly1305; the Unlock Key wraps only that inner key, so rekeying rewraps one blob without rewriting the wallet. After open, only the inner key stays in memory (
secrecy-held, zeroized on drop), letting the autosave task re-seal without the Unlock Key.One seal, no tier split. The seal covers the whole file. The background-sync trade-off is the consumer's key-release policy — a keystore key minted "available after first device unlock" preserves background sync; "require per-use authentication" trades it for strength. (Rejected: a two-tier sync/spend format, which doubles serialization, API, and save paths for a knob the keystore already provides.)
Opt-in, sticky, reversible. Plaintext remains legitimate; a sealed file carries a magic header with format version and KDF identifier, and anything else parses as legacy. Once sealed, every save re-seals and only an explicit unseal goes back. (Rejected: mandatory sealing; irreversible sealing — the seed restores anywhere, so irreversibility only bricks files.)
Four operations, no mid-session lock. Open,
seal(key),rekey(new_key),unseal(key). No locked-while-alive state: the zecwallet-era lock poisoned every method with a key-absent error path, and dropping the client reaches the same security state.CLI KDF. PBKDF2 from the in-tree crate (HMAC-SHA-512, hundreds of thousands of iterations, per-wallet salt in the header), honestly documented as weaker than argon2, which would need a dependency ratification; the header's KDF identifier permits a later argon2 format without a break. The mobile path never touches a KDF.
No new dependencies. Everything builds from crates already in
Cargo.lock:chacha20poly1305,blake2b_simd,getrandom,secrecy,zeroize,pbkdf2.The rest of the disk. The wallet file is the only artifact zingolib writes to the wallet directory. Nym mixnet state stays off disk (
new_ephemeral()), pinned as an invariant by the ADR. The debug log is starved of secrets rather than encrypted, and file logging stays off by default for connected use.Contents
docs/adr/0012-sealed-wallet-at-rest-encryption.md— the decision record with the rejected alternatives.zingolib/CONTEXT.md— glossary entries for Sealed Wallet, Unlock Key, Unlock, Seal / Unseal, and Rekey in the Persistence section..agent-plans/sealed-wallet.md— the stream declaration and file claims for the eventual implementation.Caveats for review
The ADR is numbered 0012 while
devholds ADRs through 0008: numbers 0009–0011 belong to the in-flight nym stream, which this ADR references (ADR 0011 and thenew_ephemeral()construction do not exist ondevyet). If the nym stream's docs land first this merges cleanly; if this merges first, the references are forward-looking until it does.🤖 Generated with Claude Code