Skip to content

feat(vault): encrypted-cache persistence + offline unlock#10

Merged
UnbreakableMJ merged 2 commits into
mainfrom
cache-persistence
Jun 14, 2026
Merged

feat(vault): encrypted-cache persistence + offline unlock#10
UnbreakableMJ merged 2 commits into
mainfrom
cache-persistence

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

Summary

Wires up vault-store (fully built but never used) so the agent persists its vault to disk and can unlock without the network. This is the substrate for the upcoming PIN unlock (PR B), and valuable on its own: unlock now survives a restart and works offline once you've unlocked online at least once.

Decided with the user: split the PIN work into PR A (this) — cache persistence + offline unlock and PR B — PIN on top. A cache/offline session is read-only; sync/edits need a live token.

Persistence

On an online unlock (and every sync), the agent writes $XDG_DATA_HOME/vault/<account>/cache.json (<account> = sanitized host_email):

  • the /sync response encrypted under the user key (payload),
  • protected_user_key — the login token Key (the user key encrypted under the master-stretched key, safe at rest),
  • the account kdf params.

VaultCache is now schema 2; the new fields are serde-defaulted so any older file still loads.

Offline unlock

When a live login fails with a network error and a cache exists, perform_unlock falls back to unlock_from_cache: re-derive the master key locally from the cached KDF params, decrypt protected_user_key (the EncString MAC check detects a wrong password → BadPassword), and load ciphers from the encrypted payload. Bad password / 2FA still propagate (no fallback).

Capability boundary

An offline session has no access token, so Vault.client is now Option and None for cache unlocks. sync / add / edit / remove return the new typed Error::Offline ("unlock again while online…", CLI exit code 11); status / list / get / copy / TUI browse work fully from cache. (Local mutations don't re-persist the cache yet — it can lag edits until the next sync; tracked.)

Tests

KdfParams serde round-trip; VaultCache protected-key + kdf round-trip and legacy-v1 load; a pure unlock_from_cache recovery + wrong-password rejection; account_dir_name sanitization; offline-session Error::Offline gating. 140 tests green, clippy -D warnings clean, cargo deny clean, both headless builds green. No new dependencies.

Next (PR B — PIN)

pin_protected_user_key in the envelope (user key under a PIN-derived key), vault pin set/disable/status, PIN unlock path, 5-attempt lockout → wipe pin key + require master password. Reuses everything here.

🤖 Generated with Claude Code

UnbreakableMJ and others added 2 commits June 15, 2026 01:54
Wire up vault-store (built but never used) so the agent persists its
vault to disk and can unlock without the network — the substrate for the
upcoming PIN unlock, and useful on its own (unlock survives restart /
works offline).

On an online unlock (and every sync) the agent writes
$XDG_DATA_HOME/vault/<account>/cache.json (<account> = sanitized
host_email): the /sync response encrypted under the user key, plus the
protected_user_key (the login token Key — the user key encrypted under
the master-stretched key, safe at rest) and the account kdf params.
VaultCache is now schema 2; the new fields are serde-defaulted so older
files still load.

Offline unlock: when a live login fails with a network error and a cache
exists, perform_unlock falls back to unlock_from_cache — re-derive the
master key locally from the cached KDF params, decrypt protected_user_key
(the EncString MAC check detects a wrong password -> BadPassword), and
load ciphers from the encrypted payload. Bad password / 2FA still
propagate (no fallback).

An offline session has no token, so Vault.client is now Option and is
None for cache unlocks; sync/add/edit/remove return the new typed
Error::Offline (CLI exit 11). Read paths work fully from cache. Local
mutations don't re-persist the cache yet (can lag until next sync) —
tracked.

KdfParams gains serde derives. No new dependencies (vault-store was
already a listed dep).

Tests: KdfParams serde round-trip; VaultCache protected-key+kdf
round-trip and legacy-v1 load; pure unlock_from_cache recovery +
wrong-password rejection; account_dir_name sanitization; offline-session
Error::Offline gating. 140 workspace tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI's clippy caught three lints a local run missed (running cargo build
right before clippy let it reuse fingerprints and skip re-linting the
crate): backtick `EncString` in a doc comment, use Option::map_or_else
for the cache-fallback match, and rename a too-similar senc/smac test
binding pair to stretch_enc/stretch_mac.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit b1779d4 into main Jun 14, 2026
8 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the cache-persistence branch June 14, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant