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(
170 cipher: &bitwarden_vault::Cipher,
171 current_key: SymmetricKeySlotId,
172 new_key: SymmetricKeySlotId,
173 ctx: &mut KeyStoreContext<KeySlotIds>,
174) -> Result<CipherView, DataReencryptionError> {
175 if cipher.key.is_some() {
176 let mut rewrapped = cipher.clone();
177 rewrapped
178 .rewrap_cipher_key(current_key, new_key, ctx)
179 .map_err(|_| DataReencryptionError::CipherKeyRewrap)?;
180 rewrapped
181 .decrypt(ctx, new_key)
182 .map_err(|_| DataReencryptionError::Decryption)
183 } else {
184 let mut view: CipherView = cipher
185 .decrypt(ctx, current_key)
186 .map_err(|_| DataReencryptionError::Decryption)?;
187 let _ = view
188 .load_cipher_key_slot(ctx)
189 .map_err(|_| DataReencryptionError::Encryption)?;
190 Ok(view)
191 }
192}
193
194#[bitwarden_logging::instrument(name = "reencrypt_sends", fields(current_key = ?current_key, new_key = ?new_key))]
195fn reencrypt_sends(
196 sends: &[bitwarden_send::Send],
197 current_key: SymmetricKeySlotId,
198 new_key: SymmetricKeySlotId,
199 ctx: &mut KeyStoreContext<KeySlotIds>,
200) -> Result<Vec<bitwarden_send::Send>, DataReencryptionError> {
201 sends
202 .iter()
203 .map(|send| {
204 let _span = debug_span!("reencrypt_send", send_id = ?send.id).entered();
205 let send_view: SendView = send
206 .decrypt(ctx, current_key)
207 .map_err(|_| DataReencryptionError::Decryption)?;
208 send_view
209 .encrypt_composite(ctx, new_key)
210 .map_err(|_| DataReencryptionError::Encryption)
211 })
212 .collect::<Result<Vec<bitwarden_send::Send>, DataReencryptionError>>()
213}
214
215#[cfg(test)]
216mod tests {
217 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
218 use bitwarden_crypto::{CompositeEncryptable, Decryptable, KeyStore, PrimitiveEncryptable};
219 use bitwarden_send::SendView;
220 use bitwarden_vault::{
221 Attachment, Cipher, CipherRepromptType, CipherType, CipherView, EncryptMode,
222 Fido2CredentialView,
223 };
224 use chrono::Utc;
225
226 use super::check_for_old_attachments;
227 use crate::key_rotation::RotateUserKeysError;
228
229 const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
230
231 fn make_test_cipher(attachments: Option<Vec<Attachment>>) -> Cipher {
232 Cipher {
233 partial_data: None,
234 id: None,
235 organization_id: None,
236 folder_id: None,
237 collection_ids: vec![],
238 key: None,
239 name: Some(TEST_ENC_STRING.parse().unwrap()),
240 notes: None,
241 r#type: CipherType::Login,
242 login: None,
243 identity: None,
244 card: None,
245 secure_note: None,
246 ssh_key: None,
247 bank_account: None,
248 passport: None,
249 drivers_license: None,
250 favorite: false,
251 reprompt: CipherRepromptType::None,
252 organization_use_totp: false,
253 edit: true,
254 permissions: None,
255 view_password: true,
256 local_data: None,
257 attachments,
258 fields: None,
259 password_history: None,
260 creation_date: "2024-01-01T00:00:00Z".parse().unwrap(),
261 deleted_date: None,
262 revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
263 archived_date: None,
264 data: None,
265 }
266 }
267
268 #[test]
269 fn test_check_for_old_attachments_no_attachments() {
270 let ciphers = vec![make_test_cipher(None)];
271 assert!(check_for_old_attachments(&ciphers).is_ok());
272 }
273
274 #[test]
275 fn test_check_for_old_attachments_empty_ciphers() {
276 assert!(check_for_old_attachments(&[]).is_ok());
277 }
278
279 #[test]
280 fn test_check_for_old_attachments_all_have_keys() {
281 let ciphers = vec![make_test_cipher(Some(vec![Attachment {
282 id: Some("att1".to_string()),
283 url: None,
284 size: None,
285 size_name: None,
286 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
287 key: Some(TEST_ENC_STRING.parse().unwrap()),
288 }]))];
289 assert!(check_for_old_attachments(&ciphers).is_ok());
290 }
291
292 #[test]
293 fn test_check_for_old_attachments_one_missing_key() {
294 let ciphers = vec![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 assert!(matches!(
303 check_for_old_attachments(&ciphers),
304 Err(RotateUserKeysError::OldAttachments)
305 ));
306 }
307
308 #[test]
309 fn test_check_for_old_attachments_ignores_organization_ciphers() {
310 let mut cipher = make_test_cipher(Some(vec![Attachment {
311 id: Some("att1".to_string()),
312 url: None,
313 size: None,
314 size_name: None,
315 file_name: Some(TEST_ENC_STRING.parse().unwrap()),
316 key: None,
317 }]));
318 cipher.organization_id = Some(bitwarden_core::OrganizationId::new_v4());
319 let ciphers = vec![cipher];
320 assert!(check_for_old_attachments(&ciphers).is_ok());
321 }
322
323 fn make_cipher_view() -> bitwarden_vault::CipherView {
324 use bitwarden_vault::{CipherView, LoginView};
325 CipherView {
326 partial: false,
327 id: None,
328 organization_id: None,
329 folder_id: None,
330 r#type: CipherType::Login,
331 name: "Test Cipher".to_string(),
332 notes: Some("Some cipher notes".to_string()),
333 favorite: false,
334 revision_date: Utc::now(),
335 deleted_date: None,
336 fields: None,
337 login: Some(LoginView {
338 username: Some("user".to_string()),
339 password: Some("pass".to_string()),
340 totp: None,
341 uris: None,
342 autofill_on_page_load: None,
343 fido2_credentials: None,
344 password_revision_date: None,
345 }),
346 card: None,
347 identity: None,
348 secure_note: None,
349 attachments: None,
350 attachment_decryption_failures: None,
351 organization_use_totp: false,
352 collection_ids: vec![],
353 reprompt: CipherRepromptType::None,
354 local_data: None,
355 key: None,
356 ssh_key: None,
357 bank_account: None,
358 passport: None,
359 drivers_license: None,
360 permissions: None,
361 view_password: false,
362 creation_date: Utc::now(),
363 archived_date: None,
364 edit: false,
365 password_history: None,
366 }
367 }
368
369 fn assert_decrypts_to(
370 cipher: &Cipher,
371 expected: &CipherView,
372 key: SymmetricKeySlotId,
373 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
374 ) {
375 use bitwarden_vault::CipherView;
376 let decrypted: CipherView = cipher.decrypt(ctx, key).unwrap();
377 assert_eq!(expected.name, decrypted.name);
378 assert_eq!(expected.notes, decrypted.notes);
379 assert_eq!(expected.r#type, decrypted.r#type);
380 assert_eq!(
381 expected.login.as_ref().unwrap().username,
382 decrypted.login.as_ref().unwrap().username
383 );
384 assert_eq!(
385 expected.login.as_ref().unwrap().password,
386 decrypted.login.as_ref().unwrap().password
387 );
388 }
389
390 fn make_keyless_legacy_cipher(
394 view: &CipherView,
395 key: SymmetricKeySlotId,
396 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
397 ) -> Cipher {
398 use bitwarden_vault::Login;
399
400 let login = view.login.as_ref().unwrap();
401
402 Cipher {
403 name: Some(view.name.encrypt(ctx, key).unwrap()),
404 notes: view.notes.encrypt(ctx, key).unwrap(),
405 login: Some(Login {
406 username: login.username.encrypt(ctx, key).unwrap(),
407 password: login.password.encrypt(ctx, key).unwrap(),
408 password_revision_date: None,
409 uris: None,
410 totp: None,
411 autofill_on_page_load: None,
412 fido2_credentials: None,
413 }),
414 ..make_test_cipher(None)
415 }
416 }
417
418 #[test]
419 fn test_blob_gate_rewraps_existing_blob_without_re_encrypting() {
420 let store: KeyStore<KeySlotIds> = KeyStore::default();
421 let mut ctx = store.context_mut();
422
423 let user_key_old =
424 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
425 let user_key_new =
426 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XAes256Gcm);
427
428 let cipher = make_cipher_view();
430 let encrypted = EncryptMode::Blob(cipher.clone())
431 .encrypt_composite(&mut ctx, user_key_old)
432 .unwrap();
433 assert!(encrypted.is_blob_encrypted());
434
435 let reencrypted = super::reencrypt_ciphers(
436 std::slice::from_ref(&encrypted),
437 user_key_old,
438 user_key_new,
439 &mut ctx,
440 )
441 .unwrap();
442
443 assert!(reencrypted[0].is_blob_encrypted());
445 assert_eq!(encrypted.data, reencrypted[0].data);
446 assert_ne!(encrypted.key, reencrypted[0].key);
447 assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
448 }
449
450 #[test]
451 fn test_folders() {
452 let store: KeyStore<KeySlotIds> = KeyStore::default();
453 let mut ctx = store.context_mut();
454
455 let user_key_old =
456 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
457 let user_key_new =
458 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
459
460 let folder = bitwarden_vault::FolderView {
462 id: None,
463 name: "Test Folder".to_string(),
464 revision_date: Utc::now(),
465 };
466 let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
467
468 let folders = vec![encrypted_folder];
470 let reencrypted_folders =
471 super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
472 .unwrap();
473
474 let decrypted_folder = reencrypted_folders[0]
476 .decrypt(&mut ctx, user_key_new)
477 .unwrap();
478 assert_eq!(folder, decrypted_folder);
479 }
480
481 #[test]
482 fn test_sends() {
483 let store: KeyStore<KeySlotIds> = KeyStore::default();
484 let mut ctx = store.context_mut();
485
486 let user_key_old =
487 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
488 let user_key_new =
489 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
490
491 let send = bitwarden_send::SendView {
493 id: None,
494 access_id: None,
495 name: "Test Send".to_string(),
496 notes: Some("Some notes".to_string()),
497 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
498 text: Some(bitwarden_send::SendTextView {
499 text: Some("This is a test send".to_string()),
500 hidden: false,
501 }),
502 data: None,
503 r#type: bitwarden_send::SendType::Text,
504 max_access_count: None,
505 access_count: 0,
506 disabled: false,
507 hide_email: false,
508 revision_date: Utc::now(),
509 deletion_date: Utc::now(),
510 expiration_date: None,
511 new_password: None,
512 has_password: false,
513 file: None,
514 emails: vec![],
515 auth_type: bitwarden_send::AuthType::None,
516 };
517 let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
518
519 let sends = vec![encrypted_send];
521 let reencrypted_sends =
522 super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
523
524 let decrypted_send: SendView = reencrypted_sends[0]
526 .decrypt(&mut ctx, user_key_new)
527 .unwrap();
528
529 assert_eq!(send.key, decrypted_send.key);
531 }
532
533 #[test]
534 fn test_rotation_keyless_plain() {
535 let store: KeyStore<KeySlotIds> = KeyStore::default();
536 let mut ctx = store.context_mut();
537 let (old, new) = make_rotation_keys(&mut ctx);
538
539 let cipher = make_rotatable_cipher(&mut ctx, old, false, false, false);
540 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
541
542 assert_upgraded_to_blob(&mut ctx, &out[0], new);
543 }
544
545 #[test]
546 fn test_rotation_keyless_attachment() {
547 let store: KeyStore<KeySlotIds> = KeyStore::default();
548 let mut ctx = store.context_mut();
549 let (old, new) = make_rotation_keys(&mut ctx);
550
551 let cipher = make_rotatable_cipher(&mut ctx, old, false, true, false);
552 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
553
554 assert_upgraded_to_blob(&mut ctx, &out[0], new);
555 assert_attachment_key_decryptable(&mut ctx, &out[0], new);
556 }
557
558 #[test]
559 fn test_rotation_keyed_plain() {
560 let store: KeyStore<KeySlotIds> = KeyStore::default();
561 let mut ctx = store.context_mut();
562 let (old, new) = make_rotation_keys(&mut ctx);
563
564 let cipher = make_rotatable_cipher(&mut ctx, old, true, false, false);
565 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
566
567 assert_upgraded_to_blob(&mut ctx, &out[0], new);
568 }
569
570 #[test]
571 fn test_rotation_keyed_fido2() {
572 let store: KeyStore<KeySlotIds> = KeyStore::default();
573 let mut ctx = store.context_mut();
574 let (old, new) = make_rotation_keys(&mut ctx);
575
576 let cipher = make_rotatable_cipher(&mut ctx, old, true, false, true);
577 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
578
579 assert_upgraded_to_blob(&mut ctx, &out[0], new);
580 assert_fido2_decryptable(&mut ctx, &out[0], new);
581 }
582
583 #[test]
584 fn test_rotation_keyed_attachment() {
585 let store: KeyStore<KeySlotIds> = KeyStore::default();
586 let mut ctx = store.context_mut();
587 let (old, new) = make_rotation_keys(&mut ctx);
588
589 let cipher = make_rotatable_cipher(&mut ctx, old, true, true, false);
590 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
591
592 assert_upgraded_to_blob(&mut ctx, &out[0], new);
593 assert_attachment_key_decryptable(&mut ctx, &out[0], new);
594 }
595
596 #[test]
597 fn test_rotation_keyed_fido2_and_attachment() {
598 let store: KeyStore<KeySlotIds> = KeyStore::default();
599 let mut ctx = store.context_mut();
600 let (old, new) = make_rotation_keys(&mut ctx);
601
602 let cipher = make_rotatable_cipher(&mut ctx, old, true, true, true);
603 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
604
605 assert_upgraded_to_blob(&mut ctx, &out[0], new);
606 assert_fido2_decryptable(&mut ctx, &out[0], new);
607 assert_attachment_key_decryptable(&mut ctx, &out[0], new);
608 }
609
610 fn make_rotation_keys(
614 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
615 ) -> (SymmetricKeySlotId, SymmetricKeySlotId) {
616 let old = ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
617 let new =
618 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305);
619 (old, new)
620 }
621
622 fn make_rotatable_cipher(
625 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
626 user_key: SymmetricKeySlotId,
627 with_cipher_key: bool,
628 with_attachment: bool,
629 with_fido2: bool,
630 ) -> Cipher {
631 let mut view = make_cipher_view();
632 if with_fido2 {
633 let cred = Fido2CredentialView {
634 credential_id: "cred-123".to_string(),
635 key_type: "public-key".to_string(),
636 key_algorithm: "ECDSA".to_string(),
637 key_curve: "P-256".to_string(),
638 key_value: "key-value".to_string(),
639 rp_id: "example.com".to_string(),
640 user_handle: None,
641 user_name: None,
642 counter: "0".to_string(),
643 rp_name: None,
644 user_display_name: None,
645 discoverable: "true".to_string(),
646 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
647 };
648 view.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
649 }
650 let mut cipher = if with_cipher_key {
651 EncryptMode::Legacy(view)
652 .encrypt_composite(ctx, user_key)
653 .unwrap()
654 } else {
655 assert!(
656 !with_fido2,
657 "make_keyless_legacy_cipher drops FIDO2 credentials"
658 );
659 make_keyless_legacy_cipher(&view, user_key, ctx)
660 };
661 if with_attachment {
662 let cipher_key = match &cipher.key {
665 Some(key) => ctx.unwrap_symmetric_key(user_key, key).unwrap(),
666 None => user_key,
667 };
668 let content_key = ctx.generate_symmetric_key();
669 cipher.attachments = Some(vec![Attachment {
670 id: Some("att1".to_string()),
671 url: None,
672 size: None,
673 size_name: None,
674 file_name: Some("secret.txt".encrypt(ctx, cipher_key).unwrap()),
675 key: Some(ctx.wrap_symmetric_key(cipher_key, content_key).unwrap()),
676 }]);
677 }
678 cipher
679 }
680
681 fn assert_upgraded_to_blob(
684 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
685 rotated: &Cipher,
686 user_key: SymmetricKeySlotId,
687 ) {
688 assert!(rotated.is_blob_encrypted());
689 assert_decrypts_to(rotated, &make_cipher_view(), user_key, ctx);
690 }
691
692 fn assert_fido2_decryptable(
695 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
696 rotated: &Cipher,
697 user_key: SymmetricKeySlotId,
698 ) {
699 let dv: CipherView = rotated.decrypt(ctx, user_key).unwrap();
700 let creds = dv
701 .login
702 .as_ref()
703 .unwrap()
704 .fido2_credentials
705 .as_ref()
706 .unwrap();
707 assert_eq!(creds[0].credential_id, "cred-123");
708 assert_eq!(creds[0].key_value, "key-value");
709 }
710
711 fn assert_attachment_key_decryptable(
713 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
714 rotated: &Cipher,
715 user_key: SymmetricKeySlotId,
716 ) {
717 let dv: CipherView = rotated.decrypt(ctx, user_key).unwrap();
718 let att = &dv.attachments.as_ref().unwrap()[0];
719 assert_eq!(att.file_name.as_deref(), Some("secret.txt"));
720 assert!(
721 att.key.is_some(),
722 "attachment key must be present after rotation"
723 );
724 }
725}