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