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