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