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") || cookie.name.contains("token"))
707 {
708 creds.session_token = Some(cookie.value.clone());
709 }
710 creds
711 .cookies
712 .insert(cookie.name.clone(), cookie.value.clone());
713 }
714
715 if creds.session_token.is_some() || !creds.cookies.is_empty() {
716 return Some(creds);
717 }
718 }
719 }
720 }
721 }
722 }
723
724 None
725}
726
727fn extract_cookies_for_domain(
729 browser: &BrowserType,
730 cookies_path: &PathBuf,
731 domain: &str,
732) -> Result<Vec<ExtractedCookie>> {
733 match extract_cookies_internal(browser, cookies_path, domain) {
735 Ok(cookies) => Ok(cookies),
736 Err(_) => {
737 extract_cookies_with_copy(browser, cookies_path, domain)
739 }
740 }
741}
742
743fn extract_cookies_internal(
744 browser: &BrowserType,
745 cookies_path: &PathBuf,
746 domain: &str,
747) -> Result<Vec<ExtractedCookie>> {
748 let conn = Connection::open_with_flags(
749 cookies_path,
750 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
751 )
752 .context("Failed to open cookie database")?;
753
754 match browser {
755 BrowserType::Firefox => extract_firefox_cookie_values(&conn, domain, browser),
756 _ => extract_chromium_cookie_values(&conn, domain, browser),
757 }
758}
759
760fn extract_cookies_with_copy(
761 browser: &BrowserType,
762 cookies_path: &PathBuf,
763 domain: &str,
764) -> Result<Vec<ExtractedCookie>> {
765 let temp_dir = std::env::temp_dir();
766 let temp_path = temp_dir.join(format!("csm_cookies_extract_{}.db", uuid::Uuid::new_v4()));
767
768 fs::copy(cookies_path, &temp_path).context("Failed to copy cookie database")?;
769
770 let wal_path = cookies_path.with_extension("db-wal");
772 if wal_path.exists() {
773 let _ = fs::copy(&wal_path, temp_path.with_extension("db-wal"));
774 }
775 let shm_path = cookies_path.with_extension("db-shm");
776 if shm_path.exists() {
777 let _ = fs::copy(&shm_path, temp_path.with_extension("db-shm"));
778 }
779
780 let result = extract_cookies_internal(browser, &temp_path, domain);
781
782 let _ = fs::remove_file(&temp_path);
784 let _ = fs::remove_file(temp_path.with_extension("db-wal"));
785 let _ = fs::remove_file(temp_path.with_extension("db-shm"));
786
787 result
788}
789
790fn extract_firefox_cookie_values(
792 conn: &Connection,
793 domain: &str,
794 browser: &BrowserType,
795) -> Result<Vec<ExtractedCookie>> {
796 let mut cookies = Vec::new();
797
798 let mut stmt =
799 conn.prepare("SELECT name, value, host FROM moz_cookies WHERE host LIKE ? OR host LIKE ?")?;
800
801 let domain_pattern = format!("%{}", domain);
802 let dot_domain_pattern = format!("%.{}", domain);
803
804 let rows = stmt.query_map([&domain_pattern, &dot_domain_pattern], |row| {
805 Ok((
806 row.get::<_, String>(0)?,
807 row.get::<_, String>(1)?,
808 row.get::<_, String>(2)?,
809 ))
810 })?;
811
812 for row in rows.flatten() {
813 let (name, value, host) = row;
814 if !value.is_empty() {
815 cookies.push(ExtractedCookie {
816 name,
817 value,
818 domain: host,
819 browser: *browser,
820 });
821 }
822 }
823
824 Ok(cookies)
825}
826
827fn extract_chromium_cookie_values(
831 conn: &Connection,
832 domain: &str,
833 browser: &BrowserType,
834) -> Result<Vec<ExtractedCookie>> {
835 let mut cookies = Vec::new();
836
837 let mut stmt = conn.prepare(
840 "SELECT name, value, encrypted_value, host_key FROM cookies WHERE host_key LIKE ? OR host_key LIKE ?"
841 )?;
842
843 let domain_pattern = format!("%{}", domain);
844 let dot_domain_pattern = format!("%.{}", domain);
845
846 let rows = stmt.query_map([&domain_pattern, &dot_domain_pattern], |row| {
847 Ok((
848 row.get::<_, String>(0)?,
849 row.get::<_, String>(1)?,
850 row.get::<_, Vec<u8>>(2)?,
851 row.get::<_, String>(3)?,
852 ))
853 })?;
854
855 for row in rows.flatten() {
856 let (name, value, encrypted_value, host) = row;
857
858 let cookie_value = if !value.is_empty() {
860 value
861 } else if !encrypted_value.is_empty() {
862 match decrypt_chromium_cookie(&encrypted_value, browser) {
864 Ok(decrypted) => decrypted,
865 Err(_) => continue, }
867 } else {
868 continue;
869 };
870
871 if !cookie_value.is_empty() {
872 cookies.push(ExtractedCookie {
873 name,
874 value: cookie_value,
875 domain: host,
876 browser: *browser,
877 });
878 }
879 }
880
881 Ok(cookies)
882}
883
884#[cfg(windows)]
887fn decrypt_chromium_cookie(encrypted_value: &[u8], browser: &BrowserType) -> Result<String> {
888 use windows::Win32::Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB};
889
890 if encrypted_value.len() > 3 && &encrypted_value[0..3] == b"v10"
892 || &encrypted_value[0..3] == b"v20"
893 {
894 if let Some(key) = get_chromium_encryption_key(browser) {
896 return decrypt_aes_gcm(&encrypted_value[3..], &key);
897 }
898 }
899
900 unsafe {
902 let input = CRYPT_INTEGER_BLOB {
903 cbData: encrypted_value.len() as u32,
904 pbData: encrypted_value.as_ptr() as *mut u8,
905 };
906 let mut output = CRYPT_INTEGER_BLOB {
907 cbData: 0,
908 pbData: std::ptr::null_mut(),
909 };
910
911 let result = CryptUnprotectData(&input, None, None, None, None, 0, &mut output);
912
913 if result.is_ok() && !output.pbData.is_null() {
914 let slice = std::slice::from_raw_parts(output.pbData, output.cbData as usize);
915 let decrypted = String::from_utf8_lossy(slice).to_string();
916 return Ok(decrypted);
919 }
920 }
921
922 Err(anyhow!("Failed to decrypt cookie"))
923}
924
925#[cfg(windows)]
926fn get_chromium_encryption_key(browser: &BrowserType) -> Option<Vec<u8>> {
927 use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
928 use windows::Win32::Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB};
929
930 let local_state_path = browser.local_state_path()?;
931 let local_state_content = fs::read_to_string(&local_state_path).ok()?;
932 let local_state: serde_json::Value = serde_json::from_str(&local_state_content).ok()?;
933
934 let encrypted_key_b64 = local_state
935 .get("os_crypt")?
936 .get("encrypted_key")?
937 .as_str()?;
938
939 let encrypted_key = BASE64.decode(encrypted_key_b64).ok()?;
940
941 if encrypted_key.len() <= 5 || &encrypted_key[0..5] != b"DPAPI" {
943 return None;
944 }
945
946 let encrypted_key = &encrypted_key[5..];
947
948 unsafe {
950 let input = CRYPT_INTEGER_BLOB {
951 cbData: encrypted_key.len() as u32,
952 pbData: encrypted_key.as_ptr() as *mut u8,
953 };
954 let mut output = CRYPT_INTEGER_BLOB {
955 cbData: 0,
956 pbData: std::ptr::null_mut(),
957 };
958
959 let result = CryptUnprotectData(&input, None, None, None, None, 0, &mut output);
960
961 if result.is_ok() && !output.pbData.is_null() {
962 let key = std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec();
963 return Some(key);
966 }
967 }
968
969 None
970}
971
972#[cfg(windows)]
973fn decrypt_aes_gcm(encrypted_data: &[u8], key: &[u8]) -> Result<String> {
974 use aes_gcm::{
975 aead::{Aead, KeyInit},
976 Aes256Gcm, Nonce,
977 };
978
979 if encrypted_data.len() < 12 + 16 {
980 return Err(anyhow!("Encrypted data too short"));
981 }
982
983 let nonce = Nonce::from_slice(&encrypted_data[0..12]);
985 let ciphertext = &encrypted_data[12..];
986
987 let cipher =
988 Aes256Gcm::new_from_slice(key).map_err(|e| anyhow!("Failed to create cipher: {}", e))?;
989
990 let plaintext = cipher
991 .decrypt(nonce, ciphertext)
992 .map_err(|e| anyhow!("Decryption failed: {}", e))?;
993
994 String::from_utf8(plaintext).map_err(|e| anyhow!("Invalid UTF-8 in decrypted cookie: {}", e))
995}
996
997#[cfg(not(windows))]
998fn decrypt_chromium_cookie(encrypted_value: &[u8], _browser: &BrowserType) -> Result<String> {
999 if let Ok(s) = String::from_utf8(encrypted_value.to_vec()) {
1004 if s.chars()
1005 .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
1006 {
1007 return Ok(s);
1008 }
1009 }
1010
1011 Err(anyhow!(
1012 "Cookie decryption not implemented for this platform"
1013 ))
1014}
1015
1016#[allow(dead_code)]
1019pub fn get_auth_summary() -> AuthSummary {
1020 let results = scan_browser_auth();
1021 let mut summary = AuthSummary::default();
1022
1023 let mut browsers_seen = std::collections::HashSet::new();
1025
1026 for result in results {
1027 browsers_seen.insert(result.browser);
1028
1029 if result.authenticated {
1030 summary
1031 .authenticated_providers
1032 .entry(result.provider)
1033 .or_default()
1034 .push(result.browser);
1035 }
1036 }
1037
1038 summary.browsers_checked = browsers_seen.into_iter().collect();
1039 summary.total_providers_authenticated = summary.authenticated_providers.len();
1040
1041 summary
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 #[test]
1049 fn test_browser_type_name() {
1050 assert_eq!(BrowserType::Chrome.name(), "Chrome");
1051 assert_eq!(BrowserType::Edge.name(), "Edge");
1052 assert_eq!(BrowserType::Firefox.name(), "Firefox");
1053 }
1054
1055 #[test]
1056 fn test_get_installed_browsers() {
1057 let browsers = get_installed_browsers();
1058 assert!(browsers.len() <= 6);
1060 }
1061
1062 #[test]
1063 fn test_provider_auth_domains() {
1064 for provider in WEB_LLM_PROVIDERS {
1066 assert!(!provider.domain.is_empty());
1067 assert!(provider.domain.contains('.'));
1068 assert!(!provider.auth_cookie_names.is_empty());
1069 }
1070 }
1071
1072 #[test]
1073 fn test_auth_summary_default() {
1074 let summary = AuthSummary::default();
1075 assert!(summary.browsers_checked.is_empty());
1076 assert!(summary.authenticated_providers.is_empty());
1077 assert_eq!(summary.total_providers_authenticated, 0);
1078 }
1079}
1080
1081
1082