1use bitwarden_api_api::models::{
4 AccountDataRequestModel, CipherWithIdRequestModel, SendWithIdRequestModel,
5};
6use bitwarden_core::{
7 UserId,
8 key_management::{KeySlotIds, SymmetricKeySlotId},
9};
10use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStoreContext};
11use bitwarden_send::SendView;
12use bitwarden_vault::{CipherView, EncryptionContext, FolderView};
13use tracing::{debug, debug_span, instrument};
14use uuid::Uuid;
15
16use super::RotateUserKeysError;
17
18#[derive(Debug)]
20pub(crate) enum DataReencryptionError {
21 Decryption,
23 Encryption,
25 DataConversion,
27 CipherKeyRewrap,
29}
30
31pub(super) fn check_for_old_attachments(
36 ciphers: &[bitwarden_vault::Cipher],
37) -> Result<(), RotateUserKeysError> {
38 let has_old = ciphers
39 .iter()
40 .filter(|c| c.organization_id.is_none())
41 .any(|c| {
42 c.attachments
43 .as_ref()
44 .is_some_and(|atts| atts.iter().any(|a| a.key.is_none()))
45 });
46 if has_old {
47 return Err(RotateUserKeysError::OldAttachments);
48 }
49 Ok(())
50}
51
52#[instrument(name = "reencrypt_data", skip(folders, ciphers, sends, ctx))]
56pub(super) fn reencrypt_data(
57 folders: &[bitwarden_vault::Folder],
58 ciphers: &[bitwarden_vault::Cipher],
59 sends: &[bitwarden_send::Send],
60 current_user_key_id: SymmetricKeySlotId,
61 new_user_key_id: SymmetricKeySlotId,
62 ctx: &mut KeyStoreContext<KeySlotIds>,
63) -> Result<AccountDataRequestModel, DataReencryptionError> {
64 let reencrypted_folders =
66 reencrypt_folders(folders, current_user_key_id, new_user_key_id, ctx)?;
67 let reencrypted_ciphers =
68 reencrypt_ciphers(ciphers, current_user_key_id, new_user_key_id, ctx)?;
69 let reencrypted_sends = reencrypt_sends(sends, current_user_key_id, new_user_key_id, ctx)?;
70 Ok(AccountDataRequestModel {
71 folders: Some(
72 reencrypted_folders
73 .into_iter()
74 .map(|folder| (&folder).into())
75 .collect(),
76 ),
77 ciphers: Some(
78 reencrypted_ciphers
79 .into_iter()
80 .map(|cipher| {
81 EncryptionContext {
82 encrypted_for: UserId::new(Uuid::nil()),
85 cipher,
86 }
87 .try_into()
88 .map_err(|_| DataReencryptionError::DataConversion)
89 })
90 .collect::<Result<Vec<CipherWithIdRequestModel>, DataReencryptionError>>()?,
91 ),
92 sends: Some(
93 reencrypted_sends
94 .into_iter()
95 .map(|send| Ok(send.into()))
96 .collect::<Result<Vec<SendWithIdRequestModel>, DataReencryptionError>>()?,
97 ),
98 })
99}
100
101#[instrument(name = "reencrypt_folders", skip(folders, ctx))]
102fn reencrypt_folders(
103 folders: &[bitwarden_vault::Folder],
104 current_key: SymmetricKeySlotId,
105 new_key: SymmetricKeySlotId,
106 ctx: &mut KeyStoreContext<KeySlotIds>,
107) -> Result<Vec<bitwarden_vault::Folder>, DataReencryptionError> {
108 folders
109 .iter()
110 .map(|folder| {
111 let _span = debug_span!("reencrypt_folder", folder_id = ?folder.id).entered();
112 let folder_view: FolderView = folder
113 .decrypt(ctx, current_key)
114 .map_err(|_| DataReencryptionError::Decryption)?;
115 folder_view
116 .encrypt_composite(ctx, new_key)
117 .map_err(|_| DataReencryptionError::Encryption)
118 })
119 .collect::<Result<Vec<bitwarden_vault::Folder>, DataReencryptionError>>()
120}
121
122#[instrument(name = "reencrypt_ciphers", skip(ciphers, ctx))]
123fn reencrypt_ciphers(
124 ciphers: &[bitwarden_vault::Cipher],
125 current_key: SymmetricKeySlotId,
126 new_key: SymmetricKeySlotId,
127 ctx: &mut KeyStoreContext<KeySlotIds>,
128) -> Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError> {
129 ciphers
130 .iter()
131 .map(|cipher| {
132 let _span = debug_span!("reencrypt_cipher", cipher_id = ?cipher.id).entered();
133
134 if cipher.key.is_some() {
137 debug!("Re-wrapping cipher key without decrypting cipher");
138 let mut cipher = cipher.clone();
139 cipher
140 .rewrap_cipher_key(current_key, new_key, ctx)
141 .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
142 Ok(cipher)
143
144 } else {
147 debug!("Cipher has no cipher key, decrypting and re-encrypting entire cipher");
148 let cipher_view: CipherView = cipher
149 .decrypt(ctx, current_key)
150 .map_err(|_| DataReencryptionError::Decryption)?;
151 cipher_view
152 .encrypt_composite(ctx, new_key)
153 .map_err(|_| DataReencryptionError::Encryption)
154 }
155 })
156 .collect::<Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError>>()
157}
158
159#[instrument(name = "reencrypt_sends", skip(sends, ctx))]
160fn reencrypt_sends(
161 sends: &[bitwarden_send::Send],
162 current_key: SymmetricKeySlotId,
163 new_key: SymmetricKeySlotId,
164 ctx: &mut KeyStoreContext<KeySlotIds>,
165) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
166 sends
167 .iter()
168 .map(|send| {
169 let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
170 let send_view: SendView = send
171 .decrypt(ctx, current_key)
172 .map_err(|_| DataReencryptionError::Decryption)?;
173 send_view
174 .encrypt_composite(ctx, new_key)
175 .map_err(|_| DataReencryptionError::Encryption)
176 })
177 .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
178}
179
180#[cfg(test)]
181mod tests {
182 use bitwarden_core::key_management::KeySlotIds;
183 use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore};
184 use bitwarden_send::SendView;
185 use bitwarden_vault::{Attachment, Cipher, CipherRepromptType, CipherType};
186 use chrono::Utc;
187
188 use super::check_for_old_attachments;
189 use crate::key_rotation::RotateUserKeysError;
190
191 const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
192
193 fn make_test_cipher(attachments: Option<Vec<Attachment>>) -> Cipher {
194 Cipher {
195 id: None,
196 organization_id: None,
197 folder_id: None,
198 collection_ids: vec![],
199 key: None,
200 name: TEST_ENC_STRING.parse().unwrap(),
201 notes: None,
202 r#type: CipherType::Login,
203 login: None,
204 identity: None,
205 card: None,
206 secure_note: None,
207 ssh_key: None,
208 bank_account: None,
209 passport: None,
210 drivers_license: None,
211 favorite: false,
212 reprompt: CipherRepromptType::None,
213 organization_use_totp: false,
214 edit: true,
215 permissions: None,
216 view_password: true,
217 local_data: None,
218 attachments,
219 fields: None,
220 password_history: None,
221 creation_date: "2024-01-01T00:00:00Z".parse().unwrap(),
222 deleted_date: None,
223 revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
224 archived_date: None,
225 data: None,
226 }
227 }
228
229 #[test]
230 fn test_check_for_old_attachments_no_attachments() {
231 let ciphers = vec![make_test_cipher(None)];
232 assert!(check_for_old_attachments(&ciphers).is_ok());
233 }
234
235 #[test]
236 fn test_check_for_old_attachments_empty_ciphers() {
237 assert!(check_for_old_attachments(&[]).is_ok());
238 }
239
240 #[test]
241 fn test_check_for_old_attachments_all_have_keys() {
242 let ciphers = vec![make_test_cipher(Some(vec![Attachment {
243 id: Some("att1".to_string()),
244 url: None,
245 size: None,
246 size_name: None,
247 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
248 key: Some(TEST_ENC_STRING.parse().unwrap()),
249 }]))];
250 assert!(check_for_old_attachments(&ciphers).is_ok());
251 }
252
253 #[test]
254 fn test_check_for_old_attachments_one_missing_key() {
255 let ciphers = vec![make_test_cipher(Some(vec![Attachment {
256 id: Some("att1".to_string()),
257 url: None,
258 size: None,
259 size_name: None,
260 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
261 key: None,
262 }]))];
263 assert!(matches!(
264 check_for_old_attachments(&ciphers),
265 Err(RotateUserKeysError::OldAttachments)
266 ));
267 }
268
269 #[test]
270 fn test_check_for_old_attachments_ignores_organization_ciphers() {
271 let mut cipher = make_test_cipher(Some(vec![Attachment {
272 id: Some("att1".to_string()),
273 url: None,
274 size: None,
275 size_name: None,
276 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
277 key: None,
278 }]));
279 cipher.organization_id = Some(bitwarden_core::OrganizationId::new_v4());
280 let ciphers = vec![cipher];
281 assert!(check_for_old_attachments(&ciphers).is_ok());
282 }
283
284 #[test]
285 fn test_ciphers() {
286 use bitwarden_vault::{CipherType, CipherView, LoginView};
287 let store: KeyStore<KeySlotIds> = KeyStore::default();
288 let mut ctx = store.context_mut();
289
290 let user_key_old =
291 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
292 let user_key_new =
293 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
294
295 let cipher = CipherView {
296 id: None,
297 organization_id: None,
298 folder_id: None,
299 r#type: CipherType::Login,
300 name: "Test Cipher".to_string(),
301 notes: Some("Some cipher notes".to_string()),
302 favorite: false,
303 revision_date: Utc::now(),
304 deleted_date: None,
305 fields: None,
306 login: Some(LoginView {
307 username: Some("user".to_string()),
308 password: Some("pass".to_string()),
309 totp: None,
310 uris: None,
311 autofill_on_page_load: None,
312 fido2_credentials: None,
313 password_revision_date: None,
314 }),
315 card: None,
316 identity: None,
317 secure_note: None,
318 attachments: None,
319 attachment_decryption_failures: None,
320 organization_use_totp: false,
321 collection_ids: vec![],
322 reprompt: CipherRepromptType::None,
323 local_data: None,
324 key: None,
325 ssh_key: None,
326 bank_account: None,
327 passport: None,
328 drivers_license: None,
329 permissions: None,
330 view_password: false,
331 creation_date: Utc::now(),
332 archived_date: None,
333 edit: false,
334 password_history: None,
335 };
336 let encrypted_cipher = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap();
337
338 let ciphers = vec![encrypted_cipher];
340 let reencrypted_ciphers =
341 super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
342 .unwrap();
343
344 let decrypted_cipher: CipherView = reencrypted_ciphers[0]
346 .decrypt(&mut ctx, user_key_new)
347 .unwrap();
348 assert_eq!(cipher.name, decrypted_cipher.name);
349 assert_eq!(cipher.notes, decrypted_cipher.notes);
350 assert_eq!(cipher.r#type, decrypted_cipher.r#type);
351 assert_eq!(
352 cipher.login.as_ref().unwrap().username,
353 decrypted_cipher.login.as_ref().unwrap().username
354 );
355 assert_eq!(
356 cipher.login.as_ref().unwrap().password,
357 decrypted_cipher.login.as_ref().unwrap().password
358 );
359 }
360
361 #[test]
362 fn test_folders() {
363 let store: KeyStore<KeySlotIds> = KeyStore::default();
364 let mut ctx = store.context_mut();
365
366 let user_key_old =
367 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
368 let user_key_new =
369 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
370
371 let folder = bitwarden_vault::FolderView {
373 id: None,
374 name: "Test Folder".to_string(),
375 revision_date: Utc::now(),
376 };
377 let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
378
379 let folders = vec![encrypted_folder];
381 let reencrypted_folders =
382 super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
383 .unwrap();
384
385 let decrypted_folder = reencrypted_folders[0]
387 .decrypt(&mut ctx, user_key_new)
388 .unwrap();
389 assert_eq!(folder, decrypted_folder);
390 }
391
392 #[test]
393 fn test_sends() {
394 let store: KeyStore<KeySlotIds> = KeyStore::default();
395 let mut ctx = store.context_mut();
396
397 let user_key_old =
398 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
399 let user_key_new =
400 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
401
402 let send = bitwarden_send::SendView {
404 id: None,
405 access_id: None,
406 name: "Test Send".to_string(),
407 notes: Some("Some notes".to_string()),
408 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
409 text: Some(bitwarden_send::SendTextView {
410 text: Some("This is a test send".to_string()),
411 hidden: false,
412 }),
413 r#type: bitwarden_send::SendType::Text,
414 max_access_count: None,
415 access_count: 0,
416 disabled: false,
417 hide_email: false,
418 revision_date: Utc::now(),
419 deletion_date: Utc::now(),
420 expiration_date: None,
421 new_password: None,
422 has_password: false,
423 file: None,
424 emails: vec![],
425 auth_type: bitwarden_send::AuthType::None,
426 };
427 let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
428
429 let sends = vec![encrypted_send];
431 let reencrypted_sends =
432 super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
433
434 let decrypted_send: SendView = reencrypted_sends[0]
436 .decrypt(&mut ctx, user_key_new)
437 .unwrap();
438
439 assert_eq!(send.key, decrypted_send.key);
441 }
442}