use anyhow::{Context, Result};
pub(super) const CHROME_PBKDF2_SALT: &[u8] = b"saltysalt";
pub(super) const CHROME_PBKDF2_ITERATIONS: u32 = 1003;
pub(super) const CHROME_KEY_LEN: usize = 16;
pub(super) const V10_PREFIX: &[u8; 3] = b"v10";
pub(super) const AES_CBC_IV: [u8; 16] = [b' '; 16];
pub(super) const DOMAIN_SHA256_LEN: usize = 32;
pub(super) const SCHEMA_VERSION_WITH_DOMAIN_TAG: u32 = 24;
pub fn derive_cookie_key(password: &[u8]) -> Result<Vec<u8>> {
use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
anyhow::ensure!(
CHROME_PBKDF2_ITERATIONS > 0,
"PBKDF2 key derivation requires at least one iteration"
);
let mut key = vec![0u8; CHROME_KEY_LEN];
let mut offset = 0;
let mut block_index = 1u32;
while offset < key.len() {
let mut salted_block = Vec::with_capacity(CHROME_PBKDF2_SALT.len() + 4);
salted_block.extend_from_slice(CHROME_PBKDF2_SALT);
salted_block.extend_from_slice(&block_index.to_be_bytes());
let mut mac = Hmac::<Sha1>::new_from_slice(password)
.map_err(|e| anyhow::anyhow!("HMAC key setup failed: {e}"))?;
mac.update(&salted_block);
let mut block = mac.finalize().into_bytes();
let mut u = block;
for _ in 1..CHROME_PBKDF2_ITERATIONS {
let mut mac = Hmac::<Sha1>::new_from_slice(password)
.map_err(|e| anyhow::anyhow!("HMAC key setup failed: {e}"))?;
mac.update(&u);
u = mac.finalize().into_bytes();
for (lhs, rhs) in block.iter_mut().zip(u.iter()) {
*lhs ^= *rhs;
}
}
let take = (key.len() - offset).min(block.len());
key[offset..offset + take].copy_from_slice(&block[..take]);
offset += take;
block_index = block_index
.checked_add(1)
.ok_or_else(|| anyhow::anyhow!("PBKDF2 block index overflow"))?;
}
Ok(key)
}
pub fn decrypt_cookie_value(encrypted: &[u8], key: &[u8], has_domain_tag: bool) -> Result<String> {
use aes::Aes128;
use cbc::cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7};
type Aes128CbcDec = cbc::Decryptor<Aes128>;
anyhow::ensure!(
encrypted.len() > V10_PREFIX.len(),
"Encrypted blob too short ({} bytes)",
encrypted.len()
);
anyhow::ensure!(
encrypted.starts_with(V10_PREFIX),
"Unexpected cookie prefix (expected v10, got {:?})",
&encrypted[..V10_PREFIX.len()]
);
anyhow::ensure!(key.len() == CHROME_KEY_LEN, "Key must be 16 bytes");
let ciphertext = &encrypted[V10_PREFIX.len()..];
anyhow::ensure!(
!ciphertext.is_empty() && ciphertext.len().is_multiple_of(16),
"Ciphertext length {} is not a nonzero multiple of 16",
ciphertext.len()
);
let decryptor = Aes128CbcDec::new_from_slices(key, &AES_CBC_IV)
.map_err(|e| anyhow::anyhow!("AES key/IV setup failed: {e}"))?;
let mut buf = ciphertext.to_vec();
let plaintext = decryptor
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| anyhow::anyhow!("AES-CBC unpadding failed: {e}"))?;
let value_bytes = if has_domain_tag {
anyhow::ensure!(
plaintext.len() >= DOMAIN_SHA256_LEN,
"Decrypted blob too short for domain tag ({} bytes)",
plaintext.len()
);
&plaintext[DOMAIN_SHA256_LEN..]
} else {
plaintext
};
String::from_utf8(value_bytes.to_vec()).context("Decrypted cookie value is not valid UTF-8")
}