feat(desktop): retire keytar → safeStorage secret storage (dual-read migration)#772
Merged
Conversation
…allback Retires the unmaintained keytar native module as the primary secret store. Secrets now live as safeStorage ciphertext in an atomically written secure-secrets.json under userData, keyed by the exact per-site account strings (device suffixes, Google per-account token kinds, voice, pairing). Migration safety: - Dual-read at all five call sites: safeStorage store first, keytar fallback with the identical account string. - Per-key migrate-on-read: encrypt, persist, decrypt round-trip verify byte-identical, only then delete the keytar copy. Any verify failure keeps keytar authoritative. - The vault master key defers its keytar delete until the retrieved key has passed the vault verifier check (confirmMasterKeyMigrated). - Writes gate on safeStorage.isEncryptionAvailable() evaluated only after app ready; Linux basic_text (plaintext) backend refuses migration. - Idempotent and crash-resumable: a crash between persist and delete is cleaned up lazily on the next run, only while both copies match. - Plain-dev worktrees keep keytar authoritative: they share one master key through the machine-global OS keychain while userData is per-worktree, so migrating would strand the other worktrees. keytar stays a dependency, electron-rebuild target, and read-only fallback for at least one release; removal is gated on field telemetry.
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
h4yfans
marked this pull request as ready for review
July 17, 2026 20:43
This was referenced Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Retires the unmaintained
keytarnative module as the primary secret store. All five secret call sites (vault master key + sync keys, Google Calendar tokens, voice transcription API key, local agent provider API key, capture pairing token) now store secrets as ElectronsafeStorageciphertext in an atomically writtensecure-secrets.jsonunder userData, keyed by the exact account strings the OS keychain used.Closes #766
Why
keytaris archived/unmaintained and is a native-module liability on every Electron/ABI bump.safeStorageships with Electron, needs no native rebuild, and uses the same OS-level protection (Keychain on macOS, DPAPI on Windows, keyring-backed encryption on Linux).Migration safety design
This is a live-beta production app — a botched migration means permanent vault-key loss. The design is defensive throughout:
keytar.getPasswordfallback with the identical account string. Every existing account-suffix scheme is preserved (MEMRY_DEVICE per device, Google per-accountId + token kind, voice, pairing-token unsuffixed).keytar.deletePassword. Any verification failure keeps keytar authoritative, removes the bad ciphertext, and logs.confirmMasterKeyMigratedruns ingetOrInitializeLocalVaultKeyafterbindOrVerifyVaultKeysucceeds).safeStorage.isEncryptionAvailable()evaluated only after appready. On Linux, the plaintextbasic_textbackend refuses migration entirely (logged) — no silent security downgrade..corrupt-<ts>) instead of clobbered, with the keytar fallback still serving reads.devmaster key machine-globally while userData is per-worktree; migrating would strand the shared key for other worktrees (would regress fix(crypto): stabilize dev keychain master-key account across worktrees #758).Test plan
42 new unit tests (
src/main/secrets/), all with electron + keytar fully mocked (never touching the real OS keychain):isEncryptionAvailable() === false→ no migration, keytar reads/writes still workbasic_textbackend → refusalconfirmMasterKeyMigrated; vault-verifier reject → keytar copy survivesdev-<hash>→devnormalization and the unsuffixed pairing token)Full desktop suite: 11010 passed.
pnpm lint,pnpm typecheck,pnpm check:architecture,pnpm check:contracts,pnpm docs:buildall green. No contracts/preload changes, so no IPC regeneration needed.Rollout note
Merge should wait until #765 (Electron 43 upgrade) is confirmed stable in the field — this change deliberately rides on the post-upgrade safeStorage behavior and should not compound risk in the same release window.