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