pub mod custom_serde;
pub mod entry;
pub mod group;
pub mod meta;
pub mod times;
pub mod timestamp;
use serde::{Deserialize, Serialize, Serializer};
use base64::{engine::general_purpose as base64_engine, Engine as _};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
use crate::{
crypt::ciphers::Cipher,
format::xml_db::{
custom_serde::cs_opt_string, entry::UnprotectError, group::Group, meta::Meta, timestamp::Timestamp,
},
};
#[cfg(feature = "save_kdbx4")]
use crate::{crypt::CryptographyError, db::DatabaseSaveError};
pub fn parse_xml(
data: &[u8],
header_attachments: &[crate::db::Attachment],
inner_decryptor: &mut dyn Cipher,
) -> Result<crate::db::Database, ParseXmlError> {
let kdbx: KeePassFile = quick_xml::de::from_reader(data)?;
Ok(kdbx.xml_to_db(inner_decryptor, header_attachments)?)
}
#[derive(Debug, Error)]
pub enum ParseXmlError {
#[error("Error parsing XML inside KDBX: {0}")]
Xml(#[from] quick_xml::DeError),
#[error("Error unprotecting entry: {0}")]
Unprotect(#[from] UnprotectError),
}
#[cfg(feature = "save_kdbx4")]
pub fn to_xml(
db: &crate::db::Database,
inner_encryptor: &mut dyn Cipher,
) -> Result<(Vec<u8>, Vec<crate::db::Attachment>), DatabaseSaveError> {
let mut attachments = Vec::new();
let kdbx = KeePassFile::db_to_xml(db, inner_encryptor, &mut attachments)?;
let xml = quick_xml::se::to_string_with_root("KeePassFile", &kdbx)?
.as_bytes()
.to_vec();
Ok((xml, attachments))
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct KeePassFile {
meta: Meta,
root: Root,
}
impl KeePassFile {
fn xml_to_db(
mut self,
inner_decryptor: &mut dyn Cipher,
header_attachments: &[crate::db::Attachment],
) -> Result<crate::db::Database, UnprotectError> {
let mut db = crate::db::Database::new(Default::default());
db.root.uuid = self.root.group.uuid.0;
let mut attachments = header_attachments.to_vec();
if let Some(binaries) = self.meta.binaries.take() {
for binary in binaries.binaries {
attachments.push(binary.xml_to_db(inner_decryptor)?);
}
}
let custom_icons = self.meta.custom_icons.take();
let custom_icons: HashMap<Uuid, Vec<u8>> = custom_icons
.map(|ci| {
ci.icons
.into_iter()
.map(|icon| (icon.uuid.0, icon.data))
.collect()
})
.unwrap_or_default();
db.meta = self.meta.into();
db.deleted_objects = self
.root
.deleted_objects
.map(|del_objs| {
del_objs
.objects
.into_iter()
.map(|obj| (obj.uuid.0, obj.deletion_time.map(|ts| ts.time)))
.collect()
})
.unwrap_or_default();
self.root
.group
.xml_to_db_handle(&mut db.root, &attachments, &custom_icons, inner_decryptor)?;
Ok(db)
}
#[cfg(feature = "save_kdbx4")]
fn db_to_xml(
db: &crate::db::Database,
inner_cipher: &mut dyn Cipher,
attachments: &mut Vec<crate::db::Attachment>,
) -> Result<Self, CryptographyError> {
use crate::format::xml_db::meta::Icon;
let mut custom_icons = HashMap::new();
let group = Group::db_to_xml(&db.root, inner_cipher, attachments, &mut custom_icons)?;
let mut meta: Meta = db.meta.clone().into();
meta.custom_icons
.get_or_insert_default()
.icons
.extend(custom_icons.into_iter().map(|(uuid, data)| Icon {
uuid: UUID(uuid),
data,
}));
let deleted_objects = if db.deleted_objects.is_empty() {
None
} else {
Some(DeletedObjects {
objects: db
.deleted_objects
.iter()
.map(|(uuid, deletion_time)| DeletedObject {
uuid: UUID(*uuid),
deletion_time: deletion_time.map(Timestamp::new_iso8601),
})
.collect(),
})
};
Ok(KeePassFile {
meta,
root: Root {
group,
deleted_objects,
},
})
}
}
#[allow(clippy::upper_case_acronyms)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct UUID(Uuid);
impl<'de> Deserialize<'de> for UUID {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let input = String::deserialize(deserializer)?;
let v = base64_engine::STANDARD
.decode(input)
.map_err(serde::de::Error::custom)?;
let uuid = Uuid::from_slice(&v).map_err(serde::de::Error::custom)?;
Ok(UUID(uuid))
}
}
impl Serialize for UUID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let b64 = base64_engine::STANDARD.encode(self.0.as_bytes());
serializer.serialize_str(&b64)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Root {
#[serde(rename = "Group")]
pub group: Group,
#[serde(default, rename = "DeletedObjects")]
pub deleted_objects: Option<DeletedObjects>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeletedObjects {
#[serde(default, rename = "DeletedObject")]
pub objects: Vec<DeletedObject>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeletedObject {
#[serde(rename = "UUID")]
uuid: UUID,
#[serde(default, with = "cs_opt_string")]
deletion_time: Option<Timestamp>,
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize)]
struct Test<T>(T);
#[test]
fn test_deserialize_uuid() {
let uuid_str = "AAECAwQFBgcICQoLDA0ODw==";
let uuid: UUID = quick_xml::de::from_str(&format!("{}", uuid_str)).unwrap();
assert_eq!(
uuid.0.as_bytes(),
&[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]
);
}
#[test]
fn test_serialize_uuid() {
let uuid = UUID(Uuid::from_bytes([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
]));
let serialized = quick_xml::se::to_string(&Test(uuid)).unwrap();
assert_eq!(serialized, "<Test>AAECAwQFBgcICQoLDA0ODw==</Test>");
}
}