feat(desktop): resolve Chromium cookie keys on Linux and Windows - #7261
feat(desktop): resolve Chromium cookie keys on Linux and Windows#7261juliusmarminge wants to merge 4 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Thread transfer impact
This comment will update automatically after the next completed run. |
1d228da to
6a2afea
Compare
There was a problem hiding this comment.
Reviewed the new ChromiumKeys module and its call sites against the Effect service conventions. Imports, Effect.fn.Return requirement typing, and dependency acquisition (yield* FileSystem.FileSystem, ChildProcessSpawner surfaced in the requirement channel and threaded through BrowserImport's captured context) all look right. Two error-modelling issues in ChromiumKeys.ts: an underlying failure is discarded instead of being preserved as cause, and the Windows DPAPI path can turn a real failure into a silently empty key reported under a misleading reason.
Posted via Macroscope — Effect Service Conventions
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. This PR enables Chromium cookie import on Linux and Windows, adding substantial new platform-specific credential access and decryption logic. The new capability, security-adjacent nature, and unresolved findings about potentially incomplete Linux imports and domain hash handling warrant human review. No code changes detected at You can customize Macroscope's approvability policy. Learn more. |
6a2afea to
65f945a
Compare
There was a problem hiding this comment.
One finding: a typed ChromiumKeyError is re-wrapped at a translation boundary in ChromiumKeys.ts, losing its reason and nesting the error inside itself.
Posted via Macroscope — Effect Service Conventions
65f945a to
0ad3689
Compare
| // reach the keyring is not fatal — it just leaves those records skipped. | ||
| const keyringSecret = | ||
| request.keychainService && request.keychainAccount | ||
| ? yield* readKeychainSecret(request.keychainService, request.keychainAccount).pipe( |
There was a problem hiding this comment.
🟠 High BrowserImport/ChromiumKeys.ts:232
Linux imports silently omit every v11 cookie when Chromium stores its OSCrypt secret in Secret Service, making the import incomplete. readKeychainSecret uses Keyring.Entry(service, account) with the macOS-style coordinates, but Linux Chromium stores the secret under its custom Secret Service schema (for example, chrome_libsecret_os_crypt_password_v2 with an application attribute); the lookup fails and Effect.orElseSucceed suppresses the error. Use the Linux-specific Secret Service lookup and derive cbcV11 from that result.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts around line 232:
Linux imports silently omit every `v11` cookie when Chromium stores its OSCrypt secret in Secret Service, making the import incomplete. `readKeychainSecret` uses `Keyring.Entry(service, account)` with the macOS-style coordinates, but Linux Chromium stores the secret under its custom Secret Service schema (for example, `chrome_libsecret_os_crypt_password_v2` with an `application` attribute); the lookup fails and `Effect.orElseSucceed` suppresses the error. Use the Linux-specific Secret Service lookup and derive `cbcV11` from that result.
0ad3689 to
032f5da
Compare
There was a problem hiding this comment.
Reviewed the new ChromiumKeys module and its call sites in ChromiumCookies/BrowserImport against the Effect service conventions. Two findings on error modelling in ChromiumKeys.ts; the layer/import/dependency-acquisition side looks fine (resolveChromiumKeys takes FileSystem/ChildProcessSpawner from the environment and scopes the child process locally).
Posted via Macroscope — Effect Service Conventions
032f5da to
cfbd7b7
Compare
cfbd7b7 to
5896abd
Compare
2f2f7d0 to
5733055
Compare
5733055 to
1961e3f
Compare
1961e3f to
92ef11f
Compare
92ef11f to
b1069cf
Compare
b1069cf to
c13f7fe
Compare
c13f7fe to
1be05ca
Compare
There was a problem hiding this comment.
One convention finding on the new ChromiumKeys module: the known-tag recovery uses catchIf with a schema predicate rather than Effect.catchTags. Everything else in the new key/decrypt split (error structure, structural exitCode/localStatePath context, preserved cause, requirements exposed through Effect.fn.Return) matches the conventions.
Posted via Macroscope — Effect Service Conventions
1be05ca to
7f41045
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON, but a cloud agent failed to start.
Reviewed by Cursor Bugbot for commit 7f41045. Configure here.
7f41045 to
8f80c3c
Compare
8f80c3c to
46ce064
Compare
46ce064 to
8f80c3c
Compare
8f80c3c to
4516dd9
Compare
Chromium calls this OSCrypt and implements it differently per platform, so key acquisition moves into its own module and the extractor picks a key per record from its prefix. Linux may keep a key in libsecret/kwallet (`v11` records) or fall back to a hardcoded `peanuts` passphrase when no keyring is available (`v10`). Both can appear in one database, so both are derived up front. Failing to reach the keyring is not fatal — those records are skipped and the rest still import, which is more useful than refusing the whole thing. Windows unwraps an AES-256-GCM key from `Local State` via DPAPI. Shelling out to PowerShell is acceptable here in a way it is not on macOS: DPAPI is transparent to any process running as the user, so there is no consent prompt to misattribute and no ACL entry to write against the wrong binary. App-bound encryption is reported, not fought. Chrome 127+ also keeps a key only the browser binary can unwrap, and its cookies carry a `v20` prefix. Those are skipped, and a profile with no legacy key at all reports `appBoundEncryption` so the UI can say why rather than failing obscurely. The decryption paths are covered by tests that encrypt exactly as each platform does — including the v127+ domain binding, a Linux database mixing `v10` and `v11`, a missing keyring degrading to a partial import, and `v20` being skipped rather than throwing. That matters because only the macOS path can be exercised on the machine this was written on. Verified unchanged against real macOS profiles: Helium 5,028 cookies across 756 domains, Chrome 54 across 17. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A failed DPAPI `Unprotect` still exits and prints nothing, and the empty stdout decoded to a zero-length Buffer. That buffer is truthy, so `gcmV10` was populated with an unusable key: every record failed to decrypt and the import reported success having written nothing. The exit code and the key length are both checked now. `readLocalState` turned a missing, unreadable or corrupt `Local State` into an empty document, which the caller then reported as `keychainItemMissing` — the one diagnosis that sends the user to sign into the browser again, when the real problem is the file. Those failures now surface as `readFailed` with the cause attached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three ways the key error misdescribed itself: A failed `Unprotect` was reported as `keychainItemMissing` — the reason whose copy tells the user the browser has no key — with the exit status smuggled in as a synthesized `Error`. The status is stable domain context, so it is a structural attribute now and the reason is `readFailed`. An unreadable or corrupt `Local State` carried no path, so it surfaced against the cookie database the caller names rather than the metadata file that actually failed. The DPAPI call re-wrapped an existing `ChromiumKeyError` inside a second one with a fixed reason, discarding the original reason and cause chain, and labelling a missing or unlaunchable `powershell.exe` as a missing key. The structured error passes through; only genuinely unknown failures are wrapped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`resolveChromiumKeys` recovered a statically known `PlatformError` with `catchIf` and a negated schema predicate; catching the tag says the same thing directly and removes the module-private predicate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4516dd9 to
cf8dfa2
Compare

Chromium calls its cookie encryption OSCrypt and implements it differently per platform. Key acquisition moves into its own module, and the extractor now picks a key per record from its prefix.
v10AES-128-CBCv10AES-128-CBCpeanutspassphrasev11AES-128-CBCv10AES-256-GCMLocal Statev20Notes for review
A database can mix schemes, so both Linux keys are derived up front and records we hold no key for are skipped. Failing to reach the keyring is not fatal — those
v11records are skipped and the rest still import. A partial result reported honestly beats refusing everything.Shelling out to PowerShell for DPAPI is acceptable here in a way it is not on macOS: DPAPI is transparent to any process running as the user, so there's no consent prompt to misattribute and no ACL entry to write against the wrong binary.
App-bound encryption is reported, not fought. Chrome 127+ keeps a key only the browser binary can unwrap; its cookies carry a
v20prefix. Those are skipped, and a profile with no legacy key at all surfacesappBoundEncryptionso the UI can say why instead of failing obscurely.Testing
Only the macOS path can be exercised on the machine this was written on, so the decryption paths are covered by tests that encrypt exactly as each platform does — the v127+ domain binding, a Linux database mixing
v10andv11, a missing keyring degrading to a partial import, Windows AES-GCM, andv20being skipped rather than throwing.Real macOS profiles still decrypt unchanged after the refactor: Helium 5,028 cookies across 756 domains, Chrome 54 across 17.
Not verified on real Windows or Linux hardware. The formats and key derivations follow Chromium's documented behaviour and are unit-tested against synthesised ciphertext, but neither DPAPI unwrapping nor a real Secret Service lookup has been run end to end. Worth a check on those platforms before relying on it.
Note
Add Chromium cookie key resolution for Linux and Windows in browser import
'peanuts'with optional Keyring upgrade, and Windows unwraps the Local Stateencrypted_keyvia DPAPI (PowerShell subprocess).decryptChromiumValuein ChromiumCookies.ts to route per-record decryption across CBC (v10/v11) and AES-GCM (v10) schemes; records that cannot be decrypted are counted and skipped rather than aborting the import.unavailableReason, so Chromium sources are now available on all platforms unless the browser is not installed or is currently running.appBoundEncryptionas a newBrowserImportUnavailableReasonwith user-facing copy for Windows Chrome v127+ app-bound keys that cannot be read by third-party apps.ProtectedData.Unprotect) at import time; failure maps toreadFailedrather than a partial result.Macroscope summarized cf8dfa2.
Note
Medium Risk
Touches cookie decryption and spawns PowerShell for DPAPI at import time; wrong key handling could silently skip cookies, though empty DPAPI results are guarded and undecryptable rows are counted.
Overview
Chromium browser import is no longer macOS-only: key resolution moves into
ChromiumKeys, and cookies decrypt per record fromv10/v11/v20prefixes instead of a single macOS-only path.Linux derives
v10from Chromium’speanutsfallback and optionallyv11from libsecret/kwallet; a missing keyring yields a partial import, not a hard failure. Windows readsLocal State, unwraps the legacy key via DPAPI (PowerShell), and decrypts AES-GCMv10records.v20app-bound cookies are skipped; profiles with only an app-bound key surfaceappBoundEncryptionin contracts with user-facing copy.BrowserImportdrops the Chromium-on-non-macOS gate, wireslocalStatePathandChildProcessSpawner, and expands tests for cross-platform decryption and DPAPI marker handling.Reviewed by Cursor Bugbot for commit cf8dfa2. Bugbot is set up for automated code reviews on this repo. Configure here.