Skip to content

[Deferred] Spike: rollback-protect the vault with a monotonic epoch (→ ADR 0004) #71

Description

@hellno

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecuritySecurity-relevant

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions