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| {
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/// JSON export format. These are intentionally decoupled from the internal data structures to
35/// ensure internal changes are not reflected in the public exports.
36///
37/// Be careful about changing these structs to maintain compatibility with old exporters/importers.
38#[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    // Organizational IDs which are always empty in personal exports
68    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            // BankAccount/Passport/DriversLicense ciphers should be filtered out before reaching
269            // this point
270            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}