use aes::Aes256;
use anyhow::{anyhow, Context, Result};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use cbc::cipher::block_padding::{NoPadding, Pkcs7};
use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
use des::TdesEde3;
use hmac::{Hmac, KeyInit, Mac};
use pbkdf2::pbkdf2_hmac;
use rusqlite::{Connection, OptionalExtension};
use sha1::{Digest, Sha1};
use sha2::Sha256;
use super::firefox_der::{decode_der_element, der_children, oid_to_string, DerElement};
type Aes256CbcDecryptor = cbc::Decryptor<Aes256>;
type TdesCbcDecryptor = cbc::Decryptor<TdesEde3>;
type HmacSha1 = Hmac<Sha1>;
const OID_PBES2: &str = "1.2.840.113549.1.5.13";
const OID_PBE_SHA1_3DES: &str = "1.2.840.113549.1.12.5.1.3";
const PASSWORD_CHECK: &[u8] = b"password-check\x02\x02";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("Firefox primary password is set")]
pub struct PrimaryPasswordError;
fn child(children: &[DerElement], index: usize) -> Result<&DerElement> {
children
.get(index)
.ok_or_else(|| anyhow!("NSS DER structure is missing a field"))
}
fn integer_value(element: &DerElement) -> Result<u64> {
element.content.iter().try_fold(0_u64, |value, byte| {
value
.checked_mul(256)
.map(|value| value + u64::from(*byte))
.ok_or_else(|| anyhow!("NSS DER integer is too large"))
})
}
fn password_hash(global_salt: &[u8], primary_password: &[u8]) -> Vec<u8> {
Sha1::new()
.chain_update(global_salt)
.chain_update(primary_password)
.finalize()
.to_vec()
}
fn decrypt_pbes2(
algorithm_params: &DerElement,
ciphertext: &[u8],
global_salt: &[u8],
primary_password: &[u8],
) -> Result<Vec<u8>> {
let scheme = der_children(algorithm_params)?;
let kdf = der_children(child(&scheme, 0)?)?;
let encryption_scheme = der_children(child(&scheme, 1)?)?;
let params = der_children(child(&kdf, 1)?)?;
let entry_salt = &child(¶ms, 0)?.content;
let iterations = u32::try_from(integer_value(child(¶ms, 1)?)?)
.context("NSS PBKDF2 iteration count is too large")?;
let key_length = usize::try_from(integer_value(child(¶ms, 2)?)?)
.ok()
.filter(|length| *length <= 1024)
.ok_or_else(|| anyhow!("NSS PBKDF2 key length is invalid"))?;
let iv_bytes = &child(&encryption_scheme, 1)?.content;
let hash = password_hash(global_salt, primary_password);
let mut key = vec![0_u8; key_length];
pbkdf2_hmac::<Sha256>(&hash, entry_salt, iterations, &mut key);
let iv = [&[0x04_u8, 0x0e][..], iv_bytes].concat();
Aes256CbcDecryptor::new_from_slices(&key, &iv)
.map_err(|_| anyhow!("NSS AES-256-CBC key or IV has the wrong length"))?
.decrypt_padded_vec::<Pkcs7>(ciphertext)
.map_err(|_| anyhow!("NSS AES-256-CBC padding is invalid"))
}
fn pkcs7_unpad(mut buffer: Vec<u8>) -> Vec<u8> {
if let Some(&pad) = buffer.last() {
let pad = usize::from(pad);
if pad > 0 && pad <= buffer.len() {
buffer.truncate(buffer.len() - pad);
}
}
buffer
}
fn hmac_sha1(key: &[u8], parts: &[&[u8]]) -> Result<Vec<u8>> {
let mut mac = HmacSha1::new_from_slice(key).map_err(|_| anyhow!("invalid HMAC key"))?;
for part in parts {
mac.update(part);
}
Ok(mac.finalize().into_bytes().to_vec())
}
fn decrypt_3des_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
let plaintext = TdesCbcDecryptor::new_from_slices(key, iv)
.map_err(|_| anyhow!("3DES key or IV has the wrong length"))?
.decrypt_padded_vec::<NoPadding>(ciphertext)
.map_err(|_| anyhow!("3DES ciphertext is not a whole number of blocks"))?;
Ok(pkcs7_unpad(plaintext))
}
fn decrypt_pbe_sha1_3des(
algorithm_params: &DerElement,
ciphertext: &[u8],
global_salt: &[u8],
primary_password: &[u8],
) -> Result<Vec<u8>> {
let params = der_children(algorithm_params)?;
let entry_salt = &child(¶ms, 0)?.content;
let hp = password_hash(global_salt, primary_password);
let mut pes = entry_salt.clone();
pes.resize(20, 0);
let chp = Sha1::new()
.chain_update(&hp)
.chain_update(entry_salt)
.finalize()
.to_vec();
let k1 = hmac_sha1(&chp, &[&pes, entry_salt])?;
let tk = hmac_sha1(&chp, &[&pes])?;
let k2 = hmac_sha1(&chp, &[&tk, entry_salt])?;
let derived = [k1, k2].concat();
let key = &derived[..24];
let iv = &derived[derived.len() - 8..];
decrypt_3des_cbc(key, iv, ciphertext)
}
fn decrypt_nss_blob(blob: &[u8], global_salt: &[u8], primary_password: &[u8]) -> Result<Vec<u8>> {
let top = decode_der_element(blob, 0)?;
let top_children = der_children(&top)?;
let algorithm = der_children(child(&top_children, 0)?)?;
let ciphertext = &child(&top_children, 1)?.content;
let oid = oid_to_string(&child(&algorithm, 0)?.content);
let params = child(&algorithm, 1)?;
match oid.as_str() {
OID_PBES2 => decrypt_pbes2(params, ciphertext, global_salt, primary_password),
OID_PBE_SHA1_3DES => {
decrypt_pbe_sha1_3des(params, ciphertext, global_salt, primary_password)
}
_ => Err(anyhow!("Unsupported NSS key algorithm {oid}")),
}
}
pub(crate) fn recover_firefox_key_from_database(
database: &Connection,
primary_password: &[u8],
) -> Result<Vec<u8>> {
let meta: Option<(Vec<u8>, Vec<u8>)> = database
.query_row(
"SELECT item1, item2 FROM metadata WHERE id = 'password'",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()?;
let (global_salt, check_blob) =
meta.ok_or_else(|| anyhow!("key4.db has no password metadata"))?;
let check = decrypt_nss_blob(&check_blob, &global_salt, primary_password)
.map_err(|_| anyhow!(PrimaryPasswordError))?;
if !check.starts_with(PASSWORD_CHECK) {
return Err(anyhow!(PrimaryPasswordError));
}
let private: Option<Vec<u8>> = database
.query_row("SELECT a11, a102 FROM nssPrivate", [], |row| row.get(0))
.optional()?;
let private = private.ok_or_else(|| anyhow!("key4.db has no nssPrivate key"))?;
let mut decrypted = decrypt_nss_blob(&private, &global_salt, primary_password)?;
decrypted.truncate(24);
Ok(decrypted)
}
pub(crate) fn decrypt_firefox_field(base64_value: &str, key: &[u8]) -> Result<String> {
let blob = BASE64
.decode(base64_value)
.context("NSS login field is not base64")?;
let top = decode_der_element(&blob, 0)?;
let top_children = der_children(&top)?;
let algorithm = der_children(child(&top_children, 1)?)?;
let iv = &child(&algorithm, 1)?.content;
let ciphertext = &child(&top_children, 2)?.content;
let plaintext = decrypt_3des_cbc(key, iv, ciphertext)?;
String::from_utf8(plaintext).context("decrypted login field is not UTF-8")
}