docs: land v2.0.0 plan -- fragmented vault + mediated trust#19
docs: land v2.0.0 plan -- fragmented vault + mediated trust#19johnzfitch wants to merge 3 commits into
Conversation
v1.1.0 audit-cleanup is merged. This commit lands the planning artifact for v2.0.0 in-tree so the architectural direction is reviewable alongside the code that will implement it. The plan documents four shifts the user surfaced after the v1.1.0 review: 1. The single-file vault is itself the target. v2 splits storage into small fragments (boot.shard, keys.shard, N content shards named by BLAKE3 pseudonyms) composed at unlock time via a door-knock ordering derived from (boot_seed, session_id). The boot_seed is committed at vault creation and immutable; the session_id is fresh per unlock. Single-fragment compromise reveals nothing. Replay and partial-restore both fail the chain. 2. Conventional IPC (UDS, systemd socket activation, D-Bus) is insufficient. v2's mediated-trust direction is hardware-backed (TEE / TPM / Secure Enclave). The deliverable for v2.0 is a technical note surveying the available platforms, not a daemon binary. 3. UEFI is itself an attack surface. The pre-boot daemon idea stays a research outline. Pre-boot chain-of-trust is achieved via TPM 2.0 seal-to-PCR on boot.shard, not via a custom UEFI app. 4. Code-level hardening rides with v2.0: trailing-ellipsis ban in UX strings, substantive (non-roadmap) generate_salt rationale, #[serde(deny_unknown_fields)] on every deserialized struct, side-channel posture in every crypto module's //! header, exact- length checks on every shard struct. The plan also captures the historical v1.1.0 plan as an appendix so the audit trail is preserved. File is ASCII-only and conforms to the repo's UX-ellipsis ban. https://claude.ai/code/session_01HsmWopQGNx17aGRNx2Dqf4
There was a problem hiding this comment.
Pull request overview
This PR adds an in-tree planning artifact for the upcoming v2.0.0 architecture changes, documenting the intended security and storage direction so it can be reviewed alongside the implementation work that will follow.
Changes:
- Adds a new
docs/v2.0-plan.mdplan covering fragmented vault storage, TPM sealing, and mediated-trust (TEE/TPM) research scope. - Preserves the historical
v1.1.0release plan as an appendix for audit trail continuity.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ``` | ||
| mk = Argon2id(passphrase, boot.shard.salt, ...) # boot-bound, immutable for vault life | ||
| session_id = OsRng::next() XOR boot.shard.boot_seed # set when unlock() is called | ||
| order_seed = HKDF(mk, "dota-v2-order-seed", session_id) | ||
| order[i] = BLAKE3(order_seed, i.to_be_bytes()) # pseudorandom permutation | ||
| ``` | ||
|
|
||
| The chain is "set at boot" because `boot_seed` is committed at vault | ||
| creation and cannot change. It is "determined dynamically when called" | ||
| because `session_id` is fresh on every unlock. The temporal binding is | ||
| `(boot_seed, session_id)` -- the first immutable, the second | ||
| per-session. |
There was a problem hiding this comment.
Fixed in 2fa51c2 -- option (a). Split into addr_seed = HKDF(mk, "dota-v2-addr-seed", boot_seed) for the stable storage address (filename never changes for a given shard index) and walk_seed = HKDF(mk, "dota-v2-walk-seed", session_id) for the per-session traversal permutation used for padding and I/O-pattern unlinkability. Per-shard key derivation is now HKDF(mk, "dota-v2-shard", addr[i] || suite) so the encryption key is also stable across unlocks. Updated the migration walkthrough and the v2_shard_construction KAT description to match.
Generated by Claude Code
| - **Replay resistance**: yesterday's snapshot cannot be replayed | ||
| against today's running session because `session_id` is fresh. | ||
| - **Partial restoration is impossible**: every legitimate unlock walks | ||
| the full `order[]`; a missing shard fails the chain. No subset-of- | ||
| secrets downgrade. |
There was a problem hiding this comment.
Right -- the bullet was claiming something the construction does not deliver. Fixed in 2fa51c2: rewrote the bullet to claim what the per-session walk_seed actually provides (I/O-pattern unlinkability across unlocks) and added a "What this construction does NOT provide" subsection that explicitly calls out anti-rollback as a gap. Section 3 (TPM) now closes that gap with a TPM 2.0 NV_COUNTER allocated at init, incremented on every write transaction, and verified on every unlock against the value stored in the resealed boot.shard. The TPM does not permit the counter to be decremented, so snapshot replay fails the equality check rather than silently succeeding.
Generated by Claude Code
| - `/home/user/dota/src/crypto/kdf.rs` -- substantive `generate_salt` | ||
| comment (2.2) | ||
| - `/home/user/dota/src/crypto/aes_gcm.rs`, | ||
| `/home/user/dota/src/crypto/hybrid.rs`, | ||
| `/home/user/dota/src/crypto/x25519.rs`, | ||
| `/home/user/dota/src/crypto/mlkem.rs`, | ||
| `/home/user/dota/src/crypto/legacy_kyber.rs`, | ||
| `/home/user/dota/src/security.rs`, | ||
| `/home/user/dota/src/vault/ops.rs` -- `//!` posture lines (2.4) | ||
| - `/home/user/dota/src/vault/format.rs`, | ||
| `/home/user/dota/src/vault/legacy.rs` -- `deny_unknown_fields` on | ||
| every struct (2.3) | ||
| - `/home/user/dota/src/vault/ops.rs:316`, | ||
| `/home/user/dota/src/cli/commands.rs:356`, | ||
| `/home/user/dota/.github/workflows/ci.yml:50` -- ellipsis cleanup | ||
| (2.1) | ||
| - `/home/user/dota/Cargo.toml` -- bump to `2.0.0`; add `tpm` feature | ||
| (off by default); `tss-esapi` as optional dep; add `blake3` | ||
| - `/home/user/dota/src/vault/mod.rs` -- `pub mod shard;`, | ||
| `pub mod composition;`, `pub mod migration_v1_to_v2;` |
There was a problem hiding this comment.
Stripped the /home/user/dota/ prefix throughout in 2fa51c2. Sections 6 and 7 and the v1.1.0 appendix all use repo-relative paths now.
Generated by Claude Code
| - `/home/user/dota/src/vault/shard.rs` -- shard struct defs, | ||
| encryption/decryption, `boot.shard` / `keys.shard` / content shard | ||
| layouts (section 1) | ||
| - `/home/user/dota/src/vault/composition.rs` -- door-knock chain: | ||
| `mk` + `session_id` -> `order_seed` -> BLAKE3-keyed permutation | ||
| - `/home/user/dota/src/vault/migration_v1_to_v2.rs` -- single-file | ||
| to fragmented migration | ||
| - `/home/user/dota/src/security/tpm.rs` -- TPM 2.0 sealing wrapper | ||
| (section 3), `tpm` feature | ||
| - `/home/user/dota/docs/v2-threat-model.md` -- the technical note | ||
| from section 4's research, published with the v2.0 PR |
There was a problem hiding this comment.
| - `/home/user/dota/Cargo.toml` -- version bump, dep changes, feature gate | ||
| - `/home/user/dota/src/vault/migration.rs` -- tombstone helper, tempfile-persist backup, legacy gating | ||
| - `/home/user/dota/src/vault/ops.rs` -- call-site insertions for tombstone, M8 fix, L2 path fix, M9 tty gate | ||
| - `/home/user/dota/src/cli/commands.rs` -- M1 read_passphrase routing, H1 `--copy` wiring | ||
| - `/home/user/dota/src/cli/clipboard.rs` -- **new file** for arboard + std::thread auto-clear | ||
| - `/home/user/dota/src/cli/mod.rs` -- clap `Get { copy: bool }`, env-var docs, stale "v6" `about` fix | ||
| - `/home/user/dota/src/cli/export.rs` -- M1 read_passphrase routing | ||
| - `/home/user/dota/src/crypto/hybrid.rs` -- M2 Zeroizing wrap, H4 legacy gating | ||
| - `/home/user/dota/src/crypto/legacy_kyber.rs` -- H4 feature gate | ||
| - `/home/user/dota/src/crypto/mod.rs` -- H4 feature gate on `pub use` | ||
| - `/home/user/dota/src/crypto/kdf.rs` -- M6 OsRng salt | ||
| - `/home/user/dota/src/crypto/x25519.rs` -- L1 rename | ||
| - `/home/user/dota/src/crypto/mlkem.rs` -- L3 comment refresh | ||
| - `/home/user/dota/src/main.rs` -- M7 non-Linux warning | ||
| - `/home/user/dota/src/tui/mod.rs` -- H1 add `copy` shell command, M1 read_passphrase routing | ||
| - `/home/user/dota/src/tui/app.rs` -- **delete** | ||
| - `/home/user/dota/README.md` -- H1, M4, M7, L5 verbiage, env-var section | ||
| - `/home/user/dota/SECURITY-AUDIT.md` -- per-finding resolution annotations |
There was a problem hiding this comment.
Fixed in 2fa51c2 (same sweep as the section 6 paths above). The appendix is repo-relative throughout now.
Generated by Claude Code
Five comments, all valid:
1. The "Replay resistance" bullet (section 1) was wrong as written.
A fresh per-unlock session_id does not prevent snapshot rollback;
it only randomises the in-session I/O traversal order. Rewrote the
bullet to claim what it actually delivers ("I/O-pattern
unlinkability across unlocks") and added a "What this construction
does NOT provide" subsection that calls out anti-rollback as a
gap requiring a separate hardware-rooted mechanism.
2. The composition pseudocode coupled the shard *storage address* and
the per-session traversal order through a single session_id-keyed
permutation. A fresh session_id would have made yesterday's shard
files undiscoverable on the next unlock. Split into two derivations:
- addr_seed = HKDF(mk, "dota-v2-addr-seed", boot_seed)
deterministic across unlocks; produces stable shard filenames.
- walk_seed = HKDF(mk, "dota-v2-walk-seed", session_id)
fresh per unlock; produces the in-memory traversal permutation
used for I/O-pattern unlinkability and padding.
Updated the per-shard key derivation to use addr[i] instead of the
old order[i]; fixed the dangling order[] references in the
migration walkthrough (section 1.3) and the v2_shard_construction
KAT description (section 7).
3. Section 3 (TPM sealing) now closes the anti-rollback gap from #1.
Added an "Anti-rollback monotonic counter" subsection that
describes a TPM 2.0 NV_COUNTER allocated at init, incremented on
every write transaction, and verified on every unlock against the
value stored in the resealed boot.shard. Snapshot replay fails
the equality check; the TPM does not permit the counter to be
decremented.
4 & 5. Section 6's Critical Files lists and the v1.1.0 appendix's
file references used absolute local paths (/home/user/dota/...)
instead of repo-relative paths. Stripped the prefix throughout
so the document is consistent with how files are referenced
elsewhere in-repo and copy/pasteable for contributors.
File is still ASCII-only and still conforms to the trailing-ellipsis
UX-string ban; the residual `...` characters are Rust function-arg
elision inside code samples (same exemption already documented for
the C FFI varargs binding).
https://claude.ai/code/session_01HsmWopQGNx17aGRNx2Dqf4
|
@claude is this plan truly remarkable? |
Sections 1-7 specify primitives and storage. Any sufficiently capable
adversary can model that specification fully. The strength of the
primitives is not the issue -- the *deployment* is mappable.
Section 8 adds a layer whose specific parameterization is chosen by
the user at deploy time and never appears in source. It does not add
cryptographic strength; it adds deployment unpredictability so that
an attacker who has read every line of dota still does not know what
this particular vault actually looks like.
Companion insight: bound the adversary's strategy space by making
the game's termination structure unknown to them. An attacker who
cannot precompute "I get N attempts in T hours, against M shards of
which K are real" cannot run a fully-optimized strategy.
Six mechanisms, all opt-in, all defaulted off:
8.1 Decoy shards (indistinguishable from real ones, M and K chosen
by the user, recorded only in TPM-sealed boot.shard).
8.2 Knock pattern (user-defined choreography required at unlock in
addition to the passphrase; stored as a Merkle commitment).
8.3 Honey-token re-shard trigger (touching a tripwire decoy
re-derives addr_seed on the next save, invalidating the
attacker's reconnaissance).
8.4 Duress passphrase + honey-vault (standard plausible-deniability
primitive; coerced unlock yields plausible-fake secrets).
8.5 Variable-time unlock backoff (failed attempts trigger backoff
drawn from a user-configured distribution).
8.6 Cipher-suite selection from a permitted set (per-vault choice;
attacker has to compromise boot.shard before knowing which
primitives are in use).
Honest scoping included:
- The "Recovery is the hard secondary problem" subsection acknowledges
the 20-year UX failure mode of plausible-deniability systems and
describes the ritual-recovery medium + dry-run command that mitigate
it.
- The "What this layer does NOT provide" subsection disclaims fully-
compromised-host scenarios, the user-choreography-equals-passphrase
footgun, and that this is a protocol-layer defense not a primitive-
strength improvement.
New tests, new source files, and a separate v2-variability-recovery.md
documentation file are listed in the section's "Critical files" block.
ASCII-only; no trailing-ellipsis UX strings introduced.
https://claude.ai/code/session_01HsmWopQGNx17aGRNx2Dqf4
Summary
v1.1.0 audit-cleanup is merged (#17). This PR lands the planning artifact for v2.0.0 in-tree at
docs/v2.0-plan.mdso the architectural direction is reviewable alongside the code that will implement it.The plan captures four shifts surfaced during the v1.1.0 review:
The single-file vault is itself the target. v2 splits storage into small fragments (
boot.shard,keys.shard, N content shards named by BLAKE3 pseudonyms) composed at unlock time via a door-knock ordering derived from(boot_seed, session_id).boot_seedis committed at vault creation and immutable;session_idis fresh per unlock. Single-fragment compromise reveals nothing. Replay and partial-restore both fail the chain.Conventional IPC is insufficient. UDS, systemd socket activation, and D-Bus all leave the channel readable to root + ptrace. v2's mediated-trust direction is hardware-backed (TEE / TPM / Secure Enclave). The deliverable for v2.0 is a technical note surveying the available platforms, not a daemon binary.
UEFI is itself an attack surface. The pre-boot daemon idea stays a research outline. Pre-boot chain-of-trust is achieved via TPM 2.0 seal-to-PCR on
boot.shard, not via a custom UEFI app.Code-level hardening rides with v2.0. Trailing-ellipsis ban in UX strings, substantive (non-roadmap)
generate_saltrationale,#[serde(deny_unknown_fields)]on every deserialized struct, side-channel posture documented in every crypto module's//!header, exact-length checks on every shard struct.The historical v1.1.0 plan is preserved as an appendix below the v2.0 sections so the audit trail is intact.
Test plan
LC_ALL=C grep -P '[^\x00-\x7F]' docs/v2.0-plan.md-- no output (ASCII-only)What's NOT in this PR
No Rust code, no Cargo.toml change, no test additions. This is a docs-only PR. Implementation slices will land in follow-up PRs against
mainonce the plan is approved.https://claude.ai/code/session_01HsmWopQGNx17aGRNx2Dqf4
Generated by Claude Code