use crate::crypto::{CryptoError, Result};
use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
use hmac::{Hmac, KeyInit, Mac};
use security_framework::os::macos::keychain::SecKeychain;
use sha1::Sha1;
type HmacSha1 = Hmac<Sha1>;
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
const SALT: &[u8] = b"saltysalt";
const IV: &[u8] = b" "; const KEY_LENGTH: usize = 16;
const ITERATIONS: u32 = 1003;
pub fn decrypt(encrypted_value: &[u8]) -> Result<String> {
if encrypted_value.len() < 3 {
return Ok(String::new());
}
let version = &encrypted_value[..3];
match version {
b"v10" | b"v11" => {
let encrypted = &encrypted_value[3..];
let key = get_encryption_key()?;
decrypt_aes_cbc(&key, encrypted)
}
_ => String::from_utf8(encrypted_value.to_vec())
.map_err(|e| CryptoError::DecryptionFailed(e.to_string())),
}
}
fn get_encryption_key() -> Result<Vec<u8>> {
let password = get_chrome_password()?;
let mut key = vec![0u8; KEY_LENGTH];
pbkdf2_hmac_sha1(&password, SALT, ITERATIONS, &mut key);
Ok(key)
}
#[expect(clippy::expect_used)]
fn pbkdf2_hmac_sha1(password: &[u8], salt: &[u8], iterations: u32, output: &mut [u8]) {
const SHA1_LEN: usize = 20;
let block_count = output.len().div_ceil(SHA1_LEN);
for block_idx in 1..=block_count {
let mut mac = HmacSha1::new_from_slice(password).expect("HMAC accepts any key size");
mac.update(salt);
mac.update(
&u32::try_from(block_idx)
.expect("block index overflow")
.to_be_bytes(),
);
let mut u = mac.finalize().into_bytes();
let mut t = u;
for _ in 1..iterations {
let mut mac = HmacSha1::new_from_slice(password).expect("HMAC accepts any key size");
mac.update(&u);
u = mac.finalize().into_bytes();
for (t_val, u_val) in t.iter_mut().zip(u.iter()) {
*t_val ^= u_val;
}
}
let start = (block_idx - 1) * SHA1_LEN;
let end = (start + SHA1_LEN).min(output.len());
output[start..end].copy_from_slice(&t[..end - start]);
}
}
fn get_chrome_password() -> Result<Vec<u8>> {
if let Ok(pw) = get_password_from_cli() {
return Ok(pw);
}
get_password_from_keychain()
}
fn get_password_from_keychain() -> Result<Vec<u8>> {
let keychain = SecKeychain::default()
.map_err(|e| CryptoError::KeychainError(format!("Failed to access keychain: {e}")))?;
let (password_data, _item) = keychain
.find_generic_password("Chrome Safe Storage", "Chrome")
.map_err(|e| {
CryptoError::KeychainError(format!("Failed to find Chrome Safe Storage: {e}"))
})?;
Ok(password_data.as_ref().to_vec())
}
fn get_password_from_cli() -> Result<Vec<u8>> {
let output = std::process::Command::new("security")
.args([
"find-generic-password",
"-s",
"Chrome Safe Storage",
"-a",
"Chrome",
"-w",
])
.output()
.map_err(|e| CryptoError::KeychainError(format!("Failed to run security command: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CryptoError::KeychainError(format!(
"security command failed: {}",
stderr.trim()
)));
}
let password = String::from_utf8_lossy(&output.stdout);
Ok(password.trim_end_matches('\n').as_bytes().to_vec())
}
fn decrypt_aes_cbc(key: &[u8], encrypted: &[u8]) -> Result<String> {
let cipher = Aes128CbcDec::new_from_slices(key, IV)
.map_err(|e| CryptoError::DecryptionFailed(format!("Invalid key or IV length: {e}")))?;
let mut buffer = encrypted.to_vec();
let decrypted = cipher
.decrypt_padded::<cbc::cipher::block_padding::Pkcs7>(&mut buffer)
.map_err(|e| CryptoError::DecryptionFailed(format!("AES-CBC decryption failed: {e:?}")))?;
if let Ok(s) = String::from_utf8(decrypted.to_vec()) {
return Ok(s);
}
for offset in 1..decrypted.len().min(64) {
if let Ok(s) = std::str::from_utf8(&decrypted[offset..]) {
let s = s.strip_prefix('`').unwrap_or(s);
return Ok(s.to_string());
}
}
Err(CryptoError::DecryptionFailed(format!(
"UTF-8 conversion failed (first 32 bytes: {:02x?})",
&decrypted[..decrypted.len().min(32)]
)))
}