1use chrono::{DateTime, Utc};
2use thiserror::Error;
3use uuid::Uuid;
4
5use crate::{
6 Card, Cipher, CipherType, Field, Folder, Identity, Login, LoginUri, SecureNote, SshKey,
7};
8
9#[derive(Error, Debug)]
10pub enum JsonError {
11 #[error("JSON error: {0}")]
12 Serde(#[from] serde_json::Error),
13}
14
15pub(crate) fn export_json(folders: Vec<Folder>, ciphers: Vec<Cipher>) -> Result<String, JsonError> {
16 let export = JsonExport {
17 encrypted: false,
18 folders: folders.into_iter().map(|f| f.into()).collect(),
19 items: ciphers
20 .into_iter()
21 .filter(|c| !matches!(c.r#type, CipherType::BankAccount))
22 .map(|c| c.into())
23 .collect(),
24 };
25
26 Ok(serde_json::to_string_pretty(&export)?)
27}
28
29#[derive(serde::Serialize)]
34#[serde(rename_all = "camelCase")]
35struct JsonExport {
36 encrypted: bool,
37 folders: Vec<JsonFolder>,
38 items: Vec<JsonCipher>,
39}
40
41#[derive(serde::Serialize)]
42#[serde(rename_all = "camelCase")]
43struct JsonFolder {
44 id: Uuid,
45 name: String,
46}
47
48impl From<Folder> for JsonFolder {
49 fn from(folder: Folder) -> Self {
50 JsonFolder {
51 id: folder.id,
52 name: folder.name,
53 }
54 }
55}
56
57#[derive(serde::Serialize)]
58#[serde(rename_all = "camelCase")]
59struct JsonCipher {
60 id: Uuid,
61 folder_id: Option<Uuid>,
62 organization_id: Option<Uuid>,
64 collection_ids: Option<Vec<Uuid>>,
65
66 name: String,
67 notes: Option<String>,
68
69 r#type: u8,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 login: Option<JsonLogin>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 identity: Option<JsonIdentity>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 card: Option<JsonCard>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 secure_note: Option<JsonSecureNote>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 ssh_key: Option<JsonSshKey>,
80
81 favorite: bool,
82 reprompt: u8,
83
84 #[serde(skip_serializing_if = "Vec::is_empty")]
85 fields: Vec<JsonField>,
86 password_history: Option<Vec<String>>,
87
88 revision_date: DateTime<Utc>,
89 creation_date: DateTime<Utc>,
90 deleted_date: Option<DateTime<Utc>>,
91}
92
93#[derive(serde::Serialize)]
94#[serde(rename_all = "camelCase")]
95struct JsonLogin {
96 username: Option<String>,
97 password: Option<String>,
98 uris: Vec<JsonLoginUri>,
99 totp: Option<String>,
100 fido2_credentials: Vec<String>,
101}
102
103impl From<Login> for JsonLogin {
104 fn from(login: Login) -> Self {
105 JsonLogin {
106 username: login.username,
107 password: login.password,
108 uris: login.login_uris.into_iter().map(|u| u.into()).collect(),
109 totp: login.totp,
110 fido2_credentials: vec![],
111 }
112 }
113}
114
115#[derive(serde::Serialize)]
116#[serde(rename_all = "camelCase")]
117struct JsonLoginUri {
118 uri: Option<String>,
119 r#match: Option<u8>,
120}
121
122impl From<LoginUri> for JsonLoginUri {
123 fn from(login_uri: LoginUri) -> Self {
124 JsonLoginUri {
125 uri: login_uri.uri,
126 r#match: login_uri.r#match,
127 }
128 }
129}
130
131#[derive(serde::Serialize)]
132#[serde(rename_all = "camelCase")]
133struct JsonSecureNote {
134 r#type: u8,
135}
136
137impl From<SecureNote> for JsonSecureNote {
138 fn from(note: SecureNote) -> Self {
139 JsonSecureNote {
140 r#type: note.r#type as u8,
141 }
142 }
143}
144
145#[derive(serde::Serialize)]
146#[serde(rename_all = "camelCase")]
147struct JsonCard {
148 cardholder_name: Option<String>,
149 exp_month: Option<String>,
150 exp_year: Option<String>,
151 code: Option<String>,
152 brand: Option<String>,
153 number: Option<String>,
154}
155
156impl From<Card> for JsonCard {
157 fn from(card: Card) -> Self {
158 JsonCard {
159 cardholder_name: card.cardholder_name,
160 exp_month: card.exp_month,
161 exp_year: card.exp_year,
162 code: card.code,
163 brand: card.brand,
164 number: card.number,
165 }
166 }
167}
168
169#[derive(serde::Serialize)]
170#[serde(rename_all = "camelCase")]
171struct JsonIdentity {
172 title: Option<String>,
173 first_name: Option<String>,
174 middle_name: Option<String>,
175 last_name: Option<String>,
176 address1: Option<String>,
177 address2: Option<String>,
178 address3: Option<String>,
179 city: Option<String>,
180 state: Option<String>,
181 postal_code: Option<String>,
182 country: Option<String>,
183 company: Option<String>,
184 email: Option<String>,
185 phone: Option<String>,
186 ssn: Option<String>,
187 username: Option<String>,
188 passport_number: Option<String>,
189 license_number: Option<String>,
190}
191
192impl From<Identity> for JsonIdentity {
193 fn from(identity: Identity) -> Self {
194 JsonIdentity {
195 title: identity.title,
196 first_name: identity.first_name,
197 middle_name: identity.middle_name,
198 last_name: identity.last_name,
199 address1: identity.address1,
200 address2: identity.address2,
201 address3: identity.address3,
202 city: identity.city,
203 state: identity.state,
204 postal_code: identity.postal_code,
205 country: identity.country,
206 company: identity.company,
207 email: identity.email,
208 phone: identity.phone,
209 ssn: identity.ssn,
210 username: identity.username,
211 passport_number: identity.passport_number,
212 license_number: identity.license_number,
213 }
214 }
215}
216
217#[derive(serde::Serialize)]
218#[serde(rename_all = "camelCase")]
219struct JsonSshKey {
220 private_key: String,
221 public_key: String,
222 key_fingerprint: String,
223}
224
225impl From<SshKey> for JsonSshKey {
226 fn from(ssh_key: SshKey) -> Self {
227 JsonSshKey {
228 private_key: ssh_key.private_key,
229 public_key: ssh_key.public_key,
230 key_fingerprint: ssh_key.fingerprint,
231 }
232 }
233}
234
235#[derive(serde::Serialize)]
236#[serde(rename_all = "camelCase")]
237struct JsonField {
238 name: Option<String>,
239 value: Option<String>,
240 r#type: u8,
241 linked_id: Option<u32>,
242}
243
244impl From<Field> for JsonField {
245 fn from(field: Field) -> Self {
246 JsonField {
247 name: field.name,
248 value: field.value,
249 r#type: field.r#type,
250 linked_id: field.linked_id,
251 }
252 }
253}
254
255impl From<Cipher> for JsonCipher {
256 fn from(cipher: Cipher) -> Self {
257 let r#type = match cipher.r#type {
258 CipherType::Login(_) => 1,
259 CipherType::SecureNote(_) => 2,
260 CipherType::Card(_) => 3,
261 CipherType::Identity(_) => 4,
262 CipherType::SshKey(_) => 5,
263 CipherType::BankAccount => unreachable!(
265 "BankAccount ciphers are not supported for export and should be filtered out"
266 ),
267 };
268
269 let (login, secure_note, card, identity, ssh_key) = match cipher.r#type {
270 CipherType::Login(l) => (Some((*l).into()), None, None, None, None),
271 CipherType::SecureNote(s) => (None, Some((*s).into()), None, None, None),
272 CipherType::Card(c) => (None, None, Some((*c).into()), None, None),
273 CipherType::Identity(i) => (None, None, None, Some((*i).into()), None),
274 CipherType::SshKey(ssh) => (None, None, None, None, Some((*ssh).into())),
275 CipherType::BankAccount => unreachable!(
276 "BankAccount ciphers are not supported for export and should be filtered out"
277 ),
278 };
279
280 JsonCipher {
281 id: cipher.id,
282 folder_id: cipher.folder_id,
283 organization_id: None,
284 collection_ids: None,
285 name: cipher.name,
286 notes: cipher.notes,
287 r#type,
288 login,
289 identity,
290 card,
291 secure_note,
292 ssh_key,
293 favorite: cipher.favorite,
294 reprompt: cipher.reprompt,
295 fields: cipher.fields.into_iter().map(|f| f.into()).collect(),
296 password_history: None,
297 revision_date: cipher.revision_date,
298 creation_date: cipher.creation_date,
299 deleted_date: cipher.deleted_date,
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use std::{fs, io::Read, path::PathBuf};
307
308 use super::*;
309 use crate::{Cipher, Field, LoginUri, SecureNoteType};
310
311 #[test]
312 fn test_convert_login() {
313 let cipher = Cipher {
314 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
315 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
316
317 name: "Bitwarden".to_string(),
318 notes: Some("My note".to_string()),
319
320 r#type: CipherType::Login(Box::new(Login {
321 username: Some("[email protected]".to_string()),
322 password: Some("asdfasdfasdf".to_string()),
323 login_uris: vec![LoginUri {
324 uri: Some("https://vault.bitwarden.com".to_string()),
325 r#match: None,
326 }],
327 totp: Some("ABC".to_string()),
328 fido2_credentials: None,
329 })),
330
331 favorite: true,
332 reprompt: 0,
333
334 fields: vec![
335 Field {
336 name: Some("Text".to_string()),
337 value: Some("A".to_string()),
338 r#type: 0,
339 linked_id: None,
340 },
341 Field {
342 name: Some("Hidden".to_string()),
343 value: Some("B".to_string()),
344 r#type: 1,
345 linked_id: None,
346 },
347 Field {
348 name: Some("Boolean (true)".to_string()),
349 value: Some("true".to_string()),
350 r#type: 2,
351 linked_id: None,
352 },
353 Field {
354 name: Some("Boolean (false)".to_string()),
355 value: Some("false".to_string()),
356 r#type: 2,
357 linked_id: None,
358 },
359 Field {
360 name: Some("Linked".to_string()),
361 value: None,
362 r#type: 3,
363 linked_id: Some(101),
364 },
365 ],
366
367 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
368 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
369 deleted_date: None,
370 };
371
372 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
373
374 let expected = r#"{
375 "passwordHistory": null,
376 "revisionDate": "2024-01-30T14:09:33.753Z",
377 "creationDate": "2024-01-30T11:23:54.416Z",
378 "deletedDate": null,
379 "id": "25c8c414-b446-48e9-a1bd-b10700bbd740",
380 "organizationId": null,
381 "folderId": "942e2984-1b9a-453b-b039-b107012713b9",
382 "type": 1,
383 "reprompt": 0,
384 "name": "Bitwarden",
385 "notes": "My note",
386 "favorite": true,
387 "fields": [
388 {
389 "name": "Text",
390 "value": "A",
391 "type": 0,
392 "linkedId": null
393 },
394 {
395 "name": "Hidden",
396 "value": "B",
397 "type": 1,
398 "linkedId": null
399 },
400 {
401 "name": "Boolean (true)",
402 "value": "true",
403 "type": 2,
404 "linkedId": null
405 },
406 {
407 "name": "Boolean (false)",
408 "value": "false",
409 "type": 2,
410 "linkedId": null
411 },
412 {
413 "name": "Linked",
414 "value": null,
415 "type": 3,
416 "linkedId": 101
417 }
418 ],
419 "login": {
420 "fido2Credentials": [],
421 "uris": [
422 {
423 "match": null,
424 "uri": "https://vault.bitwarden.com"
425 }
426 ],
427 "username": "[email protected]",
428 "password": "asdfasdfasdf",
429 "totp": "ABC"
430 },
431 "collectionIds": null
432 }"#;
433
434 assert_eq!(
435 json.parse::<serde_json::Value>().unwrap(),
436 expected.parse::<serde_json::Value>().unwrap()
437 )
438 }
439
440 #[test]
441 fn test_convert_secure_note() {
442 let cipher = Cipher {
443 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
444 folder_id: None,
445
446 name: "My secure note".to_string(),
447 notes: Some("Very secure!".to_string()),
448
449 r#type: CipherType::SecureNote(Box::new(SecureNote {
450 r#type: SecureNoteType::Generic,
451 })),
452
453 favorite: false,
454 reprompt: 0,
455
456 fields: vec![],
457
458 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
459 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
460 deleted_date: None,
461 };
462
463 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
464
465 let expected = r#"{
466 "passwordHistory": null,
467 "revisionDate": "2024-01-30T11:25:25.466Z",
468 "creationDate": "2024-01-30T11:25:25.466Z",
469 "deletedDate": null,
470 "id": "23f0f877-42b1-4820-a850-b10700bc41eb",
471 "organizationId": null,
472 "folderId": null,
473 "type": 2,
474 "reprompt": 0,
475 "name": "My secure note",
476 "notes": "Very secure!",
477 "favorite": false,
478 "secureNote": {
479 "type": 0
480 },
481 "collectionIds": null
482 }"#;
483
484 assert_eq!(
485 json.parse::<serde_json::Value>().unwrap(),
486 expected.parse::<serde_json::Value>().unwrap()
487 )
488 }
489
490 #[test]
491 fn test_convert_card() {
492 let cipher = Cipher {
493 id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
494 folder_id: None,
495
496 name: "My card".to_string(),
497 notes: None,
498
499 r#type: CipherType::Card(Box::new(Card {
500 cardholder_name: Some("John Doe".to_string()),
501 exp_month: Some("1".to_string()),
502 exp_year: Some("2032".to_string()),
503 code: Some("123".to_string()),
504 brand: Some("Visa".to_string()),
505 number: Some("4111111111111111".to_string()),
506 })),
507
508 favorite: false,
509 reprompt: 0,
510
511 fields: vec![],
512
513 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
514 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
515 deleted_date: None,
516 };
517
518 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
519
520 let expected = r#"{
521 "passwordHistory": null,
522 "revisionDate": "2024-01-30T17:55:36.150Z",
523 "creationDate": "2024-01-30T17:55:36.150Z",
524 "deletedDate": null,
525 "id": "3ed8de45-48ee-4e26-a2dc-b10701276c53",
526 "organizationId": null,
527 "folderId": null,
528 "type": 3,
529 "reprompt": 0,
530 "name": "My card",
531 "notes": null,
532 "favorite": false,
533 "card": {
534 "cardholderName": "John Doe",
535 "brand": "Visa",
536 "number": "4111111111111111",
537 "expMonth": "1",
538 "expYear": "2032",
539 "code": "123"
540 },
541 "collectionIds": null
542 }"#;
543
544 assert_eq!(
545 json.parse::<serde_json::Value>().unwrap(),
546 expected.parse::<serde_json::Value>().unwrap()
547 )
548 }
549
550 #[test]
551 fn test_convert_identity() {
552 let cipher = Cipher {
553 id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
554 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
555
556 name: "My identity".to_string(),
557 notes: None,
558
559 r#type: CipherType::Identity(Box::new(Identity {
560 title: Some("Mr".to_string()),
561 first_name: Some("John".to_string()),
562 middle_name: None,
563 last_name: Some("Doe".to_string()),
564 address1: None,
565 address2: None,
566 address3: None,
567 city: None,
568 state: None,
569 postal_code: None,
570 country: None,
571 company: Some("Bitwarden".to_string()),
572 email: None,
573 phone: None,
574 ssn: None,
575 username: Some("JDoe".to_string()),
576 passport_number: None,
577 license_number: None,
578 })),
579
580 favorite: false,
581 reprompt: 0,
582
583 fields: vec![],
584
585 revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
586 creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
587 deleted_date: None,
588 };
589
590 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
591
592 let expected = r#"{
593 "passwordHistory": null,
594 "revisionDate": "2024-01-30T17:54:50.706Z",
595 "creationDate": "2024-01-30T17:54:50.706Z",
596 "deletedDate": null,
597 "id": "41cc3bc1-c3d9-4637-876c-b10701273712",
598 "organizationId": null,
599 "folderId": "942e2984-1b9a-453b-b039-b107012713b9",
600 "type": 4,
601 "reprompt": 0,
602 "name": "My identity",
603 "notes": null,
604 "favorite": false,
605 "identity": {
606 "title": "Mr",
607 "firstName": "John",
608 "middleName": null,
609 "lastName": "Doe",
610 "address1": null,
611 "address2": null,
612 "address3": null,
613 "city": null,
614 "state": null,
615 "postalCode": null,
616 "country": null,
617 "company": "Bitwarden",
618 "email": null,
619 "phone": null,
620 "ssn": null,
621 "username": "JDoe",
622 "passportNumber": null,
623 "licenseNumber": null
624 },
625 "collectionIds": null
626 }"#;
627
628 assert_eq!(
629 json.parse::<serde_json::Value>().unwrap(),
630 expected.parse::<serde_json::Value>().unwrap()
631 )
632 }
633
634 #[test]
635 fn test_convert_ssh_key() {
636 let cipher = Cipher {
637 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
638 folder_id: None,
639
640 name: "My ssh key".to_string(),
641 notes: None,
642
643 r#type: CipherType::SshKey(Box::new(SshKey {
644 private_key: "private".to_string(),
645 public_key: "public".to_string(),
646 fingerprint: "fingerprint".to_string(),
647 })),
648
649 favorite: false,
650 reprompt: 0,
651
652 fields: vec![],
653
654 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
655 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
656 deleted_date: None,
657 };
658
659 let json = serde_json::to_string(&JsonCipher::from(cipher)).unwrap();
660
661 let expected = r#"{
662 "passwordHistory": null,
663 "revisionDate": "2024-01-30T11:25:25.466Z",
664 "creationDate": "2024-01-30T11:25:25.466Z",
665 "deletedDate": null,
666 "id": "23f0f877-42b1-4820-a850-b10700bc41eb",
667 "organizationId": null,
668 "folderId": null,
669 "type": 5,
670 "reprompt": 0,
671 "name": "My ssh key",
672 "notes": null,
673 "sshKey": {
674 "privateKey": "private",
675 "publicKey": "public",
676 "keyFingerprint": "fingerprint"
677 },
678 "favorite": false,
679 "collectionIds": null
680 }"#;
681
682 assert_eq!(
683 json.parse::<serde_json::Value>().unwrap(),
684 expected.parse::<serde_json::Value>().unwrap()
685 )
686 }
687
688 #[test]
689 pub fn test_export() {
690 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
691 d.push("resources");
692 d.push("json_export.json");
693
694 let mut file = fs::File::open(d).unwrap();
695
696 let mut expected = String::new();
697 file.read_to_string(&mut expected).unwrap();
698
699 let export = export_json(
700 vec![Folder {
701 id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(),
702 name: "Important".to_string(),
703 }],
704 vec![
705 Cipher {
706 id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
707 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
708
709 name: "Bitwarden".to_string(),
710 notes: Some("My note".to_string()),
711
712 r#type: CipherType::Login(Box::new(Login {
713 username: Some("[email protected]".to_string()),
714 password: Some("asdfasdfasdf".to_string()),
715 login_uris: vec![LoginUri {
716 uri: Some("https://vault.bitwarden.com".to_string()),
717 r#match: None,
718 }],
719 totp: Some("ABC".to_string()),
720 fido2_credentials: None,
721 })),
722
723 favorite: true,
724 reprompt: 0,
725
726 fields: vec![
727 Field {
728 name: Some("Text".to_string()),
729 value: Some("A".to_string()),
730 r#type: 0,
731 linked_id: None,
732 },
733 Field {
734 name: Some("Hidden".to_string()),
735 value: Some("B".to_string()),
736 r#type: 1,
737 linked_id: None,
738 },
739 Field {
740 name: Some("Boolean (true)".to_string()),
741 value: Some("true".to_string()),
742 r#type: 2,
743 linked_id: None,
744 },
745 Field {
746 name: Some("Boolean (false)".to_string()),
747 value: Some("false".to_string()),
748 r#type: 2,
749 linked_id: None,
750 },
751 Field {
752 name: Some("Linked".to_string()),
753 value: None,
754 r#type: 3,
755 linked_id: Some(101),
756 },
757 ],
758
759 revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
760 creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
761 deleted_date: None,
762 },
763 Cipher {
764 id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
765 folder_id: None,
766
767 name: "My secure note".to_string(),
768 notes: Some("Very secure!".to_string()),
769
770 r#type: CipherType::SecureNote(Box::new(SecureNote {
771 r#type: SecureNoteType::Generic,
772 })),
773
774 favorite: false,
775 reprompt: 0,
776
777 fields: vec![],
778
779 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
780 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
781 deleted_date: None,
782 },
783 Cipher {
784 id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
785 folder_id: None,
786
787 name: "My card".to_string(),
788 notes: None,
789
790 r#type: CipherType::Card(Box::new(Card {
791 cardholder_name: Some("John Doe".to_string()),
792 exp_month: Some("1".to_string()),
793 exp_year: Some("2032".to_string()),
794 code: Some("123".to_string()),
795 brand: Some("Visa".to_string()),
796 number: Some("4111111111111111".to_string()),
797 })),
798
799 favorite: false,
800 reprompt: 0,
801
802 fields: vec![],
803
804 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
805 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
806 deleted_date: None,
807 },
808 Cipher {
809 id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
810 folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
811
812 name: "My identity".to_string(),
813 notes: None,
814
815 r#type: CipherType::Identity(Box::new(Identity {
816 title: Some("Mr".to_string()),
817 first_name: Some("John".to_string()),
818 middle_name: None,
819 last_name: Some("Doe".to_string()),
820 address1: None,
821 address2: None,
822 address3: None,
823 city: None,
824 state: None,
825 postal_code: None,
826 country: None,
827 company: Some("Bitwarden".to_string()),
828 email: None,
829 phone: None,
830 ssn: None,
831 username: Some("JDoe".to_string()),
832 passport_number: None,
833 license_number: None,
834 })),
835
836 favorite: false,
837 reprompt: 0,
838
839 fields: vec![],
840
841 revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
842 creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
843 deleted_date: None,
844 },
845 Cipher {
846 id: "646594a9-a9cb-4082-9d57-0024c3fbcaa9".parse().unwrap(),
847 folder_id: None,
848
849 name: "My ssh key".to_string(),
850 notes: None,
851
852 r#type: CipherType::SshKey(Box::new(SshKey {
853 private_key: "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiwAAAKAy48fwMuPH\n8AAAAAtzc2gtZWQyNTUxOQAAACBinNE5chMtCHh3BV0H1+CpPlEQBwR5cD+Xb9i8MaHGiw\nAAAEAYUCIdfLI14K3XIy9V0FDZLQoZ9gcjOnvFjb4uA335HmKc0TlyEy0IeHcFXQfX4Kk+\nURAHBHlwP5dv2LwxocaLAAAAHHF1ZXh0ZW5ATWFjQm9vay1Qcm8tMTYubG9jYWwB\n-----END OPENSSH PRIVATE KEY-----".to_string(),
854 public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGKc0TlyEy0IeHcFXQfX4Kk+URAHBHlwP5dv2LwxocaL".to_string(),
855 fingerprint: "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string(),
856 })),
857
858 favorite: false,
859 reprompt: 0,
860
861 fields: vec![],
862
863 revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
864 creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
865 deleted_date: None,
866 }
867 ],
868 )
869 .unwrap();
870
871 assert_eq!(
872 export.parse::<serde_json::Value>().unwrap(),
873 expected.parse::<serde_json::Value>().unwrap()
874 )
875 }
876}