...
 
Commits (4)
# Changelog
## 0.2.1
* Actually remove APIs listed as removed in 0.2.0
* Fix off by one error in datetime parsing of years
## 0.2.0
* Add support for writing protected values
......
[package]
name = "kdbx-rs"
version = "0.2.0"
version = "0.2.1"
authors = ["Tony Finn <[email protected]>"]
edition = "2018"
description = "Keepass 2 (KDBX) password database parsing and creation"
......
......@@ -18,22 +18,22 @@
<UUID>AAAAAAAAAAAAAAAAEjRWeA==</UUID>
<Name>FooGroup</Name>
<Times>
<LastModificationTime>C2T41w4AAAA=</LastModificationTime>
<CreationTime>z2P41w4AAAA=</CreationTime>
<LastAccessTime>C/Ef2A4AAAA=</LastAccessTime>
<LocationChanged>z2P41w4AAAA=</LocationChanged>
<ExpiryTime>z2P41w4AAAA=</ExpiryTime>
<LastModificationTime>C98V1g4AAAA=</LastModificationTime>
<CreationTime>z94V1g4AAAA=</CreationTime>
<LastAccessTime>C2w91g4AAAA=</LastAccessTime>
<LocationChanged>z94V1g4AAAA=</LocationChanged>
<ExpiryTime>z94V1g4AAAA=</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>
<LastModificationTime>C98V1g4AAAA=</LastModificationTime>
<CreationTime>z94V1g4AAAA=</CreationTime>
<LastAccessTime>C2w91g4AAAA=</LastAccessTime>
<LocationChanged>z94V1g4AAAA=</LocationChanged>
<ExpiryTime>z94V1g4AAAA=</ExpiryTime>
<UsageCount>1</UsageCount>
<Expires>False</Expires>
</Times>
......
......@@ -2,7 +2,7 @@
//!
//! Primarily for verifying kdbx-rs changes
use kdbx_rs::database::{Entry, Group, Times};
use kdbx_rs::database::{Entry, Times};
use kdbx_rs::{CompositeKey, Database, Error, Kdbx};
use chrono::NaiveDate;
......@@ -10,16 +10,14 @@ use std::fs::File;
use std::path::PathBuf;
use uuid::Uuid;
fn sample_times() -> Times {
Times {
last_access_time: NaiveDate::from_ymd(2020, 5, 1).and_hms(1, 2, 3),
last_modification_time: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 2, 3),
creation_time: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3),
location_changed: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3),
expiry_time: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3),
expires: false,
usage_count: 1,
}
fn set_sample_times(times: &mut Times) {
times.last_access_time = NaiveDate::from_ymd(2020, 5, 1).and_hms(1, 2, 3);
times.last_modification_time = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 2, 3);
times.creation_time = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3);
times.location_changed = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3);
times.expiry_time = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3);
times.expires = false;
times.usage_count = 1;
}
fn main() -> Result<(), Error> {
......@@ -30,19 +28,18 @@ fn main() -> Result<(), Error> {
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.set_name("Root");
group.set_uuid(Uuid::from_u128(0x1234_5678));
group.times = sample_times();
db.set_name("BarName");
db.set_description("BazDesc".to_string());
let root = db.root_mut();
root.set_name("Root");
root.set_uuid(Uuid::from_u128(0x1234_5678));
set_sample_times(root.times_mut());
let mut entry = Entry::default();
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);
set_sample_times(entry.times_mut());
root.add_entry(entry);
let output_path = PathBuf::from("kdbx_rs.kdbx");
let mut file = File::create(output_path).expect("Could not open output file");
......
......@@ -242,9 +242,7 @@ impl Into<VariantDict> for KdfParams {
vdict.insert(
"$UUID".into(),
variant_dict::Value::Array(
uuid::Uuid::from(KdfAlgorithm::Argon2)
.as_bytes()
.to_vec()
uuid::Uuid::from(KdfAlgorithm::Argon2).as_bytes().to_vec(),
),
);
vdict.insert("M".into(), variant_dict::Value::Uint64(memory_bytes));
......
......@@ -53,13 +53,18 @@
//! let uuid = database.find_entry_mut(|f| f.title() == Some("Foo"))
//! .unwrap()
//! .uuid();
//! # let mut source_group = database.root_mut().unwrap();
//! # let mut source_group = database.root_mut();
//! let entry = source_group.remove_entry(uuid).unwrap();
//!
//! let mut target_group = database.find_group_mut(|g| g.name == "Child Group").unwrap();
//! let mut target_group = database.find_group_mut(|g| g.name() == "Child Group").unwrap();
//! target_group.add_entry(entry);
//! ```
use chrono::{NaiveDateTime, Timelike};
use std::borrow::Cow;
use std::ops::{Index, IndexMut};
use uuid::Uuid;
#[doc(hidden)]
pub fn doc_sample_db() -> Database {
let mut database = Database::default();
......@@ -86,20 +91,17 @@ pub fn doc_sample_db() -> Database {
database
}
use chrono::{NaiveDateTime, Timelike};
use std::borrow::Cow;
use std::ops::{Index, IndexMut};
use uuid::Uuid;
/// A value for a `Field` stored in an `Entry`
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
pub(crate) enum Value {
/// A value using in-memory encryption
Protected(String),
/// A value that's unencrypted in the database
Standard(String),
/// A empty value
Empty,
/// A empty value that should be protected if filled
ProtectEmpty,
}
impl Default for Value {
......@@ -112,9 +114,9 @@ impl Default for Value {
/// A key value pair
pub struct Field {
/// The name of this field
pub key: String,
pub(crate) key: String,
/// The (optionally encrypted) value of this field
pub value: Value,
pub(crate) value: Value,
}
impl Field {
......@@ -133,6 +135,66 @@ impl Field {
value: Value::Protected(value.to_string()),
}
}
/// Key for this field
pub fn key(&self) -> &str {
&self.key
}
/// Set a new key for this field
pub fn set_key(&mut self, new_key: &str) {
self.key = new_key.to_string();
}
/// Value for this field
pub fn value(&self) -> Option<&str> {
match self.value {
Value::Protected(ref s) => Some(s),
Value::Standard(ref s) => Some(s),
_ => None,
}
}
/// Set a new value for this field
pub fn set_value(&mut self, value: &str) {
if self.protected() {
self.value = Value::Protected(value.to_string());
} else {
self.value = Value::Standard(value.to_string());
}
}
/// Empty out the field stored in this value
pub fn clear(&mut self) {
if self.protected() {
self.value = Value::ProtectEmpty;
} else {
self.value = Value::Empty;
}
}
/// Get whether memory protection and extra encryption should be applied
///
/// Note: This is instructional for official clients, this library does not
/// support memory protection
pub fn protected(&self) -> bool {
matches!(self.value, Value::Protected(_))
}
/// Set whether memory protection and extra encryption should be applied
///
/// Note: This is instructional for official clients, this library does not
/// support memory protection
pub fn set_protected(&mut self, protected: bool) {
let existing_value = std::mem::take(&mut self.value);
self.value = match (protected, existing_value) {
(true, Value::Standard(s)) => Value::Protected(s),
(false, Value::Protected(s)) => Value::Standard(s),
(true, Value::Empty) => Value::ProtectEmpty,
(false, Value::ProtectEmpty) => Value::Empty,
(_, v) => v,
}
}
}
/// Historical versions of a single entry
......@@ -200,13 +262,13 @@ impl IndexMut<usize> for History {
/// A single password entry
pub struct Entry {
/// Identifier for this entry
pub uuid: Uuid,
uuid: Uuid,
/// Key-value pairs of current data for this entry
pub fields: Vec<Field>,
fields: Vec<Field>,
/// Previous versions of this entry
pub history: History,
pub(crate) history: History,
/// Information about access times
pub times: Times,
pub(crate) times: Times,
}
impl Entry {
......@@ -269,12 +331,18 @@ impl Entry {
self.fields.iter_mut().find(|i| i.key.as_str() == key)
}
/// Audit times for this entry
pub fn times(&self) -> &Times {
&self.times
}
/// Mutable audit times for this entry
pub fn times_mut(&mut self) -> &mut Times {
&mut self.times
}
fn find_string_value(&self, key: &str) -> Option<&str> {
self.find(key).and_then(|f| match &f.value {
Value::Empty => None,
Value::Standard(s) => Some(s.as_ref()),
Value::Protected(p) => Some(p.as_ref()),
})
self.find(key).and_then(|f| f.value())
}
/// Set the identifier for this item
......@@ -378,15 +446,15 @@ impl Default for Entry {
/// A group or folder of password entries and child groups
pub struct Group {
/// Identifier for this group
pub uuid: Uuid,
uuid: Uuid,
/// Name of this group
pub name: String,
name: String,
/// Password items within this group
pub entries: Vec<Entry>,
entries: Vec<Entry>,
/// Subfolders of this group
pub groups: Vec<Group>,
groups: Vec<Group>,
/// Access times for this group
pub times: Times,
pub(crate) times: Times,
}
impl Group {
......@@ -479,6 +547,16 @@ impl Group {
self.groups.iter_mut()
}
/// Count of direct child groups of this group
pub fn group_count(&self) -> usize {
self.groups.len()
}
/// Count of direct entries of this group
pub fn entry_count(&self) -> usize {
self.entries.len()
}
/// Iterate through all the direct entries of this group
pub fn entries(&self) -> impl Iterator<Item = &Entry> {
self.entries.iter()
......@@ -600,6 +678,16 @@ impl Group {
}
None
}
/// Audit times for this group
pub fn times(&self) -> &Times {
&self.times
}
/// Mutable audit times for this group
pub fn times_mut(&mut self) -> &mut Times {
&mut self.times
}
}
impl Default for Group {
......@@ -681,15 +769,25 @@ impl Default for Times {
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
/// Decrypted password database
///
/// See the [module-level documentation][crate::database] for more information.
pub struct Database {
/// Meta information about this database
pub meta: Meta,
pub(crate) meta: Meta,
/// Trees of items in this database
pub groups: Vec<Group>,
pub(crate) groups: Vec<Group>,
}
impl Default for Database {
fn default() -> Self {
let root = Group::new("Root");
Database {
meta: Meta::default(),
groups: vec![root],
}
}
}
impl Database {
......@@ -723,56 +821,49 @@ impl Database {
self.meta.database_description = desc.to_string();
}
fn ensure_root(&mut self) {
if self.groups.is_empty() {
self.groups.push(Group::new("Root"));
}
}
/// Add a entry to the root group
///
/// Creates a root group if none exist
pub fn add_entry(&mut self, entry: Entry) {
self.ensure_root();
self.groups[0].entries.push(entry);
}
/// Add a child group to the root group
///
/// Creates a root group if none exist
pub fn add_group(&mut self, entry: Group) {
self.ensure_root();
self.groups[0].groups.push(entry);
}
/// Replace the root group (and therefore all entries!) with a custom tree
pub fn replace_root(&mut self, group: Group) {
self.groups = vec![group];
}
/// Recursively searches for the first group matching a filter
pub fn find_group<F: FnMut(&Group) -> bool>(&self, f: F) -> Option<&Group> {
self.root()?.find_group(f)
self.root().find_group(f)
}
/// Recursively searches for the first group matching a filter, returns it mutably
pub fn find_group_mut<F: FnMut(&Group) -> bool>(&mut self, f: F) -> Option<&mut Group> {
self.root_mut()?.find_group_mut(f)
self.root_mut().find_group_mut(f)
}
/// Recursively searches for the first entry matching a filter
pub fn find_entry<F: FnMut(&Entry) -> bool>(&self, f: F) -> Option<&Entry> {
self.root()?.find_entry(f)
self.root().find_entry(f)
}
/// Recursively searches for the first entry matching a filter, returns it mutably
pub fn find_entry_mut<F: FnMut(&Entry) -> bool>(&mut self, f: F) -> Option<&mut Entry> {
self.root_mut()?.find_entry_mut(f)
self.root_mut().find_entry_mut(f)
}
/// Top level group for database entries
pub fn root(&self) -> Option<&Group> {
self.groups.get(0)
pub fn root(&self) -> &Group {
&self.groups[0]
}
/// Mutable top level group for database entries
pub fn root_mut(&mut self) -> Option<&mut Group> {
self.groups.get_mut(0)
pub fn root_mut(&mut self) -> &mut Group {
&mut self.groups[0]
}
}
......
......@@ -5,5 +5,6 @@ pub(crate) mod parse;
pub(crate) mod serialize;
pub use crate::stream::random::InnerStreamError;
pub use decoders::{decode_datetime, decode_uuid};
pub use parse::parse_xml;
pub use serialize::write_xml;
......@@ -2,10 +2,13 @@ use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime};
use uuid::Uuid;
pub fn keepass_epoch() -> NaiveDateTime {
NaiveDate::from_ymd(0, 1, 1).and_hms(0, 0, 0)
NaiveDate::from_ymd(1, 1, 1).and_hms(0, 0, 0)
}
pub(crate) fn decode_uuid(b64uuid: &str) -> Option<Uuid> {
/// Decode a UUID from a Keepass XML file
///
/// The UUID in Keepass XML files is stored base 64 encoded
pub fn decode_uuid(b64uuid: &str) -> Option<Uuid> {
let decoded = base64::decode(b64uuid).ok()?;
Uuid::from_slice(&decoded).ok()
}
......@@ -21,7 +24,11 @@ pub(crate) fn decode_datetime_b64(b64date: &str) -> Option<NaiveDateTime> {
keepass_epoch().checked_add_signed(timestamp)
}
pub(crate) fn decode_datetime(strdate: &str) -> Option<NaiveDateTime> {
/// Decode a Datetime from a Keepass XML file
///
/// This handles either ISO8601 date strings (as used in KDBX3)
/// or base64 encoded seconds since 1/1/1 00:00:00 as used in KDBX 4
pub fn decode_datetime(strdate: &str) -> Option<NaiveDateTime> {
if strdate.contains("-") {
let dt = DateTime::parse_from_rfc3339(strdate).ok()?;
Some(dt.naive_utc())
......@@ -30,7 +37,7 @@ pub(crate) fn decode_datetime(strdate: &str) -> Option<NaiveDateTime> {
}
}
pub(crate) fn encode_uuid(uuid: &Uuid) -> String {
pub(crate) fn encode_uuid(uuid: Uuid) -> String {
base64::encode(uuid.as_bytes())
}
......
......@@ -193,11 +193,9 @@ fn parse_entry<R: Read, S: StreamCipher + ?Sized>(
if &name.local_name == "History" {
entry.history = parse_history(xml_event_reader, stream_cipher)?;
} else if &name.local_name == "String" {
entry
.fields
.push(parse_field(xml_event_reader, "String", stream_cipher)?);
entry.add_field(parse_field(xml_event_reader, "String", stream_cipher)?);
} else if &name.local_name == "UUID" {
entry.uuid = parse_uuid(xml_event_reader)?;
entry.set_uuid(parse_uuid(xml_event_reader)?);
} else if &name.local_name == "Times" {
entry.times = parse_times(xml_event_reader)?;
}
......@@ -222,9 +220,9 @@ fn parse_group<R: Read, S: StreamCipher + ?Sized>(
} else if &name.local_name == "Entry" {
group.add_entry(parse_entry(xml_event_reader, stream_cipher)?);
} else if &name.local_name == "UUID" {
group.uuid = parse_uuid(xml_event_reader)?;
group.set_uuid(parse_uuid(xml_event_reader)?);
} else if &name.local_name == "Name" {
group.name = parse_string(xml_event_reader)?.unwrap_or_else(String::new);
group.set_name(parse_string(xml_event_reader)?.unwrap_or_else(String::new));
} else if &name.local_name == "Times" {
group.times = parse_times(xml_event_reader)?;
}
......
......@@ -47,12 +47,12 @@ fn write_field<W: Write, S: StreamCipher + ?Sized>(
writer.write(XmlEvent::start_element("Value").attr("Protected", "True"))?;
let mut encrypt_buf = v.clone().into_bytes();
stream_cipher.encrypt(&mut encrypt_buf);
let encrypted = base64::encode(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)?,
Value::Empty => {
Value::Empty | Value::ProtectEmpty => {
writer.write(XmlEvent::start_element("Value"))?;
writer.write(XmlEvent::end_element())?;
}
......@@ -125,9 +125,9 @@ fn write_entry<W: Write, S: StreamCipher + ?Sized>(
stream_cipher: &mut S,
) -> Result<()> {
writer.write(XmlEvent::start_element("Entry"))?;
write_string_tag(writer, "UUID", &encode_uuid(&entry.uuid))?;
write_string_tag(writer, "UUID", &encode_uuid(entry.uuid()))?;
write_times(writer, &entry.times)?;
for field in &entry.fields {
for field in entry.fields() {
write_field(writer, "String", field, stream_cipher)?;
}
if entry.history.len() > 0 {
......@@ -147,8 +147,8 @@ fn write_group<W: Write, S: StreamCipher + ?Sized>(
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_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, stream_cipher)?;
......
use kdbx_rs;
use kdbx_rs::binary::InnerStreamCipherAlgorithm;
use kdbx_rs::database::{Entry, Group, Times};
use kdbx_rs::database::{Entry, Times};
use kdbx_rs::xml::write_xml;
use chrono::NaiveDate;
......@@ -8,16 +8,14 @@ use std::fs::read_to_string;
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 set_sample_times(times: &mut Times) {
times.last_access_time = NaiveDate::from_ymd(2020, 5, 1).and_hms(1, 2, 3);
times.last_modification_time = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 2, 3);
times.creation_time = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3);
times.location_changed = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3);
times.expiry_time = NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3);
times.expires = false;
times.usage_count = 1;
}
#[test]
......@@ -33,17 +31,16 @@ fn generate_xml() -> Result<(), kdbx_rs::Error> {
let mut db = kdbx_rs::Database::default();
db.set_name("BarName");
db.set_description("BazDesc");
let mut group = Group::default();
let group = db.root_mut();
group.set_name("FooGroup");
group.set_uuid(Uuid::from_u128(0x12345678));
group.times = sample_times();
set_sample_times(group.times_mut());
let mut entry = Entry::default();
entry.set_title("Bar");
entry.set_password("kdbxrs");
entry.set_uuid(Uuid::from_u128(0x654321));
entry.times = sample_times();
set_sample_times(entry.times_mut());
group.add_entry(entry);
db.groups.push(group);
let mut output_buffer = Vec::new();
......
......@@ -15,18 +15,16 @@ 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 = db.database();
let root = db.database().root();
assert_eq!(1, xml.groups.len());
let root = xml.root().unwrap();
assert_eq!("Root", root.name());
assert_eq!(
"cd4233f1-fac2-4272-b309-3c5e7df90097",
root.uuid().to_string()
);
assert_eq!(1, root.entries.len());
let entry = &root.entries[0];
let entries: Vec<_> = root.entries().collect();
assert_eq!(1, entries.len());
let entry = entries[0];
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
entry.uuid().to_string()
......@@ -54,18 +52,16 @@ 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 = db.database();
assert_eq!(1, xml.groups.len());
let root = db.database().root();
let root = xml.root().unwrap();
assert_eq!("Root", root.name());
assert_eq!(
"cd4233f1-fac2-4272-b309-3c5e7df90097",
root.uuid().to_string()
);
assert_eq!(1, root.entries.len());
let entry = &root.entries[0];
let entries: Vec<_> = root.entries().collect();
assert_eq!(1, entries.len());
let entry = entries[0];
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
entry.uuid().to_string()
......@@ -94,18 +90,16 @@ 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 = db.database();
assert_eq!(1, xml.groups.len());
let root = db.database().root();
let root = xml.root().unwrap();
assert_eq!("Root", root.name());
assert_eq!(
"cd4233f1-fac2-4272-b309-3c5e7df90097",
root.uuid().to_string()
);
assert_eq!(1, root.entries.len());
let entry = &root.entries[0];
let entries: Vec<_> = root.entries().collect();
assert_eq!(1, entries.len());
let entry = entries[0];
assert_eq!(
"d5870a13-f968-41c5-a233-69b7bc86a628",
entry.uuid().to_string()
......
......@@ -15,11 +15,10 @@ fn keepassxc_otp_read_secret() -> 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 = db.database();
assert_eq!(
"ABCDEFGHIJKLMNOP",
xml.groups[0].entries[0].otp().unwrap().secret().unwrap()
);
let entry = db
.find_entry(|e| e.title().map(|e| e.contains("OTP")).unwrap_or_default())
.unwrap();
assert_eq!("ABCDEFGHIJKLMNOP", entry.otp().unwrap().secret().unwrap());
Ok(())
}
......@@ -19,18 +19,18 @@ fn key() -> CompositeKey {
#[test]
fn round_trip() -> Result<(), kdbx_rs::Error> {
let mut db = kdbx_rs::Database::default();
db.meta.database_name = DATABASE_NAME.to_string();
db.meta.database_description = DATABASE_DESC.to_string();
db.set_name(DATABASE_NAME.to_string());
db.set_description(DATABASE_DESC.to_string());
let mut group = Group::default();
group.set_name(GROUP_NAME);
let group_times = group.times.clone();
let group_times = group.times().clone();
let mut entry = Entry::default();
entry.add_field(Field::new("Title", ENTRY_NAME));
entry.set_password(ENTRY_PASSWORD);
entry.add_field(Field::new("Password", ENTRY_PASSWORD));
let entry_times = entry.times.clone();
group.entries.push(entry);
db.groups.push(group);
let entry_times = entry.times().clone();
group.add_entry(entry);
db.replace_root(group);
let mut kdbx = Kdbx::from_database(db);
let mut output_buf = Vec::new();
......@@ -41,12 +41,13 @@ fn round_trip() -> Result<(), kdbx_rs::Error> {
let unlocked = reparsed.unlock(&key())?;
assert_eq!(unlocked.meta().database_name, DATABASE_NAME);
assert_eq!(unlocked.meta().database_description, DATABASE_DESC);
let root = unlocked.root().unwrap();
let root = unlocked.root();
assert_eq!(root.name(), GROUP_NAME);
assert_eq!(root.entries[0].title().unwrap(), ENTRY_NAME);
assert_eq!(root.entries[0].password().unwrap(), ENTRY_PASSWORD);
assert_eq!(root.times, group_times);
assert_eq!(root.entries[0].times, entry_times);
let first_entry: &Entry = root.entries().collect::<Vec<_>>()[0];
assert_eq!(first_entry.title().unwrap(), ENTRY_NAME);
assert_eq!(first_entry.password().unwrap(), ENTRY_PASSWORD);
assert_eq!(root.times(), &group_times);
assert_eq!(first_entry.times(), &entry_times);
Ok(())
}