Skip to main content

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
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/// JSON export format. These are intentionally decoupled from the internal data structures to
30/// ensure internal changes are not reflected in the public exports.
31///
32/// Be careful about changing these structs to maintain compatibility with old exporters/importers.
33#[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    // Organizational IDs which are always empty in personal exports
63    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            // BankAccount ciphers should be filtered out before reaching this point
264            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}