pub(crate) mod entry;
pub(crate) mod group;
pub(crate) mod meta;
pub(crate) mod node;
#[cfg(feature = "totp")]
pub(crate) mod otp;
use std::{collections::HashMap, str::FromStr};
use chrono::NaiveDateTime;
use uuid::Uuid;
pub use crate::db::{
entry::{AutoType, AutoTypeAssociation, Entry, History, Value},
group::Group,
meta::{BinaryAttachment, BinaryAttachments, CustomIcons, Icon, MemoryProtection, Meta},
node::{Node, NodeIter, NodeRef, NodeRefMut},
};
#[cfg(feature = "totp")]
pub use crate::db::otp::{TOTPAlgorithm, TOTP};
use crate::{
config::DatabaseConfig,
error::{DatabaseIntegrityError, DatabaseOpenError, ParseColorError},
format::{
kdb::parse_kdb,
kdbx3::{decrypt_kdbx3, parse_kdbx3},
kdbx4::{decrypt_kdbx4, parse_kdbx4},
DatabaseVersion,
},
key::DatabaseKey,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct Database {
pub config: DatabaseConfig,
pub header_attachments: Vec<HeaderAttachment>,
pub root: Group,
pub deleted_objects: DeletedObjects,
pub meta: Meta,
}
impl Database {
pub fn open(source: &mut dyn std::io::Read, key: DatabaseKey) -> Result<Database, DatabaseOpenError> {
let mut data = Vec::new();
source.read_to_end(&mut data)?;
Database::parse(data.as_ref(), key)
}
pub fn parse(data: &[u8], key: DatabaseKey) -> Result<Database, DatabaseOpenError> {
let database_version = DatabaseVersion::parse(data)?;
match database_version {
DatabaseVersion::KDB(_) => parse_kdb(data, &key),
DatabaseVersion::KDB2(_) => Err(DatabaseOpenError::UnsupportedVersion.into()),
DatabaseVersion::KDB3(_) => parse_kdbx3(data, &key),
DatabaseVersion::KDB4(_) => parse_kdbx4(data, &key),
}
}
#[cfg(feature = "save_kdbx4")]
pub fn save(
&self,
destination: &mut dyn std::io::Write,
key: DatabaseKey,
) -> Result<(), crate::error::DatabaseSaveError> {
use crate::error::DatabaseSaveError;
use crate::format::kdbx4::dump_kdbx4;
match self.config.version {
DatabaseVersion::KDB(_) => Err(DatabaseSaveError::UnsupportedVersion.into()),
DatabaseVersion::KDB2(_) => Err(DatabaseSaveError::UnsupportedVersion.into()),
DatabaseVersion::KDB3(_) => Err(DatabaseSaveError::UnsupportedVersion.into()),
DatabaseVersion::KDB4(_) => dump_kdbx4(self, &key, destination),
}
}
pub fn get_xml(source: &mut dyn std::io::Read, key: DatabaseKey) -> Result<Vec<u8>, DatabaseOpenError> {
let mut data = Vec::new();
source.read_to_end(&mut data)?;
let database_version = DatabaseVersion::parse(data.as_ref())?;
let data = match database_version {
DatabaseVersion::KDB(_) => return Err(DatabaseOpenError::UnsupportedVersion),
DatabaseVersion::KDB2(_) => return Err(DatabaseOpenError::UnsupportedVersion),
DatabaseVersion::KDB3(_) => decrypt_kdbx3(data.as_ref(), &key)?.2,
DatabaseVersion::KDB4(_) => decrypt_kdbx4(data.as_ref(), &key)?.3,
};
Ok(data)
}
pub fn get_version(source: &mut dyn std::io::Read) -> Result<DatabaseVersion, DatabaseIntegrityError> {
let mut data = Vec::new();
data.resize(DatabaseVersion::get_version_header_size(), 0);
source.read(&mut data)?;
DatabaseVersion::parse(data.as_ref())
}
pub fn new(config: DatabaseConfig) -> Database {
Self {
config,
header_attachments: Vec::new(),
root: Group::new("Root"),
deleted_objects: Default::default(),
meta: Default::default(),
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct Times {
pub expires: bool,
pub usage_count: usize,
pub times: HashMap<String, NaiveDateTime>,
}
pub const EXPIRY_TIME_TAG_NAME: &str = "ExpiryTime";
pub const LAST_MODIFICATION_TIME_TAG_NAME: &str = "LastModificationTime";
pub const CREATION_TIME_TAG_NAME: &str = "CreationTime";
pub const LAST_ACCESS_TIME_TAG_NAME: &str = "LastAccessTime";
pub const LOCATION_CHANGED_TAG_NAME: &str = "LocationChanged";
impl Times {
fn get(&self, key: &str) -> Option<&NaiveDateTime> {
self.times.get(key)
}
pub fn get_expiry(&self) -> Option<&NaiveDateTime> {
self.times.get(EXPIRY_TIME_TAG_NAME)
}
pub fn set_expiry(&mut self, time: NaiveDateTime) {
self.times.insert(EXPIRY_TIME_TAG_NAME.to_string(), time);
}
pub fn get_last_modification(&self) -> Option<&NaiveDateTime> {
self.times.get(LAST_MODIFICATION_TIME_TAG_NAME)
}
pub fn set_last_modification(&mut self, time: NaiveDateTime) {
self.times
.insert(LAST_MODIFICATION_TIME_TAG_NAME.to_string(), time);
}
pub fn get_creation(&self) -> Option<&NaiveDateTime> {
self.times.get(CREATION_TIME_TAG_NAME)
}
pub fn set_creation(&mut self, time: NaiveDateTime) {
self.times.insert(CREATION_TIME_TAG_NAME.to_string(), time);
}
pub fn get_last_access(&self) -> Option<&NaiveDateTime> {
self.times.get(LAST_ACCESS_TIME_TAG_NAME)
}
pub fn set_last_access(&mut self, time: NaiveDateTime) {
self.times.insert(LAST_ACCESS_TIME_TAG_NAME.to_string(), time);
}
pub fn get_location_changed(&self) -> Option<&NaiveDateTime> {
self.times.get(LOCATION_CHANGED_TAG_NAME)
}
pub fn set_location_changed(&mut self, time: NaiveDateTime) {
self.times.insert(LOCATION_CHANGED_TAG_NAME.to_string(), time);
}
pub fn now() -> NaiveDateTime {
let now = chrono::Utc::now().timestamp();
chrono::DateTime::from_timestamp(now, 0).unwrap().naive_utc()
}
pub fn new() -> Times {
let mut response = Times::default();
let now = Times::now();
response.set_creation(now);
response.set_last_modification(now);
response.set_last_access(now);
response.set_location_changed(now);
response.set_expiry(now);
response.expires = false;
response
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct CustomData {
pub items: HashMap<String, CustomDataItem>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct CustomDataItem {
pub value: Option<Value>,
pub last_modification_time: Option<NaiveDateTime>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct CustomDataItemDenormalized {
pub key: String,
pub custom_data_item: CustomDataItem,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct HeaderAttachment {
pub flags: u8,
pub content: Vec<u8>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct DeletedObjects {
pub objects: Vec<DeletedObject>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
pub struct DeletedObject {
pub uuid: Uuid,
pub deletion_time: NaiveDateTime,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[cfg(feature = "serialization")]
impl serde::Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.starts_with('#') || s.len() != 7 {
return Err(ParseColorError(s.to_string()));
}
let v =
u64::from_str_radix(s.trim_start_matches('#'), 16).map_err(|_e| ParseColorError(s.to_string()))?;
let r = ((v >> 16) & 0xff) as u8;
let g = ((v >> 8) & 0xff) as u8;
let b = (v & 0xff) as u8;
Ok(Self { r, g, b })
}
}
impl Color {
pub fn to_string(&self) -> String {
format!("#{:0x}{:0x}{:0x}", self.r, self.g, self.b)
}
}
#[cfg(test)]
mod database_tests {
use std::fs::File;
use crate::{error::DatabaseOpenError, Database, DatabaseKey};
#[test]
fn test_xml() -> Result<(), DatabaseOpenError> {
let xml = Database::get_xml(
&mut File::open("tests/resources/test_db_with_password.kdbx")?,
DatabaseKey::new().with_password("demopass"),
)?;
assert!(xml.len() > 100);
Ok(())
}
#[test]
fn test_open_invalid_version_header_size() {
assert!(Database::parse(&[], DatabaseKey::new().with_password("testing")).is_err());
assert!(Database::parse(
&[0, 0, 0, 0, 0, 0, 0, 0],
DatabaseKey::new().with_password("testing")
)
.is_err());
assert!(Database::parse(
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
DatabaseKey::new().with_password("testing")
)
.is_err());
}
#[cfg(feature = "save_kdbx4")]
#[test]
fn test_save() {
use crate::db::Entry;
let mut db = Database::new(Default::default());
db.root.add_child(Entry::new());
db.root.add_child(Entry::new());
db.root.add_child(Entry::new());
let mut buffer = Vec::new();
db.save(&mut buffer, DatabaseKey::new().with_password("testing"))
.unwrap();
let db_loaded = Database::open(
&mut buffer.as_slice(),
DatabaseKey::new().with_password("testing"),
)
.unwrap();
assert_eq!(db, db_loaded);
}
}