ADR 0004 — Security Hardening
Context
Section titled “Context”The architectural review on 2026-05-08 identified four security gaps:
- Encryption-at-rest was theatre. The renderer’s Dexie encryption key was generated on first use and stored in the IndexedDB metadata table — right next to the ciphertext. Anyone with disk access trivially decrypted. The “AES-256-GCM at rest” README claim was misleading.
- Soft validation in the request store.
useRequestStore.updateRequestcaughtvalidateRequestUpdateerrors and applied the partial update anyway with aconsole.error. The type system’s invariants were aspirational at runtime; downstream consumers (test scripts, codegen, exporters) could see malformedKeyValueshapes withenabled: undefined. - Source-level pattern blocklist in scriptExecutor. A regex blocked
eval,Function(,__proto__,constructor[,Object.prototypeon the user’s source string before QuickJS saw it. Inside the WASM sandbox, none of these are dangerous (no host bridge to escape). The regex provided no security but broke legitimate user code. - AWS SigV4 signed too early. The renderer’s
applyAuthHeaderssigned the request before the Worker proxy potentially mutated it (e.g., re-encoding form-data with a different boundary). Users hit crypticSignatureDoesNotMatcherrors.
Decision
Section titled “Decision”Four coordinated fixes:
flowchart TB K[KeyProvider interface] K --> E[EphemeralKeyProvider<br/>web default] K --> P[WebSessionPassphraseProvider<br/>opt-in PBKDF2] K --> S[ElectronSafeStorageKeyProvider<br/>OS keychain] S --> KC[(macOS Keychain /<br/>Windows Cred Mgr /<br/>libsecret)] classDef provider fill:#7b6ef622,stroke:#7b6ef6,color:#7b6ef6; classDef kc fill:#10b98122,stroke:#10b981,color:#10b981; class E,P,S provider; class KC kc;
-
KeyProviderinterface with three implementations (EphemeralKeyProvider,WebSessionPassphraseProvider,ElectronSafeStorageKeyProvider). The Electron path uses the existingelectronAPI.storeIPC, which is itselfsafeStorage-protected viaelectron/main/store-handler.ts. Net effect: Electron data at rest is encrypted with a key the user’s OS keychain holds. Web data at rest is encrypted with an ephemeral key (better than the prior TOFU theatre because the key never persists alongside the ciphertext); a future passphrase-prompt UI swaps toWebSessionPassphraseProviderfor cross-session persistence. -
updateRequesthard-fails on validation error. The action no longer applies invalid updates. Users see atoast.errorwith the validation message; aconsole.warncaptures the offending update for debugging. -
dangerousPatternsregex deleted. The QuickJS WASM runtime is the security boundary. New tests demonstrate the boundary by running scripts that containeval('40 + 2'),Function.prototype.bind, andobj.constructor.name— all execute correctly without affecting the host. -
auth-signerinshared/protocol/. AWS SigV4 (and other auth that requires wire-byte fidelity) signs INSIDEexecuteHttpProxy/executeHttpProxyStreaming, afterbuildRequestBodyconstructs the exact body the fetcher will send. The renderer no longer pre-signs SigV4 —RequestSpec.authis the contract. Bearer / Basic / API-key / OAuth2 still flow throughapplyAuthHeadersclient-side because they don’t depend on the body.
Consequences
Section titled “Consequences”Positive
- Electron encryption is now genuinely hardware-backed via
safeStorage(macOS Keychain, Windows Credential Manager, Linux libsecret). - The store’s type-system invariants are enforced at runtime; downstream consumers can trust
KeyValue.enabledis always a boolean. - User scripts using
Function.prototype.bind,obj.constructor.name,JSON.parserevivers, etc. work without rejection. - AWS SigV4 signs the actual body bytes the upstream receives —
SignatureDoesNotMatcherrors caused by Worker mutation are gone. - Web users see a “Desktop only” badge on fields that don’t apply (mTLS, SOCKS, etc.).
Negative
- Web users with existing encrypted data on first launch after this update may see decryption fail (returns null gracefully — the app stays functional with empty stores) until they re-enter the relevant data. This is a deliberate one-time migration cost; the prior in-metadata key was not actually secure.
- The session-ephemeral default for web means data does NOT persist across reloads. A passphrase-prompt UI (deferred to a follow-up) restores cross-session persistence with PBKDF2-derived keys.
- The
RequestSpec.authfield crosses the IPC / proxy boundary; the Electron Zod validator now has a recursiveAuthConfigSchemato validate it.
Alternatives considered
Section titled “Alternatives considered”- Keep the in-metadata encryption key for backward compat: Rejected — preserves the security theatre. The right move is an honest break.
- Use Electron’s
safeStoragefor the renderer key directly via a new IPC channel: Rejected — the existingelectronAPI.storeIPC is alreadysafeStorage-protected and exposesget/set/hasthat satisfies theSecureKeyIpccontract. Adding a parallel IPC would duplicate. - Keep SigV4 client-side and add a “verify signature didn’t change” check on the worker: Rejected — fragile, defers the real fix.
- Implement source-level allowlist instead of blocklist for scripts: Rejected — there’s no allowlist that captures legitimate patterns without false negatives. The sandbox is the right mechanism.
Out of scope (future plans)
Section titled “Out of scope (future plans)”- Web passphrase-prompt UI (wire
WebSessionPassphraseProviderinto the renderer). - Per-environment encryption (separate vaults per Environment) — wider rework.
- E2E encryption between desktop and a sync server — belongs to the deferred sync/collaboration roadmap.
- Detection of leaked secrets in script logs — separate feature.
References
Section titled “References”- Source:
docs/adr/0004-security-hardening.md - Architecture overview: Security model.
- Related: ADR 0001, ADR 0006, ADR 0007.