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