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 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 let parsed_export = serde_json::from_str::<EncryptedJsonExport>(&export).unwrap();
252
253 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}