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, EncryptMode, EncryptionContext, FolderView};
13use tracing::{debug, debug_span};
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#[bitwarden_logging::instrument(name = "reencrypt_data", fields(current_user_key_id = ?current_user_key_id, new_user_key_id = ?new_user_key_id))]
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
71 let encrypted_by_key_id = ctx
75 .get_symmetric_key_id(new_user_key_id)
76 .map(|id| id.to_string());
77
78 Ok(AccountDataRequestModel {
79 folders: Some(
80 reencrypted_folders
81 .into_iter()
82 .map(|folder| (&folder).into())
83 .collect(),
84 ),
85 ciphers: Some(
86 reencrypted_ciphers
87 .into_iter()
88 .map(|cipher| {
89 EncryptionContext {
90 encrypted_for: UserId::new(Uuid::nil()),
93 encrypted_by_key_id: encrypted_by_key_id.clone(),
94 cipher,
95 }
96 .try_into()
97 .map_err(|_| DataReencryptionError::DataConversion)
98 })
99 .collect::<Result<Vec<CipherWithIdRequestModel>, DataReencryptionError>>()?,
100 ),
101 sends: Some(
102 reencrypted_sends
103 .into_iter()
104 .map(|send| Ok(send.into()))
105 .collect::<Result<Vec<SendWithIdRequestModel>, DataReencryptionError>>()?,
106 ),
107 })
108}
109
110#[bitwarden_logging::instrument(name = "reencrypt_folders", fields(current_key = ?current_key, new_key = ?new_key))]
111fn reencrypt_folders(
112 folders: &[bitwarden_vault::Folder],
113 current_key: SymmetricKeySlotId,
114 new_key: SymmetricKeySlotId,
115 ctx: &mut KeyStoreContext<KeySlotIds>,
116) -> Result<Vec<bitwarden_vault::Folder>, DataReencryptionError> {
117 folders
118 .iter()
119 .map(|folder| {
120 let _span = debug_span!("reencrypt_folder", folder_id = ?folder.id).entered();
121 let folder_view: FolderView = folder
122 .decrypt(ctx, current_key)
123 .map_err(|_| DataReencryptionError::Decryption)?;
124 folder_view
125 .encrypt_composite(ctx, new_key)
126 .map_err(|_| DataReencryptionError::Encryption)
127 })
128 .collect::<Result<Vec<bitwarden_vault::Folder>, DataReencryptionError>>()
129}
130
131#[bitwarden_logging::instrument(name = "reencrypt_ciphers", fields(current_key = ?current_key, new_key = ?new_key))]
132fn reencrypt_ciphers(
133 ciphers: &[bitwarden_vault::Cipher],
134 current_key: SymmetricKeySlotId,
135 new_key: SymmetricKeySlotId,
136 ctx: &mut KeyStoreContext<KeySlotIds>,
137) -> Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError> {
138 ciphers
139 .iter()
140 .map(|cipher| {
141 let _span = debug_span!("reencrypt_cipher", cipher_id = ?cipher.id).entered();
142
143 if cipher.is_blob_encrypted() && cipher.key.is_some() {
148 debug!("Cipher already blob-encrypted, re-wrapping cipher key");
149 let mut cipher = cipher.clone();
150 cipher
151 .rewrap_cipher_key(current_key, new_key, ctx)
152 .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
153 Ok(cipher)
154 } else {
155 debug!("Upgrading legacy cipher to blob encryption");
156 let cipher_view = decrypt_for_blob_upgrade(cipher, current_key, new_key, ctx)?;
157 EncryptMode::Blob(cipher_view)
158 .encrypt_composite(ctx, new_key)
159 .map_err(|_| DataReencryptionError::Encryption)
160 }
161 })
162 .collect::<Result<Vec<bitwarden_vault::Cipher>, DataReencryptionError>>()
163}
164
165fn decrypt_for_blob_upgrade(
171 cipher: &bitwarden_vault::Cipher,
172 current_key: SymmetricKeySlotId,
173 new_key: SymmetricKeySlotId,
174 ctx: &mut KeyStoreContext<KeySlotIds>,
175) -> Result<CipherView, DataReencryptionError> {
176 if cipher.key.is_some() {
177 let mut rewrapped = cipher.clone();
178 rewrapped
179 .rewrap_cipher_key(current_key, new_key, ctx)
180 .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
181 rewrapped
182 .decrypt(ctx, new_key)
183 .map_err(|_| DataReencryptionError::Decryption)
184 } else {
185 cipher
186 .decrypt(ctx, current_key)
187 .map_err(|_| DataReencryptionError::Decryption)
188 }
189}
190
191#[bitwarden_logging::instrument(name = "reencrypt_sends", fields(current_key = ?current_key, new_key = ?new_key))]
192fn reencrypt_sends(
193 sends: &[bitwarden_send::Send],
194 current_key: SymmetricKeySlotId,
195 new_key: SymmetricKeySlotId,
196 ctx: &mut KeyStoreContext<KeySlotIds>,
197) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
198 sends
199 .iter()
200 .map(|send| {
201 let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
202 let send_view: SendView = send
203 .decrypt(ctx, current_key)
204 .map_err(|_| DataReencryptionError::Decryption)?;
205 send_view
206 .encrypt_composite(ctx, new_key)
207 .map_err(|_| DataReencryptionError::Encryption)
208 })
209 .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
210}
211
212#[cfg(test)]
213mod tests {
214 use bitwarden_core::key_management::KeySlotIds;
215 use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore};
216 use bitwarden_send::SendView;
217 use bitwarden_vault::{Attachment, Cipher, CipherRepromptType, CipherType, EncryptMode};
218 use chrono::Utc;
219
220 use super::check_for_old_attachments;
221 use crate::key_rotation::RotateUserKeysError;
222
223 const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
224
225 fn make_test_cipher(attachments: Option<Vec<Attachment>>) -> Cipher {
226 Cipher {
227 id: None,
228 organization_id: None,
229 folder_id: None,
230 collection_ids: vec![],
231 key: None,
232 name: Some(TEST_ENC_STRING.parse().unwrap()),
233 notes: None,
234 r#type: CipherType::Login,
235 login: None,
236 identity: None,
237 card: None,
238 secure_note: None,
239 ssh_key: None,
240 bank_account: None,
241 passport: None,
242 drivers_license: None,
243 favorite: false,
244 reprompt: CipherRepromptType::None,
245 organization_use_totp: false,
246 edit: true,
247 permissions: None,
248 view_password: true,
249 local_data: None,
250 attachments,
251 fields: None,
252 password_history: None,
253 creation_date: "2024-01-01T00:00:00Z".parse().unwrap(),
254 deleted_date: None,
255 revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
256 archived_date: None,
257 data: None,
258 }
259 }
260
261 #[test]
262 fn test_check_for_old_attachments_no_attachments() {
263 let ciphers = vec![make_test_cipher(None)];
264 assert!(check_for_old_attachments(&ciphers).is_ok());
265 }
266
267 #[test]
268 fn test_check_for_old_attachments_empty_ciphers() {
269 assert!(check_for_old_attachments(&[]).is_ok());
270 }
271
272 #[test]
273 fn test_check_for_old_attachments_all_have_keys() {
274 let ciphers = vec![make_test_cipher(Some(vec![Attachment {
275 id: Some("att1".to_string()),
276 url: None,
277 size: None,
278 size_name: None,
279 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
280 key: Some(TEST_ENC_STRING.parse().unwrap()),
281 }]))];
282 assert!(check_for_old_attachments(&ciphers).is_ok());
283 }
284
285 #[test]
286 fn test_check_for_old_attachments_one_missing_key() {
287 let ciphers = vec![make_test_cipher(Some(vec![Attachment {
288 id: Some("att1".to_string()),
289 url: None,
290 size: None,
291 size_name: None,
292 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
293 key: None,
294 }]))];
295 assert!(matches!(
296 check_for_old_attachments(&ciphers),
297 Err(RotateUserKeysError::OldAttachments)
298 ));
299 }
300
301 #[test]
302 fn test_check_for_old_attachments_ignores_organization_ciphers() {
303 let mut cipher = make_test_cipher(Some(vec![Attachment {
304 id: Some("att1".to_string()),
305 url: None,
306 size: None,
307 size_name: None,
308 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
309 key: None,
310 }]));
311 cipher.organization_id = Some(bitwarden_core::OrganizationId::new_v4());
312 let ciphers = vec![cipher];
313 assert!(check_for_old_attachments(&ciphers).is_ok());
314 }
315
316 fn make_cipher_view() -> bitwarden_vault::CipherView {
317 use bitwarden_vault::{CipherView, LoginView};
318 CipherView {
319 id: None,
320 organization_id: None,
321 folder_id: None,
322 r#type: CipherType::Login,
323 name: "Test Cipher".to_string(),
324 notes: Some("Some cipher notes".to_string()),
325 favorite: false,
326 revision_date: Utc::now(),
327 deleted_date: None,
328 fields: None,
329 login: Some(LoginView {
330 username: Some("user".to_string()),
331 password: Some("pass".to_string()),
332 totp: None,
333 uris: None,
334 autofill_on_page_load: None,
335 fido2_credentials: None,
336 password_revision_date: None,
337 }),
338 card: None,
339 identity: None,
340 secure_note: None,
341 attachments: None,
342 attachment_decryption_failures: None,
343 organization_use_totp: false,
344 collection_ids: vec![],
345 reprompt: CipherRepromptType::None,
346 local_data: None,
347 key: None,
348 ssh_key: None,
349 bank_account: None,
350 passport: None,
351 drivers_license: None,
352 permissions: None,
353 view_password: false,
354 creation_date: Utc::now(),
355 archived_date: None,
356 edit: false,
357 password_history: None,
358 }
359 }
360
361 fn assert_decrypts_to(
362 cipher: &Cipher,
363 expected: &bitwarden_vault::CipherView,
364 key: bitwarden_core::key_management::SymmetricKeySlotId,
365 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
366 ) {
367 use bitwarden_vault::CipherView;
368 let decrypted: CipherView = cipher.decrypt(ctx, key).unwrap();
369 assert_eq!(expected.name, decrypted.name);
370 assert_eq!(expected.notes, decrypted.notes);
371 assert_eq!(expected.r#type, decrypted.r#type);
372 assert_eq!(
373 expected.login.as_ref().unwrap().username,
374 decrypted.login.as_ref().unwrap().username
375 );
376 assert_eq!(
377 expected.login.as_ref().unwrap().password,
378 decrypted.login.as_ref().unwrap().password
379 );
380 }
381
382 #[test]
383 fn test_ciphers() {
384 let store: KeyStore<KeySlotIds> = KeyStore::default();
385 let mut ctx = store.context_mut();
386
387 let user_key_old =
388 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
389 let user_key_new =
390 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
391
392 let cipher = make_cipher_view();
393 let encrypted_cipher = EncryptMode::Legacy(cipher.clone())
394 .encrypt_composite(&mut ctx, user_key_old)
395 .unwrap();
396
397 let ciphers = vec![encrypted_cipher];
399 let reencrypted_ciphers =
400 super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
401 .unwrap();
402
403 assert!(reencrypted_ciphers[0].is_blob_encrypted());
405 assert_decrypts_to(&reencrypted_ciphers[0], &cipher, user_key_new, &mut ctx);
406 }
407
408 #[test]
409 fn test_blob_gate_upgrades_keyed_legacy_cipher() {
410 let store: KeyStore<KeySlotIds> = KeyStore::default();
411 let mut ctx = store.context_mut();
412
413 let user_key_old =
414 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
415 let user_key_new =
416 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XAes256Gcm);
417
418 let mut cipher = make_cipher_view();
420 cipher.generate_cipher_key(&mut ctx, user_key_old).unwrap();
421 let encrypted = EncryptMode::Legacy(cipher.clone())
422 .encrypt_composite(&mut ctx, user_key_old)
423 .unwrap();
424 assert!(!encrypted.is_blob_encrypted());
425 assert!(encrypted.key.is_some());
426
427 let reencrypted =
428 super::reencrypt_ciphers(&[encrypted], user_key_old, user_key_new, &mut ctx).unwrap();
429
430 assert!(reencrypted[0].is_blob_encrypted());
432 assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
433 }
434
435 #[test]
436 fn test_blob_gate_rewraps_existing_blob_without_re_encrypting() {
437 let store: KeyStore<KeySlotIds> = KeyStore::default();
438 let mut ctx = store.context_mut();
439
440 let user_key_old =
441 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
442 let user_key_new =
443 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XAes256Gcm);
444
445 let cipher = make_cipher_view();
447 let encrypted = EncryptMode::Blob(cipher.clone())
448 .encrypt_composite(&mut ctx, user_key_old)
449 .unwrap();
450 assert!(encrypted.is_blob_encrypted());
451
452 let reencrypted = super::reencrypt_ciphers(
453 std::slice::from_ref(&encrypted),
454 user_key_old,
455 user_key_new,
456 &mut ctx,
457 )
458 .unwrap();
459
460 assert!(reencrypted[0].is_blob_encrypted());
462 assert_eq!(encrypted.data, reencrypted[0].data);
463 assert_ne!(encrypted.key, reencrypted[0].key);
464 assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
465 }
466
467 #[test]
468 fn test_folders() {
469 let store: KeyStore<KeySlotIds> = KeyStore::default();
470 let mut ctx = store.context_mut();
471
472 let user_key_old =
473 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
474 let user_key_new =
475 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
476
477 let folder = bitwarden_vault::FolderView {
479 id: None,
480 name: "Test Folder".to_string(),
481 revision_date: Utc::now(),
482 };
483 let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
484
485 let folders = vec![encrypted_folder];
487 let reencrypted_folders =
488 super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
489 .unwrap();
490
491 let decrypted_folder = reencrypted_folders[0]
493 .decrypt(&mut ctx, user_key_new)
494 .unwrap();
495 assert_eq!(folder, decrypted_folder);
496 }
497
498 #[test]
499 fn test_sends() {
500 let store: KeyStore<KeySlotIds> = KeyStore::default();
501 let mut ctx = store.context_mut();
502
503 let user_key_old =
504 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
505 let user_key_new =
506 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
507
508 let send = bitwarden_send::SendView {
510 id: None,
511 access_id: None,
512 name: "Test Send".to_string(),
513 notes: Some("Some notes".to_string()),
514 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
515 text: Some(bitwarden_send::SendTextView {
516 text: Some("This is a test send".to_string()),
517 hidden: false,
518 }),
519 data: None,
520 r#type: bitwarden_send::SendType::Text,
521 max_access_count: None,
522 access_count: 0,
523 disabled: false,
524 hide_email: false,
525 revision_date: Utc::now(),
526 deletion_date: Utc::now(),
527 expiration_date: None,
528 new_password: None,
529 has_password: false,
530 file: None,
531 emails: vec![],
532 auth_type: bitwarden_send::AuthType::None,
533 };
534 let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
535
536 let sends = vec![encrypted_send];
538 let reencrypted_sends =
539 super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
540
541 let decrypted_send: SendView = reencrypted_sends[0]
543 .decrypt(&mut ctx, user_key_new)
544 .unwrap();
545
546 assert_eq!(send.key, decrypted_send.key);
548 }
549}