use super::{CookieSource, crypto::*, db::*};
fn encrypt_v10(inner_plaintext: &[u8], key: &[u8]) -> Vec<u8> {
use aes::Aes128;
use cbc::cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7};
type Aes128CbcEnc = cbc::Encryptor<Aes128>;
let out_len = inner_plaintext.len() + 16;
let mut out = vec![0u8; out_len];
let enc = Aes128CbcEnc::new_from_slices(key, &AES_CBC_IV).unwrap();
let ciphertext = enc
.encrypt_padded_b2b_mut::<Pkcs7>(inner_plaintext, &mut out)
.expect("output buffer is always large enough");
let mut blob = V10_PREFIX.to_vec();
blob.extend_from_slice(ciphertext);
blob
}
fn encrypt_v10_v24(value: &[u8], key: &[u8], host_key: &str) -> Vec<u8> {
use sha2::{Digest, Sha256};
let mut inner = Sha256::digest(host_key.as_bytes()).to_vec();
inner.extend_from_slice(value);
encrypt_v10(&inner, key)
}
#[test]
fn cookie_source_variants_are_distinct() {
let sources = [
CookieSource::Chrome,
CookieSource::Firefox,
CookieSource::Brave,
CookieSource::Safari,
];
for (i, a) in sources.iter().enumerate() {
for (j, b) in sources.iter().enumerate() {
if i != j {
assert_ne!(
format!("{a:?}"),
format!("{b:?}"),
"variants {i} and {j} should differ"
);
}
}
}
}
#[test]
fn from_browser_name_maps_known_browsers_consistently() {
assert!(matches!(
CookieSource::from_browser_name("brave"),
CookieSource::Brave
));
assert!(matches!(
CookieSource::from_browser_name("chrome"),
CookieSource::Chrome
));
assert!(matches!(
CookieSource::from_browser_name("firefox"),
CookieSource::Firefox
));
assert!(matches!(
CookieSource::from_browser_name("safari"),
CookieSource::Safari
));
}
#[test]
fn from_browser_name_uses_chrome_family_fallback_for_edge_and_unknown() {
assert!(matches!(
CookieSource::from_browser_name("edge"),
CookieSource::Chrome
));
assert!(matches!(
CookieSource::from_browser_name("dia"),
CookieSource::Chrome
));
assert!(matches!(
CookieSource::from_browser_name("unknown"),
CookieSource::Chrome
));
}
#[test]
fn keychain_service_brave_and_chrome_are_nonempty() {
assert!(!CookieSource::Brave.keychain_service().is_empty());
assert!(!CookieSource::Chrome.keychain_service().is_empty());
}
#[test]
fn keychain_service_firefox_safari_are_empty() {
assert!(CookieSource::Firefox.keychain_service().is_empty());
assert!(CookieSource::Safari.keychain_service().is_empty());
}
#[cfg(target_os = "macos")]
#[test]
fn cookie_paths_use_macos_locations() {
let app_support = dirs::config_dir().expect("macOS should expose Application Support");
let home = dirs::home_dir().expect("home directory should be available");
assert_eq!(
CookieSource::Brave.cookie_path().unwrap(),
app_support.join("BraveSoftware/Brave-Browser/Default/Cookies")
);
assert_eq!(
CookieSource::Chrome.cookie_path().unwrap(),
app_support.join("Google/Chrome/Default/Cookies")
);
assert_eq!(
CookieSource::Firefox.cookie_path().unwrap(),
app_support.join("Firefox/Profiles")
);
assert_eq!(
CookieSource::Safari.cookie_path().unwrap(),
home.join("Library/Cookies/Cookies.binarycookies")
);
}
#[cfg(target_os = "linux")]
#[test]
fn cookie_paths_use_linux_locations() {
let config_dir = dirs::config_dir().expect("Linux should expose ~/.config");
let home = dirs::home_dir().expect("home directory should be available");
assert_eq!(
CookieSource::Brave.cookie_path().unwrap(),
config_dir.join("BraveSoftware/Brave-Browser/Default/Cookies")
);
assert_eq!(
CookieSource::Chrome.cookie_path().unwrap(),
config_dir.join("google-chrome/Default/Cookies")
);
assert_eq!(
CookieSource::Firefox.cookie_path().unwrap(),
home.join(".mozilla/firefox")
);
assert!(
CookieSource::Safari.cookie_path().is_none(),
"Safari should not advertise a Linux cookie store"
);
}
#[cfg(target_os = "windows")]
#[test]
fn cookie_paths_use_windows_locations() {
let local_data = dirs::data_local_dir().expect("Windows should expose LocalAppData");
let config_dir = dirs::config_dir().expect("Windows should expose AppData/Roaming");
assert_eq!(
CookieSource::Brave.cookie_path().unwrap(),
local_data.join("BraveSoftware/Brave-Browser/User Data/Default/Cookies")
);
assert_eq!(
CookieSource::Chrome.cookie_path().unwrap(),
local_data.join("Google/Chrome/User Data/Default/Cookies")
);
assert_eq!(
CookieSource::Firefox.cookie_path().unwrap(),
config_dir.join("Mozilla/Firefox/Profiles")
);
assert!(
CookieSource::Safari.cookie_path().is_none(),
"Safari should not advertise a Windows cookie store"
);
}
#[cfg(not(target_os = "macos"))]
#[test]
fn non_macos_keychain_lookup_returns_fallback_error() {
let err = CookieSource::Chrome
.get_keychain_key()
.expect_err("non-macOS should not attempt native keychain lookup");
assert!(
err.to_string().contains("Python cookie fallback"),
"error should direct callers toward the Python fallback: {err}"
);
}
#[test]
fn derive_cookie_key_known_vector() {
let password = b"peanuts";
let key = derive_cookie_key(password).expect("key derivation must succeed");
assert_eq!(key.len(), CHROME_KEY_LEN, "derived key must be 16 bytes");
assert_eq!(hex::encode(key), "d9a09d499b4e1b7461f28e67972c6dbd");
}
#[test]
fn derive_cookie_key_empty_password_succeeds() {
let key = derive_cookie_key(b"").expect("derivation must not panic on empty input");
assert_eq!(key.len(), CHROME_KEY_LEN);
}
#[test]
fn derive_cookie_key_is_deterministic() {
let pw = b"my-brave-password";
let k1 = derive_cookie_key(pw).unwrap();
let k2 = derive_cookie_key(pw).unwrap();
assert_eq!(k1, k2);
}
#[test]
fn aes_iv_is_16_space_bytes_not_zero_bytes() {
assert_eq!(AES_CBC_IV, [0x20u8; 16], "IV must be 16 space bytes (0x20)");
assert_ne!(AES_CBC_IV, [0u8; 16], "IV must NOT be zero bytes");
}
#[test]
fn decrypt_cookie_value_round_trip_simple() {
let password = b"test-key";
let key = derive_cookie_key(password).unwrap();
let plaintext = b"session_token_abc123";
let blob = encrypt_v10(plaintext, &key);
let result = decrypt_cookie_value(&blob, &key, false).expect("decryption must succeed");
assert_eq!(result, "session_token_abc123");
}
#[test]
fn decrypt_cookie_value_round_trip_v24_domain_tag_stripped() {
let key = derive_cookie_key(b"brave-key").unwrap();
let host = ".linkedin.com";
let value = b"my_session_value";
let blob = encrypt_v10_v24(value, &key, host);
let result = decrypt_cookie_value(&blob, &key, true).unwrap();
assert_eq!(result, "my_session_value");
}
#[test]
fn decrypt_cookie_value_v24_without_tag_flag_returns_garbage() {
let key = derive_cookie_key(b"brave-key-2").unwrap();
let blob = encrypt_v10_v24(b"val", &key, ".example.com");
let result = decrypt_cookie_value(&blob, &key, false).unwrap_or_default();
assert_ne!(
result, "val",
"domain tag must be stripped for correct result"
);
}
#[test]
fn decrypt_cookie_value_round_trip_unicode() {
let key = derive_cookie_key(b"unicode-test").unwrap();
let plaintext = "café=résumé".as_bytes();
let blob = encrypt_v10(plaintext, &key);
let result = decrypt_cookie_value(&blob, &key, false).unwrap();
assert_eq!(result, "café=résumé");
}
#[test]
fn decrypt_cookie_value_round_trip_exactly_16_bytes() {
let key = derive_cookie_key(b"block-aligned").unwrap();
let plaintext = b"0123456789abcdef"; let blob = encrypt_v10(plaintext, &key);
let result = decrypt_cookie_value(&blob, &key, false).unwrap();
assert_eq!(result, "0123456789abcdef");
}
#[test]
fn decrypt_cookie_value_empty_blob_returns_error() {
let err = decrypt_cookie_value(&[], &[0u8; 16], false).unwrap_err();
assert!(
err.to_string().contains("too short"),
"error should mention too short: {err}"
);
}
#[test]
fn decrypt_cookie_value_wrong_prefix_returns_error() {
let mut blob = b"v11".to_vec();
blob.extend_from_slice(&[0u8; 16]);
let err = decrypt_cookie_value(&blob, &[0u8; 16], false).unwrap_err();
assert!(
err.to_string().contains("v10"),
"error should mention expected prefix: {err}"
);
}
#[test]
fn decrypt_cookie_value_only_prefix_no_ciphertext_returns_error() {
let blob = V10_PREFIX.to_vec();
let err = decrypt_cookie_value(&blob, &[0u8; 16], false).unwrap_err();
assert!(!err.to_string().is_empty());
}
#[test]
fn decrypt_cookie_value_wrong_key_length_returns_error() {
let blob = encrypt_v10(b"hello", &[0u8; 16]);
let err = decrypt_cookie_value(&blob, &[0u8; 32], false).unwrap_err();
assert!(!err.to_string().is_empty(), "should fail: {err}");
}
#[test]
fn decrypt_cookie_value_v24_too_short_for_domain_tag_returns_error() {
let key = derive_cookie_key(b"short-test").unwrap();
let blob = encrypt_v10(b"tiny", &key);
let err = decrypt_cookie_value(&blob, &key, true).unwrap_err();
assert!(
err.to_string().contains("too short"),
"error should mention too short for domain tag: {err}"
);
}
#[test]
fn build_domain_conditions_includes_exact_and_parent() {
let conds = build_domain_conditions("login.example.com");
assert!(conds.iter().any(|c| c.contains("'login.example.com'")));
assert!(conds.iter().any(|c| c.contains("'.login.example.com'")));
assert!(conds.iter().any(|c| c.contains("'.example.com'")));
assert!(conds.iter().any(|c| c.contains("'.com'")));
}
#[test]
fn build_domain_conditions_apex_domain() {
let conds = build_domain_conditions("example.com");
assert!(conds.iter().any(|c| c.contains("'example.com'")));
assert!(conds.iter().any(|c| c.contains("'.example.com'")));
}
#[test]
fn parse_cookie_rows_plaintext_value() {
let input = "session_id\tabc123\t\n";
let rows = parse_cookie_rows(input);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].name, "session_id");
assert_eq!(rows[0].value, "abc123");
assert!(rows[0].encrypted_bytes.is_empty());
}
#[test]
fn parse_cookie_rows_hex_encrypted_value() {
let hex = "763130";
let input = format!("token\t\t{hex}\n");
let rows = parse_cookie_rows(&input);
assert_eq!(rows[0].encrypted_bytes, b"v10");
}
#[test]
fn parse_cookie_rows_malformed_lines_skipped() {
let input = "good\tvalue\t\nno_tab_here\ngood2\tval2\t\n";
let rows = parse_cookie_rows(input);
assert_eq!(rows.len(), 2);
}
#[test]
fn decrypt_rows_plaintext_passthrough() {
let rows = vec![
CookieRow {
name: "a".into(),
value: "v1".into(),
encrypted_bytes: vec![],
},
CookieRow {
name: "b".into(),
value: "v2".into(),
encrypted_bytes: vec![],
},
];
let result = decrypt_rows(rows, None, false);
assert_eq!(result["a"], "v1");
assert_eq!(result["b"], "v2");
}
#[test]
fn decrypt_rows_encrypted_without_key_is_skipped() {
let key = derive_cookie_key(b"skip-test").unwrap();
let blob = encrypt_v10(b"secret", &key);
let rows = vec![CookieRow {
name: "tok".into(),
value: String::new(),
encrypted_bytes: blob,
}];
let result = decrypt_rows(rows, None, false);
assert!(!result.contains_key("tok"));
}
#[test]
fn decrypt_rows_encrypted_with_correct_key_schema_pre24() {
let key = derive_cookie_key(b"my-browser-password").unwrap();
let blob = encrypt_v10(b"my_session_value", &key);
let rows = vec![CookieRow {
name: "session".into(),
value: String::new(),
encrypted_bytes: blob,
}];
let result = decrypt_rows(rows, Some(&key), false);
assert_eq!(result["session"], "my_session_value");
}
#[test]
fn decrypt_rows_encrypted_with_correct_key_schema_v24() {
let key = derive_cookie_key(b"brave-real-password").unwrap();
let blob = encrypt_v10_v24(b"real_cookie_value", &key, ".example.com");
let rows = vec![CookieRow {
name: "auth".into(),
value: String::new(),
encrypted_bytes: blob,
}];
let result = decrypt_rows(rows, Some(&key), true);
assert_eq!(result["auth"], "real_cookie_value");
}