1use super::types::{Cookie, Profile};
2use crate::crypto;
3use rusqlite::Connection;
4use std::path::Path;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum CookieReaderError {
9 #[error("Database error: {0}")]
10 DatabaseError(#[from] rusqlite::Error),
11
12 #[error("IO error: {0}")]
13 IoError(#[from] std::io::Error),
14
15 #[error("Decryption error: {0}")]
16 DecryptionError(#[from] crypto::CryptoError),
17
18 #[error("No cookies found for domain: {0}")]
19 NoCookiesFound(String),
20}
21
22pub type Result<T> = std::result::Result<T, CookieReaderError>;
23
24fn f64_to_i64_saturating(v: f64) -> i64 {
30 const MAX: f64 = 1_000_000_000_000_000_f64; const MIN: f64 = -1_000_000_000_000_000_f64;
32 let clamped = v.round().clamp(MIN, MAX);
34 clamped.to_string().parse().unwrap_or(0)
37}
38
39pub struct CookieReader;
40
41impl CookieReader {
42 pub fn read_cookies(profile: &Profile, domain: &str) -> Result<Vec<Cookie>> {
46 let cookies_path = profile.cookies_path();
47
48 if !cookies_path.exists() {
49 return Err(CookieReaderError::NoCookiesFound(format!(
50 "Cookies file not found: {}",
51 cookies_path.display()
52 )));
53 }
54
55 let temp_dir = std::env::temp_dir();
56 let temp_cookies = temp_dir.join(format!("cookies_{}.db", std::process::id()));
57 std::fs::copy(&cookies_path, &temp_cookies)?;
58
59 let result = if profile.browser_type.is_chromium_based() {
60 Self::read_chromium_cookies(&temp_cookies, domain, profile)
61 } else if profile.browser_type == super::types::BrowserType::Firefox {
62 Self::read_firefox_cookies(&temp_cookies, domain)
63 } else if profile.browser_type == super::types::BrowserType::Safari {
64 Self::read_safari_cookies(&cookies_path, domain)
65 } else {
66 Err(CookieReaderError::NoCookiesFound(format!(
67 "Unsupported browser type: {:?}",
68 profile.browser_type
69 )))
70 };
71
72 let _ = std::fs::remove_file(&temp_cookies);
73
74 result
75 }
76
77 fn read_chromium_cookies(
78 db_path: &Path,
79 domain: &str,
80 _profile: &Profile,
81 ) -> Result<Vec<Cookie>> {
82 let conn = Connection::open(db_path)?;
83
84 let mut stmt = conn.prepare(
85 "SELECT name, encrypted_value, host_key, path, expires_utc, is_secure, is_httponly, samesite
86 FROM cookies
87 WHERE host_key LIKE ?1 OR host_key LIKE ?2
88 ORDER BY creation_utc DESC",
89 )?;
90
91 let domain_pattern = format!("%{domain}");
92 let dot_domain_pattern = format!("%.{domain}");
93
94 let cookie_iter = stmt.query_map(
95 rusqlite::params![&domain_pattern, &dot_domain_pattern],
96 |row| {
97 let name: String = row.get(0)?;
98 let encrypted_value: Vec<u8> = row.get(1)?;
99 let host_key: String = row.get(2)?;
100 let path: String = row.get(3)?;
101 let expires_utc: i64 = row.get(4)?;
102 let is_secure: bool = row.get(5)?;
103 let is_httponly: bool = row.get(6)?;
104 let same_site: i32 = row.get(7)?;
105
106 Ok((
107 name,
108 encrypted_value,
109 host_key,
110 path,
111 expires_utc,
112 is_secure,
113 is_httponly,
114 same_site,
115 ))
116 },
117 )?;
118
119 let mut cookies = Vec::new();
120
121 for cookie_result in cookie_iter {
122 let (
123 name,
124 encrypted_value,
125 host_key,
126 path,
127 expires_utc,
128 is_secure,
129 is_httponly,
130 same_site,
131 ) = cookie_result?;
132
133 match crypto::decrypt_cookie_value(&encrypted_value) {
134 Ok(value) => {
135 cookies.push(Cookie {
136 name,
137 value,
138 domain: host_key,
139 path,
140 expires_utc,
141 is_secure,
142 is_httponly,
143 same_site,
144 });
145 }
146 Err(e) => {
147 eprintln!(" [warn] Failed to decrypt cookie '{name}': {e}");
148 }
149 }
150 }
151
152 if cookies.is_empty() {
153 return Err(CookieReaderError::NoCookiesFound(domain.to_string()));
154 }
155
156 Ok(cookies)
157 }
158
159 fn read_firefox_cookies(db_path: &Path, domain: &str) -> Result<Vec<Cookie>> {
160 let conn = Connection::open(db_path)?;
161
162 let mut stmt = conn.prepare(
163 "SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite
164 FROM moz_cookies
165 WHERE host LIKE ?1 OR host LIKE ?2
166 ORDER BY creationTime DESC",
167 )?;
168
169 let domain_pattern = format!("%{domain}");
170 let dot_domain_pattern = format!("%.{domain}");
171
172 let cookie_iter = stmt.query_map(
173 rusqlite::params![&domain_pattern, &dot_domain_pattern],
174 |row| {
175 let name: String = row.get(0)?;
176 let value: String = row.get(1)?;
177 let host: String = row.get(2)?;
178 let path: String = row.get(3)?;
179 let expiry: i64 = row.get(4)?;
180 let is_secure: i32 = row.get(5)?;
181 let is_httponly: i32 = row.get(6)?;
182 let same_site: i32 = row.get(7)?;
183
184 Ok((
185 name,
186 value,
187 host,
188 path,
189 expiry,
190 is_secure,
191 is_httponly,
192 same_site,
193 ))
194 },
195 )?;
196
197 let mut cookies = Vec::new();
198
199 for cookie_result in cookie_iter {
200 let (name, value, host, path, expiry, is_secure, is_httponly, same_site) =
201 cookie_result?;
202
203 let cookie = Cookie {
204 name,
205 value,
206 domain: host,
207 path,
208 expires_utc: expiry * 1_000_000 + 11_644_473_600_000_000,
209 is_secure: is_secure != 0,
210 is_httponly: is_httponly != 0,
211 same_site,
212 };
213
214 cookies.push(cookie);
215 }
216
217 if cookies.is_empty() {
218 return Err(CookieReaderError::NoCookiesFound(domain.to_string()));
219 }
220
221 Ok(cookies)
222 }
223
224 fn read_safari_cookies(cookies_path: &Path, domain: &str) -> Result<Vec<Cookie>> {
225 let data = std::fs::read(cookies_path)?;
226
227 if data.len() < 8 {
228 return Err(CookieReaderError::NoCookiesFound(
229 "Invalid Safari cookies file".to_string(),
230 ));
231 }
232
233 let mut pos = 4;
234 let num_pages =
235 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
236 pos += 4;
237
238 let mut cookies = Vec::new();
239
240 for _ in 0..num_pages {
241 if pos + 4 > data.len() {
242 break;
243 }
244
245 let page_offset =
246 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
247 as usize;
248 pos += 4;
249
250 if page_offset >= data.len() {
251 continue;
252 }
253
254 let page_cookies = Self::parse_safari_page(&data, page_offset, domain);
255 cookies.extend(page_cookies);
256 }
257
258 if cookies.is_empty() {
259 return Err(CookieReaderError::NoCookiesFound(domain.to_string()));
260 }
261
262 Ok(cookies)
263 }
264
265 fn parse_safari_page(data: &[u8], offset: usize, domain: &str) -> Vec<Cookie> {
266 let mut pos = offset + 4;
267
268 if pos + 4 > data.len() {
269 return Vec::new();
270 }
271
272 let num_cookies =
273 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
274 pos += 4;
275
276 let mut cookie_offsets = Vec::new();
277 for _ in 0..num_cookies {
278 if pos + 4 > data.len() {
279 break;
280 }
281 let cookie_offset =
282 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
283 as usize;
284 cookie_offsets.push(offset + cookie_offset);
285 pos += 4;
286 }
287
288 let mut cookies = Vec::new();
289 for cookie_offset in cookie_offsets {
290 if let Ok(Some(cookie)) = Self::parse_safari_cookie(data, cookie_offset, domain) {
291 cookies.push(cookie);
292 }
293 }
294
295 cookies
296 }
297
298 fn parse_safari_cookie(
299 data: &[u8],
300 offset: usize,
301 filter_domain: &str,
302 ) -> Result<Option<Cookie>> {
303 let mut pos = offset;
304
305 if pos + 4 > data.len() {
306 return Ok(None);
307 }
308
309 let _cookie_size =
310 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
311 pos += 4;
312
313 if pos + 8 > data.len() {
314 return Ok(None);
315 }
316
317 pos += 8;
318
319 if pos + 16 > data.len() {
320 return Ok(None);
321 }
322
323 let flags = u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
324 pos += 4;
325
326 pos += 4;
327
328 let url_offset =
329 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
330 pos += 4;
331 let name_offset =
332 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
333 pos += 4;
334 let path_offset =
335 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
336 pos += 4;
337 let value_offset =
338 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
339 pos += 4;
340
341 if pos + 8 > data.len() {
342 return Ok(None);
343 }
344
345 let expiry_bytes = [
346 data[pos],
347 data[pos + 1],
348 data[pos + 2],
349 data[pos + 3],
350 data[pos + 4],
351 data[pos + 5],
352 data[pos + 6],
353 data[pos + 7],
354 ];
355 let expiry = f64::from_le_bytes(expiry_bytes);
356
357 let domain = Self::read_safari_string(data, offset + url_offset)?;
358 let name = Self::read_safari_string(data, offset + name_offset)?;
359 let path = Self::read_safari_string(data, offset + path_offset)?;
360 let value = Self::read_safari_string(data, offset + value_offset)?;
361
362 if !domain.contains(filter_domain) {
363 return Ok(None);
364 }
365
366 let expiry_mac_secs = f64_to_i64_saturating((expiry + 978_307_200.0).round());
369 let expires_utc = expiry_mac_secs
370 .saturating_mul(1_000_000)
371 .saturating_add(11_644_473_600_000_000);
372
373 Ok(Some(Cookie {
374 name,
375 value,
376 domain,
377 path,
378 expires_utc,
379 is_secure: (flags & 0x1) != 0,
380 is_httponly: (flags & 0x4) != 0,
381 same_site: 0,
382 }))
383 }
384
385 fn read_safari_string(data: &[u8], offset: usize) -> Result<String> {
386 let mut pos = offset;
387 let mut bytes = Vec::new();
388
389 while pos < data.len() && data[pos] != 0 {
390 bytes.push(data[pos]);
391 pos += 1;
392 }
393
394 String::from_utf8(bytes)
395 .map_err(|_| CookieReaderError::NoCookiesFound("Invalid UTF-8 in cookie".to_string()))
396 }
397}