bitwarden_exporters/
encrypted_json.rs

1use bitwarden_crypto::{Kdf, KeyEncryptable, PinKey, generate_random_bytes};
2use bitwarden_encoding::B64;
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::{
8    Cipher, Folder,
9    json::{self, export_json},
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 = B64::from(salt.as_slice());
48    // The export key uses the undecoded base64 string as a salt. Do not pass the raw bytes here
49    let key = PinKey::derive(password.as_bytes(), salt.to_string().as_bytes(), &kdf)?;
50
51    let enc_key_validation = Uuid::new_v4().to_string();
52
53    let encrypted_export = EncryptedJsonExport {
54        encrypted: true,
55        password_protected: true,
56        salt: salt.to_string(),
57        kdf_type,
58        kdf_iterations,
59        kdf_memory,
60        kdf_parallelism,
61        enc_key_validation: enc_key_validation.encrypt_with_key(&key)?.to_string(),
62        data: decrypted_export.encrypt_with_key(&key)?.to_string(),
63    };
64
65    Ok(serde_json::to_string_pretty(&encrypted_export)?)
66}
67
68#[derive(Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub(crate) struct EncryptedJsonExport {
71    encrypted: bool,
72    password_protected: bool,
73    salt: String,
74    kdf_type: u32,
75    kdf_iterations: u32,
76    kdf_memory: Option<u32>,
77    kdf_parallelism: Option<u32>,
78    #[serde(rename = "encKeyValidation_DO_NOT_EDIT")]
79    enc_key_validation: String,
80    data: String,
81}
82
83#[cfg(test)]
84mod tests {
85    use std::num::NonZeroU32;
86
87    use bitwarden_crypto::KeyDecryptable;
88
89    use super::*;
90    use crate::{
91        Card, Cipher, CipherType, Field, Identity, Login, LoginUri, SecureNote, SecureNoteType,
92    };
93
94    #[test]
95    pub fn test_export() {
96        let export = export_encrypted_json(
97            vec![Folder {
98                id: "942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap(),
99                name: "Important".to_string(),
100            }],
101            vec![
102                Cipher {
103                    id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
104                    folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
105
106                    name: "Bitwarden".to_string(),
107                    notes: Some("My note".to_string()),
108
109                    r#type: CipherType::Login(Box::new(Login {
110                        username: Some("[email protected]".to_string()),
111                        password: Some("asdfasdfasdf".to_string()),
112                        login_uris: vec![LoginUri {
113                            uri: Some("https://vault.bitwarden.com".to_string()),
114                            r#match: None,
115                        }],
116                        totp: Some("ABC".to_string()),
117                        fido2_credentials: None,
118                    })),
119
120                    favorite: true,
121                    reprompt: 0,
122
123                    fields: vec![
124                        Field {
125                            name: Some("Text".to_string()),
126                            value: Some("A".to_string()),
127                            r#type: 0,
128                            linked_id: None,
129                        },
130                        Field {
131                            name: Some("Hidden".to_string()),
132                            value: Some("B".to_string()),
133                            r#type: 1,
134                            linked_id: None,
135                        },
136                        Field {
137                            name: Some("Boolean (true)".to_string()),
138                            value: Some("true".to_string()),
139                            r#type: 2,
140                            linked_id: None,
141                        },
142                        Field {
143                            name: Some("Boolean (false)".to_string()),
144                            value: Some("false".to_string()),
145                            r#type: 2,
146                            linked_id: None,
147                        },
148                        Field {
149                            name: Some("Linked".to_string()),
150                            value: None,
151                            r#type: 3,
152                            linked_id: Some(101),
153                        },
154                    ],
155
156                    revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
157                    creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
158                    deleted_date: None,
159                },
160                Cipher {
161                    id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
162                    folder_id: None,
163
164                    name: "My secure note".to_string(),
165                    notes: Some("Very secure!".to_string()),
166
167                    r#type: CipherType::SecureNote(Box::new(SecureNote {
168                        r#type: SecureNoteType::Generic,
169                    })),
170
171                    favorite: false,
172                    reprompt: 0,
173
174                    fields: vec![],
175
176                    revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
177                    creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
178                    deleted_date: None,
179                },
180                Cipher {
181                    id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
182                    folder_id: None,
183
184                    name: "My card".to_string(),
185                    notes: None,
186
187                    r#type: CipherType::Card(Box::new(Card {
188                        cardholder_name: Some("John Doe".to_string()),
189                        exp_month: Some("1".to_string()),
190                        exp_year: Some("2032".to_string()),
191                        code: Some("123".to_string()),
192                        brand: Some("Visa".to_string()),
193                        number: Some("4111111111111111".to_string()),
194                    })),
195
196                    favorite: false,
197                    reprompt: 0,
198
199                    fields: vec![],
200
201                    revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
202                    creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
203                    deleted_date: None,
204                },
205                Cipher {
206                    id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
207                    folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
208
209                    name: "My identity".to_string(),
210                    notes: None,
211
212                    r#type: CipherType::Identity(Box::new(Identity {
213                        title: Some("Mr".to_string()),
214                        first_name: Some("John".to_string()),
215                        middle_name: None,
216                        last_name: Some("Doe".to_string()),
217                        address1: None,
218                        address2: None,
219                        address3: None,
220                        city: None,
221                        state: None,
222                        postal_code: None,
223                        country: None,
224                        company: Some("Bitwarden".to_string()),
225                        email: None,
226                        phone: None,
227                        ssn: None,
228                        username: Some("JDoe".to_string()),
229                        passport_number: None,
230                        license_number: None,
231                    })),
232
233                    favorite: false,
234                    reprompt: 0,
235
236                    fields: vec![],
237
238                    revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
239                    creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
240                    deleted_date: None,
241                },
242            ],
243            "password".to_string(),
244            Kdf::PBKDF2 {
245                iterations: NonZeroU32::new(600_000).unwrap(),
246            },
247        )
248        .unwrap();
249
250        // Validate that the export is valid JSON
251        let parsed_export = serde_json::from_str::<EncryptedJsonExport>(&export).unwrap();
252
253        // First, derive a PIN key from the password and salt
254        // Next, validate the decrypted validation parses, and is a valid uuidv4. This indicates
255        // that at least the pin key is correct
256        let key = PinKey::derive(
257            "password".as_bytes(),
258            parsed_export.salt.as_bytes(),
259            &Kdf::PBKDF2 {
260                iterations: NonZeroU32::new(600_000).unwrap(),
261            },
262        )
263        .unwrap();
264        let decrypted_validation = parsed_export
265            .enc_key_validation
266            .parse::<bitwarden_crypto::EncString>()
267            .unwrap()
268            .decrypt_with_key(&key)
269            .unwrap();
270        let uuid = Uuid::parse_str(&decrypted_validation).unwrap();
271        assert_eq!(uuid.get_version_num(), 4);
272    }
273}