use std::collections::HashMap;
use std::fmt;
use crate::crypto::{kc_decrypt, private_key_decrypt};
use crate::error::{Error, Result};
use crate::logger::Logger;
use crate::parse::{
DbBlob, KEY_BLOB_LEN, KEY_BLOB_MAGIC, SECURE_STORAGE_GROUP, TableHeader, parse_key_blob,
parse_ssgp,
};
use crate::record::{Record, parse_record};
use crate::schema::DbSchema;
use crate::tables;
use crate::types::{Certificate, GenericPassword, InternetPassword, PrivateKey};
pub(crate) const PRIVATE_KEY_NAME_LEN: usize = 12;
pub(crate) const KEY_LIST_INDEX_LEN: usize = 20;
pub(crate) type KeyList = HashMap<Vec<u8>, Vec<u8>>;
pub(crate) enum State {
Locked,
Partial,
Unlocked { db_key: Vec<u8>, key_list: KeyList },
}
pub struct Keychain {
pub(crate) buf: Vec<u8>,
pub(crate) schema: DbSchema,
pub(crate) tables_map: HashMap<u32, TableHeader>,
pub(crate) db_blob: DbBlob,
pub(crate) blob_base_addr: usize,
pub(crate) state: State,
pub(crate) logger: Box<dyn Logger>,
}
impl fmt::Debug for Keychain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Keychain")
.field("unlocked", &self.unlocked())
.field("partial", &matches!(self.state, State::Partial))
.field("tables", &self.tables_map.len())
.field("buf_len", &self.buf.len())
.finish_non_exhaustive()
}
}
impl Keychain {
pub(crate) fn new_locked(
buf: Vec<u8>,
schema: DbSchema,
tables_map: HashMap<u32, TableHeader>,
db_blob: DbBlob,
blob_base_addr: usize,
logger: Box<dyn Logger>,
) -> Self {
Self {
buf,
schema,
tables_map,
db_blob,
blob_base_addr,
state: State::Locked,
logger,
}
}
pub fn generic_passwords(&self) -> Result<Vec<GenericPassword>> {
let records = self.iterate_records(tables::TABLE_GENERIC_PASSWORD)?;
let mut out = Vec::with_capacity(records.len());
let mut decrypted = 0_usize;
let mut failed = 0_usize;
for rec in &records {
let result = self.decrypt_blob(rec);
if result.is_ok() {
decrypted += 1;
} else {
failed += 1;
}
let password = result.ok();
out.push(GenericPassword {
service: rec.read_string_attr(tables::ATTR_SERVICE_NAME),
account: rec.read_string_attr(tables::ATTR_ACCOUNT_NAME),
password,
description: rec.read_string_attr(tables::ATTR_DESCRIPTION),
comment: rec.read_string_attr(tables::ATTR_COMMENT),
creator: rec.read_fourcc_attr(tables::ATTR_CREATOR),
type_: rec.read_fourcc_attr(tables::ATTR_TYPE),
print_name: rec.read_string_attr(tables::ATTR_PRINT_NAME),
alias: rec.read_string_attr(tables::ATTR_ALIAS),
created: rec.read_time_attr(tables::ATTR_CREATED),
modified: rec.read_time_attr(tables::ATTR_MODIFIED),
});
}
self.logger.debug(
"GenericPasswords extracted",
&[
("total", &out.len()),
("decrypted", &decrypted),
("failed", &failed),
],
);
Ok(out)
}
pub fn internet_passwords(&self) -> Result<Vec<InternetPassword>> {
let records = self.iterate_records(tables::TABLE_INTERNET_PASSWORD)?;
let mut out = Vec::with_capacity(records.len());
let mut decrypted = 0_usize;
let mut failed = 0_usize;
for rec in &records {
let result = self.decrypt_blob(rec);
if result.is_ok() {
decrypted += 1;
} else {
failed += 1;
}
let password = result.ok();
out.push(InternetPassword {
server: rec.read_string_attr(tables::ATTR_SERVER),
account: rec.read_string_attr(tables::ATTR_ACCOUNT_NAME),
password,
security_domain: rec.read_string_attr(tables::ATTR_SECURITY_DOMAIN),
protocol: rec.read_fourcc_attr(tables::ATTR_PROTOCOL),
auth_type: rec.read_fourcc_attr(tables::ATTR_AUTH_TYPE),
port: rec.read_u32_attr(tables::ATTR_PORT),
path: rec.read_string_attr(tables::ATTR_PATH),
description: rec.read_string_attr(tables::ATTR_DESCRIPTION),
comment: rec.read_string_attr(tables::ATTR_COMMENT),
creator: rec.read_fourcc_attr(tables::ATTR_CREATOR),
type_: rec.read_fourcc_attr(tables::ATTR_TYPE),
print_name: rec.read_string_attr(tables::ATTR_PRINT_NAME),
alias: rec.read_string_attr(tables::ATTR_ALIAS),
created: rec.read_time_attr(tables::ATTR_CREATED),
modified: rec.read_time_attr(tables::ATTR_MODIFIED),
});
}
self.logger.debug(
"InternetPasswords extracted",
&[
("total", &out.len()),
("decrypted", &decrypted),
("failed", &failed),
],
);
Ok(out)
}
pub fn private_keys(&self) -> Result<Vec<PrivateKey>> {
let records = self.iterate_records(tables::TABLE_PRIVATE_KEY)?;
let mut out = Vec::new();
let mut decrypted = 0_usize;
let mut failed = 0_usize;
for rec in &records {
match self.decrypt_private_key(rec) {
Ok(pk) => {
decrypted += 1;
out.push(pk);
}
Err(_) if matches!(self.state, State::Partial) => {
failed += 1;
out.push(PrivateKey {
print_name: rec.read_string_attr(tables::ATTR_PRINT_NAME),
label: rec.read_string_attr(tables::ATTR_LABEL),
key_class: rec.read_u32_attr(tables::ATTR_KEY_CLASS),
key_type: rec.read_u32_attr(tables::ATTR_KEY_TYPE),
key_size: rec.read_u32_attr(tables::ATTR_KEY_SIZE_IN_BITS),
..PrivateKey::default()
});
}
Err(_) => {
failed += 1;
}
}
}
self.logger.debug(
"PrivateKeys extracted",
&[
("total", &out.len()),
("decrypted", &decrypted),
("failed", &failed),
],
);
Ok(out)
}
pub fn certificates(&self) -> Result<Vec<Certificate>> {
let records = self.iterate_records(tables::TABLE_X509_CERTIFICATE)?;
let mut out = Vec::with_capacity(records.len());
for rec in &records {
out.push(Certificate {
data: rec.blob_data.to_vec(),
type_: rec.read_u32_attr(tables::ATTR_CERT_TYPE),
encoding: rec.read_u32_attr(tables::ATTR_CERT_ENCODING),
print_name: rec.read_string_attr(tables::ATTR_CERT_LABEL),
subject: rec.read_blob_attr(tables::ATTR_SUBJECT),
issuer: rec.read_blob_attr(tables::ATTR_ISSUER),
serial: rec.read_blob_attr(tables::ATTR_SERIAL),
});
}
Ok(out)
}
pub fn password_hash(&self) -> Result<String> {
let start = self
.blob_base_addr
.saturating_add(self.db_blob.start_crypto_blob as usize);
let end = self
.blob_base_addr
.saturating_add(self.db_blob.total_length as usize);
if start >= end || end > self.buf.len() {
return Err(Error::ParseFailed("encrypted db key bounds invalid".into()));
}
let cipher = self
.buf
.get(start..end)
.ok_or_else(|| Error::ParseFailed("encrypted db key slice out of bounds".into()))?;
Ok(format!(
"$keychain$*{}*{}*{}",
hex::encode(self.db_blob.salt),
hex::encode(self.db_blob.iv),
hex::encode(cipher),
))
}
fn iterate_records(&self, table_id: u32) -> Result<Vec<Record<'_>>> {
if matches!(self.state, State::Locked) {
return Err(Error::Locked);
}
let Some(table) = self.tables_map.get(&table_id) else {
return Ok(Vec::new());
};
let Some(schema) = self.schema.for_table(table_id) else {
return Err(Error::ParseFailed(format!(
"no schema for table 0x{table_id:08X}"
)));
};
let mut records = Vec::with_capacity(table.record_offsets.len());
let mut skipped = 0_usize;
for rec_offset in &table.record_offsets {
let abs_offset = table.base_offset.saturating_add(*rec_offset as usize);
match parse_record(&self.buf, abs_offset, schema) {
Ok(rec) => records.push(rec),
Err(_) => skipped += 1,
}
}
if skipped > 0 {
let table_name = tables::table_id_name(table_id);
let total = table.record_offsets.len();
self.logger.warn(
"records skipped during parse",
&[
("table", &table_name),
("skipped", &skipped),
("total", &total),
],
);
}
Ok(records)
}
fn decrypt_blob(&self, rec: &Record<'_>) -> Result<Vec<u8>> {
let State::Unlocked { key_list, .. } = &self.state else {
return Err(Error::Locked);
};
let block = parse_ssgp(rec.blob_data)?;
if &block.magic != SECURE_STORAGE_GROUP {
return Err(Error::ParseFailed(format!(
"invalid SSGP magic: {:?}",
block.magic
)));
}
if block.encrypted_password.is_empty() {
return Ok(Vec::new());
}
let mut index = Vec::with_capacity(block.magic.len() + block.label.len());
index.extend_from_slice(&block.magic);
index.extend_from_slice(&block.label);
let key = key_list
.get(&index)
.ok_or_else(|| Error::ParseFailed("no matching key for SSGP label".into()))?;
kc_decrypt(key, &block.iv, block.encrypted_password)
}
fn decrypt_private_key(&self, rec: &Record<'_>) -> Result<PrivateKey> {
let State::Unlocked { db_key, .. } = &self.state else {
return Err(Error::Locked);
};
let data = rec.raw_payload;
if data.len() < KEY_BLOB_LEN {
return Err(Error::ParseFailed("private key blob too small".into()));
}
let blob = parse_key_blob(
data.get(..KEY_BLOB_LEN)
.ok_or_else(|| Error::ParseFailed("key blob slice out of bounds".into()))?,
)?;
if blob.magic != KEY_BLOB_MAGIC {
return Err(Error::ParseFailed(format!(
"unexpected keyblob magic: 0x{:08x}",
blob.magic
)));
}
let cipher_start = blob.start_crypto_blob as usize;
let cipher_end = blob.total_length as usize;
if cipher_end > data.len() || cipher_start >= cipher_end {
return Err(Error::ParseFailed("invalid cipher bounds".into()));
}
let ciphertext = data
.get(cipher_start..cipher_end)
.ok_or_else(|| Error::ParseFailed("cipher slice out of bounds".into()))?;
let plain = private_key_decrypt(ciphertext, &blob.iv, db_key)?;
let (name, key_data) = if plain.len() > PRIVATE_KEY_NAME_LEN {
let (head, tail) = plain.split_at(PRIVATE_KEY_NAME_LEN);
(String::from_utf8_lossy(head).into_owned(), tail.to_vec())
} else {
(String::new(), plain)
};
Ok(PrivateKey {
name,
data: key_data,
print_name: rec.read_string_attr(tables::ATTR_PRINT_NAME),
label: rec.read_string_attr(tables::ATTR_LABEL),
key_class: rec.read_u32_attr(tables::ATTR_KEY_CLASS),
key_type: rec.read_u32_attr(tables::ATTR_KEY_TYPE),
key_size: rec.read_u32_attr(tables::ATTR_KEY_SIZE_IN_BITS),
})
}
}