kpdb/types/
database.rs

1// Copyright (c) 2016-2017 Martijn Rijkeboer <[email protected]>
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use chrono::{DateTime, Utc};
10use common;
11use format::{kdb2_reader, kdb2_writer};
12use io::{Log, LogReader, LogWriter};
13use std::io::{Read, Write};
14use super::binaries_map::BinariesMap;
15use super::color::Color;
16use super::comment::Comment;
17use super::composite_key::CompositeKey;
18use super::compression::Compression;
19use super::custom_data_map::CustomDataMap;
20use super::custom_icons_map::CustomIconsMap;
21use super::db_type::DbType;
22use super::entries_map::EntriesMap;
23use super::error::Error;
24use super::group::Group;
25use super::group_uuid::GroupUuid;
26use super::groups_map::GroupsMap;
27use super::history_map::HistoryMap;
28use super::icon::Icon;
29use super::master_cipher::MasterCipher;
30use super::result::Result;
31use super::stream_cipher::StreamCipher;
32use super::transform_rounds::TransformRounds;
33use super::version::Version;
34
35/// The KeePass database.
36#[derive(Clone, Debug, PartialEq)]
37pub struct Database {
38    /// Content of the comment header.
39    pub comment: Option<Comment>,
40
41    /// Composite key.
42    pub composite_key: CompositeKey,
43
44    /// Compression algorithm.
45    pub compression: Compression,
46
47    /// Type of the database.
48    pub db_type: DbType,
49
50    /// Master encryption algorithm.
51    pub master_cipher: MasterCipher,
52
53    /// Stream encryption algorithm (e.g. passwords).
54    pub stream_cipher: StreamCipher,
55
56    /// Number of times the composite key must be transformed.
57    pub transform_rounds: TransformRounds,
58
59    /// The database version.
60    pub version: Version,
61
62    /// Map with binary data.
63    pub binaries: BinariesMap,
64
65    /// Optional color.
66    pub color: Option<Color>,
67
68    /// Map with custom data.
69    pub custom_data: CustomDataMap,
70
71    /// Map with custom icons.
72    pub custom_icons: CustomIconsMap,
73
74    /// Default username for new entries.
75    pub def_username: String,
76
77    /// The date and time the default username was changed.
78    pub def_username_changed: DateTime<Utc>,
79
80    /// Description of this database.
81    pub description: String,
82
83    /// The date and time the description was changed.
84    pub description_changed: DateTime<Utc>,
85
86    /// Map with entries.
87    pub entries: EntriesMap,
88
89    /// The date and time the entry templates group was changed.
90    pub entry_templates_group_changed: DateTime<Utc>,
91
92    /// The identifier of the group containing entry templates.
93    pub entry_templates_group_uuid: GroupUuid,
94
95    /// Name of the generator.
96    pub generator: String,
97
98    /// The identifier of the root group.
99    pub group_uuid: Option<GroupUuid>,
100
101    /// Map with groups.
102    pub groups: GroupsMap,
103
104    /// Map with history entries.
105    pub history: HistoryMap,
106
107    /// Maximum number of history items.
108    pub history_max_items: i32,
109
110    /// Maximum size of the history data.
111    pub history_max_size: i32,
112
113    /// The identifier of the last selected group.
114    pub last_selected_group: GroupUuid,
115
116    /// The identifier of the last top visible group.
117    pub last_top_visible_group: GroupUuid,
118
119    /// Number of days until history entries are being deleted.
120    pub maintenance_history_days: i32,
121
122    pub master_key_change_force: i32,
123
124    pub master_key_change_rec: i32,
125
126    /// The date and time the master key was changed.
127    pub master_key_changed: DateTime<Utc>,
128
129    /// Name of this database.
130    pub name: String,
131
132    /// The date and time the name was changed.
133    pub name_changed: DateTime<Utc>,
134
135    /// Whether notes must be protected.
136    pub protect_notes: bool,
137
138    /// Whether passwords must be protected.
139    pub protect_password: bool,
140
141    /// Whether titles must be protected.
142    pub protect_title: bool,
143
144    /// Whether URL's must be protected.
145    pub protect_url: bool,
146
147    /// Whether usernames must be protected.
148    pub protect_username: bool,
149
150    /// The date and time the recycle bin was changed.
151    pub recycle_bin_changed: DateTime<Utc>,
152
153    /// Whether the recycle bin is enabled.
154    pub recycle_bin_enabled: bool,
155
156    /// The identifier of the recycle bin.
157    pub recycle_bin_uuid: GroupUuid,
158}
159
160impl Database {
161    /// Create a new database.
162    ///
163    /// # Examples
164    ///
165    /// ```rust
166    /// use kpdb::{CompositeKey, Database};
167    ///
168    /// let key = CompositeKey::from_password("password");
169    /// let db = Database::new(&key);
170    /// ```
171    pub fn new(key: &CompositeKey) -> Database {
172        let now = Utc::now();
173        let mut root = Group::new(common::ROOT_GROUP_NAME);
174        let mut recycle_bin = Group::new(common::RECYCLE_BIN_NAME);
175        let mut groups = GroupsMap::new();
176
177        recycle_bin.enable_auto_type = Some(false);
178        recycle_bin.enable_searching = Some(false);
179        recycle_bin.icon = Icon::RecycleBin;
180
181        let root_uuid = root.uuid;
182        let recycle_bin_uuid = recycle_bin.uuid;
183        root.groups.push(recycle_bin_uuid);
184        groups.insert(root_uuid, root);
185        groups.insert(recycle_bin_uuid, recycle_bin);
186
187        Database {
188            comment: None,
189            composite_key: key.clone(),
190            compression: Compression::GZip,
191            db_type: DbType::Kdb2,
192            master_cipher: MasterCipher::Aes256,
193            stream_cipher: StreamCipher::Salsa20,
194            transform_rounds: TransformRounds(10000),
195            version: Version::new_kdb2(),
196            binaries: BinariesMap::new(),
197            color: None,
198            custom_data: CustomDataMap::new(),
199            custom_icons: CustomIconsMap::new(),
200            def_username: String::new(),
201            def_username_changed: now,
202            description: String::new(),
203            description_changed: now,
204            entries: EntriesMap::new(),
205            entry_templates_group_changed: now,
206            entry_templates_group_uuid: GroupUuid::nil(),
207            generator: String::from(common::GENERATOR_NAME),
208            group_uuid: Some(root_uuid),
209            groups: groups,
210            history: HistoryMap::new(),
211            history_max_items: common::HISTORY_MAX_ITEMS_DEFAULT,
212            history_max_size: common::HISTORY_MAX_SIZE_DEFAULT,
213            last_selected_group: GroupUuid::nil(),
214            last_top_visible_group: GroupUuid::nil(),
215            maintenance_history_days: common::MAINTENANCE_HISTORY_DAYS_DEFAULT,
216            master_key_change_force: common::MASTER_KEY_CHANGE_FORCE_DEFAULT,
217            master_key_change_rec: common::MASTER_KEY_CHANGE_REC_DEFAULT,
218            master_key_changed: now,
219            name: String::new(),
220            name_changed: now,
221            protect_notes: common::PROTECT_NOTES_DEFAULT,
222            protect_password: common::PROTECT_PASSWORD_DEFAULT,
223            protect_title: common::PROTECT_TITLE_DEFAULT,
224            protect_url: common::PROTECT_URL_DEFAULT,
225            protect_username: common::PROTECT_USERNAME_DEFAULT,
226            recycle_bin_changed: now,
227            recycle_bin_enabled: common::RECYCLE_BIN_ENABLED_DEFAULT,
228            recycle_bin_uuid: recycle_bin_uuid,
229        }
230    }
231
232    /// Attempts to open an existing database.
233    ///
234    /// # Examples
235    ///
236    /// ```rust,no_run
237    /// # use kpdb::Result;
238    /// use kpdb::{CompositeKey, Database};
239    /// use std::fs::File;
240    ///
241    /// # fn open_example() -> Result<()> {
242    /// let mut file = try!(File::open("passwords.kdbx"));
243    /// let key = CompositeKey::from_password("password");
244    /// let db = try!(Database::open(&mut file, &key));
245    /// # Ok(())
246    /// # }
247    /// ```
248    pub fn open<R: Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
249        let mut reader = LogReader::new(reader);
250        let mut buffer = [0u8; 4];
251
252        try!(reader.read(&mut buffer));
253        if buffer != common::DB_SIGNATURE {
254            return Err(Error::InvalidDbSignature(buffer));
255        }
256
257        try!(reader.read(&mut buffer));
258        if buffer == common::KDB1_SIGNATURE {
259            return Err(Error::UnhandledDbType(buffer));
260        } else if buffer == common::KDB2_SIGNATURE {
261            Database::open_kdb2(&mut reader, key)
262        } else {
263            return Err(Error::UnhandledDbType(buffer));
264        }
265    }
266
267    /// Attempts to save the database.
268    ///
269    /// # Examples
270    ///
271    /// ```rust,no_run
272    /// # use kpdb::Result;
273    /// use kpdb::{CompositeKey, Database};
274    /// use std::fs::File;
275    ///
276    /// # fn save_example() -> Result<()> {
277    /// let key = CompositeKey::from_password("password");
278    /// let db = Database::new(&key);
279    /// let mut file = try!(File::create("new.kdbx"));
280    ///
281    /// try!(db.save(&mut file));
282    /// # Ok(())
283    /// # }
284    /// ```
285    pub fn save<W: Write>(&self, writer: &mut W) -> Result<()> {
286        let mut writer = LogWriter::new(writer);
287        match self.db_type {
288            DbType::Kdb1 => Err(Error::Unimplemented(String::from("KeePass v1 not supported"))),
289            DbType::Kdb2 => kdb2_writer::write(&mut writer, self),
290        }
291    }
292
293    fn open_kdb2<R: Log + Read>(reader: &mut R, key: &CompositeKey) -> Result<Database> {
294        let (meta_data, xml_data) = try!(kdb2_reader::read(reader, key));
295        match xml_data.header_hash {
296            Some(header_hash) => {
297                if meta_data.header_hash != header_hash {
298                    return Err(Error::InvalidHeaderHash);
299                }
300            }
301            None => {}
302        }
303
304        let db = Database {
305            comment: meta_data.comment,
306            composite_key: key.clone(),
307            compression: meta_data.compression,
308            db_type: DbType::Kdb2,
309            master_cipher: meta_data.master_cipher,
310            stream_cipher: meta_data.stream_cipher,
311            transform_rounds: meta_data.transform_rounds,
312            version: meta_data.version,
313
314            binaries: xml_data.binaries,
315            color: xml_data.color,
316            custom_data: xml_data.custom_data,
317            custom_icons: xml_data.custom_icons,
318            def_username: xml_data.def_username,
319            def_username_changed: xml_data.def_username_changed,
320            description: xml_data.description,
321            description_changed: xml_data.description_changed,
322            entries: xml_data.entries,
323            entry_templates_group_changed: xml_data.entry_templates_group_changed,
324            entry_templates_group_uuid: xml_data.entry_templates_group_uuid,
325            generator: xml_data.generator,
326            group_uuid: xml_data.group_uuid,
327            groups: xml_data.groups,
328            history: xml_data.history,
329            history_max_items: xml_data.history_max_items,
330            history_max_size: xml_data.history_max_size,
331            last_selected_group: xml_data.last_selected_group,
332            last_top_visible_group: xml_data.last_top_visible_group,
333            maintenance_history_days: xml_data.maintenance_history_days,
334            master_key_change_force: xml_data.master_key_change_force,
335            master_key_change_rec: xml_data.master_key_change_rec,
336            master_key_changed: xml_data.master_key_changed,
337            name: xml_data.name,
338            name_changed: xml_data.name_changed,
339            protect_notes: xml_data.protect_notes,
340            protect_password: xml_data.protect_password,
341            protect_title: xml_data.protect_title,
342            protect_url: xml_data.protect_url,
343            protect_username: xml_data.protect_username,
344            recycle_bin_changed: xml_data.recycle_bin_changed,
345            recycle_bin_enabled: xml_data.recycle_bin_enabled,
346            recycle_bin_uuid: xml_data.recycle_bin_uuid,
347        };
348
349        Ok(db)
350    }
351}
352
353#[cfg(test)]
354mod tests {
355
356    use chrono::Utc;
357    use super::*;
358    use types::BinariesMap;
359    use types::CompositeKey;
360    use types::Compression;
361    use types::CustomDataMap;
362    use types::CustomIconsMap;
363    use types::DbType;
364    use types::EntriesMap;
365    use types::GroupUuid;
366    use types::HistoryMap;
367    use types::MasterCipher;
368    use types::StreamCipher;
369    use types::TransformRounds;
370    use types::Version;
371    use utils::test::approx_equal_datetime;
372
373    #[test]
374    fn test_new_returns_correct_instance() {
375        let now = Utc::now();
376        let key = CompositeKey::from_password("5pZ5mgpTkLCDaM46IuH7yGafZFIICyvC");
377        let db = Database::new(&key);
378        assert_eq!(db.comment, None);
379        assert_eq!(db.composite_key, key);
380        assert_eq!(db.compression, Compression::GZip);
381        assert_eq!(db.db_type, DbType::Kdb2);
382        assert_eq!(db.master_cipher, MasterCipher::Aes256);
383        assert_eq!(db.stream_cipher, StreamCipher::Salsa20);
384        assert_eq!(db.transform_rounds, TransformRounds(10000));
385        assert_eq!(db.version, Version::new_kdb2());
386        assert_eq!(db.binaries, BinariesMap::new());
387        assert_eq!(db.color, None);
388        assert_eq!(db.custom_data, CustomDataMap::new());
389        assert_eq!(db.custom_icons, CustomIconsMap::new());
390        assert_eq!(db.def_username, "");
391        assert!(approx_equal_datetime(db.def_username_changed, now));
392        assert_eq!(db.description, "");
393        assert!(approx_equal_datetime(db.description_changed, now));
394        assert_eq!(db.entries, EntriesMap::new());
395        assert!(approx_equal_datetime(db.entry_templates_group_changed, now));
396        assert_eq!(db.entry_templates_group_uuid, GroupUuid::nil());
397        assert_eq!(db.generator, "rust-kpdb");
398        assert!(db.group_uuid != None);
399        assert!(db.group_uuid != Some(GroupUuid::nil()));
400        assert_eq!(db.groups.len(), 2);
401        assert_eq!(db.history, HistoryMap::new());
402        assert_eq!(db.history_max_items, 10);
403        assert_eq!(db.history_max_size, 6291456);
404        assert_eq!(db.last_selected_group, GroupUuid::nil());
405        assert_eq!(db.last_top_visible_group, GroupUuid::nil());
406        assert_eq!(db.maintenance_history_days, 365);
407        assert_eq!(db.master_key_change_force, -1);
408        assert_eq!(db.master_key_change_rec, -1);
409        assert!(approx_equal_datetime(db.master_key_changed, now));
410        assert_eq!(db.name, "");
411        assert!(approx_equal_datetime(db.name_changed, now));
412        assert_eq!(db.protect_notes, false);
413        assert_eq!(db.protect_password, true);
414        assert_eq!(db.protect_title, false);
415        assert_eq!(db.protect_url, false);
416        assert_eq!(db.protect_username, false);
417        assert!(approx_equal_datetime(db.recycle_bin_changed, now));
418        assert_eq!(db.recycle_bin_enabled, true);
419        assert!(db.recycle_bin_uuid != GroupUuid::nil());
420    }
421}