use crate::{
config::{CompressionConfig, DatabaseConfig, InnerCipherConfig, KdfConfig, OuterCipherConfig},
crypt::calculate_sha256,
db::*,
error::{DatabaseIntegrityError, DatabaseKeyError, DatabaseOpenError},
format::DatabaseVersion,
key::DatabaseKey,
};
use byteorder::{ByteOrder, LittleEndian};
use cipher::generic_array::GenericArray;
use std::{collections::HashMap, convert::TryInto, str};
#[derive(Debug)]
struct KDBHeader {
pub flags: u32,
pub subversion: u32,
pub master_seed: Vec<u8>, pub encryption_iv: Vec<u8>, pub num_groups: u32,
pub num_entries: u32,
pub contents_hash: Vec<u8>, pub transform_seed: Vec<u8>, pub transform_rounds: u32,
}
const HEADER_SIZE: usize = 4 + 4 + 4 + 4 + 16 + 16 + 4 + 4 + 32 + 32 + 4;
fn parse_header(data: &[u8]) -> Result<KDBHeader, DatabaseIntegrityError> {
if data.len() < HEADER_SIZE {
return Err(DatabaseIntegrityError::InvalidFixedHeader { size: data.len() });
}
Ok(KDBHeader {
flags: LittleEndian::read_u32(&data[8..]),
subversion: LittleEndian::read_u32(&data[12..]),
master_seed: data[16..32].to_vec(),
encryption_iv: data[32..48].to_vec(),
num_groups: LittleEndian::read_u32(&data[48..]),
num_entries: LittleEndian::read_u32(&data[52..]),
contents_hash: data[56..88].to_vec(),
transform_seed: data[88..120].to_vec(),
transform_rounds: LittleEndian::read_u32(&data[120..]),
})
}
fn from_utf8(data: &[u8]) -> String {
String::from_utf8_lossy(data).trim_end_matches('\0').to_owned()
}
fn ensure_length(field_type: u16, field_size: u32, expected_field_size: u32) -> Result<(), DatabaseIntegrityError> {
if field_size == expected_field_size {
Ok(())
} else {
Err(DatabaseIntegrityError::InvalidKDBFieldLength {
field_type,
field_size,
expected_field_size,
})
}
}
fn entry_name(field_type: u16) -> &'static str {
match field_type {
0x0004 => "Title",
0x0005 => "URL",
0x0006 => "UserName",
0x0008 => "Additional",
0x000d => "BinaryDesc",
_ => {
panic!("Unsupported field type!");
}
}
}
fn collapse_tail_groups(branch: &mut Vec<NodePtr>, level: usize, root: &NodePtr) -> Option<()> {
while level < branch.len() {
let leaf = branch.pop()?; let parent = match branch.last() {
Some(parent) => parent,
None => root,
};
let count = group_get_children(parent)?.len();
group_add_child(parent, leaf, count).ok()?;
}
Some(())
}
type GidMap = HashMap<u32, Vec<String>>;
fn parse_groups(root: &NodePtr, header_num_groups: u32, data: &mut &[u8]) -> Result<GidMap, DatabaseIntegrityError> {
let mut gid_map: HashMap<u32, Vec<String>> = HashMap::new(); let mut branch: Vec<NodePtr> = Vec::new(); let mut group = rc_refcell_node(Group::new("")); let mut level: Option<u16> = None; let mut gid: Option<u32> = None; let mut group_path: Vec<String> = Vec::new(); let mut num_groups = 0; while num_groups < header_num_groups as usize {
let field_type = LittleEndian::read_u16(&data[0..]);
let field_size = LittleEndian::read_u32(&data[2..]);
let field_value = &data[6..6 + field_size as usize];
match field_type {
0x0000 => {} 0x0001 => {
ensure_length(field_type, field_size, 4)?;
gid = Some(LittleEndian::read_u32(field_value));
}
0x0002 => group.borrow_mut().set_title(Some(&from_utf8(field_value))), 0x0003..=0x0006 => {
ensure_length(field_type, field_size, 5)?;
}
0x0007 | 0x0009 => {
ensure_length(field_type, field_size, 4)?;
}
0x0008 => {
ensure_length(field_type, field_size, 2)?;
level = Some(LittleEndian::read_u16(field_value));
}
0xffff => {
ensure_length(field_type, field_size, 0)?;
let level = level.ok_or(DatabaseIntegrityError::MissingKDBGroupLevel)? as usize;
if level < branch.len() {
group_path.truncate(level);
collapse_tail_groups(&mut branch, level, root).ok_or(DatabaseIntegrityError::IncompleteKDBGroup)?;
}
if level == branch.len() {
group_path.push(group.borrow().get_title().unwrap_or("").to_string());
branch.push(group);
} else {
#[allow(clippy::cast_possible_truncation)]
return Err(DatabaseIntegrityError::InvalidKDBGroupLevel {
group_level: level as u16,
current_level: branch.len() as u16,
});
}
let group_id = gid.ok_or(DatabaseIntegrityError::MissingKDBGroupId)?;
gid_map.insert(group_id, group_path.clone());
group = rc_refcell_node(Group::new(""));
gid = None;
num_groups += 1;
}
_ => {
return Err(DatabaseIntegrityError::InvalidKDBGroupFieldType { field_type });
}
}
*data = &data[6 + field_size as usize..];
}
if gid.is_some() {
return Err(DatabaseIntegrityError::IncompleteKDBGroup);
}
collapse_tail_groups(&mut branch, 0, root).ok_or(DatabaseIntegrityError::IncompleteKDBGroup)?;
Ok(gid_map)
}
fn parse_entries(root: &NodePtr, gid_map: &GidMap, header_num_entries: u32, data: &mut &[u8]) -> Result<(), DatabaseIntegrityError> {
let mut entry = Entry::default(); let mut gid: Option<u32> = None; let mut num_entries = 0;
while num_entries < header_num_entries {
let field_type = LittleEndian::read_u16(&data[0..]);
let field_size = LittleEndian::read_u32(&data[2..]);
let field_value = &data[6..6 + field_size as usize];
match field_type {
0x0000 => {} 0x0001 => {
ensure_length(field_type, field_size, 16)?;
}
0x0002 => {
ensure_length(field_type, field_size, 4)?;
gid = Some(LittleEndian::read_u32(field_value));
}
0x0003 => {
ensure_length(field_type, field_size, 4)?;
}
0x0004 | 0x0005 | 0x0006 | 0x0008 | 0x000d => {
entry.set_unprotected_field_pair(entry_name(field_type), Some(&from_utf8(field_value)));
}
0x0007 => {
entry.set_protected_field_pair("Password", Some(field_value));
}
0x0009..=0x000c => {
ensure_length(field_type, field_size, 5)?;
}
0x000e => {
entry.set_binary_field_pair("BinaryData", Some(field_value));
}
0xffff => {
ensure_length(field_type, field_size, 0)?;
let group_id = gid.ok_or(DatabaseIntegrityError::MissingKDBGroupId)?;
let group_path: Vec<&str> = gid_map
.get(&group_id)
.ok_or(DatabaseIntegrityError::InvalidKDBGroupId { group_id })?
.iter()
.map(std::string::String::as_str)
.collect();
let group = Group::get(root, group_path.as_slice()).ok_or(DatabaseIntegrityError::IncompleteKDBGroup)?;
with_node_mut::<Group, _, _>(&group, |group| {
let count = group.get_children().len();
group.add_child(rc_refcell_node(entry), count);
Ok::<(), DatabaseIntegrityError>(())
})
.ok_or(DatabaseIntegrityError::IncompleteKDBGroup)??;
entry = Entry::default();
gid = None;
num_entries += 1;
}
_ => {
return Err(DatabaseIntegrityError::InvalidKDBEntryFieldType { field_type });
}
}
*data = &data[6 + field_size as usize..];
}
if gid.is_some() {
return Err(DatabaseIntegrityError::IncompleteKDBEntry);
}
Ok(())
}
fn parse_db(header: &KDBHeader, data: &[u8]) -> Result<NodePtr, DatabaseIntegrityError> {
let root = rc_refcell_node(Group::new("Root"));
let mut pos = data;
let gid_map = parse_groups(&root, header.num_groups, &mut pos)?;
parse_entries(&root, &gid_map, header.num_entries, &mut pos)?;
Ok(root)
}
pub(crate) fn parse_kdb(data: &[u8], db_key: &DatabaseKey) -> Result<Database, DatabaseOpenError> {
let header = parse_header(data)?;
#[allow(clippy::cast_possible_truncation)]
let version = DatabaseVersion::KDB(header.subversion as u16);
let payload_encrypted = &data[HEADER_SIZE..];
let key_elements = db_key.get_key_elements()?;
let key_elements: Vec<&[u8]> = key_elements.iter().map(|v| &v[..]).collect();
let composite_key = if key_elements.len() == 1 {
let key_element: [u8; 32] = key_elements[0].try_into().unwrap();
GenericArray::from(key_element) } else {
calculate_sha256(&key_elements) };
let kdf_config = KdfConfig::Aes {
rounds: u64::from(header.transform_rounds),
};
let transformed_key = kdf_config.get_kdf_seeded(&header.transform_seed).transform_key(&composite_key)?;
let master_key = calculate_sha256(&[&header.master_seed, transformed_key.as_slice()]);
let outer_cipher_config = if header.flags & 2 != 0 {
OuterCipherConfig::AES256
} else if header.flags & 8 != 0 {
OuterCipherConfig::Twofish
} else {
return Err(DatabaseIntegrityError::InvalidFixedCipherID { cid: header.flags }.into());
};
let payload_padded = outer_cipher_config
.get_cipher(master_key.as_slice(), header.encryption_iv.as_ref())?
.decrypt(payload_encrypted)?;
let padlen = payload_padded[payload_padded.len() - 1] as usize;
let payload = &payload_padded[..payload_padded.len() - padlen];
let hash = calculate_sha256(&[payload]);
if header.contents_hash != hash.as_slice() {
return Err(DatabaseKeyError::IncorrectKey.into());
}
let root_group = parse_db(&header, payload)?;
let config = DatabaseConfig {
version,
outer_cipher_config,
compression_config: CompressionConfig::None,
inner_cipher_config: InnerCipherConfig::Plain,
kdf_config,
public_custom_data: Default::default(),
};
Ok(Database {
config,
header_attachments: Vec::default(),
root: root_group.into(),
deleted_objects: DeletedObjects::default(),
meta: Meta::new(),
})
}