1use super::binaries_map::BinariesMap;
10use super::color::Color;
11use super::comment::Comment;
12use super::composite_key::CompositeKey;
13use super::compression::Compression;
14use super::custom_data_map::CustomDataMap;
15use super::custom_icons_map::CustomIconsMap;
16use super::db_type::DbType;
17use super::entry::Entry;
18use super::entry_uuid::EntryUuid;
19use super::error::Error;
20use super::group::Group;
21use super::group_uuid::GroupUuid;
22use super::master_cipher::MasterCipher;
23use super::result::Result;
24use super::stream_cipher::StreamCipher;
25use super::string_value::StringValue;
26use super::transform_rounds::TransformRounds;
27use super::version::Version;
28use crate::common;
29use crate::format::{kdb2_reader, kdb2_writer};
30use crate::io::{Log, LogReader, LogWriter};
31use chrono::{DateTime, Utc};
32use std::io::{Read, Write};
33
34#[derive(Clone, Debug, PartialEq)]
36pub struct Database {
37 pub comment: Option<Comment>,
39
40 pub composite_key: CompositeKey,
42
43 pub compression: Compression,
45
46 pub db_type: DbType,
48
49 pub master_cipher: MasterCipher,
51
52 pub stream_cipher: StreamCipher,
54
55 pub transform_rounds: TransformRounds,
57
58 pub version: Version,
60
61 pub binaries: BinariesMap,
63
64 pub color: Option<Color>,
66
67 pub custom_data: CustomDataMap,
69
70 pub custom_icons: CustomIconsMap,
72
73 pub def_username: String,
75
76 pub def_username_changed: DateTime<Utc>,
78
79 pub description: String,
81
82 pub description_changed: DateTime<Utc>,
84
85 pub entry_templates_group_changed: DateTime<Utc>,
87
88 pub entry_templates_group_uuid: GroupUuid,
90
91 pub generator: String,
93
94 pub history_max_items: i32,
96
97 pub history_max_size: i32,
99
100 pub last_selected_group: GroupUuid,
102
103 pub last_top_visible_group: GroupUuid,
105
106 pub maintenance_history_days: i32,
108
109 pub master_key_change_force: i32,
110
111 pub master_key_change_rec: i32,
112
113 pub master_key_changed: DateTime<Utc>,
115
116 pub name: String,
118
119 pub name_changed: DateTime<Utc>,
121
122 pub protect_notes: bool,
124
125 pub protect_password: bool,
127
128 pub protect_title: bool,
130
131 pub protect_url: bool,
133
134 pub protect_username: bool,
136
137 pub recycle_bin_changed: DateTime<Utc>,
139
140 pub recycle_bin_enabled: bool,
142
143 pub recycle_bin_uuid: GroupUuid,
145
146 pub root_group: Group,
148}
149
150impl Database {
151 pub fn new(key: &CompositeKey) -> Database {
162 let now = Utc::now();
163 Database {
164 comment: None,
165 composite_key: key.clone(),
166 compression: Compression::GZip,
167 db_type: DbType::Kdb2,
168 master_cipher: MasterCipher::Aes256,
169 stream_cipher: StreamCipher::Salsa20,
170 transform_rounds: TransformRounds(10000),
171 version: Version::new_kdb2(),
172 binaries: BinariesMap::new(),
173 color: None,
174 custom_data: CustomDataMap::new(),
175 custom_icons: CustomIconsMap::new(),
176 def_username: String::new(),
177 def_username_changed: now,
178 description: String::new(),
179 description_changed: now,
180 entry_templates_group_changed: now,
181 entry_templates_group_uuid: GroupUuid::nil(),
182 generator: String::from(common::GENERATOR_NAME),
183 history_max_items: common::HISTORY_MAX_ITEMS_DEFAULT,
184 history_max_size: common::HISTORY_MAX_SIZE_DEFAULT,
185 last_selected_group: GroupUuid::nil(),
186 last_top_visible_group: GroupUuid::nil(),
187 maintenance_history_days: common::MAINTENANCE_HISTORY_DAYS_DEFAULT,
188 master_key_change_force: common::MASTER_KEY_CHANGE_FORCE_DEFAULT,
189 master_key_change_rec: common::MASTER_KEY_CHANGE_REC_DEFAULT,
190 master_key_changed: now,
191 name: String::new(),
192 name_changed: now,
193 protect_notes: common::PROTECT_NOTES_DEFAULT,
194 protect_password: common::PROTECT_PASSWORD_DEFAULT,
195 protect_title: common::PROTECT_TITLE_DEFAULT,
196 protect_url: common::PROTECT_URL_DEFAULT,
197 protect_username: common::PROTECT_USERNAME_DEFAULT,
198 recycle_bin_changed: now,
199 recycle_bin_enabled: common::RECYCLE_BIN_ENABLED_DEFAULT,
200 recycle_bin_uuid: GroupUuid::nil(),
201 root_group: Group::new(common::ROOT_GROUP_NAME),
202 }
203 }
204
205 pub fn find_entries<'a, S: Into<String>>(&'a self, text: S) -> Vec<&'a Entry> {
231 let mut list = Vec::new();
232 let text = &text.into().to_lowercase();
233 for group in self.root_group.iter() {
234 for entry in group.entries.iter() {
235 if entry_contains_string(entry, text) {
236 list.push(entry);
237 }
238 }
239 }
240 list
241 }
242
243 pub fn find_entries_mut<'a, S: Into<String>>(&'a mut self, text: S) -> Vec<&'a mut Entry> {
266 let mut list = Vec::new();
267 let text = &text.into().to_lowercase();
268 for group in self.root_group.iter_mut() {
269 for entry in group.entries.iter_mut() {
270 if entry_contains_string(entry, text) {
271 list.push(entry);
272 }
273 }
274 }
275 list
276 }
277
278 pub fn find_groups<'a, S: Into<String>>(&'a self, name: S) -> Vec<&'a Group> {
297 let name = &name.into().to_lowercase();
298 self.root_group
299 .iter()
300 .filter(|g| g.name.to_lowercase().contains(name))
301 .collect::<Vec<&'a Group>>()
302 }
303
304 pub fn find_groups_mut<'a, S: Into<String>>(&'a mut self, name: S) -> Vec<&'a mut Group> {
320 let name = &name.into().to_lowercase();
321 self.root_group
322 .iter_mut()
323 .filter(|g| g.name.to_lowercase().contains(name))
324 .collect::<Vec<&'a mut Group>>()
325 }
326
327 pub fn get_entry<'a>(&'a self, uuid: EntryUuid) -> Option<&'a Entry> {
344 for group in self.root_group.iter() {
345 for entry in group.entries.iter() {
346 if entry.uuid == uuid {
347 return Some(entry);
348 }
349 }
350 }
351 None
352 }
353
354 pub fn get_entry_mut<'a>(&'a mut self, uuid: EntryUuid) -> Option<&'a mut Entry> {
371 for group in self.root_group.iter_mut() {
372 for entry in group.entries.iter_mut() {
373 if entry.uuid == uuid {
374 return Some(entry);
375 }
376 }
377 }
378 None
379 }
380
381 pub fn get_group<'a>(&'a self, uuid: GroupUuid) -> Option<&'a Group> {
398 self.root_group.iter().find(|g| g.uuid == uuid)
399 }
400
401 pub fn get_group_mut<'a>(&'a mut self, uuid: GroupUuid) -> Option<&'a mut Group> {
418 self.root_group.iter_mut().find(|g| g.uuid == uuid)
419 }
420
421 pub fn open<R: Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
438 let mut reader = LogReader::new(reader);
439 let mut buffer = [0u8; 4];
440
441 reader.read(&mut buffer)?;
442 if buffer != common::DB_SIGNATURE {
443 return Err(Error::InvalidDbSignature(buffer));
444 }
445
446 reader.read(&mut buffer)?;
447 if buffer == common::KDB1_SIGNATURE {
448 return Err(Error::UnhandledDbType(buffer));
449 } else if buffer == common::KDB2_SIGNATURE {
450 Database::open_kdb2(&mut reader, key)
451 } else {
452 return Err(Error::UnhandledDbType(buffer));
453 }
454 }
455
456 pub fn save<W: Write>(&self, writer: &mut W) -> Result<()> {
475 let mut writer = LogWriter::new(writer);
476 match self.db_type {
477 DbType::Kdb1 => Err(Error::Unimplemented(String::from("KeePass v1 not supported"))),
478 DbType::Kdb2 => kdb2_writer::write(&mut writer, self),
479 }
480 }
481
482 fn open_kdb2<R: Log + Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
483 let (meta_data, xml_data) = kdb2_reader::read(reader, key)?;
484 match xml_data.header_hash {
485 Some(header_hash) => {
486 if meta_data.header_hash != header_hash {
487 return Err(Error::InvalidHeaderHash);
488 }
489 }
490 None => {}
491 }
492
493 let root_group = match xml_data.root_group {
494 Some(group) => group,
495 None => Group::new(common::ROOT_GROUP_NAME),
496 };
497
498 let db = Database {
499 comment: meta_data.comment,
500 composite_key: key.clone(),
501 compression: meta_data.compression,
502 db_type: DbType::Kdb2,
503 master_cipher: meta_data.master_cipher,
504 stream_cipher: meta_data.stream_cipher,
505 transform_rounds: meta_data.transform_rounds,
506 version: meta_data.version,
507
508 binaries: xml_data.binaries,
509 color: xml_data.color,
510 custom_data: xml_data.custom_data,
511 custom_icons: xml_data.custom_icons,
512 def_username: xml_data.def_username,
513 def_username_changed: xml_data.def_username_changed,
514 description: xml_data.description,
515 description_changed: xml_data.description_changed,
516 entry_templates_group_changed: xml_data.entry_templates_group_changed,
517 entry_templates_group_uuid: xml_data.entry_templates_group_uuid,
518 generator: xml_data.generator,
519 history_max_items: xml_data.history_max_items,
520 history_max_size: xml_data.history_max_size,
521 last_selected_group: xml_data.last_selected_group,
522 last_top_visible_group: xml_data.last_top_visible_group,
523 maintenance_history_days: xml_data.maintenance_history_days,
524 master_key_change_force: xml_data.master_key_change_force,
525 master_key_change_rec: xml_data.master_key_change_rec,
526 master_key_changed: xml_data.master_key_changed,
527 name: xml_data.name,
528 name_changed: xml_data.name_changed,
529 protect_notes: xml_data.protect_notes,
530 protect_password: xml_data.protect_password,
531 protect_title: xml_data.protect_title,
532 protect_url: xml_data.protect_url,
533 protect_username: xml_data.protect_username,
534 recycle_bin_changed: xml_data.recycle_bin_changed,
535 recycle_bin_enabled: xml_data.recycle_bin_enabled,
536 recycle_bin_uuid: xml_data.recycle_bin_uuid,
537 root_group: root_group,
538 };
539
540 Ok(db)
541 }
542}
543
544fn entry_contains_string(entry: &Entry, name: &String) -> bool {
545 for value in entry.strings.values() {
546 match *value {
547 StringValue::Plain(ref string) => {
548 if string.to_lowercase().contains(name) {
549 return true;
550 }
551 }
552 StringValue::Protected(_) => {}
553 }
554 }
555 false
556}
557
558#[cfg(test)]
559mod tests {
560
561 use super::*;
562 use crate::types::BinariesMap;
563 use crate::types::CompositeKey;
564 use crate::types::Compression;
565 use crate::types::CustomDataMap;
566 use crate::types::CustomIconsMap;
567 use crate::types::DbType;
568 use crate::types::GroupUuid;
569 use crate::types::MasterCipher;
570 use crate::types::StreamCipher;
571 use crate::types::TransformRounds;
572 use crate::types::Version;
573 use crate::utils::test::approx_equal_datetime;
574 use chrono::Utc;
575
576 #[test]
577 fn test_new_returns_correct_instance() {
578 let now = Utc::now();
579 let key = CompositeKey::from_password("5pZ5mgpTkLCDaM46IuH7yGafZFIICyvC");
580 let db = Database::new(&key);
581 assert_eq!(db.comment, None);
582 assert_eq!(db.composite_key, key);
583 assert_eq!(db.compression, Compression::GZip);
584 assert_eq!(db.db_type, DbType::Kdb2);
585 assert_eq!(db.master_cipher, MasterCipher::Aes256);
586 assert_eq!(db.stream_cipher, StreamCipher::Salsa20);
587 assert_eq!(db.transform_rounds, TransformRounds(10000));
588 assert_eq!(db.version, Version::new_kdb2());
589 assert_eq!(db.binaries, BinariesMap::new());
590 assert_eq!(db.color, None);
591 assert_eq!(db.custom_data, CustomDataMap::new());
592 assert_eq!(db.custom_icons, CustomIconsMap::new());
593 assert_eq!(db.def_username, "");
594 assert!(approx_equal_datetime(db.def_username_changed, now));
595 assert_eq!(db.description, "");
596 assert!(approx_equal_datetime(db.description_changed, now));
597 assert!(approx_equal_datetime(db.entry_templates_group_changed, now));
598 assert_eq!(db.entry_templates_group_uuid, GroupUuid::nil());
599 assert_eq!(db.generator, "rust-kpdb");
600 assert_eq!(db.history_max_items, 10);
601 assert_eq!(db.history_max_size, 6291456);
602 assert_eq!(db.last_selected_group, GroupUuid::nil());
603 assert_eq!(db.last_top_visible_group, GroupUuid::nil());
604 assert_eq!(db.maintenance_history_days, 365);
605 assert_eq!(db.master_key_change_force, -1);
606 assert_eq!(db.master_key_change_rec, -1);
607 assert!(approx_equal_datetime(db.master_key_changed, now));
608 assert_eq!(db.name, "");
609 assert!(approx_equal_datetime(db.name_changed, now));
610 assert_eq!(db.protect_notes, false);
611 assert_eq!(db.protect_password, true);
612 assert_eq!(db.protect_title, false);
613 assert_eq!(db.protect_url, false);
614 assert_eq!(db.protect_username, false);
615 assert!(approx_equal_datetime(db.recycle_bin_changed, now));
616 assert_eq!(db.recycle_bin_enabled, true);
617 assert_eq!(db.recycle_bin_uuid, GroupUuid::nil());
618 assert!(db.root_group.uuid != GroupUuid::nil());
619 }
620
621 #[test]
622 fn test_find_entries_returns_correct_entries() {
623 let db = db_with_groups_and_entries();
624 let result = db.find_entries("Proton");
625 assert_eq!(result.len(), 2);
626
627 let result = db.find_entries("Unknown");
628 assert_eq!(result.len(), 0);
629 }
630
631 #[test]
632 fn test_find_entries_mut_returns_correct_entries() {
633 let mut db = db_with_groups_and_entries();
634 {
635 let result = db.find_entries_mut("Proton");
636 assert_eq!(result.len(), 2);
637 }
638 {
639 let result = db.find_entries_mut("Unknown");
640 assert_eq!(result.len(), 0);
641 }
642 }
643
644 #[test]
645 fn test_find_groups_returns_correct_groups() {
646 let db = db_with_groups_and_entries();
647 let result = db.find_groups("mail");
648 assert_eq!(result.len(), 1);
649
650 let result = db.find_groups("Unknown");
651 assert_eq!(result.len(), 0);
652 }
653
654 #[test]
655 fn test_find_groups_mut_returns_correct_groups() {
656 let mut db = db_with_groups_and_entries();
657 {
658 let result = db.find_groups_mut("mail");
659 assert_eq!(result.len(), 1);
660 }
661 {
662 let result = db.find_groups_mut("Unknown");
663 assert_eq!(result.len(), 0);
664 }
665 }
666
667 #[test]
668 fn test_get_entry_returns_correct_entry() {
669 let entry = Entry::new();
670 let entry_uuid = entry.uuid;
671
672 let mut group = Group::new("Group");
673 group.add_entry(entry.clone());
674
675 let mut db = Database::new(&CompositeKey::from_password("test"));
676 assert_eq!(db.get_entry(entry_uuid), None);
677
678 db.root_group.add_group(group);
679 assert_eq!(db.get_entry(entry_uuid), Some(&entry));
680 }
681
682 #[test]
683 fn test_get_entry_mut_returns_correct_entry() {
684 let mut entry = Entry::new();
685 let entry_uuid = entry.uuid;
686
687 let mut group = Group::new("Group");
688 group.add_entry(entry.clone());
689
690 let mut db = Database::new(&CompositeKey::from_password("test"));
691 assert_eq!(db.get_entry_mut(entry_uuid), None);
692
693 db.root_group.add_group(group);
694 assert_eq!(db.get_entry_mut(entry_uuid), Some(&mut entry));
695 }
696
697 #[test]
698 fn test_get_group_returns_correct_group() {
699 let group = Group::new("Group");
700 let group_uuid = group.uuid;
701
702 let mut db = Database::new(&CompositeKey::from_password("test"));
703 assert_eq!(db.get_group(group_uuid), None);
704
705 db.root_group.add_group(group.clone());
706 assert_eq!(db.get_group(group_uuid), Some(&group));
707 }
708
709 #[test]
710 fn test_get_group_mut_returns_correct_group() {
711 let mut group = Group::new("Group");
712 let group_uuid = group.uuid;
713
714 let mut db = Database::new(&CompositeKey::from_password("test"));
715 assert_eq!(db.get_group_mut(group_uuid), None);
716
717 db.root_group.add_group(group.clone());
718 assert_eq!(db.get_group_mut(group_uuid), Some(&mut group));
719 }
720
721 fn db_with_groups_and_entries() -> Database {
722 let mut gmail = Entry::new();
723 gmail.set_title("Gmail");
724 gmail.set_username("guser");
725 gmail.set_password("gpass");
726 gmail.set_url("https://mail.google.com");
727
728 let mut protonmail = Entry::new();
729 protonmail.set_title("ProtonMail");
730 protonmail.set_username("puser");
731 protonmail.set_password("ppass");
732 protonmail.set_url("https://mail.protonmail.com");
733
734 let mut protonvpn = Entry::new();
735 protonvpn.set_title("ProtonVPN");
736 protonvpn.set_username("puser");
737 protonvpn.set_password("ppass");
738 protonvpn.set_url("https://prontvpn.com");
739
740 let mut email_group = Group::new("Email");
741 email_group.add_entry(gmail);
742 email_group.add_entry(protonmail);
743
744 let mut vpn_group = Group::new("VPN");
745 vpn_group.add_entry(protonvpn);
746
747 let mut db = Database::new(&CompositeKey::from_password("test"));
748 db.root_group.add_group(email_group);
749 db.root_group.add_group(vpn_group);
750 db
751 }
752}