...
 
Commits (4)
......@@ -35,7 +35,7 @@
"kind": "bin"
}
},
"args": ["res/kdbx4-argon2.kdbx"],
"args": ["res/test-input/kdbx4-argon2.kdbx"],
"cwd": "${workspaceFolder}"
},
{
......@@ -53,7 +53,25 @@
"kind": "bin"
}
},
"args": ["kdbx_rs.kdbx"],
"args": ["res/test-input/kdbx-argon2-twofish.kdbx"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'kdbx-decrypt'",
"cargo": {
"args": [
"build",
"--bin=kdbx-decrypt",
"--package=kdbx-rs"
],
"filter": {
"name": "kdbx-decrypt",
"kind": "bin"
}
},
"args": ["res/test_input/kdbx31-aes256.kdbx"],
"cwd": "${workspaceFolder}"
},
{
......
# Changelog
## 0.2.0
* Add support for writing protected values
* New APIs;
* `Group::recursive_entries()` / `Group::recursive_entries_mut()` for iterating
through all entries in a group and its child groups
* `utils::NullStreamCipher` to parse/write XML files without inner encryption
* `Group`
* `recursive_entries()` / `recursive_entries_mut()` for iterating
through all entries in a group and its child groups
* `find_entry(Fn(&Entry) -> bool)` and `find_entry_mut(Fn(&Entry) -> bool)`
for finding an entry recursively
* `find_group(Fn(&Group) -> bool)` and `find_group_mut(Fn(&Group) -> bool)`
for finding an entry recursively
* Many group APIs are mirrored on `Database` for operating on the root group.
* Actually support AES KDF
* Read only support for KDBX 3.1
* Removed APIs
* `kdbx_rs::xml::default_stream_cipher` - use `NullStreamCipher` or `InnerStreamCipherAlgorithm::stream_cipher()`
* Struct fields on many database types are now hidden, use accessor fields instead
## 0.1.2
* Add support for reading KeepassXC OTP secrets (no code generation yet)
* Improve docs
## 0.1.1
Correct link to GPL
......
[package]
name = "kdbx-rs"
version = "0.1.1"
version = "0.2.0"
authors = ["Tony Finn <[email protected]>"]
edition = "2018"
description = "Keepass 2 (KDBX) password database parsing and creation"
......
......@@ -39,6 +39,7 @@ database.add_entry(Entry);
Saving a database to a file
```rust
let mut database = Database::default();
let kdbx = Kdbx::from_database(database);
kdbx.set_key(CompositeKey::from_password("foo123"))?;
......@@ -53,10 +54,10 @@ kdbx.write(&mut file)?;
| License | GPLv3+ | MIT | MIT | MIT/Apache | ISC |
| **Formats** | | | | | |
| .kdbx 4 | Yes | Read only | Read only | No | No |
| .kdbx 3 | No | Read only | No | Yes | No |
| .kdbx 3 | Read only | Read only | No | Yes | No |
| .kdb | No | No | No | No | Yes |
| **Algorithms** | | | | | |
| *KDFS* | | | | | |
| **Algorithms** |
| *KDFs* |
| AES | Yes | Yes | Yes | Yes | Yes |
| Argon 2 | Yes | Yes | Yes | No | Yes |
|*Database Ciphers*| | | | | |
......
......@@ -156,6 +156,39 @@ The master key is used as the basis of all other keys used for encryption. It is
used directly in encryption. It is calculated by runing the configured KDF against the
processed composite key.
**Argon2**
When the Argon2 KDF function is in use, the following parameters are read from the KDF parameter
dictionary:
* "I" - the number of passes/iterations of Argon2 to use
* "V" - The argon 2 version to use, currently only 0x13 is widely used
* "M" - The amount of memory (in bytes) for the function to use.
* "P" - The number of parallel computations to perform
* "S" - The salt to use to create the cipher
In addition, the variant is hardcoded to Argon2d. The output of the Argon2d KDF is then
used directly as the master key.
**AES**
When the AES KDF is used, the following parameters are read from the KDF parameter dictionary in KDBX 4
* "R" - The number of rounds of AES to run
* "S" - The key used for the cipher
For KDBX 3.1, these are read from the `TransformRounds` / `TransformSeed` parameters
in the outer head instead.
The key is derived as follows:
* Create an AES cipher in ECB mode using `S` as the key.
* Create a buffer for the key with the initial value being the composite key.
* Run `key = AES(key)` for the number of rounds specified by `R`
* Note that `S` is normally 32 bytes long. Depending on your AES library, you
might have to split the key into two blocks and run them in parallel manually.
* The final key is then `sha256(key)`
#### Cipher Key
The cipher key is used for encrypting/decrypting the database. It is calculated as
......@@ -213,11 +246,13 @@ The block index is the position of the block in the file.
In KDBX 3, each of these blocks have the following format:
* Block ID (4 bytes)
* SHA256 of plain text (32 bytes)
* Ciphertext length (4 bytes)
* Ciphertext (variable length)
The block is verified by decrypting the ciphertext, then calculating the sha256 hash.
The block is verified by decrypting the ciphertext, then calculating the sha256 hash of
the decrypted plain text.
### Inner header - KDBX 4
......@@ -294,6 +329,7 @@ Certain data types are encoded in the XML document. The following transformation
* Datetimes (KDBX 4+): base64(seconds since 1/1/1 00:00:00)
* It's worth emphasising: Seconds since Year 1, not the unix epoch
* In KDBX 3, they're stored as ISO 8601 strings instead (e.g. "2020-05-01T00:00:00Z")
* UUIDs: base64(uuid as byte array)
Note that KeePass's export as XML stores datetimes as ISO 8601 strings, which is not how they
......
......@@ -43,7 +43,7 @@
</String>
<String>
<Key>Password</Key>
<Value>kdbxrs</Value>
<Value Protected="True">JPrgr9K0</Value>
</String>
</Entry>
</Group>
......
......@@ -42,6 +42,7 @@ fn main() -> Result<(), kdbx_rs::Error> {
}
let kdbx = kdbx_rs::open(&args[1])?;
let header = kdbx.header();
println!("Version: {}.{}", kdbx.major_version(), kdbx.minor_version());
println!("Cipher: {:?}", header.cipher);
println!("Compression: {:?}", header.compression_type);
print_kdf(&header.kdf_params);
......
......@@ -2,7 +2,7 @@
//!
//! Primarily for verifying kdbx-rs changes
use kdbx_rs::database::{Entry, Field, Group, Times};
use kdbx_rs::database::{Entry, Group, Times};
use kdbx_rs::{CompositeKey, Database, Error, Kdbx};
use chrono::NaiveDate;
......@@ -33,13 +33,13 @@ fn main() -> Result<(), Error> {
db.meta.database_name = "BarName".to_string();
db.meta.database_description = "BazDesc".to_string();
let mut group = Group::default();
group.name = "Root".to_string();
group.uuid = Uuid::from_u128(0x1234_5678);
group.set_name("Root");
group.set_uuid(Uuid::from_u128(0x1234_5678));
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(0x0065_4321);
entry.set_title("Bar");
entry.set_password("kdbxrs");
entry.set_uuid(Uuid::from_u128(0x0065_4321));
entry.times = sample_times();
group.entries.push(entry);
db.groups.push(group);
......
......@@ -28,8 +28,11 @@ pub enum OpenError {
/// Errors encountered unlocking a encrypted database
pub enum UnlockError {
/// The HMAC signature check failed. This indicates an invalid password or corrupt DB
#[error("Header validation failed - wrong password or corrupt database")]
#[error("Header HMAC validation failed - wrong password or corrupt database")]
HmacInvalid,
/// The start bytes check failed. This indicates an invalid password or corrupt DB
#[error("Header start validation failed - wrong password or corrupt database")]
StartBytesInvalid,
/// There was some error generating the keys, likely incorrect or unsupported KDF options
#[error("Key generation failed - {0}")]
KeyGen(#[from] crypto::KeyGenerationError),
......@@ -85,4 +88,7 @@ pub enum WriteError {
/// The database could not be written to as `set_key()` has not been called.
#[error("No key to write database with")]
MissingKeys,
/// The inner stream is invalid
#[error("Cannot create inner stream to write protected values - {0}")]
InnerStream(#[from] InnerStreamError),
}
......@@ -170,13 +170,20 @@ where
}
}
pub(crate) fn read_one_header(&mut self) -> Result<HeaderField<T>> {
pub(crate) fn read_one_header(&mut self, major_version: u16) -> Result<HeaderField<T>> {
let mut ty_buffer = [0u8];
self.reader.read_exact(&mut ty_buffer)?;
let ty = T::from(ty_buffer[0]);
let mut len_buffer = [0u8; 4];
self.reader.read_exact(&mut len_buffer)?;
let len = u32::from_le_bytes(len_buffer.clone());
let len = if major_version >= 4 {
let mut len_buffer = [0u8; 4];
self.reader.read_exact(&mut len_buffer)?;
u32::from_le_bytes(len_buffer.clone())
} else {
let mut len_buffer = [0u8; 2];
self.reader.read_exact(&mut len_buffer)?;
u16::from_le_bytes(len_buffer.clone()) as u32
};
let mut header_buffer = utils::buffer(len as usize);
self.reader.read_exact(&mut header_buffer)?;
......@@ -186,12 +193,12 @@ where
})
}
pub(crate) fn read_all_headers(&mut self) -> Result<Vec<HeaderField<T>>> {
pub(crate) fn read_all_headers(&mut self, major_version: u16) -> Result<Vec<HeaderField<T>>> {
let mut headers = Vec::new();
let mut header = self.read_one_header()?;
let mut header = self.read_one_header(major_version)?;
while !header.ty.is_final() {
headers.push(header);
header = self.read_one_header()?;
header = self.read_one_header(major_version)?;
}
Ok(headers)
......@@ -203,6 +210,7 @@ pub struct KdbxHeaderBuilder {
pub cipher: Option<header_fields::Cipher>,
pub kdf_params: Option<header_fields::KdfParams>,
pub compression_type: Option<header_fields::CompressionType>,
pub stream_start_bytes: Option<Vec<u8>>,
pub other_headers: Vec<HeaderField<OuterHeaderId>>,
pub master_seed: Option<Vec<u8>>,
pub encryption_iv: Option<Vec<u8>>,
......@@ -220,6 +228,9 @@ impl KdbxHeaderBuilder {
self.cipher = Some(cipher);
}
OuterHeaderId::StreamStartBytes => {
self.stream_start_bytes = Some(header.data);
}
OuterHeaderId::KdfParameters => {
self.kdf_params = match variant_dict::parse_variant_dict(&*header.data) {
Ok(vdict) => Some(vdict.try_into()?),
......@@ -255,7 +266,34 @@ impl KdbxHeaderBuilder {
Ok(())
}
fn build(self) -> Result<KdbxHeader> {
fn get_kdf_params(&mut self) -> Option<header_fields::KdfParams> {
if self.kdf_params.is_some() {
self.kdf_params.take()
} else {
let rounds = self
.other_headers
.iter()
.find(|h| h.ty == OuterHeaderId::LegacyTransformRounds)
.map(|h| {
let mut buf = [0u8; 8];
buf.clone_from_slice(&h.data[0..8]);
u64::from_le_bytes(buf)
});
let seed = self
.other_headers
.iter()
.find(|h| h.ty == OuterHeaderId::LegacyTransformSeed)
.map(|h| h.data.clone());
match (rounds, seed) {
(Some(r), Some(s)) => Some(header_fields::KdfParams::Aes { rounds: r, salt: s }),
_ => None,
}
}
}
fn build(mut self) -> Result<KdbxHeader> {
let kdf_params = self.get_kdf_params();
Ok(KdbxHeader {
cipher: self
.cipher
......@@ -269,9 +307,9 @@ impl KdbxHeaderBuilder {
encryption_iv: self
.encryption_iv
.ok_or_else(|| Error::MissingRequiredField(OuterHeaderId::EncryptionIv))?,
kdf_params: self
.kdf_params
kdf_params: kdf_params
.ok_or_else(|| Error::MissingRequiredField(OuterHeaderId::KdfParameters))?,
stream_start_bytes: self.stream_start_bytes,
other_headers: self.other_headers,
})
}
......@@ -290,6 +328,8 @@ pub struct KdbxHeader {
pub kdf_params: header_fields::KdfParams,
/// Compression applied prior to encryption
pub compression_type: header_fields::CompressionType,
/// First 32 bytes, used to check kdbx3 archives
pub stream_start_bytes: Option<Vec<u8>>,
/// Custom and unrecognized header types
pub other_headers: Vec<HeaderField<OuterHeaderId>>,
/// Master seed used to make crypto keys DB specific
......@@ -329,6 +369,7 @@ impl KdbxHeader {
},
other_headers: Vec::new(),
compression_type: super::CompressionType::None,
stream_start_bytes: None,
master_seed,
encryption_iv,
}
......@@ -336,15 +377,20 @@ impl KdbxHeader {
pub(crate) fn read<R: Read>(
mut caching_reader: utils::CachingReader<R>,
major_version: u16,
) -> Result<(KdbxHeader, Vec<u8>)> {
let mut header_builder = KdbxHeaderBuilder::default();
let headers = HeaderParser::new(&mut caching_reader).read_all_headers()?;
let headers = HeaderParser::new(&mut caching_reader).read_all_headers(major_version)?;
for header in headers {
header_builder.add_header(header)?;
}
let (header_bin, input) = caching_reader.into_inner();
if major_version < 4 {
return Ok((header_builder.build()?, header_bin));
}
let mut sha = utils::buffer(Sha256::output_size());
input.read_exact(&mut sha)?;
......@@ -434,6 +480,31 @@ pub struct KdbxInnerHeader {
}
impl KdbxInnerHeader {
pub(crate) fn from_legacy_fields(header: &KdbxHeader) -> Result<KdbxInnerHeader> {
let cipher = &header
.other_headers
.iter()
.find(|h| h.ty == OuterHeaderId::InnerRandomStreamId)
.ok_or_else(|| Error::MissingRequiredField(OuterHeaderId::InnerRandomStreamId))?
.data;
let key = header
.other_headers
.iter()
.find(|h| h.ty == OuterHeaderId::ProtectedStreamKey)
.ok_or_else(|| Error::MissingRequiredField(OuterHeaderId::ProtectedStreamKey))?
.data
.clone();
let mut cipher_id_buf = [0u8; 4];
cipher_id_buf.clone_from_slice(&cipher[0..4]);
let cipher_id = u32::from_le_bytes(cipher_id_buf);
Ok(KdbxInnerHeader {
inner_stream_cipher: cipher_id.into(),
inner_stream_key: key.into(),
other_headers: Vec::new(),
})
}
/// Returns an inner header setup for a default stream cipher and OS random keys
///
/// Currently the default stream cipher is ChaCha20
......@@ -456,9 +527,9 @@ impl KdbxInnerHeader {
}
}
pub(crate) fn read<R: Read>(reader: &mut R) -> Result<KdbxInnerHeader> {
pub(crate) fn read<R: Read>(reader: &mut R, major_version: u16) -> Result<KdbxInnerHeader> {
let mut header_builder = KdbxInnerHeaderBuilder::default();
let headers = HeaderParser::new(reader).read_all_headers()?;
let headers = HeaderParser::new(reader).read_all_headers(major_version)?;
for header in headers {
header_builder.add_header(header)?;
}
......
use super::{errors, header};
use crate::{crypto, stream, types};
use crate::{crypto, database, stream};
use std::io::{Read, Write};
use std::ops::{Deref, DerefMut};
pub trait KdbxState: std::fmt::Debug {
fn header(&self) -> &header::KdbxHeader;
fn header_mut(&mut self) -> &mut header::KdbxHeader;
fn major_version(&self) -> u16;
fn minor_version(&self) -> u16;
fn write<W: Write>(&self, output: W) -> Result<(), errors::WriteError>;
}
......@@ -16,7 +18,7 @@ pub trait KdbxState: std::fmt::Debug {
/// or `Kdbx<Unlocked>`.
///
/// A keepass 2 archive can be obtained in one of two ways. You may read
/// an existing archive using [`kdbx_rs::open`][crate::open] or
/// an existing archive using [`kdbx_rs::open`][crate::open] or
/// [`kdbx_rs::from_reader`][crate::from_reader].
///
/// You can also create a password database using [`Database`][crate::Database],
......@@ -39,6 +41,16 @@ impl<T: KdbxState> Kdbx<T> {
self.state.header_mut()
}
/// Major archive version
pub fn major_version(&self) -> u16 {
self.state.major_version()
}
/// Major archive version
pub fn minor_version(&self) -> u16 {
self.state.minor_version()
}
/// Write this archive to the given output stream
pub fn write<W: Write>(&self, output: W) -> Result<(), errors::WriteError> {
self.state.write(output)?;
......@@ -96,8 +108,15 @@ impl Unlocked {
self.header.compression_type,
)?;
self.inner_header.write(&mut encrypted_stream)?;
crate::xml::write_xml(&mut encrypted_stream, &self.database)?;
let mut stream_cipher = self
.inner_header
.inner_stream_cipher
.stream_cipher(&self.inner_header.inner_stream_key)?;
crate::xml::write_xml(
&mut encrypted_stream,
&self.database,
stream_cipher.as_mut(),
)?;
encrypted_stream.finish()?;
Ok(encrypted_buf)
......@@ -113,6 +132,14 @@ impl KdbxState for Unlocked {
&mut self.header
}
fn major_version(&self) -> u16 {
self.major_version
}
fn minor_version(&self) -> u16 {
self.minor_version
}
fn write<W: Write>(&self, mut output: W) -> Result<(), errors::WriteError> {
let master_key = self
.master_key
......@@ -205,15 +232,15 @@ impl Kdbx<Unlocked> {
}
impl Deref for Kdbx<Unlocked> {
type Target = types::Database;
type Target = database::Database;
fn deref(&self) -> &types::Database {
fn deref(&self) -> &database::Database {
&self.state.database
}
}
impl DerefMut for Kdbx<Unlocked> {
fn deref_mut(&mut self) -> &mut types::Database {
fn deref_mut(&mut self) -> &mut database::Database {
&mut self.state.database
}
}
......@@ -230,7 +257,7 @@ pub struct Locked {
/// Minor version of the database file format
pub(crate) minor_version: u16,
/// hmac code to verify keys and header integrity
pub(crate) hmac: Vec<u8>,
pub(crate) hmac: Option<Vec<u8>>,
/// Encrypted vault data
pub(crate) encrypted_data: Vec<u8>,
}
......@@ -244,6 +271,14 @@ impl KdbxState for Locked {
&mut self.header
}
fn major_version(&self) -> u16 {
self.major_version
}
fn minor_version(&self) -> u16 {
self.minor_version
}
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;
......@@ -253,15 +288,17 @@ impl KdbxState for Locked {
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)?;
if self.major_version >= 4 {
output.write_all(&crypto::sha256(&header_buf))?;
output.write_all(&self.hmac.as_ref().unwrap())?;
}
output.write_all(&self.encrypted_data)?;
Ok(())
}
}
impl Kdbx<Locked> {
fn decrypt_data(
fn decrypt_v4(
&self,
master_key: &crypto::MasterKey,
) -> Result<(header::KdbxInnerHeader, Vec<u8>), errors::UnlockError> {
......@@ -275,7 +312,8 @@ impl Kdbx<Locked> {
&self.state.header.encryption_iv,
self.state.header.compression_type,
)?;
let inner_header = header::KdbxInnerHeader::read(&mut input_stream)?;
let inner_header =
header::KdbxInnerHeader::read(&mut input_stream, self.state.major_version)?;
let mut output_buffer = Vec::new();
input_stream.read_to_end(&mut output_buffer)?;
Ok((inner_header, output_buffer))
......@@ -285,18 +323,79 @@ impl Kdbx<Locked> {
///
/// If unlock fails, returns the locked kdbx file along with the error
pub fn unlock(self, key: &crypto::CompositeKey) -> Result<Kdbx<Unlocked>, FailedUnlock> {
if self.state.major_version >= 4 {
self.unlock_v4(&key)
} else {
self.unlock_v3(&key)
}
}
fn decrypt_v3(
&self,
master_key: &crypto::MasterKey,
) -> Result<(header::KdbxInnerHeader, Vec<u8>), errors::UnlockError> {
let cipher_key = master_key.cipher_key(&self.state.header.master_seed);
let mut input_stream = stream::kdbx3_read_stream(
&*self.state.encrypted_data,
cipher_key,
self.state.header.cipher,
&self.state.header.encryption_iv,
self.state.header.compression_type,
self.header().stream_start_bytes.as_ref().unwrap(),
)?;
let inner_header = header::KdbxInnerHeader::from_legacy_fields(&self.state.header)?;
let mut output_buffer = Vec::new();
input_stream.read_to_end(&mut output_buffer)?;
Ok((inner_header, output_buffer))
}
fn unlock_v3(self, key: &crypto::CompositeKey) -> Result<Kdbx<Unlocked>, FailedUnlock> {
let composed_key = key.composed();
let master_key = match composed_key.master_key(&self.state.header.kdf_params) {
let master_key = match composed_key.master_key(&self.header().kdf_params) {
Ok(master_key) => master_key,
Err(e) => return Err(FailedUnlock(self, errors::UnlockError::from(e))),
};
let parsed = self
.decrypt_v3(&master_key)
.and_then(|(inner_header, data)| {
let mut stream_cipher = inner_header
.inner_stream_cipher
.stream_cipher(inner_header.inner_stream_key.as_ref())?;
let parsed = crate::xml::parse_xml(data.as_slice(), stream_cipher.as_mut())?;
Ok((inner_header, data, parsed))
});
match parsed {
Ok((inner_header, data, db)) => Ok(Kdbx {
state: Unlocked {
inner_header,
header: self.state.header,
major_version: self.state.major_version,
minor_version: self.state.minor_version,
composed_key: Some(composed_key),
master_key: Some(master_key),
database: db,
xml_data: Some(data),
},
}),
Err(e) => Err(FailedUnlock(self, e)),
}
}
fn unlock_v4(self, key: &crypto::CompositeKey) -> Result<Kdbx<Unlocked>, FailedUnlock> {
let composed_key = key.composed();
let master_key = match composed_key.master_key(&self.header().kdf_params) {
Ok(master_key) => master_key,
Err(e) => return Err(FailedUnlock(self, errors::UnlockError::from(e))),
};
let hmac_key = master_key.hmac_key(&self.state.header.master_seed);
let header_block_key = hmac_key.block_key(u64::MAX);
if header_block_key.verify_header_block(&self.state.hmac, &self.state.header_data) {
let hmac = self.state.hmac.clone().unwrap();
if header_block_key.verify_header_block(hmac.as_ref(), &self.state.header_data) {
let parsed = self
.decrypt_data(&master_key)
.decrypt_v4(&master_key)
.and_then(|(inner_header, data)| {
let mut stream_cipher = inner_header
.inner_stream_cipher
......
......@@ -30,14 +30,18 @@ pub fn from_reader<R: Read>(mut input: R) -> Result<Kdbx<Locked>, errors::OpenEr
let minor_version = u16::from_le_bytes([buffer[0], buffer[1]]);
let major_version = u16::from_le_bytes([buffer[2], buffer[3]]);
if major_version != 4 {
if major_version < 3 || major_version > 4 {
return Err(errors::OpenError::UnsupportedMajorVersion(major_version));
}
let (header, header_data) = header::KdbxHeader::read(caching_reader)?;
let mut hmac = utils::buffer(Sha256::output_size());
input.read_exact(&mut hmac)?;
let (header, header_data) = header::KdbxHeader::read(caching_reader, major_version)?;
let hmac = if major_version >= 4 {
let mut hmac = utils::buffer(Sha256::output_size());
input.read_exact(&mut hmac)?;
Some(hmac)
} else {
None
};
let mut encrypted_data = Vec::new();
input.read_to_end(&mut encrypted_data)?;
......
use crate::binary;
use aes::{block_cipher_trait::generic_array::GenericArray, Aes256};
use block_modes::{block_padding::ZeroPadding, BlockMode, Ecb};
use hmac::crypto_mac::MacResult;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256, Sha512};
......@@ -84,6 +86,25 @@ impl ComposedKey {
Ok(MasterKey(hash))
}
binary::KdfParams::Aes { rounds, salt } => {
let mut cipher: Ecb<Aes256, ZeroPadding> =
Ecb::new_var(&salt, Default::default()).unwrap();
let chunked: Vec<GenericArray<u8, _>> = self
.0
.chunks_exact(16)
.map(|chunk| GenericArray::from_slice(chunk).clone())
.collect();
let mut blocks = [chunked[0], chunked[1]];
for _ in 0..*rounds {
cipher.encrypt_blocks(&mut blocks);
}
let mut transformed_hasher = Sha256::new();
transformed_hasher.input(blocks[0]);
transformed_hasher.input(blocks[1]);
let transformed = transformed_hasher.result();
Ok(MasterKey(transformed.as_slice().to_vec()))
}
_ => Ok(MasterKey(Vec::new())),
}
}
......
This diff is collapsed.
......@@ -25,6 +25,9 @@
//! # }
//! ```
//!
//! Alternatively, [`kdbx_rs::from_reader`] can be used to open a database
//! from a non file source (such as in-memory or a network stream)
//!
//! # Generating a new password database
//!
//! A database can be created in memory by using the [`Database::default()`]
......@@ -75,8 +78,11 @@
//! # Ok(())
//! # }
//! ```
//! Alternatively, [`kdbx_rs::from_reader`] can be used to open a database
//! from a non file source (such as in-memory or a network stream)
//!
//! # Database operations
//!
//! See the [`database`][crate::database] module-level documentation for examples
//! of database operations.
//!
//! [`CompositeKey`]: crate::CompositeKey
//! [`Database`]: crate::Database
......@@ -91,17 +97,13 @@
pub mod binary;
mod crypto;
pub mod database;
pub mod errors;
mod stream;
mod types;
mod utils;
pub mod utils;
pub mod xml;
pub use crate::types::Database;
/// Password database datatypes
pub mod database {
pub use crate::types::*;
}
pub use crate::database::Database;
pub use binary::{from_reader, open, Kdbx};
pub use crypto::CompositeKey;
pub use errors::Error;
mod block_cipher;
mod hmac;
mod kdbx3;
mod pipeline;
pub(crate) mod random;
mod stream_cipher;
......@@ -9,4 +10,5 @@ pub(crate) use self::stream_cipher::{
StreamCipherReader, StreamCipherWriter, StreamCipherWriterExt,
};
pub(crate) use block_cipher::{BlockCipherReader, BlockCipherWriter, BlockCipherWriterExt};
pub(crate) use pipeline::{kdbx4_read_stream, kdbx4_write_stream};
pub(crate) use kdbx3::HashedBlockReader;
pub(crate) use pipeline::{kdbx3_read_stream, kdbx4_read_stream, kdbx4_write_stream};
use crate::crypto;
use std::io::{self, Read};
pub struct HashedBlockReader<R>
where
R: Read,
{
inner: R,
buffer: Vec<u8>,
buf_idx: usize,
}
impl<R> HashedBlockReader<R>
where
R: Read,
{
pub(crate) fn new(inner: R) -> HashedBlockReader<R> {
HashedBlockReader {
inner,
buffer: Vec::new(),
buf_idx: 0,
}
}
fn buffer_next_block(&mut self) -> io::Result<usize> {
let mut id_buf = [0u8; 4];
self.inner.read_exact(&mut id_buf)?;
let mut hash_buf = [0u8; 32];
self.inner.read_exact(&mut hash_buf)?;
let mut len_buf = [0u8; 4];
self.inner.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf) as usize;
self.buffer.resize_with(len, Default::default);
self.inner.read_exact(&mut self.buffer)?;
self.buf_idx = 0;
if crypto::verify_sha256(&self.buffer, &hash_buf) {
Ok(len)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"Block failed hash verification",
))
}
}
}
impl<R> Read for HashedBlockReader<R>
where
R: Read,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut remaining_in_buffer = self.buffer.len() - self.buf_idx;
if remaining_in_buffer == 0 {
remaining_in_buffer = self.buffer_next_block()?;
}
let copy_len = usize::min(remaining_in_buffer, buf.len());
for i in 0..copy_len {
buf[i] = self.buffer[self.buf_idx + i];
}
self.buf_idx += copy_len;
Ok(copy_len)
}
}
......@@ -9,8 +9,8 @@ use derive_more::From;
use twofish::Twofish;
use super::{
BlockCipherReader, BlockCipherWriter, BlockCipherWriterExt, HMacReader, HmacWriter,
StreamCipherWriterExt,
BlockCipherReader, BlockCipherWriter, BlockCipherWriterExt, HMacReader, HashedBlockReader,
HmacWriter, StreamCipherWriterExt,
};
fn block_cipher_read_stream<C, R>(
......@@ -30,29 +30,25 @@ where
})
}
pub(crate) fn kdbx4_read_stream<'a, R: io::Read + 'a>(
pub(crate) fn decryption_stream<'a, R: io::Read + 'a>(
inner: R,
hmac_key: crypto::HmacKey,
cipher_key: crypto::CipherKey,
cipher: binary::Cipher,
iv: &[u8],
compression: binary::CompressionType,
) -> io::Result<Box<dyn io::Read + 'a>> {
let buffered = io::BufReader::new(inner);
let verified = HMacReader::new(buffered, hmac_key);
let decrypted: Box<dyn io::Read> = match cipher {
let stream: Box<dyn io::Read> = match cipher {
binary::Cipher::Aes256 => Box::new(block_cipher_read_stream::<Aes256, _>(
verified, cipher_key, iv,
inner, cipher_key, iv,
)?),
binary::Cipher::Aes128 => Box::new(block_cipher_read_stream::<Aes128, _>(
verified, cipher_key, iv,
inner, cipher_key, iv,
)?),
binary::Cipher::TwoFish => Box::new(block_cipher_read_stream::<Twofish, _>(
verified, cipher_key, iv,
inner, cipher_key, iv,
)?),
binary::Cipher::ChaCha20 => {
let cipher = ChaCha20::new_var(&cipher_key.0, &iv).unwrap();
Box::new(super::StreamCipherReader::new(verified, cipher))
Box::new(super::StreamCipherReader::new(inner, cipher))
}
_ => {
return Err(io::Error::new(
......@@ -61,6 +57,54 @@ pub(crate) fn kdbx4_read_stream<'a, R: io::Read + 'a>(
))
}
};
Ok(stream)
}
pub(crate) fn kdbx3_read_stream<'a, R: io::Read + 'a>(
inner: R,
cipher_key: crypto::CipherKey,
cipher: binary::Cipher,
iv: &[u8],
compression: binary::CompressionType,
expected_start_bytes: &[u8],
) -> io::Result<Box<dyn io::Read + 'a>> {
let buffered = io::BufReader::new(inner);
let mut decrypted = decryption_stream(buffered, cipher_key, cipher, iv)?;
let mut start_bytes = [0u8; 32];
decrypted.read_exact(&mut start_bytes)?;
if &start_bytes != expected_start_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Could not validate start bytes",
));
}
let verified = HashedBlockReader::new(decrypted);
let decompressed: Box<dyn io::Read> = match compression {
binary::CompressionType::None => Box::new(verified),
binary::CompressionType::Gzip => Box::new(libflate::gzip::Decoder::new(verified)?),
binary::CompressionType::Unknown(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unsupported compression type {:?}", compression),
))
}
};
Ok(decompressed)
}
pub(crate) fn kdbx4_read_stream<'a, R: io::Read + 'a>(
inner: R,
hmac_key: crypto::HmacKey,
cipher_key: crypto::CipherKey,
cipher: binary::Cipher,
iv: &[u8],
compression: binary::CompressionType,
) -> io::Result<Box<dyn io::Read + 'a>> {
let buffered = io::BufReader::new(inner);
let verified = HMacReader::new(buffered, hmac_key);
let decrypted = decryption_stream(verified, cipher_key, cipher, iv)?;
let decompressed: Box<dyn io::Read> = match compression {
binary::CompressionType::None => Box::new(decrypted),
binary::CompressionType::Gzip => Box::new(libflate::gzip::Decoder::new(decrypted)?),
......
use chacha20::ChaCha20;
use rand::{rngs::OsRng, RngCore};
use salsa20::Salsa20;
use sha2::{Digest, Sha256, Sha512};
use stream_cipher::{NewStreamCipher, StreamCipher};
......@@ -18,10 +17,8 @@ pub enum InnerStreamError {
}
impl InnerStreamCipherAlgorithm {
pub(crate) fn stream_cipher(
self,
key: &[u8],
) -> Result<Box<dyn StreamCipher>, InnerStreamError> {
/// Create a stream cipher instance for this algorithm
pub fn stream_cipher(self, key: &[u8]) -> Result<Box<dyn StreamCipher>, InnerStreamError> {
match self {
InnerStreamCipherAlgorithm::ChaCha20 => {
let iv = Sha512::digest(key);
......@@ -37,15 +34,3 @@ impl InnerStreamCipherAlgorithm {
}
}
}
/// Return a default stream cipher and its key
///
/// The stream cipher is created using ChaCha20, and the key is generated from OS randomness.
pub fn default_stream_cipher() -> (impl StreamCipher, Vec<u8>) {
let mut key = vec![0u8; 64];
OsRng.fill_bytes(&mut key);
let iv = Sha256::digest(key.as_ref());
let cipher = ChaCha20::new_var(&iv[0..32], &iv[32..44]).unwrap();
(cipher, key)
}
//! Utilities to help working with kdbx-rs
use std::io;
use uuid::Uuid;
......@@ -73,4 +75,12 @@ pub(crate) fn to_hex_string(data: &[u8]) -> String {
}
output
}
\ No newline at end of file
}
/// No-op stream cipher that does no encryption or decryption
pub struct NullStreamCipher;
impl stream_cipher::StreamCipher for NullStreamCipher {
fn encrypt(&mut self, _data: &mut [u8]) {}
fn decrypt(&mut self, _data: &mut [u8]) {}
}
//! Inner XML format and decrypted database data
//! Work directly with the KDBX decrypted inner XML format
mod decoders;
pub(crate) mod parse;
pub(crate) mod serialize;
pub use crate::stream::random::{default_stream_cipher, InnerStreamError};
pub use crate::stream::random::InnerStreamError;
pub use parse::parse_xml;
pub use serialize::write_xml;
use chrono::{Duration, NaiveDate, NaiveDateTime};
use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime};
use uuid::Uuid;
pub fn keepass_epoch() -> NaiveDateTime {
......@@ -10,7 +10,7 @@ pub(crate) fn decode_uuid(b64uuid: &str) -> Option<Uuid> {
Uuid::from_slice(&decoded).ok()
}
pub(crate) fn decode_datetime(b64date: &str) -> Option<NaiveDateTime> {
pub(crate) fn decode_datetime_b64(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()) {
......@@ -21,6 +21,15 @@ pub(crate) fn decode_datetime(b64date: &str) -> Option<NaiveDateTime> {
keepass_epoch().checked_add_signed(timestamp)
}
pub(crate) fn decode_datetime(strdate: &str) -> Option<NaiveDateTime> {
if strdate.contains("-") {
let dt = DateTime::parse_from_rfc3339(strdate).ok()?;
Some(dt.naive_utc())
} else {
decode_datetime_b64(strdate)
}
}
pub(crate) fn encode_uuid(uuid: &Uuid) -> String {
base64::encode(uuid.as_bytes())
}
......
use super::decoders::{decode_datetime, decode_uuid};
use crate::types::{Database, Entry, Field, Group, MemoryProtection, Meta, Times, Value};
use crate::database::{
Database, Entry, Field, Group, History, MemoryProtection, Meta, Times, Value,
};
use chrono::NaiveDateTime;
use std::io::Read;
use stream_cipher::StreamCipher;
......@@ -80,7 +82,7 @@ fn parse_uuid<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Uuid> {
fn parse_datetime<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<NaiveDateTime> {
parse_string(xml_event_reader)?
.and_then(|dt| decode_datetime(&dt))
.ok_or(Error::InvalidUuid)
.ok_or(Error::InvalidDatetime)
}
fn parse_bool<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<bool> {
......@@ -138,8 +140,8 @@ fn parse_field<R: Read, S: StreamCipher + ?Sized>(
fn parse_history<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Vec<Entry>> {
let mut history = Vec::new();
) -> Result<History> {
let mut history = History::default();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } if &name.local_name == "Entry" => {
......@@ -216,13 +218,9 @@ fn parse_group<R: Read, S: StreamCipher + ?Sized>(
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } => {
if &name.local_name == "Group" {
group
.children
.push(parse_group(xml_event_reader, stream_cipher)?);
group.add_group(parse_group(xml_event_reader, stream_cipher)?);
} else if &name.local_name == "Entry" {
group
.entries
.push(parse_entry(xml_event_reader, stream_cipher)?);
group.add_entry(parse_entry(xml_event_reader, stream_cipher)?);
} else if &name.local_name == "UUID" {
group.uuid = parse_uuid(xml_event_reader)?;
} else if &name.local_name == "Name" {
......@@ -356,6 +354,11 @@ fn parse_file<R: Read, S: StreamCipher + ?Sized>(
}
/// Parse decrypted XML into a database
///
/// If you need to obtain a stream cipher, consider using
/// [`InnerStreamCipherAlgorithm::stream_cipher`][crate::binary::InnerStreamCipherAlgorithm#stream_cipher]
/// if the XML contains encrypted data, or [`utils::NullStreamCipher`][crate::utils::NullStreamCipher]
/// if it does not (such as an export from the official client).
pub fn parse_xml<R: Read, S: StreamCipher + ?Sized>(
xml_data: R,
stream_cipher: &mut S,
......
use super::decoders::{encode_datetime, encode_uuid};
use crate::types::{Database, Entry, Field, Group, MemoryProtection, Meta, Times, Value};
use crate::database::{Database, Entry, Field, Group, MemoryProtection, Meta, Times, Value};
use std::io::Write;
use stream_cipher::StreamCipher;
use thiserror::Error;
use xml::writer::events::XmlEvent;
use xml::writer::EventWriter as XmlWriter;
......@@ -33,13 +34,21 @@ fn write_string_tag<W: Write, S: AsRef<str>>(
Ok(())
}
fn write_field<W: Write>(writer: &mut XmlWriter<W>, wrapper: &str, field: &Field) -> Result<()> {
fn write_field<W: Write, S: StreamCipher + ?Sized>(
writer: &mut XmlWriter<W>,
wrapper: &str,
field: &Field,
stream_cipher: &mut S,
) -> Result<()> {
writer.write(XmlEvent::start_element(wrapper))?;
write_string_tag(writer, "Key", &field.key)?;
match &field.value {
Value::Protected(v) => {
writer.write(XmlEvent::start_element("Value").attr("Protected", "True"))?;
writer.write(XmlEvent::characters(&v))?;
let mut encrypt_buf = v.clone().into_bytes();
stream_cipher.encrypt(&mut encrypt_buf);
let encrypted = base64::encode(encrypt_buf);
writer.write(XmlEvent::characters(&encrypted))?;
writer.write(XmlEvent::end_element())?;
}
Value::Standard(v) => write_string_tag(writer, "Value", &v)?,
......@@ -66,14 +75,18 @@ fn write_memory_protection<W: Write>(
Ok(())
}
fn write_meta<W: Write>(writer: &mut XmlWriter<W>, meta: &Meta) -> Result<()> {
fn write_meta<W: Write, S: StreamCipher + ?Sized>(
writer: &mut XmlWriter<W>,
meta: &Meta,
stream_cipher: &mut S,
) -> Result<()> {
writer.write(XmlEvent::start_element("Meta"))?;
write_string_tag(writer, "Generator", "kdbx-rs")?;
write_string_tag(writer, "DatabaseName", &meta.database_name)?;
write_string_tag(writer, "DatabaseDescription", &meta.database_description)?;
writer.write(XmlEvent::start_element("CustomData"))?;
for field in &meta.custom_data {
write_field(writer, "Item", field)?;
write_field(writer, "Item", field, stream_cipher)?;
}
writer.write(XmlEvent::end_element())?;
write_memory_protection(writer, &meta.memory_protection)?;
......@@ -106,17 +119,21 @@ fn write_times<W: Write>(writer: &mut XmlWriter<W>, times: &Times) -> Result<()>
Ok(())
}
fn write_entry<W: Write>(writer: &mut XmlWriter<W>, entry: &Entry) -> Result<()> {
fn write_entry<W: Write, S: StreamCipher + ?Sized>(
writer: &mut XmlWriter<W>,
entry: &Entry,
stream_cipher: &mut S,
) -> Result<()> {
writer.write(XmlEvent::start_element("Entry"))?;
write_string_tag(writer, "UUID", &encode_uuid(&entry.uuid))?;
write_times(writer, &entry.times)?;
for field in &entry.fields {
write_field(writer, "String", field)?;
write_field(writer, "String", field, stream_cipher)?;
}
if !entry.history.is_empty() {
if entry.history.len() > 0 {
writer.write(XmlEvent::start_element("History"))?;
for old_entry in &entry.history {
write_entry(writer, old_entry)?;
for old_entry in entry.history.entries() {
write_entry(writer, old_entry, stream_cipher)?;
}
writer.write(XmlEvent::end_element())?;
}
......@@ -124,32 +141,45 @@ fn write_entry<W: Write>(writer: &mut XmlWriter<W>, entry: &Entry) -> Result<()>
Ok(())
}
fn write_group<W: Write>(writer: &mut XmlWriter<W>, group: &Group) -> Result<()> {
fn write_group<W: Write, S: StreamCipher + ?Sized>(
writer: &mut XmlWriter<W>,
group: &Group,
stream_cipher: &mut S,
) -> Result<()> {
writer.write(XmlEvent::start_element("Group"))?;
write_string_tag(writer, "UUID", encode_uuid(&group.uuid))?;
write_string_tag(writer, "Name", &group.name)?;
write_times(writer, &group.times)?;
for entry in &group.entries {
write_entry(writer, &entry)?;
for entry in group.entries() {
write_entry(writer, &entry, stream_cipher)?;
}
for group in &group.children {
write_group(writer, &group)?;
for group in group.groups() {
write_group(writer, &group, stream_cipher)?;
}
writer.write(XmlEvent::end_element())?;
Ok(())
}
/// Write the decrypted XML for a database to a file
pub fn write_xml<W: Write>(output: W, database: &Database) -> Result<()> {
///
/// If you need to obtain a stream cipher, try
/// [`InnerStreamCipherAlgorithm::stream_cipher`][crate::binary::InnerStreamCipherAlgorithm#stream_cipher]
/// if the XML contains encrypted data, or [`utils::NullStreamCipher`][crate::utils::NullStreamCipher]
/// if it does not (such as an export from the official client).
pub fn write_xml<W: Write, S: StreamCipher + ?Sized>(
output: W,
database: &Database,
stream_cipher: &mut S,
) -> Result<()> {
let config = xml::EmitterConfig::default()
.perform_indent(true)
.indent_string("\t");
let mut writer = xml::EventWriter::new_with_config(output, config);
writer.write(XmlEvent::start_element("KeePassFile"))?;
write_meta(&mut writer, &database.meta)?;
write_meta(&mut writer, &database.meta, stream_cipher)?;
writer.write(XmlEvent::start_element("Root"))?;
for group in &database.groups {
write_group(&mut writer, group)?;
write_group(&mut writer, group, stream_cipher)?;
}
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::end_element())?;
......
use kdbx_rs;
use kdbx_rs::binary::InnerStreamCipherAlgorithm;
use kdbx_rs::database::{Entry, Group, Times};
use kdbx_rs::xml::write_xml;
use chrono::NaiveDate;
use std::fs::read_to_string;
......@@ -32,19 +34,24 @@ fn generate_xml() -> Result<(), kdbx_rs::Error> {
db.set_name("BarName");
db.set_description("BazDesc");
let mut group = Group::default();
group.name = "FooGroup".to_string();
group.uuid = Uuid::from_u128(0x12345678);
group.set_name("FooGroup");
group.set_uuid(Uuid::from_u128(0x12345678));
group.times = sample_times();
let mut entry = Entry::default();
entry.set_title("Bar");
entry.set_password("kdbxrs");
entry.uuid = Uuid::from_u128(0x654321);
entry.set_uuid(Uuid::from_u128(0x654321));
entry.times = sample_times();
group.entries.push(entry);
group.add_entry(entry);
db.groups.push(group);
let mut output_buffer = Vec::new();
kdbx_rs::xml::write_xml(&mut output_buffer, &db)?;
let key = vec![0xA0; 16];
let mut stream_cipher = InnerStreamCipherAlgorithm::ChaCha20
.stream_cipher(&key)
.unwrap();
write_xml(&mut output_buffer, &db, stream_cipher.as_mut())?;
let xml_string = String::from_utf8(output_buffer).unwrap();
assert_eq!(expected_xml_string, xml_string);
Ok(())
......
......@@ -14,6 +14,8 @@ fn load_kdbx4_argon2() {
let db = kdbx_rs::from_reader(file).unwrap();
assert_eq!(db.major_version(), 4);
assert_eq!(db.minor_version(), 0);
assert_eq!(db.header().cipher, kdbx_rs::binary::Cipher::Aes256);
assert_eq!(
db.header().compression_type,
......@@ -60,10 +62,10 @@ fn load_kdbx4_aes256() {
assert_eq!(
db.header().kdf_params,
kdbx_rs::binary::KdfParams::Aes {
rounds: 33908044,
rounds: 20000,
salt: vec![
248, 143, 74, 209, 60, 251, 247, 195, 28, 176, 139, 132, 158, 203, 40, 14, 146, 7,
250, 201, 104, 43, 51, 248, 107, 115, 120, 186, 178, 164, 10, 3
233, 186, 106, 9, 212, 114, 158, 27, 10, 91, 5, 111, 115, 106, 184, 135, 7, 58, 99,
250, 194, 27, 26, 192, 114, 189, 192, 96, 127, 48, 201, 242
]
}
);
......@@ -91,3 +93,43 @@ fn load_kdbx4_aes256_legacy() {
}
);
}
#[test]
fn load_kdbx31_aes256() {
let mut file_path = PathBuf::new();
file_path.push(env!("CARGO_MANIFEST_DIR"));
file_path.push("res");
file_path.push("test_input");
file_path.push("kdbx31-aes256.kdbx");
let file = fs::File::open(file_path).unwrap();
let db = kdbx_rs::from_reader(file).unwrap();
assert_eq!(db.major_version(), 3);
assert_eq!(db.minor_version(), 1);
assert_eq!(
db.header().master_seed,
[
58, 27, 198, 230, 93, 182, 12, 4, 92, 244, 37, 71, 253, 32, 60, 26, 74, 85, 238, 187,
132, 179, 254, 40, 243, 61, 127, 236, 181, 109, 80, 203
]
);
assert_eq!(
db.header().encryption_iv,
[162, 71, 175, 238, 61, 36, 113, 2, 152, 63, 98, 1, 132, 112, 96, 176]
);
assert_eq!(
db.header().compression_type,
kdbx_rs::binary::CompressionType::Gzip
);
assert_eq!(
db.header().kdf_params,
kdbx_rs::binary::KdfParams::Aes {
rounds: 20000,
salt: vec![
36, 163, 46, 56, 122, 135, 118, 6, 177, 152, 12, 38, 88, 55, 178, 100, 99, 207, 62,
101, 199, 191, 63, 72, 47, 153, 41, 120, 5, 104, 242, 247
]
}
);
}
......@@ -18,23 +18,26 @@ fn kdbx4_parsing() -> Result<(), kdbx_rs::Error> {
let xml = db.database();
assert_eq!(1, xml.groups.len());
assert_eq!("Root", xml.groups[0].name);
let root = xml.root().unwrap();
assert_eq!("Root", root.name());
assert_eq!(
"cd4233f1-fac2-4272-b309-3c5e7df90097",
xml.groups[0].uuid.to_string()
root.uuid().to_string()
);
assert_eq!(1, xml.groups[0].entries.len());
assert_eq!(1, root.entries.len());
let entry = &root.entries[0];
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
xml.groups[0].entries[0].uuid.to_string()
entry.uuid().to_string()
);
assert_eq!(Some("password2"), xml.groups[0].entries[0].password());
assert_eq!(1, xml.groups[0].entries[0].history.len());
assert_eq!("password2", entry.password().unwrap());
let history = entry.history();
assert_eq!(1, history.len());
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
xml.groups[0].entries[0].history[0].uuid.to_string()
history[0].uuid().to_string()
);
Ok(())
}
......@@ -54,20 +57,25 @@ fn kdbx4_parsing_twofish() -> Result<(), kdbx_rs::Error> {
let xml = db.database();
assert_eq!(1, xml.groups.len());
assert_eq!("Root", xml.groups[0].name);
let root = xml.root().unwrap();
assert_eq!("Root", root.name());
assert_eq!(
"cd4233f1-fac2-4272-b309-3c5e7df90097",
xml.groups[0].uuid.to_string()
root.uuid().to_string()
);
assert_eq!(1, xml.groups[0].entries.len());
assert_eq!(1, root.entries.len());
let entry = &root.entries[0];
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
xml.groups[0].entries[0].uuid.to_string()
entry.uuid().to_string()
);
assert_eq!(1, xml.groups[0].entries[0].history.len());
assert_eq!("password2", entry.password().unwrap());
let history = entry.history();
assert_eq!(1, history.len());
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
xml.groups[0].entries[0].history[0].uuid.to_string()
history[0].uuid().to_string()
);
Ok(())
......@@ -89,20 +97,25 @@ fn kdbx4_parsing_chacha20() -> Result<(), kdbx_rs::Error> {
let xml = db.database();
assert_eq!(1, xml.groups.len());
assert_eq!("Root", xml.groups[0].name);
let root = xml.root().unwrap();
assert_eq!("Root", root.name());
assert_eq!(
"cd4233f1-fac2-4272-b309-3c5e7df90097",
xml.groups[0].uuid.to_string()
root.uuid().to_string()
);
assert_eq!(1, xml.groups[0].entries.len());
assert_eq!(1, root.entries.len());
let entry = &root.entries[0];
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
xml.groups[0].entries[0].uuid.to_string()
entry.uuid().to_string()
);
assert_eq!(1, xml.groups[0].entries[0].history.len());
assert_eq!("password2", entry.password().unwrap());
let history = entry.history();