Skip to main content

seher/browser/
cookie_reader.rs

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}