Skip to main content

keepass_ng/db/types/
entry.rs

1use crate::db::{
2    Attachment, AutoType, Color, CustomDataItem, History, IconId, Times, Value,
3    node::{Node, NodePtr},
4    rc_refcell_node,
5};
6use secrecy::ExposeSecret;
7use std::collections::HashMap;
8use uuid::Uuid;
9
10/// A database entry containing several key-value fields.
11#[derive(Debug, Clone)]
12#[cfg_attr(feature = "serialization", derive(serde::Serialize))]
13pub struct Entry {
14    pub(crate) uuid: Uuid,
15    pub(crate) fields: HashMap<String, Value<String>>,
16    pub(crate) autotype: Option<AutoType>,
17    pub(crate) tags: Vec<String>,
18
19    pub(crate) times: Times,
20
21    pub(crate) custom_data: HashMap<String, CustomDataItem>,
22
23    pub(crate) icon_id: Option<IconId>,
24    pub(crate) custom_icon: Option<Uuid>,
25
26    pub(crate) foreground_color: Option<Color>,
27    pub(crate) background_color: Option<Color>,
28
29    pub(crate) override_url: Option<String>,
30    pub(crate) quality_check: Option<bool>,
31
32    pub attachments: HashMap<String, Attachment>,
33
34    pub(crate) history: Option<History>,
35
36    pub(crate) parent: Option<Uuid>,
37
38    pub(crate) previous_parent_group: Option<Uuid>,
39}
40
41impl Default for Entry {
42    fn default() -> Self {
43        Self {
44            uuid: Uuid::new_v4(),
45            fields: HashMap::new(),
46            autotype: None,
47            tags: Vec::new(),
48            times: Times::new(),
49            custom_data: Default::default(),
50            icon_id: Some(IconId::KEY),
51            custom_icon: None,
52            foreground_color: None,
53            background_color: None,
54            override_url: None,
55            quality_check: None,
56            attachments: HashMap::new(),
57            history: None,
58            parent: None,
59            previous_parent_group: None,
60        }
61    }
62}
63
64impl PartialEq for Entry {
65    fn eq(&self, other: &Self) -> bool {
66        self.uuid == other.uuid
67            && self.fields == other.fields
68            && self.autotype == other.autotype
69            && self.tags == other.tags
70            && self.times == other.times
71            && self.custom_data == other.custom_data
72            && self.icon_id == other.icon_id
73            && self.custom_icon == other.custom_icon
74            && self.foreground_color == other.foreground_color
75            && self.background_color == other.background_color
76            && self.override_url == other.override_url
77            && self.quality_check == other.quality_check
78            && self.attachments == other.attachments
79            && self.history == other.history
80        // && self.parent == other.parent
81    }
82}
83
84impl Eq for Entry {}
85
86impl Node for Entry {
87    fn duplicate(&self) -> NodePtr {
88        let mut tmp = self.clone();
89        tmp.parent = None;
90        rc_refcell_node(tmp)
91    }
92
93    fn get_uuid(&self) -> Uuid {
94        self.uuid
95    }
96
97    fn set_uuid(&mut self, uuid: Uuid) {
98        self.uuid = uuid;
99    }
100
101    fn get_title(&self) -> Option<&str> {
102        self.get("Title")
103    }
104
105    fn set_title(&mut self, title: Option<&str>) {
106        self.set_unprotected_field_pair("Title", title);
107    }
108
109    fn get_notes(&self) -> Option<&str> {
110        self.get("Notes")
111    }
112
113    fn set_notes(&mut self, notes: Option<&str>) {
114        self.set_unprotected_field_pair("Notes", notes);
115    }
116
117    fn get_icon_id(&self) -> Option<IconId> {
118        self.icon_id
119    }
120
121    fn set_icon_id(&mut self, icon_id: Option<IconId>) {
122        self.icon_id = icon_id;
123    }
124
125    fn get_custom_icon_uuid(&self) -> Option<Uuid> {
126        self.custom_icon
127    }
128
129    fn get_times(&self) -> &Times {
130        &self.times
131    }
132
133    fn get_times_mut(&mut self) -> &mut Times {
134        &mut self.times
135    }
136
137    fn get_parent(&self) -> Option<Uuid> {
138        self.parent
139    }
140
141    fn set_parent(&mut self, parent: Option<Uuid>) {
142        self.parent = parent;
143    }
144}
145
146impl Entry {
147    pub fn set_custom_icon_uuid(&mut self, uuid: Option<Uuid>) {
148        self.custom_icon = uuid;
149    }
150
151    pub fn quality_check(&self) -> bool {
152        self.quality_check.unwrap_or(true)
153    }
154
155    pub fn previous_parent_group(&self) -> Option<Uuid> {
156        self.previous_parent_group
157    }
158
159    pub fn custom_icon_uuid(&self) -> Option<Uuid> {
160        self.custom_icon
161    }
162
163    pub fn custom_data(&self) -> &HashMap<String, CustomDataItem> {
164        &self.custom_data
165    }
166
167    pub fn custom_data_mut(&mut self) -> &mut HashMap<String, CustomDataItem> {
168        &mut self.custom_data
169    }
170
171    pub fn get_history(&self) -> &Option<History> {
172        &self.history
173    }
174
175    pub fn purge_history(&mut self) {
176        self.history = None;
177    }
178
179    pub(crate) fn set_unprotected_field_pair(&mut self, field_name: &str, field_value: Option<&str>) {
180        if let Some(field_value) = field_value {
181            let v = Value::Unprotected(field_value.to_string());
182            self.fields.insert(field_name.to_string(), v);
183        } else {
184            self.fields.remove(field_name);
185        }
186    }
187
188    pub(crate) fn set_protected_field_pair<T: AsRef<[u8]>>(&mut self, field_name: &str, field_value: Option<T>) {
189        if let Some(field_value) = field_value {
190            let value = String::from_utf8_lossy(field_value.as_ref()).into_owned();
191            let v = Value::protected(value);
192            self.fields.insert(field_name.to_string(), v);
193        } else {
194            self.fields.remove(field_name);
195        }
196    }
197
198    pub(crate) fn set_binary_field_pair<T: AsRef<[u8]>>(&mut self, field_name: &str, field_value: Option<T>) {
199        if let Some(field_value) = field_value {
200            self.attachments.insert(
201                field_name.to_string(),
202                Attachment {
203                    data: Value::unprotected(field_value.as_ref().to_vec()),
204                },
205            );
206        } else {
207            self.attachments.remove(field_name);
208        }
209    }
210}
211
212impl<'a> Entry {
213    /// Get a field by name, taking care of unprotecting Protected values automatically
214    pub fn get(&'a self, key: &str) -> Option<&'a str> {
215        match self.fields.get(key) {
216            None => None,
217            Some(Value::Protected(pv)) => Some(pv.expose_secret()),
218            Some(Value::Unprotected(uv)) => Some(uv),
219        }
220    }
221
222    /// Get a bytes field by name
223    pub fn get_bytes(&'a self, key: &str) -> Option<&'a [u8]> {
224        self.attachments.get(key).map(|attachment| attachment.data.get().as_slice())
225    }
226
227    pub fn get_autotype(&self) -> Option<&AutoType> {
228        self.autotype.as_ref()
229    }
230
231    pub fn set_autotype(&mut self, autotype: Option<AutoType>) {
232        self.autotype = autotype;
233    }
234
235    /// Convenience method for getting tags
236    /// Returns a Vec of tags
237    pub fn get_tags(&self) -> &Vec<String> {
238        self.tags.as_ref()
239    }
240
241    pub fn get_tags_mut(&mut self) -> &mut Vec<String> {
242        self.tags.as_mut()
243    }
244
245    #[rustfmt::skip]
246    const EXCLUDED_FIELDS: [&'static str; 9] = ["Password", "BinaryData", "otp", "Title", "URL", "UserName", "Notes", "Additional", "BinaryDesc"];
247
248    /// Set or remove additional attributes (custom string data)
249    pub fn set_additional_attribute(&mut self, key: &str, value: Option<&str>) -> crate::Result<()> {
250        if Self::EXCLUDED_FIELDS.contains(&key) {
251            return Err(format!("Cannot set additional attribute for field {key}").into());
252        }
253        self.set_unprotected_field_pair(key, value);
254        Ok(())
255    }
256
257    /// Get an additional attribute (custom string data)
258    pub fn get_additional_attribute(&self, key: &str) -> Option<&str> {
259        if Self::EXCLUDED_FIELDS.contains(&key) {
260            return None;
261        }
262        self.get(key)
263    }
264
265    /// Get all additional string attributes stored directly on this entry.
266    pub fn additional_attributes(&self) -> Vec<(String, String)> {
267        self.fields
268            .keys()
269            .filter(|key| !Self::EXCLUDED_FIELDS.contains(&key.as_str()))
270            .filter_map(|key| self.get(key).map(|value| (key.clone(), value.to_string())))
271            .collect()
272    }
273
274    /// Convenience method for getting the value of the `UserName` field
275    pub fn get_username(&'a self) -> Option<&'a str> {
276        self.get("UserName")
277    }
278
279    pub fn set_username(&mut self, username: Option<&str>) {
280        self.set_unprotected_field_pair("UserName", username);
281    }
282
283    /// Convenience method for getting the value of the 'Password' field
284    pub fn get_password(&self) -> Option<&str> {
285        self.get("Password")
286    }
287
288    pub fn set_password(&mut self, password: Option<&str>) {
289        self.set_protected_field_pair("Password", password.map(|p| p.as_bytes()));
290    }
291
292    /// Convenience method for getting the value of the 'URL' field
293    pub fn get_url(&self) -> Option<&str> {
294        self.get("URL")
295    }
296
297    pub fn set_url(&mut self, url: Option<&str>) {
298        self.set_unprotected_field_pair("URL", url);
299    }
300
301    /// Adds the current version of the entry to the entry's history
302    /// and updates the last modification timestamp.
303    /// The history will only be updated if the entry has
304    /// uncommited changes.
305    ///
306    /// Returns whether or not a new history entry was added.
307    pub fn update_history(&mut self) -> bool {
308        if self.history.is_none() {
309            self.history = Some(History::default());
310        }
311
312        if !self.has_uncommited_changes() {
313            return false;
314        }
315
316        self.times.set_last_modification(Some(Times::now()));
317
318        let mut new_history_entry = self.clone();
319        new_history_entry.history = None;
320
321        // TODO should we validate that the history is enabled?
322        // TODO should we validate the maximum size of the history?
323        if let Some(h) = self.history.as_mut() {
324            h.add_entry(new_history_entry);
325        }
326
327        true
328    }
329
330    /// Determines if the entry was modified since the last
331    /// history update.
332    pub(crate) fn has_uncommited_changes(&self) -> bool {
333        if let Some(history) = self.history.as_ref() {
334            if history.entries.is_empty() {
335                return true;
336            }
337
338            let new_times = Times::default();
339
340            let mut sanitized_entry = self.clone();
341            sanitized_entry.times = new_times.clone();
342            sanitized_entry.history.take();
343
344            let mut last_history_entry = history.entries.first().unwrap().clone();
345            last_history_entry.times = new_times;
346            last_history_entry.history.take();
347
348            if sanitized_entry.eq(&last_history_entry) {
349                return false;
350            }
351        }
352        true
353    }
354}
355
356#[cfg(test)]
357mod entry_tests {
358    use super::{Entry, Node};
359    use std::{thread, time};
360
361    #[test]
362    fn byte_values() {
363        let mut entry = Entry::default();
364        entry.set_binary_field_pair("a-bytes", Some(&[1, 2, 3]));
365
366        entry.set_unprotected_field_pair("a-unprotected", Some("asdf"));
367        entry.set_protected_field_pair("a-protected", Some("asdf".as_bytes()));
368
369        assert_eq!(entry.get_bytes("a-bytes"), Some(&[1, 2, 3][..]));
370        assert_eq!(entry.get_bytes("a-unprotected"), None);
371        assert_eq!(entry.get_bytes("a-protected"), None);
372
373        assert_eq!(entry.get("a-bytes"), None);
374
375        assert!(!entry.attachments["a-bytes"].data.is_empty());
376        entry.set_binary_field_pair::<&[u8]>("a-bytes", None);
377        assert_eq!(entry.get_bytes("a-bytes"), None);
378    }
379
380    #[test]
381    fn update_history() {
382        let mut entry = Entry::default();
383        let mut last_modification_time = entry.times.get_last_modification().unwrap();
384
385        entry.set_username(Some("user"));
386        // Making sure to wait 1 sec before update the history, to make
387        // sure that we get a different modification timestamp.
388        thread::sleep(time::Duration::from_secs(1));
389
390        assert!(entry.update_history());
391        assert!(entry.history.is_some());
392        assert_eq!(entry.history.as_ref().unwrap().entries.len(), 1);
393        assert_ne!(entry.times.get_last_modification().unwrap(), last_modification_time);
394        last_modification_time = entry.times.get_last_modification().unwrap();
395        thread::sleep(time::Duration::from_secs(1));
396
397        // Updating the history without making any changes
398        // should not do anything.
399        assert!(!entry.update_history());
400        assert!(entry.history.is_some());
401        assert_eq!(entry.history.as_ref().unwrap().entries.len(), 1);
402        assert_eq!(entry.times.get_last_modification().unwrap(), last_modification_time);
403
404        entry.set_title(Some("first title"));
405
406        assert!(entry.update_history());
407        assert!(entry.history.is_some());
408        assert_eq!(entry.history.as_ref().unwrap().entries.len(), 2);
409        assert_ne!(entry.times.get_last_modification().unwrap(), last_modification_time);
410        last_modification_time = entry.times.get_last_modification().unwrap();
411        thread::sleep(time::Duration::from_secs(1));
412
413        assert!(!entry.update_history());
414        assert!(entry.history.is_some());
415        assert_eq!(entry.history.as_ref().unwrap().entries.len(), 2);
416        assert_eq!(entry.times.get_last_modification().unwrap(), last_modification_time);
417
418        entry.set_title(Some("second title"));
419
420        assert!(entry.update_history());
421        assert!(entry.history.is_some());
422        assert_eq!(entry.history.as_ref().unwrap().entries.len(), 3);
423        assert_ne!(entry.times.get_last_modification().unwrap(), last_modification_time);
424        last_modification_time = entry.times.get_last_modification().unwrap();
425        thread::sleep(time::Duration::from_secs(1));
426
427        assert!(!entry.update_history());
428        assert!(entry.history.is_some());
429        assert_eq!(entry.history.as_ref().unwrap().entries.len(), 3);
430        assert_eq!(entry.times.get_last_modification().unwrap(), last_modification_time);
431
432        let last_history_entry = entry.history.as_ref().unwrap().entries.first().unwrap();
433        assert_eq!(last_history_entry.get_title().unwrap(), "second title");
434
435        for history_entry in &entry.history.unwrap().entries {
436            assert!(history_entry.history.is_none());
437        }
438    }
439
440    #[cfg(feature = "totp")]
441    #[test]
442    fn totp() {
443        let mut entry = Entry::default();
444        entry.set_raw_otp_value(
445            Some("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"),
446        );
447
448        assert!(entry.get_otp().is_ok());
449    }
450
451    #[cfg(feature = "serialization")]
452    #[test]
453    fn serialization() {
454        use super::Value;
455        assert_eq!(
456            serde_json::to_string(&Value::Unprotected(vec![65, 66, 67])).unwrap(),
457            "[65,66,67]".to_string()
458        );
459
460        assert_eq!(
461            serde_json::to_string(&Value::Unprotected("ABC".to_string())).unwrap(),
462            "\"ABC\"".to_string()
463        );
464
465        assert_eq!(
466            serde_json::to_string(&Value::<String>::protected("ABC")).unwrap(),
467            "\"ABC\"".to_string()
468        );
469    }
470}