Skip to main content

keepass_ng/db/types/
entry.rs

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