Skip to content

feat(security): encrypt API keys at rest via safeStorage#629

Open
gabrielste1n wants to merge 3 commits intomainfrom
feat/safestorage-api-keys
Open

feat(security): encrypt API keys at rest via safeStorage#629
gabrielste1n wants to merge 3 commits intomainfrom
feat/safestorage-api-keys

Conversation

@gabrielste1n
Copy link
Copy Markdown
Collaborator

@gabrielste1n gabrielste1n commented Apr 20, 2026

Why

API keys (OpenAI, Anthropic, Gemini, Groq, Mistral, custom endpoints, AWS Bedrock, Azure OpenAI, GCP Vertex) have been sitting in plaintext — half in userData/.env, half in renderer localStorage. Anyone on the machine reading either file pulled usable secrets.

What changes

Secrets now live in per-key encrypted blobs under userData/secure-keys/ via Electron's safeStorage (Keychain on macOS, DPAPI on Windows, libsecret/kwallet on Linux). Non-secret preferences stay in .env where they belong. The renderer holds secrets only in Zustand memory and hydrates from main on startup.

On first launch after upgrade, any existing plaintext secrets migrate automatically — round-trip verified before .env is rewritten, with a sentinel file so partial failures are retried cleanly rather than silently half-completing. Stale localStorage copies self-heal on every boot.

Also removed a few long-standing apiKeyPreview debug-log leaks that printed the first 8 chars of secrets — effectively leaking the full key for short-prefix providers like Groq.

Compatibility

No IPC surface changes, no new deps, no UI changes. Existing users with plaintext .env secrets migrate transparently on first run. Linux systems without a configured keyring fall back to plaintext (matching Electron's own behavior) and log a warning; SECURITY.md will reflect that gap.

Closes #532.

Closes #532.

Migrates the 12 secret-class env vars (7 AI API keys + 5 AWS/Azure/Vertex
credentials) from plaintext .env / localStorage to per-key encrypted files
in userData/secure-keys/. Non-secret preferences (regions, endpoints,
hotkeys, flags) continue to live in .env.

Main process:
- EnvironmentManager.init() runs once post-whenReady. First launch migrates
  any existing plaintext secrets with round-trip verification before
  stripping .env; a sentinel file makes the migration idempotent and
  re-tryable on partial failure.
- _saveKey routes SECRET_KEYS through safeStorage.encryptString with atomic
  temp+rename writes; process.env is updated synchronously so in-process
  reads see the new value immediately.
- saveAllKeysToEnvFile excludes secrets from .env output whenever
  safeStorage.isEncryptionAvailable() is true; falls back to plaintext on
  Linux systems without a keyring (matches Electron's default behavior).
- Corrupt .enc files (e.g., keychain reset, userData copied between
  machines) log an error and leave process.env unset rather than crashing.

Renderer:
- Secret setters drop localStorage writes and debounce IPC saves at 250ms
  to avoid per-keystroke encryption + disk writes.
- initializeSettings hydrates all 12 secrets in parallel from the main
  process and removes stale plaintext copies from localStorage (self-
  healing on every startup).
- byokDetection reads Zustand state instead of localStorage; the
  cloudTranscriptionMode BYOK default is promoted to post-hydration.
- Removes apiKeyPreview debug log leaks in audioManager and ReasoningService.
- Replace 40-line switch for secret IPC savers with a simple lookup map.
- Inline single-use _rewriteEnvFileWithoutSecrets helper.
- Shorten explanatory comments to keep only the non-obvious WHYs.
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.

safeStorage claimed in SECURITY.md but not implemented — API keys stored in plaintext

1 participant