Skip to main content

keepass_ng/format/xml_db/
entry.rs

1#[cfg(feature = "save_kdbx4")]
2use crate::crypt::CryptographyError;
3#[cfg(feature = "save_kdbx4")]
4use crate::format::xml_db::tags::join_tags;
5use crate::{
6    crypt::ciphers::Cipher,
7    db::Color,
8    format::xml_db::{
9        UUID,
10        custom_serde::{cs_bool, cs_opt_bool, cs_opt_fromstr, cs_opt_string},
11        meta::CustomDataXml,
12        tags::split_tags,
13        times::TimesXml,
14    },
15};
16use base64::{Engine as _, engine::general_purpose as base64_engine};
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Serialize, Deserialize)]
20#[serde(rename = "Entry", rename_all = "PascalCase")]
21pub(crate) struct EntryXml {
22    #[serde(rename = "UUID")]
23    pub uuid: UUID,
24
25    #[serde(default, rename = "IconID", with = "cs_opt_fromstr", skip_serializing_if = "Option::is_none")]
26    pub icon_id: Option<usize>,
27
28    #[serde(
29        default,
30        rename = "CustomIconUUID",
31        with = "cs_opt_string",
32        skip_serializing_if = "Option::is_none"
33    )]
34    pub custom_icon_uuid: Option<UUID>,
35
36    #[serde(default, with = "cs_opt_string", skip_serializing_if = "Option::is_none")]
37    pub foreground_color: Option<Color>,
38
39    #[serde(default, with = "cs_opt_string", skip_serializing_if = "Option::is_none")]
40    pub background_color: Option<Color>,
41
42    #[serde(default, rename = "OverrideURL", with = "cs_opt_string", skip_serializing_if = "Option::is_none")]
43    pub override_url: Option<String>,
44
45    #[serde(default, with = "cs_opt_string", skip_serializing_if = "Option::is_none")]
46    pub tags: Option<String>,
47
48    #[serde(default, with = "cs_opt_bool", skip_serializing_if = "Option::is_none")]
49    pub quality_check: Option<bool>,
50
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub previous_parent_group: Option<UUID>,
53
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub times: Option<TimesXml>,
56
57    #[serde(default, rename = "String")]
58    pub string_fields: Vec<StringFieldXml>,
59
60    #[serde(default, rename = "Binary")]
61    pub binary_fields: Vec<BinaryFieldXml>,
62
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub auto_type: Option<AutoTypeXml>,
65
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub history: Option<HistoryXml>,
68
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub custom_data: Option<CustomDataXml>,
71}
72
73impl EntryXml {
74    pub(crate) fn xml_to_db_handle(
75        self,
76        target: &mut crate::db::Entry,
77        header_attachments: &[crate::db::Attachment],
78        inner_decryptor: &mut dyn Cipher,
79    ) -> std::io::Result<()> {
80        target.icon_id = self.icon_id.and_then(|id| id.try_into().ok());
81
82        target.custom_icon = self.custom_icon_uuid.map(|uuid| uuid.0);
83
84        target.foreground_color = self.foreground_color;
85        target.background_color = self.background_color;
86        target.override_url = self.override_url;
87        target.quality_check = self.quality_check;
88        target.previous_parent_group = self.previous_parent_group.map(|uuid| uuid.0);
89        target.tags = self.tags.as_deref().map(split_tags).unwrap_or_default();
90
91        target.times = self.times.map(|t| t.into()).unwrap_or_default();
92
93        for field in self.string_fields {
94            let fval = field.value.value.unwrap_or_default();
95            let value = if field.value.protected {
96                let fval = base64_engine::STANDARD.decode(fval).map_err(std::io::Error::other)?;
97                let fval = inner_decryptor.decrypt(&fval).map_err(std::io::Error::other)?;
98                let fval = String::from_utf8_lossy(&fval).to_string();
99
100                crate::db::Value::protected(fval)
101            } else {
102                crate::db::Value::unprotected(fval)
103            };
104            target.fields.insert(field.key, value);
105        }
106
107        for field in self.binary_fields {
108            if let Some(attachment) = header_attachments.get(field.value.value_ref) {
109                target.attachments.insert(field.key.clone(), attachment.clone());
110            }
111        }
112
113        target.autotype = self.auto_type.map(|at| at.into());
114
115        if let Some(h) = self.history {
116            target.history = Some(crate::db::History {
117                entries: h
118                    .entries
119                    .into_iter()
120                    .map(|e| {
121                        let mut he = crate::db::Entry {
122                            uuid: e.uuid.0,
123                            ..Default::default()
124                        };
125
126                        e.xml_to_db_handle(&mut he, header_attachments, inner_decryptor)?;
127                        he.history = None; // history entries cannot have their own history
128                        Ok(he)
129                    })
130                    .collect::<Result<_, std::io::Error>>()?,
131            });
132        }
133
134        if let Some(cd) = self.custom_data {
135            target.custom_data = cd.into();
136        }
137
138        Ok(())
139    }
140
141    #[cfg(feature = "save_kdbx4")]
142    pub(crate) fn db_to_xml(
143        db: &crate::db::Entry,
144        inner_encryptor: &mut dyn Cipher,
145        attachments: &mut Vec<crate::db::Attachment>,
146    ) -> Result<Self, CryptographyError> {
147        let custom_icon_uuid = db.custom_icon.map(UUID);
148
149        let mut string_fields = Vec::with_capacity(db.fields.len());
150        for (k, v) in &db.fields {
151            let value = if v.is_protected() {
152                let encrypted = inner_encryptor.encrypt(v.get().as_bytes())?;
153                let encoded = base64_engine::STANDARD.encode(&encrypted);
154
155                StringValueXml {
156                    protected: true,
157                    value: Some(encoded),
158                }
159            } else {
160                StringValueXml {
161                    protected: false,
162                    value: Some(v.as_str().to_string()),
163                }
164            };
165
166            string_fields.push(StringFieldXml { key: k.clone(), value });
167        }
168
169        let mut binary_fields = Vec::with_capacity(db.attachments.len());
170        for (key, attachment) in &db.attachments {
171            binary_fields.push(BinaryFieldXml {
172                key: key.clone(),
173                value: BinaryValueXml {
174                    value_ref: attachments.len(),
175                },
176            });
177            attachments.push(attachment.clone());
178        }
179
180        let history = if let Some(h) = db.history.as_ref() {
181            Some(HistoryXml {
182                entries: h
183                    .entries
184                    .iter()
185                    .map(|e| EntryXml::db_to_xml(e, inner_encryptor, attachments))
186                    .collect::<Result<_, CryptographyError>>()?,
187            })
188        } else {
189            None
190        };
191
192        let custom_data: Option<CustomDataXml> = if db.custom_data.is_empty() {
193            None
194        } else {
195            Some(db.custom_data.clone().into())
196        };
197
198        Ok(EntryXml {
199            uuid: UUID(db.uuid),
200            icon_id: db.icon_id.map(|id| id.into()),
201            custom_icon_uuid,
202            foreground_color: db.foreground_color,
203            background_color: db.background_color,
204            override_url: db.override_url.clone(),
205            tags: join_tags(&db.tags),
206            quality_check: db.quality_check,
207            previous_parent_group: db.previous_parent_group.map(UUID),
208            times: Some(db.times.clone().into()),
209            string_fields,
210            binary_fields,
211            auto_type: db.autotype.as_ref().map(|at| at.clone().into()),
212            history,
213            custom_data,
214        })
215    }
216}
217
218#[derive(Debug, Serialize, Deserialize)]
219#[serde(rename_all = "PascalCase")]
220pub(crate) struct StringFieldXml {
221    pub key: String,
222    pub value: StringValueXml,
223}
224
225#[derive(Debug, Deserialize)]
226pub(crate) struct StringValueXml {
227    #[serde(default, rename = "@Protected", with = "cs_bool")]
228    protected: bool,
229
230    #[serde(default, rename = "$value", with = "cs_opt_string", skip_serializing_if = "Option::is_none")]
231    value: Option<String>,
232}
233
234impl Serialize for StringValueXml {
235    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
236    where
237        S: serde::Serializer,
238    {
239        use serde::ser::SerializeStruct;
240
241        if self.protected {
242            let mut state = serializer.serialize_struct("StringValue", 2)?;
243            state.serialize_field("@Protected", if self.protected { "True" } else { "False" })?;
244
245            if let Some(ref val) = self.value {
246                state.serialize_field("$value", val)?;
247            } else {
248                state.serialize_field("$value", "")?;
249            }
250            state.end()
251        } else {
252            let mut state = serializer.serialize_struct("StringValue", 1)?;
253
254            if let Some(ref val) = self.value {
255                state.serialize_field("$value", val)?;
256            } else {
257                state.serialize_field("$value", "")?;
258            }
259            state.end()
260        }
261    }
262}
263
264#[derive(Debug, Serialize, Deserialize)]
265#[serde(rename_all = "PascalCase")]
266pub(crate) struct BinaryFieldXml {
267    pub key: String,
268    pub value: BinaryValueXml,
269}
270
271#[derive(Debug, Serialize, Deserialize)]
272pub(crate) struct BinaryValueXml {
273    #[serde(rename = "@Ref")]
274    pub value_ref: usize,
275}
276
277#[derive(Debug, Serialize, Deserialize)]
278#[serde(rename_all = "PascalCase")]
279pub(crate) struct AutoTypeXml {
280    #[serde(default, with = "cs_bool")]
281    pub enabled: bool,
282
283    #[serde(default, with = "cs_opt_fromstr", skip_serializing_if = "Option::is_none")]
284    pub data_transfer_obfuscation: Option<usize>,
285
286    #[serde(default, with = "cs_opt_string", skip_serializing_if = "Option::is_none")]
287    pub default_sequence: Option<String>,
288
289    #[serde(rename = "Association", default)]
290    pub associations: Vec<AutoTypeAssociationXml>,
291}
292
293impl From<AutoTypeXml> for crate::db::AutoType {
294    fn from(value: AutoTypeXml) -> Self {
295        crate::db::AutoType {
296            enabled: value.enabled,
297            default_sequence: value.default_sequence,
298            data_transfer_obfuscation: value.data_transfer_obfuscation.map(|d| d.into()).unwrap_or_default(),
299            associations: value.associations.into_iter().map(|a| a.into()).collect(),
300        }
301    }
302}
303
304impl From<crate::db::AutoType> for AutoTypeXml {
305    fn from(value: crate::db::AutoType) -> Self {
306        Self {
307            enabled: value.enabled,
308            data_transfer_obfuscation: Some(value.data_transfer_obfuscation.into()),
309            default_sequence: value.default_sequence,
310            associations: value.associations.into_iter().map(|a| a.into()).collect(),
311        }
312    }
313}
314
315impl From<usize> for crate::db::DataTransferObfuscation {
316    fn from(value: usize) -> Self {
317        match value {
318            0 => Self::None,
319            1 => Self::UseClipboard,
320            _ => Self::None, // default to None for unknown values
321        }
322    }
323}
324
325impl From<crate::db::DataTransferObfuscation> for usize {
326    fn from(value: crate::db::DataTransferObfuscation) -> Self {
327        match value {
328            crate::db::DataTransferObfuscation::None => 0,
329            crate::db::DataTransferObfuscation::UseClipboard => 1,
330        }
331    }
332}
333
334#[derive(Debug, Serialize, Deserialize)]
335#[serde(rename_all = "PascalCase")]
336pub(crate) struct AutoTypeAssociationXml {
337    #[serde(default, with = "cs_opt_string")]
338    pub window: Option<String>,
339
340    #[serde(default, with = "cs_opt_string")]
341    pub keystroke_sequence: Option<String>,
342}
343
344impl From<AutoTypeAssociationXml> for crate::db::AutoTypeAssociation {
345    fn from(val: AutoTypeAssociationXml) -> Self {
346        crate::db::AutoTypeAssociation {
347            window: val.window,
348            sequence: val.keystroke_sequence,
349        }
350    }
351}
352
353impl From<crate::db::AutoTypeAssociation> for AutoTypeAssociationXml {
354    fn from(source: crate::db::AutoTypeAssociation) -> Self {
355        Self {
356            window: source.window,
357            keystroke_sequence: source.sequence,
358        }
359    }
360}
361
362#[derive(Debug, Serialize, Deserialize)]
363#[serde(rename_all = "PascalCase")]
364pub(crate) struct HistoryXml {
365    #[serde(default, rename = "Entry")]
366    pub entries: Vec<EntryXml>,
367}
368
369#[cfg(test)]
370mod tests {
371
372    use super::*;
373
374    #[derive(Debug, Serialize, Deserialize)]
375    struct Test<T>(T);
376
377    #[test]
378    fn test_deserialize_string_field() {
379        let xml = r#"<String>
380            <Key>Title</Key>
381            <Value>Example Title</Value>
382        </String>"#;
383
384        let deserialized: Test<StringFieldXml> = quick_xml::de::from_str(xml).unwrap();
385        assert_eq!(deserialized.0.key, "Title");
386        assert_eq!(deserialized.0.value.value.unwrap(), "Example Title");
387        assert!(!deserialized.0.value.protected);
388
389        let xml_protected = r#"<String>
390            <Key>Password</Key>
391            <Value Protected="True">cGFzc3dvcmQ=</Value>
392        </String>"#;
393
394        let deserialized_protected: Test<StringFieldXml> = quick_xml::de::from_str(xml_protected).unwrap();
395        assert_eq!(deserialized_protected.0.key, "Password");
396        assert_eq!(deserialized_protected.0.value.value.unwrap(), "cGFzc3dvcmQ=");
397        assert!(deserialized_protected.0.value.protected);
398    }
399
400    #[test]
401    fn test_serialize_string_field() {
402        let string_field = StringFieldXml {
403            key: "Username".to_string(),
404            value: StringValueXml {
405                protected: false,
406                value: Some("user123".to_string()),
407            },
408        };
409
410        let serialized = quick_xml::se::to_string(&Test(string_field)).unwrap();
411        assert_eq!(serialized, r#"<Test><Key>Username</Key><Value>user123</Value></Test>"#);
412
413        let string_field_protected = StringFieldXml {
414            key: "Password".to_string(),
415            value: StringValueXml {
416                protected: true,
417                value: Some("cGFzc3dvcmQ=".to_string()),
418            },
419        };
420
421        let serialized_protected = quick_xml::se::to_string(&Test(string_field_protected)).unwrap();
422        assert_eq!(
423            serialized_protected,
424            r#"<Test><Key>Password</Key><Value Protected="True">cGFzc3dvcmQ=</Value></Test>"#
425        );
426    }
427
428    #[test]
429    fn test_deserialize_binary_field() {
430        let xml = r#"<Binary>
431            <Key>Attachment</Key>
432            <Value Ref="1"/>
433        </Binary>"#;
434
435        let deserialized: Test<BinaryFieldXml> = quick_xml::de::from_str(xml).unwrap();
436        assert_eq!(deserialized.0.key, "Attachment");
437        assert_eq!(deserialized.0.value.value_ref, 1);
438    }
439
440    #[test]
441    fn test_serialize_binary_field() {
442        let binary_field = BinaryFieldXml {
443            key: "Attachment".to_string(),
444            value: BinaryValueXml { value_ref: 1 },
445        };
446        let serialized = quick_xml::se::to_string(&Test(binary_field)).unwrap();
447        assert_eq!(serialized, r#"<Test><Key>Attachment</Key><Value Ref="1"/></Test>"#);
448    }
449
450    #[test]
451    fn test_deserialize_autotype() {
452        let xml = r#"
453        <AutoType>
454            <Enabled>True</Enabled>
455            <DataTransferObfuscation>0</DataTransferObfuscation>
456            <DefaultSequence>{USERNAME}{TAB}{PASSWORD}{ENTER}</DefaultSequence>
457        </AutoType>"#;
458
459        let deserialized: Test<AutoTypeXml> = quick_xml::de::from_str(xml).unwrap();
460        assert!(deserialized.0.enabled);
461        assert_eq!(deserialized.0.data_transfer_obfuscation, Some(0));
462        assert_eq!(deserialized.0.default_sequence.unwrap(), "{USERNAME}{TAB}{PASSWORD}{ENTER}");
463    }
464
465    #[test]
466    fn test_serialize_autotype() {
467        let autotype = AutoTypeXml {
468            enabled: true,
469            data_transfer_obfuscation: Some(0),
470            default_sequence: Some("{USERNAME}{TAB}{PASSWORD}{ENTER}".to_string()),
471            associations: vec![AutoTypeAssociationXml {
472                window: Some("Example Window".to_string()),
473                keystroke_sequence: Some("{USERNAME}{TAB}{PASSWORD}{ENTER}".to_string()),
474            }],
475        };
476
477        let serialized = quick_xml::se::to_string(&Test(autotype)).unwrap();
478        assert_eq!(
479            serialized,
480            r#"<Test><Enabled>True</Enabled><DataTransferObfuscation>0</DataTransferObfuscation><DefaultSequence>{USERNAME}{TAB}{PASSWORD}{ENTER}</DefaultSequence><Association><Window>Example Window</Window><KeystrokeSequence>{USERNAME}{TAB}{PASSWORD}{ENTER}</KeystrokeSequence></Association></Test>"#
481        );
482    }
483
484    #[test]
485    fn test_deserialize_entry() {
486        let xml = r#"
487        <Entry>
488            <UUID>AAECAwQFBgcICQoLDA0ODw==</UUID>
489            <IconID>1</IconID>
490            <ForegroundColor>#FF0000</ForegroundColor>
491            <BackgroundColor>#00FF00</BackgroundColor>
492            <OverrideURL>https://example.com</OverrideURL>
493            <Tags>tag1;tag2</Tags>
494            <Times>
495                <CreationTime>2023-10-05T12:34:56Z</CreationTime>
496                <LastModificationTime>2023-10-06T12:34:56Z</LastModificationTime>
497                <LastAccessTime>2023-10-07T12:34:56Z</LastAccessTime>
498                <ExpiryTime>2024-10-05T12:34:56Z</ExpiryTime>
499                <Expires>True</Expires>
500                <UsageCount>5</UsageCount>
501                <LocationChanged>2023-10-08T12:34:56Z</LocationChanged>
502            </Times>
503            <String>
504                <Key>Title</Key>
505                <Value>Example Title</Value>
506            </String>
507            <Binary>
508                <Key>Attachment</Key>
509                <Value Ref="1"/>
510            </Binary>
511            <AutoType>
512                <Enabled>True</Enabled>
513                <DataTransferObfuscation>0</DataTransferObfuscation>
514                <DefaultSequence>{USERNAME}{TAB}{PASSWORD}{ENTER}</DefaultSequence>
515            </AutoType>
516        </Entry>"#;
517
518        let deserialized: Test<EntryXml> = quick_xml::de::from_str(xml).unwrap();
519        assert_eq!(
520            deserialized.0.uuid.0.as_bytes(),
521            &[
522                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
523            ]
524        );
525        assert_eq!(deserialized.0.icon_id.unwrap(), 1);
526        assert_eq!(deserialized.0.foreground_color.unwrap().to_string(), "#FF0000");
527        assert_eq!(deserialized.0.background_color.unwrap().to_string(), "#00FF00");
528        assert_eq!(deserialized.0.override_url.unwrap(), "https://example.com");
529        assert_eq!(deserialized.0.tags.unwrap(), "tag1;tag2");
530        assert_eq!(deserialized.0.string_fields.len(), 1);
531        assert_eq!(deserialized.0.string_fields[0].key, "Title");
532        assert_eq!(deserialized.0.string_fields[0].value.value.as_ref().unwrap(), "Example Title");
533        assert_eq!(deserialized.0.binary_fields.len(), 1);
534        assert_eq!(deserialized.0.binary_fields[0].key, "Attachment");
535        assert_eq!(deserialized.0.binary_fields[0].value.value_ref, 1);
536        assert!(deserialized.0.auto_type.is_some());
537        let autotype = deserialized.0.auto_type.unwrap();
538        assert!(autotype.enabled);
539        assert_eq!(autotype.data_transfer_obfuscation, Some(0));
540        assert_eq!(autotype.default_sequence.unwrap(), "{USERNAME}{TAB}{PASSWORD}{ENTER}");
541
542        assert!(deserialized.0.history.is_none());
543    }
544
545    #[test]
546    fn test_deserialize_entry_minimal() {
547        let xml = r#"<Entry>
548            <UUID>AAECAwQFBgcICQoLDA0ODw==</UUID>
549            <IconID/>
550            <ForegroundColor/>
551            <BackgroundColor/>
552            <OverrideURL/>
553            <Tags/>
554            <Times/>
555            <AutoType/>
556        </Entry>"#;
557
558        let deserialized: Test<EntryXml> = quick_xml::de::from_str(xml).unwrap();
559
560        println!("{:#?}", deserialized);
561
562        assert!(deserialized.0.icon_id.is_none());
563        assert!(deserialized.0.foreground_color.is_none());
564        assert!(deserialized.0.background_color.is_none());
565        assert!(deserialized.0.override_url.is_none());
566        assert!(deserialized.0.tags.is_none());
567        assert!(deserialized.0.string_fields.is_empty());
568        assert!(deserialized.0.binary_fields.is_empty());
569    }
570}