Why
vault.bin is AEAD-encrypted, so an attacker with filesystem write can't forge a new valid vault — but they can replace it with an older, genuine copy of the user's own vault (malware as the same user, a careless backup/restore, a sync conflict). The stale vault opens cleanly under the right passphrase and looks normal. For an agent wallet this is the dangerous case: a rolled-back vault can resurrect old state the user thought they'd moved past. Today nothing detects this — a grep finds zero OS-keychain use in deckard-core / deckard-signerd. This is the same "filesystem-write" attacker THREAT-MODEL.md already names but doesn't yet address.
What we're protecting
Only the encrypted vault, against replay of an old genuine snapshot (not forgery — the AEAD already blocks that). The mechanism: a monotonic epoch bumped on every save, stored both in the vault header and mirrored to an anchor in a different trust domain (the OS keychain). On unlock, a file epoch behind the anchor = rollback → refuse or warn.
Spike questions to answer
- Anchor crate. Candidates from a quick scan:
keyring (cross-platform Keychain / Credential Manager / Secret Service — widely used; on Linux pulls dbus, statically linkable via vendored), or keyring-core + a store. What's the real new-dep cost across macOS/Linux, and does it survive the no-new-deps DoD bar (needs explicit approval)?
- The two correctness details. (1) Bind
epoch into the AEAD associated data so an attacker can't roll the file back and edit the plaintext epoch to match. (2) Check the anchor only after the passphrase verifies, so a wrong-password caller can't probe "is there a wallet here."
- Legitimate restore-from-backup. Define the accept path when the file is genuinely older than the anchor on purpose (new machine, anchor missing/reset) so we don't brick real restores.
- Honest residual. The anchor is itself deletable/rewritable by a privileged same-uid attacker — document that this raises the bar via a separate, hardware-backable trust domain, not an absolute (
THREAT-MODEL.md severity-context style).
Likely shape (inspiration, not a spec)
~100 LOC plus the anchor crate; the DEK envelope in keystore.rs is the natural seam for the epoch, and the unlock path in daemon.rs is where the check lands. Don't over-build — the spike decides whether it's this small.
Out of scope
policy.json integrity — it's plaintext/unauthenticated today, a different and larger gap (direct tampering, not just rollback). Tracked separately; do not fold it in here.
- Touch ID / biometric unlock — separate effort. (Non-binding note: if the anchor ends up on
keyring's apple-native path, it already pulls security-framework, the macOS biometric hook — so this needn't foreclose that later. The spike doesn't owe a decision on it.)
Done when
A short design note or ADR: chosen anchor + new-dep cost, the AAD-binding and check-ordering decisions, the restore-from-backup path, and the documented residual — or a recorded decision to defer with concrete blockers.
Files
crates/deckard-core/src/keystore.rs, crates/deckard-core/src/config.rs, crates/deckard-signerd/src/daemon.rs (unlock path).
Grounding: THREAT-MODEL.md.
Why
vault.binis AEAD-encrypted, so an attacker with filesystem write can't forge a new valid vault — but they can replace it with an older, genuine copy of the user's own vault (malware as the same user, a careless backup/restore, a sync conflict). The stale vault opens cleanly under the right passphrase and looks normal. For an agent wallet this is the dangerous case: a rolled-back vault can resurrect old state the user thought they'd moved past. Today nothing detects this — a grep finds zero OS-keychain use indeckard-core/deckard-signerd. This is the same "filesystem-write" attackerTHREAT-MODEL.mdalready names but doesn't yet address.What we're protecting
Only the encrypted vault, against replay of an old genuine snapshot (not forgery — the AEAD already blocks that). The mechanism: a monotonic
epochbumped on every save, stored both in the vault header and mirrored to an anchor in a different trust domain (the OS keychain). On unlock, a file epoch behind the anchor = rollback → refuse or warn.Spike questions to answer
keyring(cross-platform Keychain / Credential Manager / Secret Service — widely used; on Linux pulls dbus, statically linkable viavendored), orkeyring-core+ a store. What's the real new-dep cost across macOS/Linux, and does it survive the no-new-deps DoD bar (needs explicit approval)?epochinto the AEAD associated data so an attacker can't roll the file back and edit the plaintext epoch to match. (2) Check the anchor only after the passphrase verifies, so a wrong-password caller can't probe "is there a wallet here."THREAT-MODEL.mdseverity-context style).Likely shape (inspiration, not a spec)
~100 LOC plus the anchor crate; the DEK envelope in
keystore.rsis the natural seam for the epoch, and the unlock path indaemon.rsis where the check lands. Don't over-build — the spike decides whether it's this small.Out of scope
policy.jsonintegrity — it's plaintext/unauthenticated today, a different and larger gap (direct tampering, not just rollback). Tracked separately; do not fold it in here.keyring'sapple-nativepath, it already pullssecurity-framework, the macOS biometric hook — so this needn't foreclose that later. The spike doesn't owe a decision on it.)Done when
A short design note or ADR: chosen anchor + new-dep cost, the AAD-binding and check-ordering decisions, the restore-from-backup path, and the documented residual — or a recorded decision to defer with concrete blockers.
Files
crates/deckard-core/src/keystore.rs,crates/deckard-core/src/config.rs,crates/deckard-signerd/src/daemon.rs(unlock path).Grounding:
THREAT-MODEL.md.