diff --git a/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs b/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs index 6b210ee0f3..40d8ff79a8 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs @@ -42,7 +42,7 @@ pub fn import_browser_cookies( provider_id: String, browser_type: String, ) -> Result, String> { - use codexbar::browser::cookies::{CookieError, CookieExtractor}; + use codexbar::browser::cookies::CookieExtractor; use codexbar::browser::detection::BrowserDetector; // Resolve the provider to get its cookie domain. @@ -66,10 +66,8 @@ pub fn import_browser_cookies( .ok_or_else(|| format!("Browser '{browser_type}' not found or not installed"))?; // Extract the cookie header. - let cookies = CookieExtractor::extract_for_domain(&browser, domain).map_err(|e| match e { - CookieError::Dpapi(msg) => format!("DPAPI error: {msg}"), - other => other.to_string(), - })?; + let cookies = CookieExtractor::extract_for_domain(&browser, domain) + .map_err(|e| e.user_message_for_browser(browser.browser_type))?; if cookies.is_empty() { return Err(format!( diff --git a/docs/COOKIES.md b/docs/COOKIES.md index 8193198450..804562d077 100644 --- a/docs/COOKIES.md +++ b/docs/COOKIES.md @@ -2,20 +2,21 @@ Win-CodexBar can extract browser cookies for providers that use web authentication (Claude, Cursor, Kimi, and others). This is the Windows rewrite of upstream cookie/Keychain concepts: **DPAPI + browser profiles**, not macOS Keychain prompts. - ## Supported Browsers | Browser | Encryption | Status | |---------|-----------|--------| -| Chrome | DPAPI + AES-256-GCM | ✅ Automatic | -| Edge | DPAPI + AES-256-GCM | ✅ Automatic | -| Brave | DPAPI + AES-256-GCM | ✅ Automatic | +| Chrome | DPAPI + AES-256-GCM; modern profiles may use Chromium ABE (`v20`) | ⚠️ Automatic only when the needed cookies are not App-Bound | +| Edge | DPAPI + AES-256-GCM; modern profiles may use Chromium ABE (`v20`) | ⚠️ Automatic only when the needed cookies are not App-Bound | +| Brave | DPAPI + AES-256-GCM; modern profiles may use Chromium ABE (`v20`) | ⚠️ Automatic only when the needed cookies are not App-Bound | | Firefox | Unencrypted SQLite | ✅ Automatic | +Chromium App-Bound Encryption (ABE) binds protected cookie keys to the browser installation. Win-CodexBar does not bypass that protection. If the selected Chromium profile stores the provider cookies as App-Bound `v20` values, automatic import cannot decrypt them with the normal user DPAPI key. Use a manual Cookie header or Firefox instead. Chromium browser choices remain available because older or unmigrated profiles can still contain readable DPAPI/AES-GCM cookies. + ## How It Works 1. CodexBar reads the browser's cookie database from its standard location -2. For Chromium-based browsers, cookies are encrypted with Windows DPAPI — CodexBar decrypts them using the current user's credentials +2. For Chromium-based browsers, CodexBar can decrypt legacy DPAPI/AES-GCM cookies using the current user's credentials; App-Bound `v20` cookies are intentionally not bypassed 3. Only cookies for enabled providers are extracted (e.g., `claude.ai`, `cursor.com`) 4. Cookies are stored in-memory and refreshed on each provider poll @@ -28,7 +29,7 @@ Win-CodexBar can extract browser cookies for providers that use web authenticati ## Manual Cookies -If automatic extraction fails (e.g., browser is locked, profile is encrypted, or running in WSL): +If automatic extraction fails (for example, Chromium App-Bound Encryption is active, the browser database cannot be read, or CodexBar is running in WSL): 1. Open your browser and navigate to the provider's website (e.g., `claude.ai`) 2. Open DevTools (F12) → **Network** tab @@ -38,7 +39,8 @@ If automatic extraction fails (e.g., browser is locked, profile is encrypted, or ## Troubleshooting -- **"Cookie decryption failed"**: Close the browser and retry — some browsers lock the cookie database while running +- **"Chromium App-Bound Encryption"**: Modern Chrome, Edge, Brave, and other Chromium-based profiles can protect cookies with ABE. Closing the browser does not remove ABE; use a manual Cookie header or Firefox for the same login +- **"Cookie decryption failed"**: Close the browser and retry if the cookie database itself is locked - **Empty cookies**: Make sure you're logged into the provider's web interface in that browser - **WSL**: Chromium DPAPI cookies cannot be decrypted from WSL. Use manual cookies or CLI-based auth instead @@ -47,4 +49,3 @@ If automatic extraction fails (e.g., browser is locked, profile is encrypted, or - [CONFIGURATION.md](./CONFIGURATION.md) — where manual cookies and settings live on disk - [PROVIDERS.md](./PROVIDERS.md) — web vs cli vs oauth sources - [WSL.md](./WSL.md) — why automatic Chromium decrypt fails in WSL - diff --git a/rust/src/browser/cookies.rs b/rust/src/browser/cookies.rs index b88448e79e..0d21c4251d 100755 --- a/rust/src/browser/cookies.rs +++ b/rust/src/browser/cookies.rs @@ -18,7 +18,7 @@ use base64::Engine; use rusqlite::Connection; use thiserror::Error; -use super::detection::{BrowserProfile, DetectedBrowser}; +use super::detection::{BrowserProfile, BrowserType, DetectedBrowser}; /// Errors that can occur during cookie extraction #[derive(Debug, Error)] @@ -46,16 +46,32 @@ pub enum CookieError { /// Chromium App-Bound Encryption (ABE) is protecting cookie values. /// The user-level DPAPI key in Local State can no longer decrypt cookies encrypted - /// after the ABE migration. Modern Chrome and Edge write these cookies with a - /// `v20` prefix; older migrated profiles can also fail every AES-GCM decrypt while - /// exposing `app_bound_encrypted_key` in Local State. + /// after the ABE migration. Modern Chromium-based browsers can write these cookies + /// with a `v20` prefix; older migrated profiles can also fail every AES-GCM decrypt + /// while exposing `app_bound_encrypted_key` in Local State. #[error( - "Chrome/Edge App-Bound Encryption is blocking automatic browser import. \ + "Chromium App-Bound Encryption is blocking automatic browser import. \ Paste the Cookie header manually, or use Firefox if that browser has the same login." )] AppBoundEncryption, } +impl CookieError { + /// Format a browser-specific message for UI surfaces that know which browser + /// the user selected. ABE is a Chromium capability boundary, so name the + /// concrete browser instead of implying the problem only affects Chrome/Edge. + pub fn user_message_for_browser(&self, browser_type: BrowserType) -> String { + match self { + Self::AppBoundEncryption => format!( + "{} is using Chromium App-Bound Encryption, which is blocking automatic cookie import for this profile. \ + Paste the Cookie header manually, or use Firefox if that browser has the same login.", + browser_type.display_name() + ), + _ => self.to_string(), + } + } +} + /// A browser cookie #[derive(Debug, Clone)] pub struct Cookie { @@ -136,9 +152,9 @@ impl CookieExtractor { } } - /// Detect whether Chrome App-Bound Encryption (ABE, Chrome 127+) is active for + /// Detect whether Chromium App-Bound Encryption (ABE) is active for /// this browser profile by checking for the `app_bound_encrypted_key` field in - /// the Local State JSON. The field is written by Chrome when it migrates the + /// the Local State JSON. Chromium browsers write this field when migrating the /// cookie-encryption key to the ABE system; its presence means the user-level /// DPAPI key stored in `encrypted_key` will no longer decrypt newly written /// cookies. @@ -154,7 +170,7 @@ impl CookieExtractor { .and_then(|v| v.get("app_bound_encrypted_key")) .is_some(); if present { - tracing::debug!("Chrome App-Bound Encryption detected in Local State"); + tracing::debug!("Chromium App-Bound Encryption detected in Local State"); } present } @@ -275,7 +291,7 @@ impl CookieExtractor { let _cleanup = std::fs::remove_file(&temp_db); // If every candidate cookie failed to decrypt and no cookies were recovered, - // check whether Chrome App-Bound Encryption (Chrome 127+) is the culprit. + // check whether Chromium App-Bound Encryption is the culprit. // ABE replaces the user-level DPAPI cookie key with a system-level key that // cannot be read by third-party tools, causing systematic AES-GCM auth failures. if cookies.is_empty() @@ -416,7 +432,7 @@ impl CookieExtractor { let has_v20_prefix = encrypted_value.len() >= 3 && &encrypted_value[0..3] == b"v20"; if has_v20_prefix { - // Chrome/Edge 127+ on Windows use App-Bound Encryption for these + // Modern Chromium-based browsers on Windows use App-Bound Encryption for these // cookies. Treating the blob as old DPAPI data produces a misleading // DPAPI or "no cookies found" error even though the user is signed in. return Err(CookieError::AppBoundEncryption); @@ -672,8 +688,8 @@ pub fn get_cookies_for_domain(domain: &str) -> Result, CookieError> } Ok(_) => continue, Err(CookieError::AppBoundEncryption) => { - // Chrome ABE is blocking this browser; log a warning and keep - // trying Edge / Firefox which are unaffected by ABE. + // Chromium ABE is blocking this browser; log a warning and keep + // trying the remaining browsers; Firefox does not use Chromium ABE. tracing::warn!( browser = %browser.browser_type.display_name(), "App-Bound Encryption prevents automatic cookie import; \ @@ -784,7 +800,7 @@ mod tests { "ABE error should mention App-Bound Encryption" ); assert!( - msg.contains("Chrome/Edge"), + msg.contains("Chromium"), "ABE error should identify Chromium browsers" ); assert!( @@ -793,6 +809,20 @@ mod tests { ); } + #[test] + fn test_abe_error_names_selected_brave_browser() { + let msg = CookieError::AppBoundEncryption.user_message_for_browser(BrowserType::Brave); + assert!(msg.contains("Brave"), "ABE error should name Brave"); + assert!( + msg.contains("Chromium App-Bound Encryption"), + "ABE error should explain the Chromium mechanism" + ); + assert!( + msg.contains("Paste") || msg.contains("manual"), + "ABE error should preserve an actionable fallback" + ); + } + /// Verify that modern Chromium `v20` cookies are recognized as App-Bound /// Encryption instead of being misrouted through legacy DPAPI decryption. #[test]