bitwarden_exporters/
json.rs

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