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