Commit 1335752c authored by Tony Finn's avatar Tony Finn

Add support for inner stream decoding

parent ba041a25
# 0.1.0
Initial release!
\ No newline at end of file
......@@ -31,6 +31,7 @@ chacha20 = "0.3.4"
hmac = "0.7.1"
salsa20 = "0.4.1"
sha2 = "0.8.1"
stream-cipher = "0.3.2"
twofish = "0.2.0"
[[bin]]
......
......@@ -56,12 +56,16 @@ kdbx.write(&mut file)?;
| .kdbx 3 | No | Read only | No | Yes | No |
| .kdb | No | No | No | No | Yes |
| **Algorithms** | | | | | |
| AES KDF | Yes | Yes | Yes | Yes | Yes |
| Argon 2 KDF | Yes | Yes | Yes | No | Yes |
| AES Cipher | Yes | Yes | Yes | Yes | Yes |
| TwoFish Cipher | Yes | Yes | No | Yes | No |
| Chacha20 Cipher | Yes | Yes | Yes | No | No |
| Salsa20 Cipher | No | Yes | Yes | Yes | No |
| *KDFS* | | | | | |
| AES | Yes | Yes | Yes | Yes | Yes |
| Argon 2 | Yes | Yes | Yes | No | Yes |
|*Database Ciphers*| | | | | |
| AES | Yes | Yes | Yes | Yes | Yes |
| TwoFish | Yes | Yes | No | Yes | No |
| Chacha20 | Yes | Yes | Yes | No | No |
| *Value Ciphers* | | | | | |
| Chacha20 | Yes | Yes | Yes | No | No |
| Salsa20 | Yes | Yes | Yes | Yes | No |
| **Features** | | | | | |
| Memory protection| No | Yes | No | No | Yes |
| Keyfile auth | Yes | Yes | Yes | Yes | Yes |
......
......@@ -309,14 +309,20 @@ To perform this encryption, a stream cipher is used. The configuration values fo
in the inner header in KDBX 4, or the outer header in earlier versions. There are two configuration
value. The first is the cipher ID, which is used to select which stream cipher to use.
Currently used Ciphers:
Currently defined Ciphers:
* 1 - ArcFour
* 1 - ArcFour (Only used for `.kdb` files)
* 2 - Salsa20
* 3 - Chacha20
The second is the inner stream key, which is used to setup the stream cipher. The size of this depends
on the algorithm in use.
The second is the inner stream key, which is used to setup the stream cipher.
The stream cipher is set up as follows:
* For Salsa20, the key is the first 32 bytes of sha256(inner stream key) and the IV is the constant
value: [0xe8, 0x30, 0x09, 0x4b, 0x97, 0x20, 0x5d, 0x2a]
* For Chacha20, the key is the first 32 bytes of sha512(inner stream key) and the IV is the following
12 bytes
Each protected value is decrypted by the stream cipher in the order they appear in the database. The
same cipher should be used to decrypt every value.
......
......@@ -13,7 +13,6 @@ fn main() -> Result<(), kdbx_rs::Error> {
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)?;
let xml = kdbx_rs::xml::parse_xml(kdbx.raw_xml().unwrap());
println!("{:#?}", xml);
println!("{:#?}", kdbx.database());
Ok(())
}
......@@ -8,7 +8,9 @@ mod read;
mod variant_dict;
pub use header::{InnerHeaderId, KdbxHeader, KdbxInnerHeader, OuterHeaderId};
pub use header_fields::{Cipher, CompressionType, KdfAlgorithm, KdfParams};
pub use header_fields::{
Cipher, CompressionType, InnerStreamCipherAlgorithm, KdfAlgorithm, KdfParams,
};
pub(crate) use header_fields::{KDBX_MAGIC_NUMBER, KEEPASS_MAGIC_NUMBER};
pub use kdbx::{FailedUnlock, Kdbx, Locked, Unlocked};
pub use read::{from_reader, open};
......
use super::header;
use super::header_fields;
use crate::crypto;
use crate::stream::random::InnerStreamError;
use thiserror::Error;
#[derive(Error, Debug)]
......@@ -38,6 +39,9 @@ pub enum UnlockError {
/// The inner header is invalid
#[error("Inner header invalid - {0}")]
InvalidInnerHeader(#[from] HeaderError),
/// The inner stream is invalid
#[error("Cannot create inner stream to read protected values - {0}")]
InnerStream(#[from] InnerStreamError),
/// The inner header is invalid
#[error("Corrupt database. XML data is invald - {0}")]
InvalidXml(#[from] crate::errors::XmlReadError),
......@@ -81,4 +85,4 @@ 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,
}
\ No newline at end of file
}
use super::errors::{HeaderError as Error};
use super::errors::HeaderError as Error;
use super::header_fields;
use super::variant_dict;
use crate::crypto;
use crate::utils;
use rand::{RngCore, rngs::OsRng};
use rand::{rngs::OsRng, RngCore};
use sha2::{Digest, Sha256};
use std::convert::TryInto;
use std::io::{Read, Write};
......@@ -388,7 +388,7 @@ impl KdbxHeader {
#[derive(Default)]
pub struct KdbxInnerHeaderBuilder {
pub inner_stream_cipher: Option<header_fields::InnerStreamCipher>,
pub inner_stream_cipher: Option<header_fields::InnerStreamCipherAlgorithm>,
pub inner_stream_key: Option<Vec<u8>>,
/// Custom and unrecognized header types
pub other_headers: Vec<HeaderField<InnerHeaderId>>,
......@@ -426,7 +426,7 @@ impl KdbxInnerHeaderBuilder {
#[derive(Debug, PartialEq, Eq)]
pub struct KdbxInnerHeader {
/// Cipher identifier for data encrypted in memory
pub inner_stream_cipher: header_fields::InnerStreamCipher,
pub inner_stream_cipher: header_fields::InnerStreamCipherAlgorithm,
/// Cipher key for data encrypted in memory
pub inner_stream_key: Vec<u8>,
/// Headers not handled by this library
......@@ -445,7 +445,7 @@ impl KdbxInnerHeader {
/// [`rand`]: https://docs.rs/rand/
/// [`OsRng`]: https://docs.rs/rand/0.7/rand/rngs/struct.OsRng.html
pub fn from_os_random() -> KdbxInnerHeader {
let inner_stream_cipher = header_fields::InnerStreamCipher::ChaCha20;
let inner_stream_cipher = header_fields::InnerStreamCipherAlgorithm::ChaCha20;
let mut inner_stream_key = vec![0u8; 44]; // 32 bit key + 12 bit nonce for chacha20
OsRng.fill_bytes(&mut inner_stream_key);
......
......@@ -63,8 +63,8 @@ impl From<Cipher> for HeaderField<OuterHeaderId> {
}
/// Inner stream cipher identifier used for encrypting protected fields
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum InnerStreamCipher {
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum InnerStreamCipherAlgorithm {
/// ArcFour algorithm
ArcFour,
/// Salsa20 stream cipher
......@@ -75,8 +75,8 @@ pub enum InnerStreamCipher {
Unknown(u32),
}
impl From<InnerStreamCipher> for HeaderField<InnerHeaderId> {
fn from(cipher: InnerStreamCipher) -> HeaderField<InnerHeaderId> {
impl From<InnerStreamCipherAlgorithm> for HeaderField<InnerHeaderId> {
fn from(cipher: InnerStreamCipherAlgorithm) -> HeaderField<InnerHeaderId> {
HeaderField::new(
InnerHeaderId::InnerRandomStreamCipherId,
u32::from(cipher).to_le_bytes().as_ref().to_vec(),
......@@ -84,24 +84,24 @@ impl From<InnerStreamCipher> for HeaderField<InnerHeaderId> {
}
}
impl From<u32> for InnerStreamCipher {
fn from(id: u32) -> InnerStreamCipher {
impl From<u32> for InnerStreamCipherAlgorithm {
fn from(id: u32) -> InnerStreamCipherAlgorithm {
match id {
1 => InnerStreamCipher::ArcFour,
2 => InnerStreamCipher::Salsa20,
3 => InnerStreamCipher::ChaCha20,
x => InnerStreamCipher::Unknown(x),
1 => InnerStreamCipherAlgorithm::ArcFour,
2 => InnerStreamCipherAlgorithm::Salsa20,
3 => InnerStreamCipherAlgorithm::ChaCha20,
x => InnerStreamCipherAlgorithm::Unknown(x),
}
}
}
impl From<InnerStreamCipher> for u32 {
fn from(id: InnerStreamCipher) -> u32 {
impl From<InnerStreamCipherAlgorithm> for u32 {
fn from(id: InnerStreamCipherAlgorithm) -> u32 {
match id {
InnerStreamCipher::ArcFour => 1,
InnerStreamCipher::Salsa20 => 2,
InnerStreamCipher::ChaCha20 => 3,
InnerStreamCipher::Unknown(x) => x,
InnerStreamCipherAlgorithm::ArcFour => 1,
InnerStreamCipherAlgorithm::Salsa20 => 2,
InnerStreamCipherAlgorithm::ChaCha20 => 3,
InnerStreamCipherAlgorithm::Unknown(x) => x,
}
}
}
......
......@@ -139,18 +139,21 @@ impl Kdbx<Unlocked> {
self.state.xml_data.as_deref()
}
/// Password database stored in this kdbx archive
pub fn database(&self) -> &crate::Database {
&self.state.database
}
/// Generate a new .kdbx from the given database
///
/// Uses OS randomness provided by the `rand` crates's [`OsRng`] to
/// Uses OS randomness provided by the `rand` crates's [`OsRng`] to
/// generate all required seeds and IVs.
///
/// Note that you need to set a key with [`Kdbx::set_key`]
/// to be able to write the database
///
/// [`OsRng`]: https://docs.rs/rand/0.7/rand/rngs/struct.OsRng.html
pub fn from_database(
database: crate::Database,
) -> Kdbx<Unlocked> {
pub fn from_database(database: crate::Database) -> Kdbx<Unlocked> {
let header = header::KdbxHeader::from_os_random();
let inner_header = header::KdbxInnerHeader::from_os_random();
let unlocked = Unlocked {
......@@ -257,7 +260,10 @@ impl Kdbx<Locked> {
let parsed = self
.decrypt_data(&master_key)
.and_then(|(inner_header, data)| {
let parsed = crate::xml::parse_xml(data.as_slice())?;
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))
});
......
......@@ -13,11 +13,11 @@ type HmacSha256 = Hmac<Sha256>;
/// of password, keyfile or both.
///
/// For the compmon case of creating credentials from just a password,
/// you can use
/// you can use
///
/// ```
/// # use kdbx_rs::CompositeKey;
/// CompositeKey::from_password("abcdef");
/// CompositeKey::from_password("abcdef");
/// ```
///
/// Otherwise you can use [`CompositeKey::new`] to provide other combinations
......
//! Error types for kdbx-rs
pub use crate::binary::errors::{
HeaderError, OpenError, UnlockError, WriteError,
};
pub use crate::binary::errors::{HeaderError, OpenError, UnlockError, WriteError};
pub use crate::binary::FailedUnlock;
pub use crate::crypto::KeyGenerationError;
pub use crate::stream::random::InnerStreamError;
pub use crate::xml::parse::Error as XmlReadError;
pub use crate::xml::serialize::Error as XmlWriteError;
use thiserror::Error;
......
mod block_cipher;
mod hmac;
mod pipeline;
pub(crate) mod random;
mod stream_cipher;
pub(crate) use self::hmac::{HMacReader, HmacWriter};
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 stream_cipher::{StreamCipherReader, StreamCipherWriter, StreamCipherWriterExt};
use chacha20::ChaCha20;
use rand::{RngCore, rngs::OsRng};
use salsa20::Salsa20;
use sha2::{Digest, Sha256, Sha512};
use stream_cipher::{NewStreamCipher, StreamCipher};
use thiserror::Error;
use crate::binary::InnerStreamCipherAlgorithm;
pub const SALSA20_IV: [u8; 8] = [0xE8, 0x30, 0x09, 0x4b, 0x97, 0x20, 0x5d, 0x2a];
#[derive(Debug, Error)]
/// Errors creating inner stream used to decrypt protected values
pub enum InnerStreamError {
#[error("Unsupported inner stream type: {0:?}")]
/// The cipher type is not supported by this library
UnsupportedCipher(InnerStreamCipherAlgorithm),
}
impl InnerStreamCipherAlgorithm {
pub(crate) fn stream_cipher(
&self,
key: &[u8],
) -> Result<Box<dyn StreamCipher>, InnerStreamError> {
match self {
InnerStreamCipherAlgorithm::ChaCha20 => {
let iv = Sha512::digest(key);
Ok(Box::new(
ChaCha20::new_var(&iv[0..32], &iv[32..44]).unwrap(),
))
},
InnerStreamCipherAlgorithm::Salsa20 => {
let iv = Sha256::digest(key);
Ok(Box::new(Salsa20::new_var(&iv[0..32], &SALSA20_IV).unwrap()))
}
_ => Err(InnerStreamError::UnsupportedCipher(*self)),
}
}
}
/// 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)
}
use chacha20::stream_cipher::SyncStreamCipher;
use std::io::{self, Read, Write};
use stream_cipher::SyncStreamCipher;
pub(crate) struct StreamCipherReader<C, R>
where
......
......@@ -4,5 +4,6 @@ mod decoders;
pub(crate) mod parse;
pub(crate) mod serialize;
pub use crate::stream::random::{default_stream_cipher, InnerStreamError};
pub use parse::parse_xml;
pub use serialize::write_xml;
......@@ -2,6 +2,7 @@ use super::decoders::{decode_datetime, decode_uuid};
use crate::types::{Database, Entry, Field, Group, MemoryProtection, Meta, Times, Value};
use chrono::NaiveDateTime;
use std::io::Read;
use stream_cipher::StreamCipher;
use thiserror::Error;
use uuid::Uuid;
use xml::reader::{EventReader, XmlEvent};
......@@ -24,6 +25,9 @@ pub enum Error {
/// A numeric field is not valid
#[error("Invalid numeric value")]
InvalidNumber,
/// A string field did not decrypt correctly
#[error("Could not decrypt value for Key {0:?}")]
DecryptFailed(String),
}
pub type Result<T> = std::result::Result<T, Error>;
......@@ -85,13 +89,18 @@ fn parse_bool<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<bool> {
.unwrap_or_default())
}
fn parse_field<R: Read>(xml_event_reader: &mut EventReader<R>, tag_name: &str) -> Result<Field> {
fn parse_field<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
tag_name: &str,
stream_cipher: &mut S,
) -> Result<Field> {
let mut field = Field::default();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } if &name.local_name == "Key" => {
field.key = parse_string(xml_event_reader)
.and_then(|val| val.ok_or(Error::KeyEmptyName))?;
let parse_result = parse_string(xml_event_reader)?;
let val = parse_result.ok_or(Error::KeyEmptyName)?;
field.key = val;
}
XmlEvent::StartElement {
name, attributes, ..
......@@ -101,7 +110,17 @@ fn parse_field<R: Read>(xml_event_reader: &mut EventReader<R>, tag_name: &str) -
});
field.value = if let Some(contents) = parse_string(xml_event_reader)? {
if protected {
Value::Protected(contents)
// Would be nice to avoid the clone but it gets moved into the map_err closure
let key_clone = field.key.clone();
match base64::decode(&contents) {
Ok(mut decoded) => {
stream_cipher.decrypt(decoded.as_mut());
let to_str = String::from_utf8(decoded)
.map_err(|_| Error::DecryptFailed(key_clone))?;
Value::Protected(to_str)
},
Err(_) => return Err(Error::DecryptFailed(key_clone))
}
} else {
Value::Standard(contents)
}
......@@ -116,12 +135,15 @@ fn parse_field<R: Read>(xml_event_reader: &mut EventReader<R>, tag_name: &str) -
Ok(field)
}
fn parse_history<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Vec<Entry>> {
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();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } if &name.local_name == "Entry" => {
history.push(parse_entry(xml_event_reader)?);
history.push(parse_entry(xml_event_reader, stream_cipher)?);
}
XmlEvent::EndElement { name, .. } if &name.local_name == "History" => break,
_ => {}
......@@ -158,15 +180,20 @@ fn parse_times<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Times>
Ok(times)
}
fn parse_entry<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Entry> {
fn parse_entry<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Entry> {
let mut entry = Entry::default();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } => {
if &name.local_name == "History" {
entry.history = parse_history(xml_event_reader)?;
entry.history = parse_history(xml_event_reader, stream_cipher)?;
} else if &name.local_name == "String" {
entry.fields.push(parse_field(xml_event_reader, "String")?);
entry
.fields
.push(parse_field(xml_event_reader, "String", stream_cipher)?);
} else if &name.local_name == "UUID" {
entry.uuid = parse_uuid(xml_event_reader)?;
} else if &name.local_name == "Times" {
......@@ -180,15 +207,22 @@ fn parse_entry<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Entry>
Ok(entry)
}
fn parse_group<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Group> {
fn parse_group<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Group> {
let mut group = Group::default();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } => {
if &name.local_name == "Group" {
group.children.push(parse_group(xml_event_reader)?);
group
.children
.push(parse_group(xml_event_reader, stream_cipher)?);
} else if &name.local_name == "Entry" {
group.entries.push(parse_entry(xml_event_reader)?);
group
.entries
.push(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" {
......@@ -204,12 +238,15 @@ fn parse_group<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Group>
Ok(group)
}
fn parse_root<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Vec<Group>> {
fn parse_root<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Vec<Group>> {
let mut groups = Vec::new();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } if &name.local_name == "Group" => {
groups.push(parse_group(xml_event_reader)?);
groups.push(parse_group(xml_event_reader, stream_cipher)?);
}
XmlEvent::EndElement { name, .. } if &name.local_name == "Root" => break,
_ => {}
......@@ -218,13 +255,16 @@ fn parse_root<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Vec<Grou
Ok(groups)
}
fn parse_custom_data<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Vec<Field>> {
fn parse_custom_data<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Vec<Field>> {
let mut fields = Vec::new();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } => match name.local_name.as_ref() {
"Item" => {
fields.push(parse_field(xml_event_reader, "Item")?);
fields.push(parse_field(xml_event_reader, "Item", stream_cipher)?);
}
_ => {}
},
......@@ -266,7 +306,10 @@ fn parse_memory_protection<R: Read>(
Ok(protection)
}
fn parse_meta<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Meta> {
fn parse_meta<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Meta> {
let mut meta = Meta::default();
loop {
match xml_event_reader.next()? {
......@@ -281,7 +324,7 @@ fn parse_meta<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Meta> {
meta.database_description = parse_string(xml_event_reader)?.unwrap_or_default();
}
"CustomData" => {
meta.custom_data = parse_custom_data(xml_event_reader)?;
meta.custom_data = parse_custom_data(xml_event_reader, stream_cipher)?;
}
"MemoryProtection" => {
meta.memory_protection = parse_memory_protection(xml_event_reader)?;
......@@ -295,15 +338,18 @@ fn parse_meta<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Meta> {
Ok(meta)
}
fn parse_file<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Database> {
fn parse_file<R: Read, S: StreamCipher + ?Sized>(
xml_event_reader: &mut EventReader<R>,
stream_cipher: &mut S,
) -> Result<Database> {
let mut db = Database::default();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } if &name.local_name == "Root" => {
db.groups = parse_root(xml_event_reader)?;
db.groups = parse_root(xml_event_reader, stream_cipher)?;
}
XmlEvent::StartElement { name, .. } if &name.local_name == "Meta" => {
db.meta = parse_meta(xml_event_reader)?;
db.meta = parse_meta(xml_event_reader, stream_cipher)?;
}
XmlEvent::EndElement { name, .. } if &name.local_name == "KeePassFile" => break,
_ => {}
......@@ -313,10 +359,10 @@ fn parse_file<R: Read>(xml_event_reader: &mut EventReader<R>) -> Result<Database
}
/// Parse decrypted XML into a database
pub fn parse_xml<R: Read>(xml_data: R) -> Result<Database> {
pub fn parse_xml<R: Read, S: StreamCipher + ?Sized>(xml_data: R, stream_cipher: &mut S) -> Result<Database> {
let xml_config = xml::ParserConfig::new()
.trim_whitespace(true)
.cdata_to_characters(true);
let mut xml_event_reader = EventReader::new_with_config(xml_data, xml_config);
parse_file(&mut xml_event_reader)
parse_file(&mut xml_event_reader, stream_cipher)
}
......@@ -15,7 +15,7 @@ fn kdbx4_parsing() -> Result<(), kdbx_rs::Error> {
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
let db = db.unlock(&key)?;
let xml = kdbx_rs::xml::parse_xml(db.raw_xml().unwrap())?;
let xml = db.database();
assert_eq!(1, xml.groups.len());
assert_eq!("Root", xml.groups[0].name);
......@@ -28,6 +28,7 @@ fn kdbx4_parsing() -> Result<(), kdbx_rs::Error> {
"d5870a13-f968-41c5-a233-69b7bc86a628",
xml.groups[0].entries[0].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!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
......@@ -50,7 +51,7 @@ fn kdbx4_parsing_twofish() -> Result<(), kdbx_rs::Error> {
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
let db = db.unlock(&key)?;
let xml = kdbx_rs::xml::parse_xml(db.raw_xml().unwrap())?;
let xml = db.database();
assert_eq!(1, xml.groups.len());
assert_eq!("Root", xml.groups[0].name);
......@@ -85,7 +86,7 @@ fn kdbx4_parsing_chacha20() -> Result<(), kdbx_rs::Error> {
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
let db = db.unlock(&key)?;
let xml = kdbx_rs::xml::parse_xml(db.raw_xml().unwrap())?;
let xml = db.database();
assert_eq!(1, xml.groups.len());
assert_eq!("Root", xml.groups[0].name);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment