Skip to content

feat(desktop): resolve Chromium cookie keys on Linux and Windows - #7261

Open
juliusmarminge wants to merge 4 commits into
browser-import-more-sourcesfrom
browser-import-linux-windows
Open

feat(desktop): resolve Chromium cookie keys on Linux and Windows#7261
juliusmarminge wants to merge 4 commits into
browser-import-more-sourcesfrom
browser-import-linux-windows

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Aug 16, 2026

Copy link
Copy Markdown
Member

Stacked on #7260 — review that first; this PR's base is browser-import-more-sources.

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.

Platform Scheme Key source
macOS v10 AES-128-CBC login keychain (consent prompt)
Linux v10 AES-128-CBC hardcoded peanuts passphrase
Linux v11 AES-128-CBC libsecret / kwallet
Windows v10 AES-256-GCM DPAPI-wrapped key in Local State
Windows v20 app-bound — not readable, by design

Notes 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 v11 records 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 v20 prefix. Those are skipped, and a profile with no legacy key at all surfaces appBoundEncryption so 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 v10 and v11, a missing keyring degrading to a partial import, Windows AES-GCM, and v20 being 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

  • Introduces ChromiumKeys.ts with platform-specific key resolution: macOS uses Keychain, Linux derives a CBC key from 'peanuts' with optional Keyring upgrade, and Windows unwraps the Local State encrypted_key via DPAPI (PowerShell subprocess).
  • Adds decryptChromiumValue in 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.
  • Removes the macOS-only gate from unavailableReason, so Chromium sources are now available on all platforms unless the browser is not installed or is currently running.
  • Adds appBoundEncryption as a new BrowserImportUnavailableReason with user-facing copy for Windows Chrome v127+ app-bound keys that cannot be read by third-party apps.
  • Risk: Windows key resolution spawns a PowerShell process (ProtectedData.Unprotect) at import time; failure maps to readFailed rather 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 from v10 / v11 / v20 prefixes instead of a single macOS-only path.

Linux derives v10 from Chromium’s peanuts fallback and optionally v11 from libsecret/kwallet; a missing keyring yields a partial import, not a hard failure. Windows reads Local State, unwraps the legacy key via DPAPI (PowerShell), and decrypts AES-GCM v10 records. v20 app-bound cookies are skipped; profiles with only an app-bound key surface appBoundEncryption in contracts with user-facing copy.

BrowserImport drops the Chromium-on-non-macOS gate, wires localStatePath and ChildProcessSpawner, 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.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bc7ad5e5-07f1-4b51-8d95-715f5ad63f93

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Aug 16, 2026
@github-actions github-actions Bot added the size:L 100-499 changed lines (additions + deletions). label Aug 16, 2026
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts Outdated
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 46ce064.

This comment will update automatically after the next completed run.

@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 1d228da to 6a2afea Compare August 16, 2026 22:08
@github-actions github-actions Bot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 16, 2026
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts Outdated
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
@juliusmarminge
juliusmarminge marked this pull request as ready for review August 16, 2026 22:18
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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 cf8dfa2. Prior analysis still applies.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 6a2afea to 65f945a Compare August 16, 2026 22:53

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts Outdated
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 65f945a to 0ad3689 Compare August 16, 2026 22:58
// 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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.

Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 0ad3689 to 032f5da Compare August 16, 2026 23:10
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 16, 2026

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts Outdated
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 032f5da to cfbd7b7 Compare August 16, 2026 23:14
Comment thread apps/desktop/src/preview/BrowserImport/ChromiumCookies.ts Outdated
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from cfbd7b7 to 5896abd Compare August 16, 2026 23:22
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 2f2f7d0 to 5733055 Compare August 16, 2026 23:59
@github-actions github-actions Bot removed the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 17, 2026
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 5733055 to 1961e3f Compare August 17, 2026 01:01
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 1961e3f to 92ef11f Compare August 17, 2026 01:06
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 92ef11f to b1069cf Compare August 17, 2026 01:24
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from b1069cf to c13f7fe Compare August 17, 2026 01:45
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from c13f7fe to 1be05ca Compare August 17, 2026 02:06

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts Outdated
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 1be05ca to 7f41045 Compare August 17, 2026 02:41

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit 7f41045. Configure here.

Comment thread apps/desktop/src/preview/BrowserImport/ChromiumKeys.ts
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 7f41045 to 8f80c3c Compare August 17, 2026 08:23
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 8f80c3c to 46ce064 Compare August 17, 2026 08:26
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 46ce064 to 8f80c3c Compare August 17, 2026 08:35
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 8f80c3c to 4516dd9 Compare August 17, 2026 09:12
juliusmarminge and others added 4 commits August 17, 2026 12:00
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>
@juliusmarminge
juliusmarminge force-pushed the browser-import-linux-windows branch from 4516dd9 to cf8dfa2 Compare August 17, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant