bitwarden_exporters/
encrypted_json.rs

1use base64::{engine::general_purpose::STANDARD, Engine};
2use bitwarden_crypto::{generate_random_bytes, Kdf, KeyEncryptable, PinKey};
3use serde::Serialize;
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::{
8    json::{self, export_json},
9    Cipher, Folder,
10};
11
12#[derive(Error, Debug)]
13pub enum EncryptedJsonError {
14    #[error(transparent)]
15    JsonExport(#[from] json::JsonError),
16
17    #[error("JSON error: {0}")]
18    Serde(#[from] serde_json::Error),
19
20    #[error("Cryptography error, {0}")]
21    Crypto(#[from] bitwarden_crypto::CryptoError),
22}
23
24pub(crate) fn export_encrypted_json(
25    folders: Vec<Folder>,
26    ciphers: Vec<Cipher>,
27    password: String,
28    kdf: Kdf,
29) -> Result<String, EncryptedJsonError> {
30    let decrypted_export = export_json(folders, ciphers)?;
31
32    let (kdf_type, kdf_iterations, kdf_memory, kdf_parallelism) = match kdf {
33        Kdf::PBKDF2 { iterations } => (0, iterations.get(), None, None),
34        Kdf::Argon2id {
35            iterations,
36            memory,
37            parallelism,
38        } => (
39            1,
40            iterations.get(),
41            Some(memory.get()),
42            Some(parallelism.get()),
43        ),
44    };
45
46    let salt = generate_random_bytes::<[u8; 16]>();
47    let salt = STANDARD.encode(salt);
48    let key = PinKey::derive(password.as_bytes(), salt.as_bytes(), &kdf)?;
49
50    let enc_key_validation = Uuid::new_v4().to_string();
51
52    let encrypted_export = EncryptedJsonExport {
53        encrypted: true,
54        password_protected: true,
55        salt,
56        kdf_type,
57        kdf_iterations,
58        kdf_memory,
59        kdf_parallelism,
60        enc_key_validation: enc_key_validation.encrypt_with_key(&key)?.to_string(),
61        data: decrypted_export.encrypt_with_key(&key)?.to_string(),
62    };
63
64    Ok(serde_json::to_string_pretty(&encrypted_export)?)
65}
66
67#[derive(Serialize)]
68#[serde(rename_all = "camelCase")]
69pub(crate) struct EncryptedJsonExport {
70    encrypted: bool,
71    password_protected: bool,
72    salt: String,
73    kdf_type: u32,
74    kdf_iterations: u32,
75    kdf_memory: Option<u32>,
76    kdf_parallelism: Option<u32>,
77    #[serde(rename = "encKeyValidation_DO_NOT_EDIT")]
78    enc_key_validation: String,
79    data: String,
80}
81
82#[cfg(test)]
83mod tests {
84    use std::num::NonZeroU32;
85
86    use super::*;
87    use crate::{
88        Card, Cipher, CipherType, Field, Identity, Login, LoginUri, SecureNote, SecureNoteType,
89    };
90
91    #[test]
92    pub fn test_export() {
93        let _export = export_encrypted_json(
94            vec![Folder {
95                id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(),
96                name: "Important".to_string(),
97            }],
98            vec![
99                Cipher {
100                    id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
101                    folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
102
103                    name: "Bitwarden".to_string(),
104                    notes: Some("My note".to_string()),
105
106                    r#type: CipherType::Login(Box::new(Login {
107                        username: Some("[email protected]".to_string()),
108                        password: Some("asdfasdfasdf".to_string()),
109                        login_uris: vec![LoginUri {
110                            uri: Some("https://vault.bitwarden.com".to_string()),
111                            r#match: None,
112                        }],
113                        totp: Some("ABC".to_string()),
114                        fido2_credentials: None,
115                    })),
116
117                    favorite: true,
118                    reprompt: 0,
119
120                    fields: vec![
121                        Field {
122                            name: Some("Text".to_string()),
123                            value: Some("A".to_string()),
124                            r#type: 0,
125                            linked_id: None,
126                        },
127                        Field {
128                            name: Some("Hidden".to_string()),
129                            value: Some("B".to_string()),
130                            r#type: 1,
131                            linked_id: None,
132                        },
133                        Field {
134                            name: Some("Boolean (true)".to_string()),
135                            value: Some("true".to_string()),
136                            r#type: 2,
137                            linked_id: None,
138                        },
139                        Field {
140                            name: Some("Boolean (false)".to_string()),
141                            value: Some("false".to_string()),
142                            r#type: 2,
143                            linked_id: None,
144                        },
145                        Field {
146                            name: Some("Linked".to_string()),
147                            value: None,
148                            r#type: 3,
149                            linked_id: Some(101),
150                        },
151                    ],
152
153                    revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
154                    creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
155                    deleted_date: None,
156                },
157                Cipher {
158                    id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
159                    folder_id: None,
160
161                    name: "My secure note".to_string(),
162                    notes: Some("Very secure!".to_string()),
163
164                    r#type: CipherType::SecureNote(Box::new(SecureNote {
165                        r#type: SecureNoteType::Generic,
166                    })),
167
168                    favorite: false,
169                    reprompt: 0,
170
171                    fields: vec![],
172
173                    revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
174                    creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
175                    deleted_date: None,
176                },
177                Cipher {
178                    id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
179                    folder_id: None,
180
181                    name: "My card".to_string(),
182                    notes: None,
183
184                    r#type: CipherType::Card(Box::new(Card {
185                        cardholder_name: Some("John Doe".to_string()),
186                        exp_month: Some("1".to_string()),
187                        exp_year: Some("2032".to_string()),
188                        code: Some("123".to_string()),
189                        brand: Some("Visa".to_string()),
190                        number: Some("4111111111111111".to_string()),
191                    })),
192
193                    favorite: false,
194                    reprompt: 0,
195
196                    fields: vec![],
197
198                    revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
199                    creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
200                    deleted_date: None,
201                },
202                Cipher {
203                    id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
204                    folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
205
206                    name: "My identity".to_string(),
207                    notes: None,
208
209                    r#type: CipherType::Identity(Box::new(Identity {
210                        title: Some("Mr".to_string()),
211                        first_name: Some("John".to_string()),
212                        middle_name: None,
213                        last_name: Some("Doe".to_string()),
214                        address1: None,
215                        address2: None,
216                        address3: None,
217                        city: None,
218                        state: None,
219                        postal_code: None,
220                        country: None,
221                        company: Some("Bitwarden".to_string()),
222                        email: None,
223                        phone: None,
224                        ssn: None,
225                        username: Some("JDoe".to_string()),
226                        passport_number: None,
227                        license_number: None,
228                    })),
229
230                    favorite: false,
231                    reprompt: 0,
232
233                    fields: vec![],
234
235                    revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
236                    creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
237                    deleted_date: None,
238                },
239            ],
240            "password".to_string(),
241            Kdf::PBKDF2 {
242                iterations: NonZeroU32::new(600_000).unwrap(),
243            },
244        )
245        .unwrap();
246    }
247}