1use anyhow::{anyhow, Context, Result};
9use colored::Colorize;
10use rusqlite::{Connection, OpenFlags};
11use std::collections::HashMap;
12use std::fs;
13use std::path::PathBuf;
14
15#[allow(dead_code)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum BrowserType {
20 Chrome,
21 Edge,
22 Firefox,
23 Brave,
24 Vivaldi,
25 Opera,
26}
27
28impl BrowserType {
29 pub fn name(&self) -> &'static str {
30 match self {
31 BrowserType::Chrome => "Chrome",
32 BrowserType::Edge => "Edge",
33 BrowserType::Firefox => "Firefox",
34 BrowserType::Brave => "Brave",
35 BrowserType::Vivaldi => "Vivaldi",
36 BrowserType::Opera => "Opera",
37 }
38 }
39
40 #[cfg(windows)]
42 pub fn profile_path(&self) -> Option<PathBuf> {
43 let local_app_data = dirs::data_local_dir()?;
44 let roaming_app_data = dirs::data_dir()?;
45
46 let path = match self {
47 BrowserType::Chrome => local_app_data.join("Google/Chrome/User Data/Default"),
48 BrowserType::Edge => local_app_data.join("Microsoft/Edge/User Data/Default"),
49 BrowserType::Brave => {
50 local_app_data.join("BraveSoftware/Brave-Browser/User Data/Default")
51 }
52 BrowserType::Vivaldi => local_app_data.join("Vivaldi/User Data/Default"),
53 BrowserType::Opera => roaming_app_data.join("Opera Software/Opera Stable"),
54 BrowserType::Firefox => {
55 let profiles_dir = roaming_app_data.join("Mozilla/Firefox/Profiles");
58 if profiles_dir.exists() {
59 if let Ok(entries) = fs::read_dir(&profiles_dir) {
60 let mut best_profile: Option<(PathBuf, u64)> = None;
61
62 for entry in entries.flatten() {
63 let profile_path = entry.path();
64 let cookies_path = profile_path.join("cookies.sqlite");
65
66 if cookies_path.exists() {
67 if let Ok(metadata) = fs::metadata(&cookies_path) {
68 let size = metadata.len();
69 if best_profile.as_ref().map_or(true, |(_, s)| size > *s) {
70 best_profile = Some((profile_path, size));
71 }
72 }
73 }
74 }
75
76 if let Some((path, _)) = best_profile {
77 return Some(path);
78 }
79 }
80 }
81 return None;
82 }
83 };
84
85 if path.exists() {
86 Some(path)
87 } else {
88 None
89 }
90 }
91
92 #[cfg(not(windows))]
93 pub fn profile_path(&self) -> Option<PathBuf> {
94 let home = dirs::home_dir()?;
95
96 let path = match self {
97 BrowserType::Chrome => {
98 #[cfg(target_os = "macos")]
99 {
100 home.join("Library/Application Support/Google/Chrome/Default")
101 }
102 #[cfg(target_os = "linux")]
103 {
104 home.join(".config/google-chrome/Default")
105 }
106 }
107 BrowserType::Edge => {
108 #[cfg(target_os = "macos")]
109 {
110 home.join("Library/Application Support/Microsoft Edge/Default")
111 }
112 #[cfg(target_os = "linux")]
113 {
114 home.join(".config/microsoft-edge/Default")
115 }
116 }
117 BrowserType::Firefox => {
118 #[cfg(target_os = "macos")]
119 let profiles_dir = home.join("Library/Application Support/Firefox/Profiles");
120 #[cfg(target_os = "linux")]
121 let profiles_dir = home.join(".mozilla/firefox");
122
123 if profiles_dir.exists() {
124 if let Ok(entries) = fs::read_dir(&profiles_dir) {
125 for entry in entries.flatten() {
126 let name = entry.file_name().to_string_lossy().to_string();
127 if name.ends_with(".default-release") || name.ends_with(".default") {
128 return Some(entry.path());
129 }
130 }
131 }
132 }
133 return None;
134 }
135 BrowserType::Brave => {
136 #[cfg(target_os = "macos")]
137 {
138 home.join("Library/Application Support/BraveSoftware/Brave-Browser/Default")
139 }
140 #[cfg(target_os = "linux")]
141 {
142 home.join(".config/BraveSoftware/Brave-Browser/Default")
143 }
144 }
145 _ => return None,
146 };
147
148 if path.exists() {
149 Some(path)
150 } else {
151 None
152 }
153 }
154
155 pub fn cookies_path(&self) -> Option<PathBuf> {
157 let profile = self.profile_path()?;
158
159 match self {
160 BrowserType::Firefox => {
161 let path = profile.join("cookies.sqlite");
162 if path.exists() {
163 Some(path)
164 } else {
165 None
166 }
167 }
168 _ => {
169 let network_path = profile.join("Network/Cookies");
171 if network_path.exists() {
172 return Some(network_path);
173 }
174 let old_path = profile.join("Cookies");
175 if old_path.exists() {
176 Some(old_path)
177 } else {
178 None
179 }
180 }
181 }
182 }
183
184 #[cfg(windows)]
187 #[allow(dead_code)]
188 pub fn local_state_path(&self) -> Option<PathBuf> {
189 let local_app_data = dirs::data_local_dir()?;
190 let roaming_app_data = dirs::data_dir()?;
191
192 let path = match self {
193 BrowserType::Chrome => local_app_data.join("Google/Chrome/User Data/Local State"),
194 BrowserType::Edge => local_app_data.join("Microsoft/Edge/User Data/Local State"),
195 BrowserType::Brave => {
196 local_app_data.join("BraveSoftware/Brave-Browser/User Data/Local State")
197 }
198 BrowserType::Vivaldi => local_app_data.join("Vivaldi/User Data/Local State"),
199 BrowserType::Opera => roaming_app_data.join("Opera Software/Opera Stable/Local State"),
200 BrowserType::Firefox => return None, };
202
203 if path.exists() {
204 Some(path)
205 } else {
206 None
207 }
208 }
209}
210
211#[derive(Debug, Clone)]
213pub struct ProviderAuth {
214 pub name: &'static str,
215 pub domain: &'static str,
216 pub auth_cookie_names: &'static [&'static str],
217 #[allow(dead_code)]
218 pub description: &'static str,
219}
220
221pub const WEB_LLM_PROVIDERS: &[ProviderAuth] = &[
223 ProviderAuth {
224 name: "ChatGPT",
225 domain: "chatgpt.com", auth_cookie_names: &[
227 "__Secure-next-auth.session-token",
228 "_puid",
229 "__cf_bm",
230 "cf_clearance",
231 ],
232 description: "OpenAI ChatGPT",
233 },
234 ProviderAuth {
235 name: "Claude",
236 domain: "claude.ai",
237 auth_cookie_names: &["sessionKey", "__cf_bm"],
238 description: "Anthropic Claude",
239 },
240 ProviderAuth {
241 name: "Gemini",
242 domain: "gemini.google.com",
243 auth_cookie_names: &["SID", "HSID", "SSID"],
244 description: "Google Gemini",
245 },
246 ProviderAuth {
247 name: "Perplexity",
248 domain: "perplexity.ai",
249 auth_cookie_names: &["pplx.visitor-id", "__Secure-next-auth.session-token"],
250 description: "Perplexity AI",
251 },
252 ProviderAuth {
253 name: "DeepSeek",
254 domain: "chat.deepseek.com",
255 auth_cookie_names: &["token", "sessionid"],
256 description: "DeepSeek Chat",
257 },
258 ProviderAuth {
259 name: "Poe",
260 domain: "poe.com",
261 auth_cookie_names: &["p-b", "p-lat"],
262 description: "Quora Poe",
263 },
264 ProviderAuth {
265 name: "HuggingChat",
266 domain: "huggingface.co",
267 auth_cookie_names: &["token", "hf-chat"],
268 description: "HuggingFace Chat",
269 },
270 ProviderAuth {
271 name: "Copilot",
272 domain: "copilot.microsoft.com",
273 auth_cookie_names: &["_U", "MUID"],
274 description: "Microsoft Copilot",
275 },
276 ProviderAuth {
277 name: "Mistral",
278 domain: "chat.mistral.ai",
279 auth_cookie_names: &["__Secure-next-auth.session-token"],
280 description: "Mistral Le Chat",
281 },
282 ProviderAuth {
283 name: "Cohere",
284 domain: "coral.cohere.com",
285 auth_cookie_names: &["session", "auth_token"],
286 description: "Cohere Coral",
287 },
288 ProviderAuth {
289 name: "Groq",
290 domain: "groq.com",
291 auth_cookie_names: &["__Secure-next-auth.session-token"],
292 description: "Groq Cloud",
293 },
294 ProviderAuth {
295 name: "Phind",
296 domain: "phind.com",
297 auth_cookie_names: &["__Secure-next-auth.session-token", "phind-session"],
298 description: "Phind AI",
299 },
300 ProviderAuth {
301 name: "Character.AI",
302 domain: "character.ai",
303 auth_cookie_names: &["token", "web-next-auth.session-token"],
304 description: "Character.AI",
305 },
306 ProviderAuth {
307 name: "You.com",
308 domain: "you.com",
309 auth_cookie_names: &["stytch_session", "youchat_session"],
310 description: "You.com AI",
311 },
312 ProviderAuth {
313 name: "Pi",
314 domain: "pi.ai",
315 auth_cookie_names: &["__Secure-next-auth.session-token"],
316 description: "Inflection Pi",
317 },
318];
319
320#[derive(Debug, Clone)]
322pub struct BrowserAuthResult {
323 pub browser: BrowserType,
324 pub provider: String,
325 pub authenticated: bool,
326 #[allow(dead_code)]
327 pub cookies_found: Vec<String>,
328}
329
330pub fn scan_browser_auth() -> Vec<BrowserAuthResult> {
332 scan_browser_auth_internal(false)
333}
334
335pub fn scan_browser_auth_verbose() -> Vec<BrowserAuthResult> {
337 scan_browser_auth_internal(true)
338}
339
340fn scan_browser_auth_internal(verbose: bool) -> Vec<BrowserAuthResult> {
341 let mut results = Vec::new();
342
343 let browsers = [
344 BrowserType::Edge,
345 BrowserType::Chrome,
346 BrowserType::Brave,
347 BrowserType::Firefox,
348 BrowserType::Vivaldi,
349 BrowserType::Opera,
350 ];
351
352 for browser in browsers {
353 if let Some(cookies_path) = browser.cookies_path() {
354 if verbose {
355 println!(
356 " {} {} cookies: {}",
357 "->".dimmed(),
358 browser.name(),
359 cookies_path.display()
360 );
361 }
362 match scan_browser_cookies_internal(&browser, &cookies_path, verbose) {
363 Ok(browser_results) => results.extend(browser_results),
364 Err(e) => {
365 if verbose {
366 println!(" {} Direct access failed: {}", "!".yellow(), e);
367 println!(" {} Trying copy method...", "->".dimmed());
368 }
369 match scan_browser_cookies_with_copy_internal(&browser, &cookies_path, verbose)
372 {
373 Ok(browser_results) => results.extend(browser_results),
374 Err(e2) => {
375 if verbose {
376 println!(" {} Copy method also failed: {}", "x".red(), e2);
377 }
378 }
379 }
380 }
381 }
382 }
383 }
384
385 results
386}
387
388pub fn get_installed_browsers() -> Vec<BrowserType> {
390 let browsers = [
391 BrowserType::Edge,
392 BrowserType::Chrome,
393 BrowserType::Brave,
394 BrowserType::Firefox,
395 BrowserType::Vivaldi,
396 BrowserType::Opera,
397 ];
398
399 browsers
400 .into_iter()
401 .filter(|b| b.profile_path().is_some())
402 .collect()
403}
404
405#[allow(dead_code)]
407fn scan_browser_cookies(
408 browser: &BrowserType,
409 cookies_path: &PathBuf,
410) -> Result<Vec<BrowserAuthResult>> {
411 scan_browser_cookies_internal(browser, cookies_path, false)
412}
413
414fn scan_browser_cookies_internal(
415 browser: &BrowserType,
416 cookies_path: &PathBuf,
417 verbose: bool,
418) -> Result<Vec<BrowserAuthResult>> {
419 let mut results = Vec::new();
420
421 let conn = Connection::open_with_flags(
423 cookies_path,
424 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
425 )
426 .context("Failed to open cookie database")?;
427
428 let cookies = match *browser {
430 BrowserType::Firefox => get_firefox_cookies(&conn)?,
431 _ => get_chromium_cookies(&conn)?,
432 };
433
434 if verbose {
435 println!(
436 " {} Found {} domains with cookies",
437 "->".dimmed(),
438 cookies.len()
439 );
440
441 let llm_domains: Vec<_> = cookies
443 .keys()
444 .filter(|d| {
445 let dl = d.to_lowercase();
446 dl.contains("openai")
447 || dl.contains("claude")
448 || dl.contains("anthropic")
449 || dl.contains("google")
450 || dl.contains("perplexity")
451 || dl.contains("deepseek")
452 || dl.contains("poe")
453 || dl.contains("huggingface")
454 || dl.contains("microsoft")
455 || dl.contains("copilot")
456 || dl.contains("mistral")
457 || dl.contains("cohere")
458 || dl.contains("groq")
459 || dl.contains("phind")
460 || dl.contains("character")
461 })
462 .collect();
463
464 if !llm_domains.is_empty() {
465 println!(" {} LLM-related domains found:", "->".dimmed());
466 for domain in &llm_domains {
467 let cookie_names = cookies
468 .get(*domain)
469 .map(|v| v.join(", "))
470 .unwrap_or_default();
471 println!(
472 " {} {} -> [{}]",
473 "*".dimmed(),
474 domain,
475 cookie_names.dimmed()
476 );
477 }
478 }
479 }
480
481 for provider in WEB_LLM_PROVIDERS {
483 let domain_cookies: Vec<&String> = cookies
488 .iter()
489 .filter(|(domain, _)| {
490 let domain_clean = domain.trim_start_matches('.');
491 let provider_domain = provider.domain.trim_start_matches('.');
492 domain_clean.ends_with(provider_domain) || provider_domain.ends_with(domain_clean)
493 })
494 .flat_map(|(_, names)| names)
495 .collect();
496
497 let found_auth_cookies: Vec<String> = provider
498 .auth_cookie_names
499 .iter()
500 .filter(|name| {
501 domain_cookies
502 .iter()
503 .any(|c| c == *name || c.contains(*name))
504 })
505 .map(|s| s.to_string())
506 .collect();
507
508 let authenticated = !found_auth_cookies.is_empty();
509
510 if verbose && !domain_cookies.is_empty() {
511 println!(
512 " {} {}: domain cookies={:?}, auth cookies={:?}, authenticated={}",
513 "->".dimmed(),
514 provider.name,
515 domain_cookies.iter().take(5).collect::<Vec<_>>(),
516 found_auth_cookies,
517 authenticated
518 );
519 }
520
521 results.push(BrowserAuthResult {
522 browser: *browser,
523 provider: provider.name.to_string(),
524 authenticated,
525 cookies_found: found_auth_cookies,
526 });
527 }
528
529 Ok(results)
530}
531
532#[allow(dead_code)]
534fn scan_browser_cookies_with_copy(
535 browser: &BrowserType,
536 cookies_path: &PathBuf,
537) -> Result<Vec<BrowserAuthResult>> {
538 scan_browser_cookies_with_copy_internal(browser, cookies_path, false)
539}
540
541fn scan_browser_cookies_with_copy_internal(
542 browser: &BrowserType,
543 cookies_path: &PathBuf,
544 verbose: bool,
545) -> Result<Vec<BrowserAuthResult>> {
546 let temp_dir = std::env::temp_dir();
547 let temp_path = temp_dir.join(format!("csm_cookies_{}.db", uuid::Uuid::new_v4()));
548
549 fs::copy(cookies_path, &temp_path).context("Failed to copy cookie database")?;
551
552 let wal_path = cookies_path.with_extension("db-wal");
554 if wal_path.exists() {
555 let _ = fs::copy(&wal_path, temp_path.with_extension("db-wal"));
556 }
557 let shm_path = cookies_path.with_extension("db-shm");
558 if shm_path.exists() {
559 let _ = fs::copy(&shm_path, temp_path.with_extension("db-shm"));
560 }
561
562 let ff_wal = cookies_path.with_file_name(format!(
564 "{}-wal",
565 cookies_path
566 .file_name()
567 .unwrap_or_default()
568 .to_string_lossy()
569 ));
570 if ff_wal.exists() {
571 let _ = fs::copy(
572 &ff_wal,
573 temp_dir.join(format!("csm_cookies_{}.db-wal", uuid::Uuid::new_v4())),
574 );
575 }
576
577 if verbose {
578 println!(
579 " {} Copied to temp: {}",
580 "->".dimmed(),
581 temp_path.display()
582 );
583 }
584
585 let result = scan_browser_cookies_internal(browser, &temp_path, verbose);
586
587 let _ = fs::remove_file(&temp_path);
589 let _ = fs::remove_file(temp_path.with_extension("db-wal"));
590 let _ = fs::remove_file(temp_path.with_extension("db-shm"));
591
592 result
593}
594
595fn get_chromium_cookies(conn: &Connection) -> Result<HashMap<String, Vec<String>>> {
597 let mut cookies: HashMap<String, Vec<String>> = HashMap::new();
598
599 let mut stmt = conn.prepare(
601 "SELECT host_key, name FROM cookies WHERE host_key LIKE '%.%' GROUP BY host_key, name",
602 )?;
603
604 let rows = stmt.query_map([], |row| {
605 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
606 })?;
607
608 for row in rows.flatten() {
609 let (host, name) = row;
610 cookies.entry(host).or_default().push(name);
611 }
612
613 Ok(cookies)
614}
615
616fn get_firefox_cookies(conn: &Connection) -> Result<HashMap<String, Vec<String>>> {
618 let mut cookies: HashMap<String, Vec<String>> = HashMap::new();
619
620 let mut stmt = conn
621 .prepare("SELECT host, name FROM moz_cookies WHERE host LIKE '%.%' GROUP BY host, name")?;
622
623 let rows = stmt.query_map([], |row| {
624 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
625 })?;
626
627 for row in rows.flatten() {
628 let (host, name) = row;
629 cookies.entry(host).or_default().push(name);
630 }
631
632 Ok(cookies)
633}
634
635#[allow(dead_code)]
638#[derive(Debug, Default)]
639pub struct AuthSummary {
640 pub browsers_checked: Vec<BrowserType>,
641 pub authenticated_providers: HashMap<String, Vec<BrowserType>>,
642 pub total_providers_authenticated: usize,
643}
644
645#[derive(Debug, Clone)]
647#[allow(dead_code)]
648pub struct ExtractedCookie {
649 pub name: String,
650 pub value: String,
651 pub domain: String,
652 pub browser: BrowserType,
653}
654
655#[derive(Debug, Clone, Default)]
657#[allow(dead_code)]
658pub struct ProviderCredentials {
659 pub provider: String,
660 pub session_token: Option<String>,
661 pub cookies: HashMap<String, String>,
662 pub browser: Option<BrowserType>,
663}
664
665pub fn extract_provider_cookies(provider_name: &str) -> Option<ProviderCredentials> {
667 let provider_auth = WEB_LLM_PROVIDERS
668 .iter()
669 .find(|p| p.name.eq_ignore_ascii_case(provider_name))?;
670
671 let domains_to_try: Vec<&str> = if provider_name.eq_ignore_ascii_case("chatgpt") {
673 vec!["chatgpt.com", "openai.com", "chat.openai.com"]
674 } else {
675 vec![provider_auth.domain]
676 };
677
678 let browsers = [
679 BrowserType::Edge,
680 BrowserType::Chrome,
681 BrowserType::Brave,
682 BrowserType::Firefox,
683 BrowserType::Vivaldi,
684 BrowserType::Opera,
685 ];
686
687 for browser in browsers {
688 if let Some(cookies_path) = browser.cookies_path() {
689 for domain in &domains_to_try {
690 if let Ok(cookies) = extract_cookies_for_domain(&browser, &cookies_path, domain) {
692 if !cookies.is_empty() {
693 let mut creds = ProviderCredentials {
694 provider: provider_name.to_string(),
695 session_token: None,
696 cookies: HashMap::new(),
697 browser: Some(browser),
698 };
699
700 for cookie in &cookies {
701 if provider_auth
703 .auth_cookie_names
704 .iter()
705 .any(|name| cookie.name.contains(name))
706 && (cookie.name.contains("session")
707 || cookie.name.contains("token"))
708 {
709 creds.session_token = Some(cookie.value.clone());
710 }
711 creds
712 .cookies
713 .insert(cookie.name.clone(), cookie.value.clone());
714 }
715
716 if creds.session_token.is_some() || !creds.cookies.is_empty() {
717 return Some(creds);
718 }
719 }
720 }
721 }
722 }
723 }
724
725 None
726}
727
728fn extract_cookies_for_domain(
730 browser: &BrowserType,
731 cookies_path: &PathBuf,
732 domain: &str,
733) -> Result<Vec<ExtractedCookie>> {
734 match extract_cookies_internal(browser, cookies_path, domain) {
736 Ok(cookies) => Ok(cookies),
737 Err(_) => {
738 extract_cookies_with_copy(browser, cookies_path, domain)
740 }
741 }
742}
743
744fn extract_cookies_internal(
745 browser: &BrowserType,
746 cookies_path: &PathBuf,
747 domain: &str,
748) -> Result<Vec<ExtractedCookie>> {
749 let conn = Connection::open_with_flags(
750 cookies_path,
751 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
752 )
753 .context("Failed to open cookie database")?;
754
755 match browser {
756 BrowserType::Firefox => extract_firefox_cookie_values(&conn, domain, browser),
757 _ => extract_chromium_cookie_values(&conn, domain, browser),
758 }
759}
760
761fn extract_cookies_with_copy(
762 browser: &BrowserType,
763 cookies_path: &PathBuf,
764 domain: &str,
765) -> Result<Vec<ExtractedCookie>> {
766 let temp_dir = std::env::temp_dir();
767 let temp_path = temp_dir.join(format!("csm_cookies_extract_{}.db", uuid::Uuid::new_v4()));
768
769 fs::copy(cookies_path, &temp_path).context("Failed to copy cookie database")?;
770
771 let wal_path = cookies_path.with_extension("db-wal");
773 if wal_path.exists() {
774 let _ = fs::copy(&wal_path, temp_path.with_extension("db-wal"));
775 }
776 let shm_path = cookies_path.with_extension("db-shm");
777 if shm_path.exists() {
778 let _ = fs::copy(&shm_path, temp_path.with_extension("db-shm"));
779 }
780
781 let result = extract_cookies_internal(browser, &temp_path, domain);
782
783 let _ = fs::remove_file(&temp_path);
785 let _ = fs::remove_file(temp_path.with_extension("db-wal"));
786 let _ = fs::remove_file(temp_path.with_extension("db-shm"));
787
788 result
789}
790
791fn extract_firefox_cookie_values(
793 conn: &Connection,
794 domain: &str,
795 browser: &BrowserType,
796) -> Result<Vec<ExtractedCookie>> {
797 let mut cookies = Vec::new();
798
799 let mut stmt =
800 conn.prepare("SELECT name, value, host FROM moz_cookies WHERE host LIKE ? OR host LIKE ?")?;
801
802 let domain_pattern = format!("%{}", domain);
803 let dot_domain_pattern = format!("%.{}", domain);
804
805 let rows = stmt.query_map([&domain_pattern, &dot_domain_pattern], |row| {
806 Ok((
807 row.get::<_, String>(0)?,
808 row.get::<_, String>(1)?,
809 row.get::<_, String>(2)?,
810 ))
811 })?;
812
813 for row in rows.flatten() {
814 let (name, value, host) = row;
815 if !value.is_empty() {
816 cookies.push(ExtractedCookie {
817 name,
818 value,
819 domain: host,
820 browser: *browser,
821 });
822 }
823 }
824
825 Ok(cookies)
826}
827
828fn extract_chromium_cookie_values(
832 conn: &Connection,
833 domain: &str,
834 browser: &BrowserType,
835) -> Result<Vec<ExtractedCookie>> {
836 let mut cookies = Vec::new();
837
838 let mut stmt = conn.prepare(
841 "SELECT name, value, encrypted_value, host_key FROM cookies WHERE host_key LIKE ? OR host_key LIKE ?"
842 )?;
843
844 let domain_pattern = format!("%{}", domain);
845 let dot_domain_pattern = format!("%.{}", domain);
846
847 let rows = stmt.query_map([&domain_pattern, &dot_domain_pattern], |row| {
848 Ok((
849 row.get::<_, String>(0)?,
850 row.get::<_, String>(1)?,
851 row.get::<_, Vec<u8>>(2)?,
852 row.get::<_, String>(3)?,
853 ))
854 })?;
855
856 for row in rows.flatten() {
857 let (name, value, encrypted_value, host) = row;
858
859 let cookie_value = if !value.is_empty() {
861 value
862 } else if !encrypted_value.is_empty() {
863 match decrypt_chromium_cookie(&encrypted_value, browser) {
865 Ok(decrypted) => decrypted,
866 Err(_) => continue, }
868 } else {
869 continue;
870 };
871
872 if !cookie_value.is_empty() {
873 cookies.push(ExtractedCookie {
874 name,
875 value: cookie_value,
876 domain: host,
877 browser: *browser,
878 });
879 }
880 }
881
882 Ok(cookies)
883}
884
885#[cfg(windows)]
888fn decrypt_chromium_cookie(encrypted_value: &[u8], browser: &BrowserType) -> Result<String> {
889 use windows::Win32::Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB};
890
891 if encrypted_value.len() > 3 && &encrypted_value[0..3] == b"v10"
893 || &encrypted_value[0..3] == b"v20"
894 {
895 if let Some(key) = get_chromium_encryption_key(browser) {
897 return decrypt_aes_gcm(&encrypted_value[3..], &key);
898 }
899 }
900
901 unsafe {
903 let input = CRYPT_INTEGER_BLOB {
904 cbData: encrypted_value.len() as u32,
905 pbData: encrypted_value.as_ptr() as *mut u8,
906 };
907 let mut output = CRYPT_INTEGER_BLOB {
908 cbData: 0,
909 pbData: std::ptr::null_mut(),
910 };
911
912 let result = CryptUnprotectData(&input, None, None, None, None, 0, &mut output);
913
914 if result.is_ok() && !output.pbData.is_null() {
915 let slice = std::slice::from_raw_parts(output.pbData, output.cbData as usize);
916 let decrypted = String::from_utf8_lossy(slice).to_string();
917 return Ok(decrypted);
920 }
921 }
922
923 Err(anyhow!("Failed to decrypt cookie"))
924}
925
926#[cfg(windows)]
927fn get_chromium_encryption_key(browser: &BrowserType) -> Option<Vec<u8>> {
928 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
929 use windows::Win32::Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB};
930
931 let local_state_path = browser.local_state_path()?;
932 let local_state_content = fs::read_to_string(&local_state_path).ok()?;
933 let local_state: serde_json::Value = serde_json::from_str(&local_state_content).ok()?;
934
935 let encrypted_key_b64 = local_state
936 .get("os_crypt")?
937 .get("encrypted_key")?
938 .as_str()?;
939
940 let encrypted_key = BASE64.decode(encrypted_key_b64).ok()?;
941
942 if encrypted_key.len() <= 5 || &encrypted_key[0..5] != b"DPAPI" {
944 return None;
945 }
946
947 let encrypted_key = &encrypted_key[5..];
948
949 unsafe {
951 let input = CRYPT_INTEGER_BLOB {
952 cbData: encrypted_key.len() as u32,
953 pbData: encrypted_key.as_ptr() as *mut u8,
954 };
955 let mut output = CRYPT_INTEGER_BLOB {
956 cbData: 0,
957 pbData: std::ptr::null_mut(),
958 };
959
960 let result = CryptUnprotectData(&input, None, None, None, None, 0, &mut output);
961
962 if result.is_ok() && !output.pbData.is_null() {
963 let key = std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec();
964 return Some(key);
967 }
968 }
969
970 None
971}
972
973#[cfg(windows)]
974fn decrypt_aes_gcm(encrypted_data: &[u8], key: &[u8]) -> Result<String> {
975 use aes_gcm::{
976 aead::{Aead, KeyInit},
977 Aes256Gcm, Nonce,
978 };
979
980 if encrypted_data.len() < 12 + 16 {
981 return Err(anyhow!("Encrypted data too short"));
982 }
983
984 let nonce = Nonce::from_slice(&encrypted_data[0..12]);
986 let ciphertext = &encrypted_data[12..];
987
988 let cipher =
989 Aes256Gcm::new_from_slice(key).map_err(|e| anyhow!("Failed to create cipher: {}", e))?;
990
991 let plaintext = cipher
992 .decrypt(nonce, ciphertext)
993 .map_err(|e| anyhow!("Decryption failed: {}", e))?;
994
995 String::from_utf8(plaintext).map_err(|e| anyhow!("Invalid UTF-8 in decrypted cookie: {}", e))
996}
997
998#[cfg(not(windows))]
999fn decrypt_chromium_cookie(encrypted_value: &[u8], _browser: &BrowserType) -> Result<String> {
1000 if let Ok(s) = String::from_utf8(encrypted_value.to_vec()) {
1005 if s.chars()
1006 .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
1007 {
1008 return Ok(s);
1009 }
1010 }
1011
1012 Err(anyhow!(
1013 "Cookie decryption not implemented for this platform"
1014 ))
1015}
1016
1017#[allow(dead_code)]
1020pub fn get_auth_summary() -> AuthSummary {
1021 let results = scan_browser_auth();
1022 let mut summary = AuthSummary::default();
1023
1024 let mut browsers_seen = std::collections::HashSet::new();
1026
1027 for result in results {
1028 browsers_seen.insert(result.browser);
1029
1030 if result.authenticated {
1031 summary
1032 .authenticated_providers
1033 .entry(result.provider)
1034 .or_default()
1035 .push(result.browser);
1036 }
1037 }
1038
1039 summary.browsers_checked = browsers_seen.into_iter().collect();
1040 summary.total_providers_authenticated = summary.authenticated_providers.len();
1041
1042 summary
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048
1049 #[test]
1050 fn test_browser_type_name() {
1051 assert_eq!(BrowserType::Chrome.name(), "Chrome");
1052 assert_eq!(BrowserType::Edge.name(), "Edge");
1053 assert_eq!(BrowserType::Firefox.name(), "Firefox");
1054 }
1055
1056 #[test]
1057 fn test_get_installed_browsers() {
1058 let browsers = get_installed_browsers();
1059 assert!(browsers.len() <= 6);
1061 }
1062
1063 #[test]
1064 fn test_provider_auth_domains() {
1065 for provider in WEB_LLM_PROVIDERS {
1067 assert!(!provider.domain.is_empty());
1068 assert!(provider.domain.contains('.'));
1069 assert!(!provider.auth_cookie_names.is_empty());
1070 }
1071 }
1072
1073 #[test]
1074 fn test_auth_summary_default() {
1075 let summary = AuthSummary::default();
1076 assert!(summary.browsers_checked.is_empty());
1077 assert!(summary.authenticated_providers.is_empty());
1078 assert_eq!(summary.total_providers_authenticated, 0);
1079 }
1080}