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}