mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-28 06:46:08 +02:00
fix: cookie copying for wayfern
This commit is contained in:
+452
-73
@@ -7,15 +7,15 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::AppHandle;
|
||||
|
||||
/// Chromium cookie encryption/decryption support.
|
||||
/// On macOS: uses "Chromium Safe Storage" key from Keychain with PBKDF2 + AES-128-CBC.
|
||||
/// On Linux: uses os_crypt_key file from profile directory with PBKDF2 + AES-128-CBC.
|
||||
/// Chromium cookie decryption support for reading existing encrypted cookies.
|
||||
/// Writes always go through the plaintext `value` column (see `write_chrome_cookies`),
|
||||
/// so no encryption path is needed here — Chromium reads plaintext when
|
||||
/// `encrypted_value` is empty, regardless of what other cookies store.
|
||||
pub mod chrome_decrypt {
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
use std::path::Path;
|
||||
|
||||
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
|
||||
|
||||
const PBKDF2_ITERATIONS: u32 = 1;
|
||||
const KEY_LEN: usize = 16; // AES-128
|
||||
@@ -31,7 +31,6 @@ pub mod chrome_decrypt {
|
||||
/// Get the encryption key for Chrome cookies.
|
||||
/// Wayfern stores os_crypt_key as a file inside the profile's user-data-dir on all platforms.
|
||||
/// On macOS/Linux the key is a base64 string used as PBKDF2 password.
|
||||
/// On Windows the key is raw bytes (32 bytes) used directly.
|
||||
pub fn get_encryption_key(profile_data_path: &Path) -> Option<[u8; KEY_LEN]> {
|
||||
let key_file = profile_data_path.join("os_crypt_key");
|
||||
if let Ok(contents) = std::fs::read_to_string(&key_file) {
|
||||
@@ -89,28 +88,6 @@ pub mod chrome_decrypt {
|
||||
|
||||
String::from_utf8(decrypted.to_vec()).ok()
|
||||
}
|
||||
|
||||
/// Encrypt a cookie value in Chrome format (v10/v11 prefix + AES-128-CBC).
|
||||
pub fn encrypt(plaintext: &str, key: &[u8; KEY_LEN]) -> Vec<u8> {
|
||||
let pt = plaintext.as_bytes();
|
||||
let block_size = 16usize;
|
||||
// Allocate buffer with space for PKCS7 padding (up to one extra block)
|
||||
let padded_len = pt.len() + (block_size - pt.len() % block_size);
|
||||
let mut buf = vec![0u8; padded_len];
|
||||
buf[..pt.len()].copy_from_slice(pt);
|
||||
|
||||
let encrypted = Aes128CbcEnc::new(key.into(), &IV.into())
|
||||
.encrypt_padded_mut::<Pkcs7>(&mut buf, pt.len())
|
||||
.expect("encryption buffer too small");
|
||||
|
||||
let mut result = Vec::with_capacity(3 + encrypted.len());
|
||||
#[cfg(target_os = "macos")]
|
||||
result.extend_from_slice(b"v10");
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
result.extend_from_slice(b"v11");
|
||||
result.extend_from_slice(encrypted);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified cookie representation that works across both browser types
|
||||
@@ -328,7 +305,14 @@ impl CookieManager {
|
||||
Ok(cookies)
|
||||
}
|
||||
|
||||
/// Write cookies to a Firefox/Camoufox profile
|
||||
/// Write cookies to a Firefox/Camoufox profile.
|
||||
///
|
||||
/// Firefox's `moz_cookies.expiry` is "seconds since Unix epoch", so `expiry = 0`
|
||||
/// is interpreted as 1970-01-01 and purged on read. To let imported session
|
||||
/// cookies survive browser restart, we rewrite them to a far-future expiry.
|
||||
///
|
||||
/// `schemeMap` is a bitfield (1 = HTTP, 2 = HTTPS, 3 = both). Setting it based
|
||||
/// on `is_secure` preserves Firefox's scheme-bound cookie enforcement.
|
||||
fn write_firefox_cookies(
|
||||
db_path: &Path,
|
||||
cookies: &[UnifiedCookie],
|
||||
@@ -338,7 +322,22 @@ impl CookieManager {
|
||||
let mut copied = 0;
|
||||
let mut replaced = 0;
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
// Session cookies get 30 days of persistence so they survive restart.
|
||||
let session_cookie_expiry = now + 30 * 86400;
|
||||
|
||||
for cookie in cookies {
|
||||
let expiry = if cookie.expires > 0 {
|
||||
cookie.expires
|
||||
} else {
|
||||
session_cookie_expiry
|
||||
};
|
||||
// schemeMap bitfield: 1 = HTTP, 2 = HTTPS
|
||||
let scheme_map: i32 = if cookie.is_secure { 2 } else { 1 };
|
||||
|
||||
let existing: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT id FROM moz_cookies WHERE host = ?1 AND name = ?2 AND path = ?3",
|
||||
@@ -351,15 +350,17 @@ impl CookieManager {
|
||||
conn
|
||||
.execute(
|
||||
"UPDATE moz_cookies SET value = ?1, expiry = ?2, isSecure = ?3,
|
||||
isHttpOnly = ?4, sameSite = ?5, lastAccessed = ?6
|
||||
WHERE host = ?7 AND name = ?8 AND path = ?9",
|
||||
isHttpOnly = ?4, sameSite = ?5, rawSameSite = ?5,
|
||||
lastAccessed = ?6, schemeMap = ?7
|
||||
WHERE host = ?8 AND name = ?9 AND path = ?10",
|
||||
params![
|
||||
&cookie.value,
|
||||
cookie.expires,
|
||||
expiry,
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
cookie.same_site,
|
||||
cookie.last_accessed * 1_000_000,
|
||||
scheme_map,
|
||||
&cookie.domain,
|
||||
&cookie.name,
|
||||
&cookie.path,
|
||||
@@ -373,18 +374,19 @@ impl CookieManager {
|
||||
"INSERT INTO moz_cookies
|
||||
(originAttributes, name, value, host, path, expiry, lastAccessed,
|
||||
creationTime, isSecure, isHttpOnly, sameSite, rawSameSite, schemeMap)
|
||||
VALUES ('', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, 2)",
|
||||
VALUES ('', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11)",
|
||||
params![
|
||||
&cookie.name,
|
||||
&cookie.value,
|
||||
&cookie.domain,
|
||||
&cookie.path,
|
||||
cookie.expires,
|
||||
expiry,
|
||||
cookie.last_accessed * 1_000_000,
|
||||
cookie.creation_time * 1_000_000,
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
cookie.same_site,
|
||||
scheme_map,
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert cookie: {e}"))?;
|
||||
@@ -396,11 +398,17 @@ impl CookieManager {
|
||||
}
|
||||
|
||||
/// Write cookies to a Chrome/Wayfern profile.
|
||||
/// If an encryption key is available, stores cookies encrypted in encrypted_value.
|
||||
///
|
||||
/// Always writes values as plaintext in the `value` column with an empty
|
||||
/// `encrypted_value`. Chromium reads plaintext on a per-row basis when
|
||||
/// `encrypted_value` is empty, so this mixes cleanly with any pre-existing
|
||||
/// encrypted cookies in the database. We avoid encrypting on write because
|
||||
/// the os_crypt key derivation between Wayfern's runtime and an external
|
||||
/// writer is not guaranteed to match, and a ciphertext Chromium can't
|
||||
/// decrypt silently produces an empty cookie value at runtime.
|
||||
fn write_chrome_cookies(
|
||||
db_path: &Path,
|
||||
cookies: &[UnifiedCookie],
|
||||
encryption_key: Option<&[u8; 16]>,
|
||||
) -> Result<(usize, usize), String> {
|
||||
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
|
||||
|
||||
@@ -413,11 +421,14 @@ impl CookieManager {
|
||||
.as_secs() as i64;
|
||||
|
||||
for cookie in cookies {
|
||||
// Prepare value/encrypted_value based on whether we have an encryption key
|
||||
let (value_str, encrypted_bytes): (&str, Vec<u8>) = match encryption_key {
|
||||
Some(key) => ("", chrome_decrypt::encrypt(&cookie.value, key)),
|
||||
None => (cookie.value.as_str(), Vec::new()),
|
||||
};
|
||||
// Session cookies (no expiry) must have has_expires/is_persistent = 0.
|
||||
// Otherwise Chromium interprets expires_utc=0 as 1601-01-01 (expired).
|
||||
let has_expires = if cookie.expires > 0 { 1 } else { 0 };
|
||||
let is_persistent = has_expires;
|
||||
// HTTPS cookies use 443, HTTP uses 80. source_port participates in
|
||||
// Chromium's scheme-bound cookie enforcement.
|
||||
let source_port: i32 = if cookie.is_secure { 443 } else { 80 };
|
||||
let source_scheme: i32 = if cookie.is_secure { 2 } else { 1 };
|
||||
|
||||
let existing: Option<i64> = conn
|
||||
.query_row(
|
||||
@@ -430,18 +441,22 @@ impl CookieManager {
|
||||
if existing.is_some() {
|
||||
conn
|
||||
.execute(
|
||||
"UPDATE cookies SET value = ?1, encrypted_value = ?2, expires_utc = ?3, is_secure = ?4,
|
||||
is_httponly = ?5, samesite = ?6, last_access_utc = ?7, last_update_utc = ?8
|
||||
WHERE host_key = ?9 AND name = ?10 AND path = ?11",
|
||||
"UPDATE cookies SET value = ?1, encrypted_value = x'', expires_utc = ?2, is_secure = ?3,
|
||||
is_httponly = ?4, samesite = ?5, last_access_utc = ?6, last_update_utc = ?7,
|
||||
has_expires = ?8, is_persistent = ?9, source_scheme = ?10, source_port = ?11
|
||||
WHERE host_key = ?12 AND name = ?13 AND path = ?14",
|
||||
params![
|
||||
value_str,
|
||||
encrypted_bytes,
|
||||
&cookie.value,
|
||||
Self::unix_to_chrome_time(cookie.expires),
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
cookie.same_site,
|
||||
Self::unix_to_chrome_time(cookie.last_accessed),
|
||||
Self::unix_to_chrome_time(now),
|
||||
has_expires,
|
||||
is_persistent,
|
||||
source_scheme,
|
||||
source_port,
|
||||
&cookie.domain,
|
||||
&cookie.name,
|
||||
&cookie.path,
|
||||
@@ -450,29 +465,33 @@ impl CookieManager {
|
||||
.map_err(|e| format!("Failed to update cookie: {e}"))?;
|
||||
replaced += 1;
|
||||
} else {
|
||||
conn.execute(
|
||||
"INSERT INTO cookies
|
||||
conn
|
||||
.execute(
|
||||
"INSERT INTO cookies
|
||||
(creation_utc, host_key, top_frame_site_key, name, value, encrypted_value,
|
||||
path, expires_utc, is_secure, is_httponly, last_access_utc, has_expires,
|
||||
is_persistent, priority, samesite, source_scheme, source_port, source_type,
|
||||
has_cross_site_ancestor, last_update_utc)
|
||||
VALUES (?1, ?2, '', ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 1, 1, 1, ?11, 2, -1, 0, 0, ?12)",
|
||||
params![
|
||||
Self::unix_to_chrome_time(cookie.creation_time),
|
||||
&cookie.domain,
|
||||
&cookie.name,
|
||||
value_str,
|
||||
encrypted_bytes,
|
||||
&cookie.path,
|
||||
Self::unix_to_chrome_time(cookie.expires),
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
Self::unix_to_chrome_time(cookie.last_accessed),
|
||||
cookie.same_site,
|
||||
Self::unix_to_chrome_time(now),
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert cookie: {e}"))?;
|
||||
VALUES (?1, ?2, '', ?3, ?4, x'', ?5, ?6, ?7, ?8, ?9, ?10, ?11, 1, ?12, ?13, ?14, 0, 0, ?15)",
|
||||
params![
|
||||
Self::unix_to_chrome_time(cookie.creation_time),
|
||||
&cookie.domain,
|
||||
&cookie.name,
|
||||
&cookie.value,
|
||||
&cookie.path,
|
||||
Self::unix_to_chrome_time(cookie.expires),
|
||||
cookie.is_secure as i32,
|
||||
cookie.is_http_only as i32,
|
||||
Self::unix_to_chrome_time(cookie.last_accessed),
|
||||
has_expires,
|
||||
is_persistent,
|
||||
cookie.same_site,
|
||||
source_scheme,
|
||||
source_port,
|
||||
Self::unix_to_chrome_time(now),
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to insert cookie: {e}"))?;
|
||||
copied += 1;
|
||||
}
|
||||
}
|
||||
@@ -623,10 +642,7 @@ impl CookieManager {
|
||||
|
||||
let write_result = match target.browser.as_str() {
|
||||
"camoufox" => Self::write_firefox_cookies(&target_db_path, &cookies_to_copy),
|
||||
"wayfern" => {
|
||||
let key = Self::get_chrome_encryption_key(target, &profiles_dir);
|
||||
Self::write_chrome_cookies(&target_db_path, &cookies_to_copy, key.as_ref())
|
||||
}
|
||||
"wayfern" => Self::write_chrome_cookies(&target_db_path, &cookies_to_copy),
|
||||
_ => {
|
||||
results.push(CookieCopyResult {
|
||||
target_profile_id: target_id.clone(),
|
||||
@@ -891,10 +907,7 @@ impl CookieManager {
|
||||
|
||||
let write_result = match profile.browser.as_str() {
|
||||
"camoufox" => Self::write_firefox_cookies(&db_path, &cookies),
|
||||
"wayfern" => {
|
||||
let key = Self::get_chrome_encryption_key(profile, &profiles_dir);
|
||||
Self::write_chrome_cookies(&db_path, &cookies, key.as_ref())
|
||||
}
|
||||
"wayfern" => Self::write_chrome_cookies(&db_path, &cookies),
|
||||
_ => return Err(format!("Unsupported browser type: {}", profile.browser)),
|
||||
};
|
||||
|
||||
@@ -1158,4 +1171,370 @@ mod tests {
|
||||
let chrome = CookieManager::unix_to_chrome_time(unix);
|
||||
assert_eq!(CookieManager::chrome_time_to_unix(chrome), unix);
|
||||
}
|
||||
|
||||
/// Set up a minimal Chrome cookie SQLite schema for testing writes.
|
||||
fn create_chrome_cookies_db(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn
|
||||
.execute_batch(
|
||||
"CREATE TABLE cookies (
|
||||
creation_utc INTEGER NOT NULL,
|
||||
host_key TEXT NOT NULL,
|
||||
top_frame_site_key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
encrypted_value BLOB NOT NULL DEFAULT '',
|
||||
path TEXT NOT NULL,
|
||||
expires_utc INTEGER NOT NULL,
|
||||
is_secure INTEGER NOT NULL,
|
||||
is_httponly INTEGER NOT NULL,
|
||||
last_access_utc INTEGER NOT NULL,
|
||||
has_expires INTEGER NOT NULL DEFAULT 1,
|
||||
is_persistent INTEGER NOT NULL DEFAULT 1,
|
||||
priority INTEGER NOT NULL DEFAULT 1,
|
||||
samesite INTEGER NOT NULL DEFAULT -1,
|
||||
source_scheme INTEGER NOT NULL DEFAULT 0,
|
||||
source_port INTEGER NOT NULL DEFAULT -1,
|
||||
last_update_utc INTEGER NOT NULL DEFAULT 0,
|
||||
source_type INTEGER NOT NULL DEFAULT 0,
|
||||
has_cross_site_ancestor INTEGER NOT NULL DEFAULT 0
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Set up a minimal Firefox moz_cookies SQLite schema for testing writes.
|
||||
fn create_firefox_cookies_db(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn
|
||||
.execute_batch(
|
||||
"CREATE TABLE moz_cookies (
|
||||
id INTEGER PRIMARY KEY,
|
||||
originAttributes TEXT NOT NULL DEFAULT '',
|
||||
name TEXT,
|
||||
value TEXT,
|
||||
host TEXT,
|
||||
path TEXT,
|
||||
expiry INTEGER,
|
||||
lastAccessed INTEGER,
|
||||
creationTime INTEGER,
|
||||
isSecure INTEGER,
|
||||
isHttpOnly INTEGER,
|
||||
inBrowserElement INTEGER DEFAULT 0,
|
||||
sameSite INTEGER DEFAULT 0,
|
||||
rawSameSite INTEGER DEFAULT 0,
|
||||
schemeMap INTEGER DEFAULT 0,
|
||||
CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_chrome_cookies_stores_plaintext_values() {
|
||||
let tmp = std::env::temp_dir().join(format!("donut_cookie_test_{}.db", uuid::Uuid::new_v4()));
|
||||
create_chrome_cookies_db(&tmp);
|
||||
|
||||
let cookies = vec![UnifiedCookie {
|
||||
name: "c_user".to_string(),
|
||||
value: "100012345".to_string(),
|
||||
domain: ".facebook.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1800000000,
|
||||
is_secure: true,
|
||||
is_http_only: true,
|
||||
same_site: 0,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
}];
|
||||
|
||||
let (inserted, replaced) = CookieManager::write_chrome_cookies(&tmp, &cookies).unwrap();
|
||||
assert_eq!(inserted, 1);
|
||||
assert_eq!(replaced, 0);
|
||||
|
||||
let conn = Connection::open(&tmp).unwrap();
|
||||
let (value, encrypted, has_expires, is_persistent, source_scheme, source_port): (
|
||||
String,
|
||||
Vec<u8>,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
) = conn
|
||||
.query_row(
|
||||
"SELECT value, encrypted_value, has_expires, is_persistent, source_scheme, source_port
|
||||
FROM cookies WHERE name = ?1",
|
||||
params!["c_user"],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
row.get(5)?,
|
||||
))
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Core fix: plaintext in value, empty encrypted_value
|
||||
assert_eq!(value, "100012345");
|
||||
assert!(encrypted.is_empty());
|
||||
// Persistent cookie since expires > 0
|
||||
assert_eq!(has_expires, 1);
|
||||
assert_eq!(is_persistent, 1);
|
||||
// Secure cookie gets HTTPS scheme + port 443
|
||||
assert_eq!(source_scheme, 2);
|
||||
assert_eq!(source_port, 443);
|
||||
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_chrome_cookies_session_cookie_not_expired() {
|
||||
let tmp = std::env::temp_dir().join(format!("donut_cookie_test_{}.db", uuid::Uuid::new_v4()));
|
||||
create_chrome_cookies_db(&tmp);
|
||||
|
||||
let cookies = vec![UnifiedCookie {
|
||||
name: "session".to_string(),
|
||||
value: "abc".to_string(),
|
||||
domain: ".example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 0, // session cookie
|
||||
is_secure: false,
|
||||
is_http_only: false,
|
||||
same_site: 0,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
}];
|
||||
|
||||
CookieManager::write_chrome_cookies(&tmp, &cookies).unwrap();
|
||||
|
||||
let conn = Connection::open(&tmp).unwrap();
|
||||
let (has_expires, is_persistent, source_scheme, source_port): (i32, i32, i32, i32) = conn
|
||||
.query_row(
|
||||
"SELECT has_expires, is_persistent, source_scheme, source_port
|
||||
FROM cookies WHERE name = ?1",
|
||||
params!["session"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Session cookie must not be persistent — otherwise Chromium treats
|
||||
// expires_utc=0 as 1601-01-01 (immediately expired).
|
||||
assert_eq!(has_expires, 0);
|
||||
assert_eq!(is_persistent, 0);
|
||||
// Non-secure cookie uses HTTP scheme + port 80
|
||||
assert_eq!(source_scheme, 1);
|
||||
assert_eq!(source_port, 80);
|
||||
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_chrome_cookies_replaces_existing() {
|
||||
let tmp = std::env::temp_dir().join(format!("donut_cookie_test_{}.db", uuid::Uuid::new_v4()));
|
||||
create_chrome_cookies_db(&tmp);
|
||||
|
||||
let cookie = UnifiedCookie {
|
||||
name: "token".to_string(),
|
||||
value: "v1".to_string(),
|
||||
domain: ".example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1800000000,
|
||||
is_secure: true,
|
||||
is_http_only: false,
|
||||
same_site: 1,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
};
|
||||
|
||||
let (inserted, _) =
|
||||
CookieManager::write_chrome_cookies(&tmp, std::slice::from_ref(&cookie)).unwrap();
|
||||
assert_eq!(inserted, 1);
|
||||
|
||||
let mut updated = cookie.clone();
|
||||
updated.value = "v2".to_string();
|
||||
let (inserted, replaced) =
|
||||
CookieManager::write_chrome_cookies(&tmp, std::slice::from_ref(&updated)).unwrap();
|
||||
assert_eq!(inserted, 0);
|
||||
assert_eq!(replaced, 1);
|
||||
|
||||
let conn = Connection::open(&tmp).unwrap();
|
||||
let (value, encrypted): (String, Vec<u8>) = conn
|
||||
.query_row(
|
||||
"SELECT value, encrypted_value FROM cookies WHERE name = ?1",
|
||||
params!["token"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(value, "v2");
|
||||
assert!(encrypted.is_empty());
|
||||
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
}
|
||||
|
||||
/// Wayfern → Camoufox: write cookies to a Chrome DB, read them back, and
|
||||
/// verify they land in a Firefox DB with values intact, correct schemeMap,
|
||||
/// and non-expired timestamps. This is the path exercised by the
|
||||
/// "copy cookies between profiles of different browser types" feature.
|
||||
#[test]
|
||||
fn test_wayfern_cookies_transfer_to_camoufox() {
|
||||
let chrome_db =
|
||||
std::env::temp_dir().join(format!("donut_xbrowser_chrome_{}.db", uuid::Uuid::new_v4()));
|
||||
let ff_db = std::env::temp_dir().join(format!("donut_xbrowser_ff_{}.db", uuid::Uuid::new_v4()));
|
||||
create_chrome_cookies_db(&chrome_db);
|
||||
create_firefox_cookies_db(&ff_db);
|
||||
|
||||
// Simulate cookies in a Wayfern profile: a persistent cookie and a
|
||||
// session cookie, both from a real-world HTTPS site.
|
||||
let source_cookies = vec![
|
||||
UnifiedCookie {
|
||||
name: "c_user".to_string(),
|
||||
value: "100012345678".to_string(),
|
||||
domain: ".facebook.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1900000000, // persistent, far in the future
|
||||
is_secure: true,
|
||||
is_http_only: true,
|
||||
same_site: 0,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
},
|
||||
UnifiedCookie {
|
||||
name: "xs".to_string(),
|
||||
value: "sessionvalue".to_string(),
|
||||
domain: ".facebook.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 0, // session cookie
|
||||
is_secure: true,
|
||||
is_http_only: true,
|
||||
same_site: 1,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
},
|
||||
];
|
||||
CookieManager::write_chrome_cookies(&chrome_db, &source_cookies).unwrap();
|
||||
|
||||
// Read back from the Chrome DB (as if reading from the Wayfern profile).
|
||||
let from_chrome = CookieManager::read_chrome_cookies(&chrome_db, None).unwrap();
|
||||
assert_eq!(from_chrome.len(), 2);
|
||||
let c_user_src = from_chrome.iter().find(|c| c.name == "c_user").unwrap();
|
||||
assert_eq!(c_user_src.value, "100012345678");
|
||||
let xs_src = from_chrome.iter().find(|c| c.name == "xs").unwrap();
|
||||
assert_eq!(xs_src.value, "sessionvalue");
|
||||
|
||||
// Write them into the Camoufox (Firefox) DB.
|
||||
let (inserted, replaced) = CookieManager::write_firefox_cookies(&ff_db, &from_chrome).unwrap();
|
||||
assert_eq!(inserted, 2);
|
||||
assert_eq!(replaced, 0);
|
||||
|
||||
// Read back from Firefox and verify values survived the round trip.
|
||||
let from_ff = CookieManager::read_firefox_cookies(&ff_db).unwrap();
|
||||
assert_eq!(from_ff.len(), 2);
|
||||
let c_user = from_ff.iter().find(|c| c.name == "c_user").unwrap();
|
||||
assert_eq!(c_user.value, "100012345678");
|
||||
assert_eq!(c_user.domain, ".facebook.com");
|
||||
assert!(c_user.is_secure);
|
||||
assert!(c_user.is_http_only);
|
||||
let xs = from_ff.iter().find(|c| c.name == "xs").unwrap();
|
||||
assert_eq!(xs.value, "sessionvalue");
|
||||
|
||||
// Raw DB checks against the Firefox schema — these would catch the bugs
|
||||
// that caused issue #265 on the Chrome path (plaintext, correct expiry,
|
||||
// correct schemeMap).
|
||||
let conn = Connection::open(&ff_db).unwrap();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
|
||||
let (c_user_expiry, c_user_scheme): (i64, i32) = conn
|
||||
.query_row(
|
||||
"SELECT expiry, schemeMap FROM moz_cookies WHERE name = ?1",
|
||||
params!["c_user"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
c_user_expiry > now,
|
||||
"persistent cookie must not be expired in firefox (expiry={c_user_expiry}, now={now})"
|
||||
);
|
||||
assert_eq!(c_user_scheme, 2, "HTTPS cookie must have schemeMap=2");
|
||||
|
||||
let (xs_expiry, xs_scheme): (i64, i32) = conn
|
||||
.query_row(
|
||||
"SELECT expiry, schemeMap FROM moz_cookies WHERE name = ?1",
|
||||
params!["xs"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
xs_expiry > now,
|
||||
"session cookie must be rewritten to a future expiry (got {xs_expiry}, now={now})"
|
||||
);
|
||||
assert_eq!(xs_scheme, 2);
|
||||
|
||||
let _ = std::fs::remove_file(&chrome_db);
|
||||
let _ = std::fs::remove_file(&ff_db);
|
||||
}
|
||||
|
||||
/// Camoufox → Wayfern: the reverse direction. Ensures the Chrome writer
|
||||
/// still produces plaintext values / empty encrypted_value when fed cookies
|
||||
/// that originated in Firefox.
|
||||
#[test]
|
||||
fn test_camoufox_cookies_transfer_to_wayfern() {
|
||||
let ff_db =
|
||||
std::env::temp_dir().join(format!("donut_xbrowser_rev_ff_{}.db", uuid::Uuid::new_v4()));
|
||||
let chrome_db = std::env::temp_dir().join(format!(
|
||||
"donut_xbrowser_rev_chrome_{}.db",
|
||||
uuid::Uuid::new_v4()
|
||||
));
|
||||
create_firefox_cookies_db(&ff_db);
|
||||
create_chrome_cookies_db(&chrome_db);
|
||||
|
||||
let source_cookies = vec![UnifiedCookie {
|
||||
name: "sessionid".to_string(),
|
||||
value: "abc123def456".to_string(),
|
||||
domain: ".example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
expires: 1900000000,
|
||||
is_secure: true,
|
||||
is_http_only: false,
|
||||
same_site: 1,
|
||||
creation_time: 1700000000,
|
||||
last_accessed: 1700000000,
|
||||
}];
|
||||
CookieManager::write_firefox_cookies(&ff_db, &source_cookies).unwrap();
|
||||
|
||||
let from_ff = CookieManager::read_firefox_cookies(&ff_db).unwrap();
|
||||
assert_eq!(from_ff.len(), 1);
|
||||
assert_eq!(from_ff[0].value, "abc123def456");
|
||||
|
||||
CookieManager::write_chrome_cookies(&chrome_db, &from_ff).unwrap();
|
||||
|
||||
let from_chrome = CookieManager::read_chrome_cookies(&chrome_db, None).unwrap();
|
||||
assert_eq!(from_chrome.len(), 1);
|
||||
assert_eq!(from_chrome[0].value, "abc123def456");
|
||||
|
||||
// Verify the raw DB state on the Chrome side — plaintext value, empty
|
||||
// encrypted_value, persistent, HTTPS.
|
||||
let conn = Connection::open(&chrome_db).unwrap();
|
||||
let (value, encrypted, is_persistent, source_scheme): (String, Vec<u8>, i32, i32) = conn
|
||||
.query_row(
|
||||
"SELECT value, encrypted_value, is_persistent, source_scheme
|
||||
FROM cookies WHERE name = ?1",
|
||||
params!["sessionid"],
|
||||
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(value, "abc123def456");
|
||||
assert!(encrypted.is_empty());
|
||||
assert_eq!(is_persistent, 1);
|
||||
assert_eq!(source_scheme, 2);
|
||||
|
||||
let _ = std::fs::remove_file(&ff_db);
|
||||
let _ = std::fs::remove_file(&chrome_db);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user