Skip to main content

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                    password_history: None,
157                    revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
158                    creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
159                    deleted_date: None,
160                },
161                Cipher {
162                    id: "23f0f877-42b1-4820-a850-b10700bc41eb".parse().unwrap(),
163                    folder_id: None,
164
165                    name: "My secure note".to_string(),
166                    notes: Some("Very secure!".to_string()),
167
168                    r#type: CipherType::SecureNote(Box::new(SecureNote {
169                        r#type: SecureNoteType::Generic,
170                    })),
171
172                    favorite: false,
173                    reprompt: 0,
174
175                    fields: vec![],
176
177                    password_history: None,
178                    revision_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
179                    creation_date: "2024-01-30T11:25:25.466Z".parse().unwrap(),
180                    deleted_date: None,
181                },
182                Cipher {
183                    id: "3ed8de45-48ee-4e26-a2dc-b10701276c53".parse().unwrap(),
184                    folder_id: None,
185
186                    name: "My card".to_string(),
187                    notes: None,
188
189                    r#type: CipherType::Card(Box::new(Card {
190                        cardholder_name: Some("John Doe".to_string()),
191                        exp_month: Some("1".to_string()),
192                        exp_year: Some("2032".to_string()),
193                        code: Some("123".to_string()),
194                        brand: Some("Visa".to_string()),
195                        number: Some("4111111111111111".to_string()),
196                    })),
197
198                    favorite: false,
199                    reprompt: 0,
200
201                    fields: vec![],
202
203                    password_history: None,
204                    revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
205                    creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
206                    deleted_date: None,
207                },
208                Cipher {
209                    id: "41cc3bc1-c3d9-4637-876c-b10701273712".parse().unwrap(),
210                    folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
211
212                    name: "My identity".to_string(),
213                    notes: None,
214
215                    r#type: CipherType::Identity(Box::new(Identity {
216                        title: Some("Mr".to_string()),
217                        first_name: Some("John".to_string()),
218                        middle_name: None,
219                        last_name: Some("Doe".to_string()),
220                        address1: None,
221                        address2: None,
222                        address3: None,
223                        city: None,
224                        state: None,
225                        postal_code: None,
226                        country: None,
227                        company: Some("Bitwarden".to_string()),
228                        email: None,
229                        phone: None,
230                        ssn: None,
231                        username: Some("JDoe".to_string()),
232                        passport_number: None,
233                        license_number: None,
234                    })),
235
236                    favorite: false,
237                    reprompt: 0,
238
239                    fields: vec![],
240
241                    password_history: None,
242                    revision_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
243                    creation_date: "2024-01-30T17:54:50.706Z".parse().unwrap(),
244                    deleted_date: None,
245                },
246            ],
247            "password".to_string(),
248            Kdf::PBKDF2 {
249                iterations: NonZeroU32::new(600_000).unwrap(),
250            },
251        )
252        .unwrap();
253
254        // Validate that the export is valid JSON
255        let parsed_export = serde_json::from_str::<EncryptedJsonExport>(&export).unwrap();
256
257        // First, derive a PIN key from the password and salt
258        // Next, validate the decrypted validation parses, and is a valid uuidv4. This indicates
259        // that at least the pin key is correct
260        let key = PinKey::derive(
261            "password".as_bytes(),
262            parsed_export.salt.as_bytes(),
263            &Kdf::PBKDF2 {
264                iterations: NonZeroU32::new(600_000).unwrap(),
265            },
266        )
267        .unwrap();
268        let decrypted_validation = parsed_export
269            .enc_key_validation
270            .parse::<bitwarden_crypto::EncString>()
271            .unwrap()
272            .decrypt_with_key(&key)
273            .unwrap();
274        let uuid = Uuid::parse_str(&decrypted_validation).unwrap();
275        assert_eq!(uuid.get_version_num(), 4);
276    }
277}