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 = cipher.encrypt_composite(&mut ctx, user_key_old).unwrap();
385
386 let ciphers = vec![encrypted_cipher];
388 let reencrypted_ciphers =
389 super::reencrypt_ciphers(ciphers.as_slice(), user_key_old, user_key_new, &mut ctx)
390 .unwrap();
391
392 assert!(reencrypted_ciphers[0].is_blob_encrypted());
394 assert_decrypts_to(&reencrypted_ciphers[0], &cipher, user_key_new, &mut ctx);
395 }
396
397 #[test]
398 fn test_blob_gate_upgrades_keyed_legacy_cipher() {
399 let store: KeyStore<KeySlotIds> = KeyStore::default();
400 let mut ctx = store.context_mut();
401
402 let user_key_old =
403 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
404 let user_key_new =
405 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305);
406
407 let mut cipher = make_cipher_view();
409 cipher.generate_cipher_key(&mut ctx, user_key_old).unwrap();
410 let encrypted = EncryptMode::Legacy(cipher.clone())
411 .encrypt_composite(&mut ctx, user_key_old)
412 .unwrap();
413 assert!(!encrypted.is_blob_encrypted());
414 assert!(encrypted.key.is_some());
415
416 let reencrypted =
417 super::reencrypt_ciphers(&[encrypted], user_key_old, user_key_new, &mut ctx).unwrap();
418
419 assert!(reencrypted[0].is_blob_encrypted());
421 assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
422 }
423
424 #[test]
425 fn test_blob_gate_rewraps_existing_blob_without_re_encrypting() {
426 let store: KeyStore<KeySlotIds> = KeyStore::default();
427 let mut ctx = store.context_mut();
428
429 let user_key_old =
430 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
431 let user_key_new =
432 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305);
433
434 let cipher = make_cipher_view();
436 let encrypted = EncryptMode::Blob(cipher.clone())
437 .encrypt_composite(&mut ctx, user_key_old)
438 .unwrap();
439 assert!(encrypted.is_blob_encrypted());
440
441 let reencrypted = super::reencrypt_ciphers(
442 std::slice::from_ref(&encrypted),
443 user_key_old,
444 user_key_new,
445 &mut ctx,
446 )
447 .unwrap();
448
449 assert!(reencrypted[0].is_blob_encrypted());
451 assert_eq!(encrypted.data, reencrypted[0].data);
452 assert_ne!(encrypted.key, reencrypted[0].key);
453 assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
454 }
455
456 #[test]
457 fn test_folders() {
458 let store: KeyStore<KeySlotIds> = KeyStore::default();
459 let mut ctx = store.context_mut();
460
461 let user_key_old =
462 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
463 let user_key_new =
464 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
465
466 let folder = bitwarden_vault::FolderView {
468 id: None,
469 name: "Test Folder".to_string(),
470 revision_date: Utc::now(),
471 };
472 let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
473
474 let folders = vec![encrypted_folder];
476 let reencrypted_folders =
477 super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
478 .unwrap();
479
480 let decrypted_folder = reencrypted_folders[0]
482 .decrypt(&mut ctx, user_key_new)
483 .unwrap();
484 assert_eq!(folder, decrypted_folder);
485 }
486
487 #[test]
488 fn test_sends() {
489 let store: KeyStore<KeySlotIds> = KeyStore::default();
490 let mut ctx = store.context_mut();
491
492 let user_key_old =
493 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
494 let user_key_new =
495 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
496
497 let send = bitwarden_send::SendView {
499 id: None,
500 access_id: None,
501 name: "Test Send".to_string(),
502 notes: Some("Some notes".to_string()),
503 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
504 text: Some(bitwarden_send::SendTextView {
505 text: Some("This is a test send".to_string()),
506 hidden: false,
507 }),
508 r#type: bitwarden_send::SendType::Text,
509 max_access_count: None,
510 access_count: 0,
511 disabled: false,
512 hide_email: false,
513 revision_date: Utc::now(),
514 deletion_date: Utc::now(),
515 expiration_date: None,
516 new_password: None,
517 has_password: false,
518 file: None,
519 emails: vec![],
520 auth_type: bitwarden_send::AuthType::None,
521 };
522 let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
523
524 let sends = vec![encrypted_send];
526 let reencrypted_sends =
527 super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
528
529 let decrypted_send: SendView = reencrypted_sends[0]
531 .decrypt(&mut ctx, user_key_new)
532 .unwrap();
533
534 assert_eq!(send.key, decrypted_send.key);
536 }
537}