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; 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, }
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}