Skip to content

fix(desktop): store dev secrets in a 0o600 file instead of the keychain - #6727

Open
Maxwellimus wants to merge 2 commits into
mainfrom
maxwell/dev-file-secret-store
Open

fix(desktop): store dev secrets in a 0o600 file instead of the keychain#6727
Maxwellimus wants to merge 2 commits into
mainfrom
maxwell/dev-file-secret-store

Conversation

@Maxwellimus

Copy link
Copy Markdown
Contributor

Problem

Unsigned dev binaries get a new code identity on every rebuild, which invalidates the macOS keychain item's "Always Allow" ACL — so every just dev relaunch demanded the login password. Painful when an agent is iterating on the app.

The keychain held exactly one item per dev build (service buzz-desktop-dev, account secrets): a JSON blob with the identity nsec, one nsec per managed agent, and a migration sentinel. Nothing else.

Change

Debug builds now default SecretStore to a file backend: secrets.<service>.json (0o600, atomic tmp+rename writes) in the app-data dir, guarded by the existing per-service flock. Release builds are keyring-only by construction — the File variant is cfg(debug_assertions). BUZZ_DEV_USE_KEYCHAIN=1 opts a debug build back into the keychain.

Deliberately no migration from the old dev keychain item: dev keys are cheap to re-import, and a one-shot migration would live on as dead code. Every legacy-keychain path short-circuits in file mode — including the prod→dev agent-key copy, whose prod-keychain read would otherwise reintroduce the prompt. Old dev keychain items are never read again; clean up manually with security delete-generic-password -s buzz-desktop-dev -a secrets.

Also:

  • BUZZ_SHARE_IDENTITY reads the secrets file first (skipped when BUZZ_DEV_USE_KEYCHAIN=1 so a stale file identity is never exported)
  • in-app reset wipes the secrets file; verify_fully_wiped checks its absence
  • onboarding copy for local-file no longer claims the keychain "wasn't available"

Heads-up for existing devs

First launch after this lands shows the identity-recovery screen once (file store starts empty, migration marker exists). Re-import your dev nsec there or set BUZZ_PRIVATE_KEY. Managed agents mint fresh keys.

Verification

  • 18 new file-backend unit tests (roundtrip, 0o600 perms, atomic writes, corrupt-file fail-closed, legacy short-circuits, wipe; includes a CI-runnable port of the cross-process stale-cache race test). Full Tauri suite 2778 pass; desktop JS 5432 pass; clippy/fmt/tsc/biome clean.
  • Runtime: first boot generated an identity into the secrets file (0o600, no keychain item created); forced rebuild + relaunch loaded the same pubkey from the file with zero keychain access and zero prompts.

Known cosmetic: cargo check --no-default-features has dead-code warnings in secret_store.rs (mostly pre-existing; one new for the unused File payload). No CI config builds that combo.

🤖 Generated with Claude Code

Unsigned dev binaries get a new code identity on every rebuild, which
invalidates the keychain item's Always-Allow ACL and made macOS demand
the login password on every tauri dev relaunch.

Debug builds now default SecretStore to a file backend:
secrets.<service>.json (0o600, atomic tmp+rename writes) in the
app-data dir, guarded by the existing per-service flock. Release
builds are keyring-only by construction (the File variant is
cfg(debug_assertions)). BUZZ_DEV_USE_KEYCHAIN=1 opts a debug build
back into the keychain.

The file store deliberately starts empty — no migration from the old
buzz-desktop-dev keychain item (dev keys are cheap to re-import, and a
one-shot migration would live on as dead code). Every legacy-keychain
path short-circuits in file mode, including the prod->dev agent-key
copy, so the OS keychain is never touched. First launch after this
change shows the identity-recovery screen once; re-import the dev nsec
or use BUZZ_PRIVATE_KEY.

BUZZ_SHARE_IDENTITY now reads the secrets file first (skipped when
BUZZ_DEV_USE_KEYCHAIN=1 to avoid exporting a stale identity), and the
in-app reset wipes the file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
@Maxwellimus
Maxwellimus requested a review from a team as a code owner August 24, 2026 21:30
…ize ratchet

secret_store.rs (1307 lines pre-change) and app_state.rs (1055) are
inherited-oversize files the CI ratchet forbids growing. Move the
interprocess lock to secret_store_lock.rs, the debug file backend to
secret_store_file.rs (each with their tests), and the migration-marker
helpers to app_state_keyring.rs where the marker naming already lives.
No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0o600 has to be applied at create (OpenOptions.mode), not chmod after write, or there's a window the file is 0644. also please drop the CONTRIBUTING.md hunk from this pr.

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.

2 participants