...
 
Commits (4)
......@@ -11,8 +11,9 @@ base64 = "0.12.1"
chrono = "0.4.11"
derive_more = "0.99.5"
thiserror = "1.0"
uuid = "0.8"
uuid = { version = "0.8", features = ["v4"] }
xml-rs = "0.8.3"
getrandom = "0.1"
libflate = "1.0"
rust-argon2 = "0.8"
......@@ -29,4 +30,12 @@ path = "src/bin/kdbx_decrypt.rs"
[[bin]]
name = "kdbx-dump-header"
path = "src/bin/kdbx_dump_header.rs"
\ No newline at end of file
path = "src/bin/kdbx_dump_header.rs"
[[bin]]
name = "kdbx-parse"
path = "src/bin/kdbx_parse.rs"
[[bin]]
name = "kdbx-generate"
path = "src/bin/kdbx_generate.rs"
\ No newline at end of file
No preview for this file type
<?xml version="1.0" encoding="utf-8"?>
<KeePassFile>
<Meta>
<Generator>kdbx-rs</Generator>
<DatabaseName>BarName</DatabaseName>
<DatabaseDescription>BazDesc</DatabaseDescription>
<CustomData />
<MemoryProtection>
<ProtectUserName>False</ProtectUserName>
<ProtectPassword>False</ProtectPassword>
<ProtectTitle>False</ProtectTitle>
<ProtectNotes>False</ProtectNotes>
<ProtectURL>False</ProtectURL>
</MemoryProtection>
</Meta>
<Root>
<Group>
<UUID>AAAAAAAAAAAAAAAAEjRWeA==</UUID>
<Name>FooGroup</Name>
<Times>
<LastModificationTime>C2T41w4AAAA=</LastModificationTime>
<CreationTime>z2P41w4AAAA=</CreationTime>
<LastAccessTime>C/Ef2A4AAAA=</LastAccessTime>
<LocationChanged>z2P41w4AAAA=</LocationChanged>
<ExpiryTime>z2P41w4AAAA=</ExpiryTime>
<UsageCount>1</UsageCount>
<Expires>False</Expires>
</Times>
<Entry>
<UUID>AAAAAAAAAAAAAAAAAGVDIQ==</UUID>
<Times>
<LastModificationTime>C2T41w4AAAA=</LastModificationTime>
<CreationTime>z2P41w4AAAA=</CreationTime>
<LastAccessTime>C/Ef2A4AAAA=</LastAccessTime>
<LocationChanged>z2P41w4AAAA=</LocationChanged>
<ExpiryTime>z2P41w4AAAA=</ExpiryTime>
<UsageCount>1</UsageCount>
<Expires>False</Expires>
</Times>
<String>
<Key>Title</Key>
<Value>Bar</Value>
</String>
<String>
<Key>Password</Key>
<Value>kdbxrs</Value>
</String>
</Entry>
</Group>
</Root>
</KeePassFile>
\ No newline at end of file
......@@ -2,10 +2,10 @@ use kdbx_rs;
fn main() -> Result<(), kdbx_rs::Error> {
let args: Vec<String> = std::env::args().collect();
let db = kdbx_rs::open(&args[1])?;
let kdbx = kdbx_rs::open(&args[1])?;
let key = kdbx_rs::CompositeKey::from_password(args.get(2).unwrap_or(&"kdbxrs".to_string()));
let db = db.unlock(&key).map_err(|(e, _db)| e)?;
let xml = kdbx_rs::parse_xml(db.xml_data());
println!("{:?}", xml);
let kdbx = kdbx.unlock(&key).map_err(|(e, _db)| e)?;
let data: Vec<u8> = kdbx.raw_xml().unwrap().iter().cloned().collect();
println!("{}", String::from_utf8(data).unwrap());
Ok(())
}
......@@ -35,8 +35,8 @@ fn print_kdf(params: &kdbx_rs::binary::KdfParams) {
fn main() -> Result<(), kdbx_rs::Error> {
let args: Vec<String> = std::env::args().collect();
let db = kdbx_rs::open(&args[1])?;
let header = db.header();
let kdbx = kdbx_rs::open(&args[1])?;
let header = kdbx.header();
println!("Cipher: {:?}", header.cipher);
println!("Compression: {:?}", header.compression_type);
print_kdf(&header.kdf_params);
......
use kdbx_rs::binary::Unlocked;
use kdbx_rs::types::{Entry, Field, Group, Times};
use kdbx_rs::{CompositeKey, Database, Error, Kdbx};
use chrono::NaiveDate;
use std::fs::File;
use std::path::PathBuf;
use uuid::Uuid;
fn sample_times() -> Times {
Times {
last_access_time: NaiveDate::from_ymd(2020, 05, 01).and_hms(1, 2, 3),
last_modification_time: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 2, 3),
creation_time: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 1, 3),
location_changed: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 1, 3),
expiry_time: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 1, 3),
expires: false,
usage_count: 1,
}
}
fn main() -> Result<(), Error> {
let mut expected_path = PathBuf::new();
expected_path.push(env!("CARGO_MANIFEST_DIR"));
expected_path.push("res");
expected_path.push("tests");
expected_path.push("generate_xml.xml");
let mut db = Database::default();
db.meta.database_name = "BarName".to_string();
db.meta.database_description = "BazDesc".to_string();
let mut group = Group::default();
group.name = "FooGroup".to_string();
group.uuid = Uuid::from_u128(0x12345678);
group.times = sample_times();
let mut entry = Entry::default();
entry.add_field(Field::new("Title", "Bar"));
entry.add_field(Field::new("Password", "kdbxrs"));
entry.uuid = Uuid::from_u128(0x654321);
entry.times = sample_times();
group.entries.push(entry);
db.groups.push(group);
let output_path = PathBuf::from("kdbx_rs.kdbx");
let mut file = File::create(output_path).expect("Could not open output file");
let mut kdbx = Kdbx::<Unlocked>::from_database(db)?;
kdbx.set_key(CompositeKey::from_password("kdbxrs"))?;
kdbx.write(&mut file).expect("Could not write to file");
Ok(())
}
use kdbx_rs;
fn main() -> Result<(), kdbx_rs::Error> {
let args: Vec<String> = std::env::args().collect();
let kdbx = kdbx_rs::open(&args[1])?;
let key = kdbx_rs::CompositeKey::from_password(args.get(2).unwrap_or(&"kdbxrs".to_string()));
let kdbx = kdbx.unlock(&key).map_err(|(e, _db)| e)?;
let xml = kdbx_rs::xml::parse_xml(kdbx.raw_xml().unwrap());
println!("{:#?}", xml);
Ok(())
}
......@@ -9,6 +9,7 @@ mod wrapper_fields;
pub use header::{InnerHeaderId, KdbxHeader, KdbxInnerHeader, OuterHeaderId};
pub use read::{from_reader, open};
pub use states::{KdbxDatabase, Locked, Unlocked};
pub use states::{Kdbx, Locked, Unlocked};
pub use variant_dict::{Value as VariantDictValue, VariantDict, VariantParseError};
pub use wrapper_fields::{Cipher, CompressionType, KdfAlgorithm, KdfParams};
pub(crate) use wrapper_fields::{KDBX_MAGIC_NUMBER, KEEPASS_MAGIC_NUMBER};
......@@ -3,17 +3,6 @@ use super::wrapper_fields;
use crate::crypto;
use thiserror::Error;
#[derive(Error, Debug)]
/// Wrapper error type for this library
pub enum Error {
/// Failed to open a database
#[error("Could not open database: {0}")]
Open(#[from] OpenError),
/// Failed unlocking a database
#[error("Could not unlock database: {0}")]
Unlock(#[from] UnlockError),
}
#[derive(Error, Debug)]
/// Errors encountered loading a database prior to decryption
pub enum OpenError {
......@@ -49,6 +38,9 @@ pub enum UnlockError {
/// The inner header is invalid
#[error("Inner header invalid - {0}")]
InvalidInnerHeader(#[from] HeaderError),
/// The inner header is invalid
#[error("Corrupt database. XML data is invald - {0}")]
InvalidXml(#[from] crate::errors::XmlReadError),
}
#[derive(Debug, Error)]
......@@ -76,3 +68,25 @@ pub enum HeaderError {
#[error("Incompatible database - Unknown cipher {0:?}")]
UnknownCipher(uuid::Uuid),
}
#[derive(Debug, Error)]
/// Errors encountered writing a database
pub enum WriteError {
/// The reader failed before the header was entirely read
#[error("Error reading database header - {0}")]
Io(#[from] std::io::Error),
/// The database could not be serialized to XML
#[error("Error serializing database to XML - {0}")]
XmlWrite(#[from] crate::xml::serialize::Error),
/// The database could not be written to as `set_key()` has not been called.
#[error("No key to write database with")]
MissingKeys,
}
#[derive(Debug, Error)]
/// Errors encountered writing a database
pub enum DatabaseCreationError {
/// Could not obtain secure random data
#[error("Error getting RNG data for keys")]
Random(#[from] getrandom::Error),
}
use super::errors::HeaderError as Error;
use super::errors::{self, HeaderError as Error};
use super::variant_dict;
use super::wrapper_fields;
use crate::crypto;
use crate::utils;
use getrandom::getrandom;
use sha2::{Digest, Sha256};
use std::convert::TryInto;
use std::io::Read;
use std::io::{Read, Write};
use std::marker::PhantomData;
use uuid::Uuid;
......@@ -141,12 +142,17 @@ impl HeaderId for InnerHeaderId {
}
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HeaderField<T> {
ty: T,
data: Vec<u8>,
}
impl<T> HeaderField<T> {
pub(crate) fn new(ty: T, data: Vec<u8>) -> HeaderField<T> {
HeaderField { ty, data }
}
}
pub struct HeaderParser<'a, R: Read + 'a, T: HeaderId> {
_id: PhantomData<T>,
reader: &'a mut R,
......@@ -263,6 +269,10 @@ impl KdbxHeaderBuilder {
#[derive(Debug, PartialEq, Eq)]
/// Unencrypted database configuration and custom data
///
/// [`KdbxHeader::from_os_random()`] will provide a header with
/// the default encryption settings and new random keys
/// from the OS secure RNG
pub struct KdbxHeader {
/// Encryption cipher used for decryption the database
pub cipher: wrapper_fields::Cipher,
......@@ -279,6 +289,40 @@ pub struct KdbxHeader {
}
impl KdbxHeader {
/// Create a new header to encrypt a database with keys from the OS Secure RNG.
///
/// Under the hood this uses the [`getrandom`] crate to access the OS RNG,
/// the actual mechanism used to get random numbers is detailed in that
/// crate's documentation.
///
/// The default encryption is currently to use AES256 as a stream cipher,
/// and Argon2d v19 with 64 MiB memory factor, and 10 iterations as the KDF.
/// This is subject to change in future crate versions
///
/// [`getrandom`]: https://docs.rs/getrandom/0.1/getrandom/index.html
pub fn from_os_random() -> std::result::Result<KdbxHeader, getrandom::Error> {
let mut master_seed = vec![0u8; 32];
let mut encryption_iv = vec![0u8; 32];
let mut cipher_salt = vec![0u8; 32];
getrandom(&mut master_seed)?;
getrandom(&mut encryption_iv)?;
getrandom(&mut cipher_salt)?;
Ok(KdbxHeader {
cipher: wrapper_fields::Cipher::Aes256,
kdf_params: wrapper_fields::KdfParams::Argon2 {
iterations: 10,
memory_bytes: 0xFFFF * 1024,
salt: cipher_salt,
version: 19,
lanes: 2,
},
other_headers: Vec::new(),
compression_type: super::CompressionType::Gzip,
master_seed,
encryption_iv,
})
}
pub(crate) fn read<R: Read>(
mut caching_reader: utils::CachingReader<R>,
) -> Result<(KdbxHeader, Vec<u8>)> {
......@@ -299,11 +343,41 @@ impl KdbxHeader {
Err(Error::ChecksumFailed)
}
}
pub(crate) fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
use std::iter::once;
let headers = self
.other_headers
.iter()
.cloned()
.chain(once(self.cipher.into()))
.chain(once(self.kdf_params.clone().into()))
.chain(once(self.compression_type.clone().into()))
.chain(once(HeaderField::new(
OuterHeaderId::MasterSeed,
self.master_seed.clone(),
)))
.chain(once(HeaderField::new(
OuterHeaderId::EncryptionIv,
self.encryption_iv.clone(),
)))
.chain(once(HeaderField::new(
OuterHeaderId::EndOfHeader,
Vec::new(),
)));
for header in headers {
writer.write_all(&[header.ty.into()])?;
writer.write_all(&(header.data.len() as u32).to_le_bytes())?;
writer.write_all(&header.data)?;
}
Ok(())
}
}
#[derive(Default)]
pub struct KdbxInnerHeaderBuilder {
pub inner_stream_cipher_id: Option<u32>,
pub inner_stream_cipher: Option<wrapper_fields::InnerStreamCipher>,
pub inner_stream_key: Option<Vec<u8>>,
/// Custom and unrecognized header types
pub other_headers: Vec<HeaderField<InnerHeaderId>>,
......@@ -314,7 +388,8 @@ impl KdbxInnerHeaderBuilder {
match header.ty {
InnerHeaderId::InnerRandomStreamCipherId => {
let d = header.data;
self.inner_stream_cipher_id = Some(u32::from_le_bytes([d[0], d[1], d[2], d[3]]));
self.inner_stream_cipher =
Some(u32::from_le_bytes([d[0], d[1], d[2], d[3]]).into());
}
InnerHeaderId::InnerRandomStreamKey => self.inner_stream_key = Some(header.data),
_ => self.other_headers.push(header),
......@@ -325,7 +400,7 @@ impl KdbxInnerHeaderBuilder {
fn build(self) -> Result<KdbxInnerHeader> {
Ok(KdbxInnerHeader {
inner_stream_cipher_id: self.inner_stream_cipher_id.ok_or_else(|| {
inner_stream_cipher: self.inner_stream_cipher.ok_or_else(|| {
Error::MissingRequiredInnerField(InnerHeaderId::InnerRandomStreamCipherId)
})?,
inner_stream_key: self.inner_stream_key.ok_or_else(|| {
......@@ -340,7 +415,7 @@ impl KdbxInnerHeaderBuilder {
#[derive(Debug, PartialEq, Eq)]
pub struct KdbxInnerHeader {
/// Cipher identifier for data encrypted in memory
pub inner_stream_cipher_id: u32,
pub inner_stream_cipher: wrapper_fields::InnerStreamCipher,
/// Cipher key for data encrypted in memory
pub inner_stream_key: Vec<u8>,
/// Headers not handled by this library
......@@ -348,6 +423,25 @@ pub struct KdbxInnerHeader {
}
impl KdbxInnerHeader {
/// Returns an inner header setup for a default stream cipher and OS random keys
///
/// Currently the default stream cipher is ChaCha20
///
/// See the [`getrandom`] crate doc for details on random number sources
///
/// [`getrandom`]: https://docs.rs/getrandom/0.1/getrandom/index.html
pub fn from_os_random() -> std::result::Result<KdbxInnerHeader, errors::DatabaseCreationError> {
let inner_stream_cipher = wrapper_fields::InnerStreamCipher::ChaCha20;
let mut inner_stream_key = vec![0u8; 44]; // 32 bit key + 12 bit nonce for chacha20
getrandom::getrandom(&mut inner_stream_key)?;
Ok(KdbxInnerHeader {
inner_stream_cipher,
inner_stream_key,
other_headers: Vec::new(),
})
}
pub(crate) fn read<R: Read>(reader: &mut R) -> Result<KdbxInnerHeader> {
let mut header_builder = KdbxInnerHeaderBuilder::default();
let headers = HeaderParser::new(reader).read_all_headers()?;
......@@ -357,4 +451,28 @@ impl KdbxInnerHeader {
header_builder.build()
}
pub(crate) fn write<W: Write>(&self, mut writer: W) -> std::io::Result<()> {
use std::iter::once;
let headers = self
.other_headers
.iter()
.cloned()
.chain(once(self.inner_stream_cipher.clone().into()))
.chain(once(HeaderField::new(
InnerHeaderId::InnerRandomStreamKey,
self.inner_stream_key.clone(),
)))
.chain(once(HeaderField::new(
InnerHeaderId::EndOfHeader,
Vec::new(),
)));
for header in headers {
writer.write_all(&[header.ty.into()])?;
writer.write_all(&(header.data.len() as u32).to_le_bytes())?;
writer.write_all(&header.data)?;
}
Ok(())
}
}
use super::{errors, header, KdbxDatabase, Locked};
use super::{errors, header, Kdbx, Locked};
use crate::utils;
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Read;
use std::path::Path;
pub const KEEPASS_MAGIC_NUMBER: u32 = 0x9AA2D903;
pub const KDBX_MAGIC_NUMBER: u32 = 0xB54BFB67;
/// Read a database from a input stream
///
/// The database starts locked, use [`KdbxDatabase.unlock`] to unlock
///
/// [`KdbxDatabase.unlock`]: ./struct.KdbxDatabase.html#method.unlock
pub fn from_reader<R: Read>(mut input: R) -> Result<KdbxDatabase<Locked>, errors::OpenError> {
pub fn from_reader<R: Read>(mut input: R) -> Result<Kdbx<Locked>, errors::OpenError> {
let mut caching_reader = utils::CachingReader::new(&mut input);
let mut buffer = [0u8; 4];
caching_reader.read_exact(&mut buffer)?;
if u32::from_le_bytes(buffer) != KEEPASS_MAGIC_NUMBER {
if u32::from_le_bytes(buffer) != super::KEEPASS_MAGIC_NUMBER {
return Err(errors::OpenError::NonKeepassFormat);
}
caching_reader.read_exact(&mut buffer)?;
if u32::from_le_bytes(buffer) != KDBX_MAGIC_NUMBER {
if u32::from_le_bytes(buffer) != super::KDBX_MAGIC_NUMBER {
return Err(errors::OpenError::UnsupportedFileFormat);
}
......@@ -53,7 +50,7 @@ pub fn from_reader<R: Read>(mut input: R) -> Result<KdbxDatabase<Locked>, errors
encrypted_data,
};
Ok(KdbxDatabase { state: state })
Ok(Kdbx { state: state })
}
/// Read a database from a given path
......@@ -61,7 +58,7 @@ pub fn from_reader<R: Read>(mut input: R) -> Result<KdbxDatabase<Locked>, errors
/// The database starts locked, use [`KdbxDatabase.unlock`] to unlock
///
/// [`KdbxDatabase.unlock`]: ./struct.KdbxDatabase.html#method.unlock
pub fn open<P: AsRef<Path>>(path: P) -> Result<KdbxDatabase<Locked>, errors::OpenError> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Kdbx<Locked>, errors::OpenError> {
let path = path.as_ref();
let mut file = File::open(path)?;
from_reader(&mut file)
......
use super::{errors, header};
use crate::{crypto, stream};
use std::io::Read;
use std::io::{Read, Write};
pub trait DatabaseState: std::fmt::Debug {
pub trait KdbxState: std::fmt::Debug {
fn header(&self) -> &header::KdbxHeader;
fn write<W: Write>(&self, output: W) -> Result<(), errors::WriteError>;
}
#[derive(Debug)]
/// A kdbx database
/// A kdbx file
///
/// Most methods are available on a specific state
/// like KdbxDatabase<Locked> or KdbxDatase<Unlocked>
pub struct KdbxDatabase<S>
/// like Kdbx<Locked> or Kdbx<Unlocked>
pub struct Kdbx<S>
where
S: DatabaseState,
S: KdbxState,
{
pub(super) state: S,
}
impl<T: DatabaseState> KdbxDatabase<T> {
impl<T: KdbxState> Kdbx<T> {
/// Unencrypted database configuration and custom data
pub fn header(&self) -> &header::KdbxHeader {
&self.state.header()
}
/// Write this database to the given output stream
pub fn write<W: Write>(&self, output: W) -> Result<(), errors::WriteError> {
self.state.write(output)?;
Ok(())
}
/// Generate a new .kdbx from the given database
///
/// Uses OS randomness provided by [`getrandom`] to generate all required seed
/// and IVs.
///
/// Note that you need to set a key with [`Kdbx::set_key`]
/// to be able to write the database
pub fn from_database(
database: crate::Database,
) -> Result<Kdbx<Unlocked>, errors::DatabaseCreationError> {
let header = header::KdbxHeader::from_os_random()?;
let inner_header = header::KdbxInnerHeader::from_os_random()?;
let unlocked = Unlocked {
header,
inner_header,
major_version: 4,
minor_version: 0,
xml_data: None,
composed_key: None,
master_key: None,
database,
};
Ok(Kdbx { state: unlocked })
}
}
#[derive(Debug)]
/// An unlocked database, allowing access to stored credentials
/// An unlocked kdbx file, allowing access to stored credentials
pub struct Unlocked {
/// Header data of the kdbx archive, includes unencrypted metadata
pub(crate) header: header::KdbxHeader,
/// Inner header data that is stored encrypted, not present on kdbx3
pub(crate) inner_header: Option<header::KdbxInnerHeader>,
pub(crate) inner_header: header::KdbxInnerHeader,
/// Major version of the database file format
pub(crate) major_version: u16,
/// Minor version of the database file format
pub(crate) minor_version: u16,
/// Master key used to derive other keys
pub(crate) composed_key: Option<crypto::ComposedKey>,
/// Master key used to derive other keys
pub(crate) master_key: Option<crypto::MasterKey>,
/// Unencrypted unparsed XML data
pub(crate) xml_data: Vec<u8>,
pub(crate) xml_data: Option<Vec<u8>>,
/// Actual password database data
pub(crate) database: crate::Database,
}
impl Unlocked {
fn encrypt_inner(&self, key: &crypto::MasterKey) -> Result<Vec<u8>, super::errors::WriteError> {
let mut encrypted_buf = Vec::new();
let mut encrypted_stream = crate::stream::kdbx4_write_stream(
&mut encrypted_buf,
key.hmac_key(&self.header.master_seed),
key.cipher_key(&self.header.master_seed),
self.header.cipher,
&self.header.encryption_iv,
self.header.compression_type,
)?;
self.inner_header.write(&mut encrypted_stream)?;
crate::xml::write_xml(&mut encrypted_stream, &self.database)?;
drop(encrypted_stream);
Ok(encrypted_buf)
}
}
impl DatabaseState for Unlocked {
impl KdbxState for Unlocked {
fn header(&self) -> &header::KdbxHeader {
&self.header
}
fn write<W: Write>(&self, mut output: W) -> Result<(), errors::WriteError> {
let master_key = self
.master_key
.as_ref()
.ok_or(errors::WriteError::MissingKeys)?;
let mut header_buf = Vec::new();
let header_writer = &mut header_buf as &mut dyn Write;
header_writer.write_all(&super::KEEPASS_MAGIC_NUMBER.to_le_bytes())?;
header_writer.write_all(&super::KDBX_MAGIC_NUMBER.to_le_bytes())?;
header_writer.write_all(&self.minor_version.to_le_bytes())?;
header_writer.write_all(&self.major_version.to_le_bytes())?;
self.header.write(&mut header_buf)?;
output.write_all(&header_buf)?;
output.write_all(&crypto::sha256(&header_buf))?;
let hmac_key = master_key.hmac_key(&self.header.master_seed);
let hmac = hmac_key
.block_key(u64::MAX)
.calculate_header_hmac(&header_buf);
output.write_all(&hmac.code())?;
let encrypted_xml = self.encrypt_inner(&master_key)?;
output.write_all(&encrypted_xml)?;
Ok(())
}
}
impl KdbxDatabase<Unlocked> {
impl Kdbx<Unlocked> {
/// Encrypted binaries and database options
pub fn inner_header(&self) -> Option<&header::KdbxInnerHeader> {
self.state.inner_header.as_ref()
pub fn inner_header(&self) -> &header::KdbxInnerHeader {
&self.state.inner_header
}
/// Raw XML data to handle fields not supported by this plugin
pub fn xml_data(&self) -> &[u8] {
&self.state.xml_data
/// Use the given composite key to encrypt the database
pub fn set_key(
&mut self,
key: crypto::CompositeKey,
) -> Result<(), crate::errors::KeyGenerationError> {
self.state.composed_key = Some(key.composed());
let composed_key = self.state.composed_key.as_ref().unwrap();
self.state.master_key = Some(composed_key.master_key(&self.header().kdf_params)?);
Ok(())
}
/// Raw parsed XML data to handle fields not supported by this plugin
///
/// Only present from databases loaded from existing sources
pub fn raw_xml(&self) -> Option<&[u8]> {
self.state.xml_data.as_deref()
}
}
#[derive(Debug, PartialEq, Eq)]
/// A locked database, use unlock(composite_key) to unlock
/// A locked kdbx file, use unlock(composite_key) to unlock
pub struct Locked {
/// Header data of the kdbx archive, includes unencrypted metadata
pub(crate) header: header::KdbxHeader,
......@@ -75,13 +171,28 @@ pub struct Locked {
pub(crate) encrypted_data: Vec<u8>,
}
impl DatabaseState for Locked {
impl KdbxState for Locked {
fn header(&self) -> &header::KdbxHeader {
&self.header
}
fn write<W: Write>(&self, mut output: W) -> Result<(), errors::WriteError> {
let mut header_buf = Vec::new();
let header_writer = &mut header_buf as &mut dyn Write;
header_writer.write_all(&super::KEEPASS_MAGIC_NUMBER.to_le_bytes())?;
header_writer.write_all(&super::KDBX_MAGIC_NUMBER.to_le_bytes())?;
header_writer.write_all(&self.minor_version.to_le_bytes())?;
header_writer.write_all(&self.major_version.to_le_bytes())?;
self.header.write(&mut header_buf)?;
output.write_all(&header_buf)?;
output.write_all(&crypto::sha256(&header_buf))?;
output.write_all(&self.hmac)?;
output.write_all(&self.encrypted_data)?;
Ok(())
}
}
impl KdbxDatabase<Locked> {
impl Kdbx<Locked> {
fn decrypt_data(
&self,
master_key: &crypto::MasterKey,
......@@ -102,14 +213,15 @@ impl KdbxDatabase<Locked> {
Ok((inner_header, output_buffer))
}
/// Unlocks the database
/// Unlocks the kdbx file
///
/// If unlock fails, returns the locked database along with the error
/// If unlock fails, returns the locked kdbx file along with the error
pub fn unlock(
self,
key: &crypto::CompositeKey,
) -> Result<KdbxDatabase<Unlocked>, (errors::UnlockError, KdbxDatabase<Locked>)> {
let master_key = match key.master_key(&self.state.header.kdf_params) {
) -> Result<Kdbx<Unlocked>, (errors::UnlockError, Kdbx<Locked>)> {
let composed_key = key.composed();
let master_key = match composed_key.master_key(&self.state.header.kdf_params) {
Ok(master_key) => master_key,
Err(e) => return Err((errors::UnlockError::from(e), self)),
};
......@@ -118,14 +230,24 @@ impl KdbxDatabase<Locked> {
let header_block_key = hmac_key.block_key(u64::MAX);
if header_block_key.verify_header_block(&self.state.hmac, &self.state.header_data) {
match self.decrypt_data(&master_key) {
Ok((inner_header, data)) => Ok(KdbxDatabase {
let parsed = self
.decrypt_data(&master_key)
.and_then(|(inner_header, data)| {
let parsed = crate::xml::parse_xml(data.as_slice())?;
Ok((inner_header, data, parsed))
});
match parsed {
Ok((inner_header, data, db)) => Ok(Kdbx {
state: Unlocked {
header: self.state.header,
inner_header: Some(inner_header),
inner_header: inner_header,
major_version: self.state.major_version,
minor_version: self.state.minor_version,
xml_data: data,
composed_key: Some(composed_key),
master_key: Some(master_key),
database: db,
xml_data: Some(data),
},
}),
Err(e) => Err((e, self)),
......
......@@ -2,7 +2,7 @@ use crate::utils::buffer;
use derive_more::TryInto;
use std::collections::HashMap;
use std::convert::TryInto;
use std::io::Read;
use std::io::{self, Read};
use thiserror::Error;
const TAG_UINT32: u8 = 0x04;
......@@ -59,7 +59,6 @@ pub enum Value {
}
impl Value {
#[allow(dead_code)]
pub(crate) fn tag(&self) -> u8 {
match self {
Value::Uint32(_) => TAG_UINT32,
......@@ -73,7 +72,6 @@ impl Value {
}
}
#[allow(dead_code)]
pub(crate) fn data(&self) -> Vec<u8> {
match self {
Value::Uint32(val) => val.to_le_bytes().iter().cloned().collect(),
......@@ -198,6 +196,23 @@ pub(crate) fn parse_variant_dict<T: Read>(mut input: T) -> Result<VariantDict> {
Ok(map)
}
pub(crate) fn write_variant_dict<W: io::Write>(
mut output: W,
vdict: &VariantDict,
) -> io::Result<()> {
output.write_all(&[0u8, 1u8])?;
for (name, value) in vdict.iter() {
output.write_all(&[value.tag()])?;
output.write_all(&(name.len() as i32).to_le_bytes())?;
output.write_all(name.as_bytes())?;
let data = value.data();
output.write_all(&(data.len() as i32).to_le_bytes())?;
output.write_all(&data)?;
}
output.write_all(&[0])?;
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
......
use super::errors::HeaderError;
use super::header::OuterHeaderId;
use super::variant_dict;
use super::header::{HeaderField, InnerHeaderId, OuterHeaderId};
use super::variant_dict::{self, VariantDict};
use crate::utils;
use std::convert::{TryFrom, TryInto};
use uuid::Uuid;
pub const KEEPASS_MAGIC_NUMBER: u32 = 0x9AA2D903;
pub const KDBX_MAGIC_NUMBER: u32 = 0xB54BFB67;
const AES128_UUID: &str = "61ab05a1-9464-41c3-8d74-3a563df8dd35";
const AES256_UUID: &str = "31c1f2e6-bf71-4350-be58-05216afc5aff";
const TWOFISH_UUID: &str = "ad68f29f-576f-4bb9-a36a-d47af965346c";
......@@ -52,6 +55,57 @@ impl From<Cipher> for uuid::Uuid {
}
}
impl From<Cipher> for HeaderField<OuterHeaderId> {
fn from(cipher: Cipher) -> HeaderField<OuterHeaderId> {
let uuid: uuid::Uuid = cipher.into();
HeaderField::new(OuterHeaderId::CipherId, uuid.as_bytes().to_vec())
}
}
/// Inner stream cipher identifier used for encrypting protected fields
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum InnerStreamCipher {
/// ArcFour algorithm
ArcFour,
/// Salsa20 stream cipher
Salsa20,
/// ChaCha20 stream cipher
ChaCha20,
/// Unknown stream cipher
Unknown(u32),
}
impl From<InnerStreamCipher> for HeaderField<InnerHeaderId> {
fn from(cipher: InnerStreamCipher) -> HeaderField<InnerHeaderId> {
HeaderField::new(
InnerHeaderId::InnerRandomStreamCipherId,
u32::from(cipher).to_le_bytes().as_ref().to_vec(),
)
}
}
impl From<u32> for InnerStreamCipher {
fn from(id: u32) -> InnerStreamCipher {
match id {
1 => InnerStreamCipher::ArcFour,
2 => InnerStreamCipher::Salsa20,
3 => InnerStreamCipher::ChaCha20,
x => InnerStreamCipher::Unknown(x),
}
}
}
impl From<InnerStreamCipher> for u32 {
fn from(id: InnerStreamCipher) -> u32 {
match id {
InnerStreamCipher::ArcFour => 1,
InnerStreamCipher::Salsa20 => 2,
InnerStreamCipher::ChaCha20 => 3,
InnerStreamCipher::Unknown(x) => x,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[allow(non_camel_case_types)]
/// Algorithm used for converting from credentials to crypto keys
......@@ -88,7 +142,7 @@ impl From<KdfAlgorithm> for uuid::Uuid {
}
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
/// Options for converting credentials to crypto keys
pub enum KdfParams {
/// Argon 2 KDF
......@@ -120,9 +174,9 @@ pub enum KdfParams {
},
}
impl TryFrom<variant_dict::VariantDict> for KdfParams {
impl TryFrom<VariantDict> for KdfParams {
type Error = HeaderError;
fn try_from(mut vdict: variant_dict::VariantDict) -> Result<Self, HeaderError> {
fn try_from(mut vdict: VariantDict) -> Result<Self, HeaderError> {
let uuid = vdict
.remove("$UUID")
.and_then(|uuid_val| uuid_val.try_into().ok())
......@@ -161,8 +215,8 @@ impl TryFrom<variant_dict::VariantDict> for KdfParams {
}
}
impl Into<variant_dict::VariantDict> for KdfParams {
fn into(self) -> variant_dict::VariantDict {
impl Into<VariantDict> for KdfParams {
fn into(self) -> VariantDict {
let mut vdict = variant_dict::VariantDict::new();
match self {
KdfParams::Argon2 {
......@@ -214,6 +268,15 @@ impl Into<variant_dict::VariantDict> for KdfParams {
}
}
impl From<KdfParams> for HeaderField<OuterHeaderId> {
fn from(params: KdfParams) -> HeaderField<OuterHeaderId> {
let mut buf = Vec::new();
let vdict: VariantDict = params.into();
variant_dict::write_variant_dict(&mut buf, &vdict).unwrap();
HeaderField::new(OuterHeaderId::KdfParameters, buf)
}
}
impl KdfParams {
fn opt_from_vdict<T>(
key: &str,
......@@ -261,6 +324,16 @@ impl From<u32> for CompressionType {
}
}
impl From<CompressionType> for HeaderField<OuterHeaderId> {
fn from(compression_type: CompressionType) -> HeaderField<OuterHeaderId> {
let compression_type_id: u32 = compression_type.into();
HeaderField::new(
OuterHeaderId::KdfParameters,
Vec::from(compression_type_id.to_le_bytes().as_ref()),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
......
use crate::binary;
use hmac::crypto_mac::MacResult;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256, Sha512};
use std::string::ToString;
......@@ -30,7 +31,7 @@ impl CompositeKey {
CompositeKey::new(Some(pw.into()), None)
}
fn composed(&self) -> Vec<u8> {
pub(crate) fn composed(&self) -> ComposedKey {
let mut buffer = Vec::new();
if let Some(ref pw) = self.pw {
buffer.extend(Sha256::digest(pw.as_bytes()))
......@@ -40,11 +41,17 @@ impl CompositeKey {
buffer.extend(Sha256::digest(keyfile))
}
Sha256::digest(&buffer).iter().cloned().collect()
ComposedKey(Sha256::digest(&buffer).iter().cloned().collect())
}
}
#[derive(Debug)]
/// Hashed combined input credentials used as KDF input
pub struct ComposedKey(Vec<u8>);
impl ComposedKey {
/// Generate a master key used to derive all other keys
pub(crate) fn master_key(
pub fn master_key(
&self,
kdf_options: &binary::KdfParams,
) -> Result<MasterKey, KeyGenerationError> {
......@@ -66,7 +73,7 @@ impl CompositeKey {
time_cost: *iterations as u32,
..Default::default()
};
let hash = argon2::hash_raw(&self.composed(), salt, &config)
let hash = argon2::hash_raw(&self.0, salt, &config)
.map_err(|e| KeyGenerationError::KeyGeneration(e.to_string()))?;
Ok(MasterKey(hash))
......@@ -77,7 +84,8 @@ impl CompositeKey {
}
/// Master key - this is generated from the user's composite key and is used to generate all other keys
pub(crate) struct MasterKey(Vec<u8>);
#[derive(Debug)]
pub struct MasterKey(Vec<u8>);
impl MasterKey {
/// Obtain a key to use for data integrity checks
......@@ -132,6 +140,28 @@ impl HmacBlockKey {
}
}
/// Calculate a HMAC for a block in the data section
pub(crate) fn calculate_data_hmac(
&self,
data: &[u8],
) -> MacResult<<HmacSha256 as Mac>::OutputSize> {
let mut calc_hmac = HmacSha256::new_varkey(&self.1).unwrap();
calc_hmac.input(&self.0.to_le_bytes());
calc_hmac.input(&(data.len() as u32).to_le_bytes());
calc_hmac.input(data);
calc_hmac.result()
}
/// Calculate a HMAC for a block in the header section
pub(crate) fn calculate_header_hmac(
&self,
data: &[u8],
) -> MacResult<<HmacSha256 as Mac>::OutputSize> {
let mut calc_hmac = HmacSha256::new_varkey(&self.1).unwrap();
calc_hmac.input(data);
calc_hmac.result()
}
/// Verify that the header block is valid
pub(crate) fn verify_header_block(&self, hmac: &[u8], data: &[u8]) -> bool {
let mut calc_hmac = HmacSha256::new_varkey(&self.1).unwrap();
......@@ -148,10 +178,17 @@ pub(crate) fn verify_sha256(data: &[u8], expected_sha: &[u8]) -> bool {
expected_sha == &*Sha256::digest(&data)
}
pub(crate) fn sha256(data: &[u8]) -> Vec<u8> {
Sha256::digest(data).as_slice().to_vec()
}
#[derive(Debug, Error)]
/// Errors encountered generating crypto keys
pub enum KeyGenerationError {
/// Unexpected error when generating a key
#[error("Could not generate key: {0}")]
KeyGeneration(String),
/// KDF Options are not supported by this library
#[error("Generation for KDF Options: {0:?} not implemented")]
UnimplementedKdfOptions(binary::KdfParams),
}
//! Error types for kdbx-rs
pub use crate::binary::errors::{HeaderError, OpenError, UnlockError};
pub use crate::xml::parse::Error as XmlError;
pub use crate::binary::errors::{
DatabaseCreationError, HeaderError, OpenError, UnlockError, WriteError,
};
pub use crate::crypto::KeyGenerationError;
pub use crate::xml::parse::Error as XmlReadError;
pub use crate::xml::serialize::Error as XmlWriteError;
use thiserror::Error;
#[derive(Error, Debug)]
......@@ -15,5 +19,14 @@ pub enum Error {
Unlock(#[from] UnlockError),
/// Failed parsing database XML
#[error("Failed to parse database XML: {0}")]
Xml(#[from] XmlError),
XmlRead(#[from] XmlReadError),
/// Failed writing database XML
#[error("Failed to write database XML: {0}")]
XmlWrite(#[from] XmlWriteError),
/// Failed to create a database in memory
#[error("Failed to create database: {0}")]
Creation(#[from] DatabaseCreationError),
/// Failed generating crypto keys
#[error("Failed to create encryption keys")]
KeyGeneration(#[from] KeyGenerationError),
}
......@@ -4,20 +4,20 @@
//!
//! Databases can be read with the [`kdbx_rs::open`] function. This provides
//! access to heder information. It can then be unlocked by providing a [`CompositeKey`]
//! to the [`KdbxDatabase.unlock`] method to access any encrypted data.
//! to the [`Kdbx.unlock`] method to access any encrypted data.
//!
//! ```
//! # fn main() -> Result<(), kdbx_rs::Error> {
//! use kdbx_rs::CompositeKey;
//!
//! # let file_path = "./res/kdbx4-argon2.kdbx";
//! let db = kdbx_rs::open(file_path)?;
//! let kdbx = kdbx_rs::open(file_path)?;
//! let key = CompositeKey::from_password("kdbxrs");
//! // If unlock fails, the locked database is returned to you
//! // so you can e.g. prompt the user to try another password
//! //
//! // Here we just return if incorrect
//! let unlocked = db.unlock(&key).map_err(|(error, locked)| error)?;
//! let unlocked = kdbx.unlock(&key).map_err(|(error, locked)| error)?;
//! # Ok(())
//! # }
//! ```
......@@ -28,16 +28,17 @@
//! [`CompositeKey`]: ./struct.CompositeKey.html
//! [`kdbx_rs::from_reader`]: ./fn.from_reader.html
//! [`kdbx_rs::open`]: ./fn.open.html
//! [`KdbxDatabase.unlock`]: ./struct.KdbxDatabase.html#method.unlock
//! [`Kdbx.unlock`]: ./struct.Kdbx.html#method.unlock
pub mod binary;
mod crypto;
pub mod errors;
mod stream;
pub mod types;
mod utils;
pub mod xml;
pub use crate::xml::{parse_xml, XmlDatabase};
pub use binary::{from_reader, open, KdbxDatabase};
pub use crate::types::Database;
pub use binary::{from_reader, open, Kdbx};
pub use crypto::CompositeKey;
pub use errors::Error;
......@@ -8,6 +8,8 @@ use block_modes::{BlockMode, Cbc};
use std::io;
use thiserror::Error;
pub const HMAC_WRITE_BLOCK_SIZE: usize = 1024 * 1024;
pub(crate) struct HMacReader<R>
where
R: io::Read,
......@@ -70,6 +72,60 @@ impl<R: io::Read> io::Read for HMacReader<R> {
}
}
pub(crate) struct HmacWriter<W>
where
W: io::Write,
{
block_idx: u64,
buffer: Vec<u8>,
inner: W,
hmac_key: crypto::HmacKey,
}
impl<W: io::Write> HmacWriter<W> {
pub(crate) fn new(inner: W, hmac_key: crypto::HmacKey) -> HmacWriter<W> {
HmacWriter {
block_idx: 0,
buffer: Vec::with_capacity(HMAC_WRITE_BLOCK_SIZE),
inner,
hmac_key,
}
}
fn write_block(&mut self) -> io::Result<()> {
let hmac = self
.hmac_key
.block_key(self.block_idx)
.calculate_data_hmac(&self.buffer);
self.inner.write_all(&hmac.code())?;
self.inner
.write_all(&(self.buffer.len() as u32).to_le_bytes())?;
self.inner.write_all(&self.buffer)?;
self.block_idx += 1;
Ok(())
}
}
impl<W: io::Write> io::Write for HmacWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let space_in_buffer = HMAC_WRITE_BLOCK_SIZE - self.buffer.len();
let write_size = usize::min(buf.len(), space_in_buffer);
self.buffer.extend_from_slice(&buf[0..write_size]);
if write_size < buf.len() {
// Internal buffer full, write it out
self.write_block()?;
self.buffer.clear();
}
Ok(write_size)
}
fn flush(&mut self) -> io::Result<()> {
self.write_block()?;
self.inner.flush()?;
Ok(())
}
}
#[derive(Debug, Error)]
pub(crate) enum BlockCipherError {
#[error("Invalid length for IV")]
......@@ -180,6 +236,82 @@ where
}
}
pub(crate) struct BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
inner: W,
buffer: GenericArray<u8, C::BlockSize>,
buf_idx: usize,
cipher: Cbc<C, Pkcs7>,
}
impl<C, W> BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
pub(crate) fn wrap(
inner: W,
key: crypto::CipherKey,
iv: &[u8],
) -> Result<BlockCipherWriter<C, W>, BlockCipherError> {
Ok(BlockCipherWriter {
inner,
cipher: Cbc::new_var(&key.0, &iv)?,
buffer: GenericArray::default(),
buf_idx: 0,
})
}
fn write_buffer(&mut self) -> io::Result<()> {
let mut blocks_to_encrypt = [std::mem::take(&mut self.buffer)];
self.cipher.encrypt_blocks(&mut blocks_to_encrypt);
self.inner.write_all(&blocks_to_encrypt[0])?;
Ok(())
}
}
impl<C, W> io::Write for BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for byte in buf.iter() {
self.buffer[self.buf_idx] = *byte;
self.buf_idx += 1;
if self.buf_idx == self.buffer.len() {
self.write_buffer()?;
self.buf_idx = 0;
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<C, W> Drop for BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
fn drop(&mut self) {
Pkcs7::pad_block(&mut self.buffer, self.buf_idx).unwrap();
if let Err(e) = self.write_buffer() {
println!(
"Error finalising database write {}! Likely database corruption!",
e
)
};
}
}
pub(crate) fn kdbx4_read_stream<'a, R: io::Read + 'a>(
inner: R,
hmac_key: crypto::HmacKey,
......@@ -225,3 +357,49 @@ pub(crate) fn kdbx4_read_stream<'a, R: io::Read + 'a>(
Ok(decompressed)
}
pub(crate) fn kdbx4_write_stream<'a, W: io::Write + 'a>(
inner: W,
hmac_key: crypto::HmacKey,
cipher_key: crypto::CipherKey,
cipher: binary::Cipher,
iv: &[u8],
compression: binary::CompressionType,
) -> io::Result<Box<dyn io::Write + 'a>> {
let buffered = io::BufWriter::new(inner);
let verified = HmacWriter::new(buffered, hmac_key);
let encrypted: Box<dyn io::Write> = match cipher {
binary::Cipher::Aes256 => BlockCipherWriter::<Aes256, _>::wrap(verified, cipher_key, iv)
.map(|w| Box::new(w) as Box<dyn io::Write>)
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid cipher params - Could not create CBC block mode".to_string(),
)
}),
binary::Cipher::Aes128 => BlockCipherWriter::<Aes128, _>::wrap(verified, cipher_key, iv)
.map(|w| Box::new(w) as Box<dyn io::Write>)
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid cipher params - Could not create CBC block mode".to_string(),
)
}),
_ => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported cipher setting {:?}", cipher),
)),
}?;
let compressed: Box<dyn io::Write> = match compression {
binary::CompressionType::None => Box::new(encrypted),
binary::CompressionType::Gzip => Box::new(libflate::gzip::Encoder::new(encrypted)?),
binary::CompressionType::Unknown(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported compression type {:?}", compression),
))
}
};
Ok(compressed)
}
//! Keepass data types
use chrono::NaiveDateTime;
use uuid::Uuid;
/// A value for a entry's field
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
/// A value using in-memory encryption
Protected(String),
/// A value that's unencrypted in the database
Standard(String),
/// A empty value
Empty,
}
impl Default for Value {
fn default() -> Value {
Value::Empty
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// A key value pair
pub struct Field {
/// The name of this field
pub key: String,
/// The (optionally encrypted) value of this field
pub value: String,
pub value: Value,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
impl Field {
/// Create a new field without memory protection
pub fn new(key: &str, value: &str) -> Field {
Field {
key: key.to_string(),
value: Value::Standard(value.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// A single password entry
pub struct Entry {
/// Identifier for this entry
......@@ -23,8 +52,26 @@ pub struct Entry {
pub times: Times,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// A group or folder of password entries
impl Entry {
/// Add a new field to the entry
pub fn add_field(&mut self, field: Field) {
self.fields.push(field);
}
}
impl Default for Entry {
fn default() -> Entry {
Entry {
uuid: Uuid::new_v4(),
fields: Vec::new(),
history: Vec::new(),
times: Times::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// A group or folder of password entries and child groups
pub struct Group {
/// Identifier for this group
pub uuid: Uuid,
......@@ -38,14 +85,38 @@ pub struct Group {
pub times: Times,
}
impl Group {
/// Add a new entry to this group
pub fn add_entry(&mut self, entry: Entry) {
self.entries.push(entry);
}
}
impl Default for Group {
fn default() -> Group {
Group {
uuid: Uuid::new_v4(),
name: String::new(),
entries: Vec::new(),
children: Vec::new(),
times: Times::default(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// Identifies which fields are encrypted in memory for official clients
pub struct MemoryProtection {
protect_title: bool,
protect_user_name: bool,
protect_password: bool,
protect_url: bool,
protet_notes: bool,
/// Whether title fields should be encrypted
pub protect_title: bool,
/// Whether username fields should be encrypted
pub protect_user_name: bool,
/// Whether password fields should be encrypted
pub protect_password: bool,
/// Whether URL fields should be encrypted
pub protect_url: bool,
/// Whether Notes fields should be encrypted
pub protect_notes: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
......@@ -99,7 +170,7 @@ impl Default for Times {
#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// Decrypted database structure
pub struct XmlDatabase {
pub struct Database {
/// Meta information about this database
pub meta: Meta,
/// Trees of items in this database
......
//! Inner XML format and decrypted database data
mod decoders;
pub(crate) mod parse;
mod type_parsers;
mod types;
pub(crate) mod serialize;
pub use parse::parse_xml;
pub use types::*;
pub use serialize::write_xml;
use chrono::{Duration, NaiveDate, NaiveDateTime};
use uuid::Uuid;
fn parse_uuid(b64uuid: &str) -> Option<Uuid> {
pub fn keepass_epoch() -> NaiveDateTime {
NaiveDate::from_ymd(0, 1, 1).and_hms(0, 0, 0)
}
pub(crate) fn decode_uuid(b64uuid: &str) -> Option<Uuid> {
let decoded = base64::decode(b64uuid).ok()?;
Uuid::from_slice(&decoded).ok()
}
fn parse_datetime(b64date: &str) -> Option<NaiveDateTime> {
pub(crate) fn decode_datetime(b64date: &str) -> Option<NaiveDateTime> {
let decoded = base64::decode(b64date).ok()?;
let mut bytes = [0u8; 8];
for i in 0..usize::min(bytes.len(), decoded.len()) {
......@@ -14,7 +18,14 @@ fn parse_datetime(b64date: &str) -> Option<NaiveDateTime> {
}
let timestamp = Duration::seconds(i64::from_le_bytes(bytes));
NaiveDate::from_ymd(0, 1, 1)
.and_hms(0, 0, 0)
.checked_add_signed(timestamp)