Skip to main content

keepass/format/xml_db/
entry.rs

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