[Bug]: Orphaned safeStorage key makes connection-catalog.json permanently undecryptable, blocking all target listing on Windows
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/desktop
Steps to reproduce
The trigger is an Electron userData directory that gets recreated while ~/.t3/userdata survives. On Windows that happens on reinstall / profile reset / roaming-dir cleanup.
- Use the desktop app so at least one connection is saved.
connection-catalog.jsonis written to%USERPROFILE%\.t3\userdata\, encrypted viasafeStorage. - Delete or let something recreate
%APPDATA%\t3code\(the ElectronuserDatadir), leaving%USERPROFILE%\.t3\userdata\intact. - Launch the app. Electron generates a fresh random
os_crypt.encrypted_keyin the newLocal State. - Open Settings → Connections, or try to add/select an environment.
Expected behavior
An undecryptable catalog should degrade gracefully — treat it as "no saved connections", log a warning, and let the next write re-key the file with the current safeStorage key. Losing saved connection entries is acceptable; being unable to reach the connections UI at all is not.
Actual behavior
The connections UI is dead and no environment can be added or listed:
Could not list targets: ConnectionTransientError: Could not load the local connection catalog:
Error: Error invoking remote method 'desktop:get-connection-catalog':
DesktopConnectionCatalogStoreProtectionError: Desktop connection catalog protection failed
during decrypt-catalog at C:\Users\<user>\.t3\userdata\connection-catalog.json.
Labelled ConnectionTransientError, but it is permanent — every launch fails identically until the file is deleted by hand.
In app.asar, connectionCatalogStore.get maps any safeStorage.decryptString failure straight into a fatal DesktopConnectionCatalogStoreProtectionError, which propagates out of desktop.bootstrap. There is no fallback branch, unlike the success path that returns Option.some(decrypted):
safeStorage.decryptString(encryptedCatalog).pipe(
Effect.mapError(
(cause) =>
new DesktopConnectionCatalogStoreProtectionError({
operation: "decrypt-catalog",
catalogPath,
cause,
}),
),
)
Root cause (verified on my machine)
The ciphertext was encrypted with an AES key that no longer exists. DPAPI itself is healthy — this is not a Windows credential problem.
- Inner cause chain from
desktop.trace.ndjson:DesktopConnectionCatalogStoreProtectionError→ElectronSafeStorageDecryptError→Error while decrypting the ciphertext provided to safeStorage.decryptString. encryptedCatalogbase64-decodes to av10-prefixed blob (622 bytes:v10+ 12-byte nonce + 607 bytes ciphertext+tag), i.e. Chromium AES-256-GCM. The key lives in%APPDATA%\t3code\Local State→os_crypt.encrypted_key, DPAPI-wrapped.- I unwrapped that key manually with
ProtectedData.Unprotect(..., CurrentUser):prefix=DPAPI blobLen=283, unprotect OK, 32-byte key. So DPAPI works and the current key is intact — it is simply a different key than the one used at encrypt time. - Timestamps confirm it.
connection-catalog.jsonwas written 2026-07-11 15:22. The entire%APPDATA%\t3codeprofile —Local State,.updaterId,Cache,GPUCache,Code Cache,Local Storage— is stamped 2026-08-08 00:17. The userData dir was recreated a month after the catalog was encrypted, generating a new random key and orphaning the file.
The underlying design issue: the catalog lives in ~/.t3/userdata but its key lives in %APPDATA%\t3code. Those two directories have independent lifecycles, so wiping either one alone leaves the app in a state it cannot recover from.
Suggested fixes
- Make
getrecover: ondecrypt-catalogfailure, fall back to an empty catalog, surface a non-fatal warning ("saved connections could not be decrypted and were reset"), and re-key on next write. Cheapest fix and it turns a hard block into a soft reset. - Optionally move the catalog into the Electron
userDatadir alongsideLocal State, or add a plaintext key-fingerprint field so a key mismatch is detectable and self-healing rather than an exception. - Reclassify — this surfaces as
ConnectionTransientError, which is misleading for a permanently unrecoverable state.
Impact
Blocks work completely
Version or commit
0.0.34 (Alpha)
Environment
Windows 11 Pro build 26200 (10.0.26200), WSL2 backend (Ubuntu 24.04.4 LTS, kernel 6.18.33.2-microsoft-standard-WSL2)
Logs or stack traces
DesktopConnectionCatalogStoreProtectionError: Desktop connection catalog protection failed during decrypt-catalog at C:\Users\<user>\.t3\userdata\connection-catalog.json.
at resources\app.asar\apps\desktop\dist-electron\main.cjs:15880:225
at desktop.connectionCatalogStore.get (main.cjs:15886:25)
at desktop.ipc.connectionCatalog.get (definition) (main.cjs:15905:25)
at desktop.ipc.method (main.cjs:720:122)
at desktop.ipc.invoke (main.cjs:669:68)
at desktop.ipc.registerInvoke (main.cjs:27741:13)
at desktop.ipc.installHandlers (main.cjs:28717:9)
at desktop.bootstrap (main.cjs:28725:23) {
[cause]: ElectronSafeStorageDecryptError: Electron safe storage failed to decrypt a string.
at catch (main.cjs:1349:22) {
[cause]: Error: Error while decrypting the ciphertext provided to safeStorage.decryptString.
at try (main.cjs:1348:36)
}
}
Workaround
Quit the app, delete (or rename) %USERPROFILE%\.t3\userdata\connection-catalog.json, relaunch, and re-add the connection. Saved connection entries are lost; clerk-tokens.json, secrets/, and state.sqlite are unaffected.
Related
- #4750 —
connection-catalog.jsonnon-atomic writes; a corrupt document bricks the app permanently. Same "unreadable catalog is fatal" failure mode, different cause (corruption vs. orphaned key). A recovery path ingetwould cover both. - #4991 — identical user-facing
Could not list targetsmessage, but fromDesktopConnectionCatalogStoreMigrationError/read-legacy-secretrather thandecrypt-catalog. - #5427 —
safeStorageunavailable on Linux breaking the connection catalog; adjacent, but that is "encryption unavailable" rather than "key rotated".
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by locating the desktop connectionCatalogStore.get implementation corresponding to the decrypt-catalog path described in the issue, then trace how its error reaches desktop.bootstrap and the connections UI. Reproduce the orphaned-key case on Windows or cover the decrypt failure path with the existing catalog tests if available. Done means an undecryptable catalog no longer blocks the connections UI, a warning is surfaced, and a later write can recover the catalog.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- desktop, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100