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 http_authentication_enabled: None,
344 fido2_credentials: None,
345 password_revision_date: None,
346 }),
347 card: None,
348 identity: None,
349 secure_note: None,
350 attachments: None,
351 attachment_decryption_failures: None,
352 organization_use_totp: false,
353 collection_ids: vec![],
354 reprompt: CipherRepromptType::None,
355 local_data: None,
356 key: None,
357 ssh_key: None,
358 bank_account: None,
359 passport: None,
360 drivers_license: None,
361 permissions: None,
362 view_password: false,
363 creation_date: Utc::now(),
364 archived_date: None,
365 edit: false,
366 password_history: None,
367 }
368 }
369
370 fn assert_decrypts_to(
371 cipher: &Cipher,
372 expected: &CipherView,
373 key: SymmetricKeySlotId,
374 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
375 ) {
376 use bitwarden_vault::CipherView;
377 let decrypted: CipherView = cipher.decrypt(ctx, key).unwrap();
378 assert_eq!(expected.name, decrypted.name);
379 assert_eq!(expected.notes, decrypted.notes);
380 assert_eq!(expected.r#type, decrypted.r#type);
381 assert_eq!(
382 expected.login.as_ref().unwrap().username,
383 decrypted.login.as_ref().unwrap().username
384 );
385 assert_eq!(
386 expected.login.as_ref().unwrap().password,
387 decrypted.login.as_ref().unwrap().password
388 );
389 }
390
391 fn make_keyless_legacy_cipher(
395 view: &CipherView,
396 key: SymmetricKeySlotId,
397 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
398 ) -> Cipher {
399 use bitwarden_vault::Login;
400
401 let login = view.login.as_ref().unwrap();
402
403 Cipher {
404 name: Some(view.name.encrypt(ctx, key).unwrap()),
405 notes: view.notes.encrypt(ctx, key).unwrap(),
406 login: Some(Login {
407 username: login.username.encrypt(ctx, key).unwrap(),
408 password: login.password.encrypt(ctx, key).unwrap(),
409 password_revision_date: None,
410 uris: None,
411 totp: None,
412 autofill_on_page_load: None,
413 http_authentication_enabled: None,
414 fido2_credentials: None,
415 }),
416 ..make_test_cipher(None)
417 }
418 }
419
420 #[test]
421 fn test_blob_gate_rewraps_existing_blob_without_re_encrypting() {
422 let store: KeyStore<KeySlotIds> = KeyStore::default();
423 let mut ctx = store.context_mut();
424
425 let user_key_old =
426 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
427 let user_key_new =
428 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XAes256Gcm);
429
430 let cipher = make_cipher_view();
432 let encrypted = EncryptMode::Blob(cipher.clone())
433 .encrypt_composite(&mut ctx, user_key_old)
434 .unwrap();
435 assert!(encrypted.is_blob_encrypted());
436
437 let reencrypted = super::reencrypt_ciphers(
438 std::slice::from_ref(&encrypted),
439 user_key_old,
440 user_key_new,
441 &mut ctx,
442 )
443 .unwrap();
444
445 assert!(reencrypted[0].is_blob_encrypted());
447 assert_eq!(encrypted.data, reencrypted[0].data);
448 assert_ne!(encrypted.key, reencrypted[0].key);
449 assert_decrypts_to(&reencrypted[0], &cipher, user_key_new, &mut ctx);
450 }
451
452 #[test]
453 fn test_folders() {
454 let store: KeyStore<KeySlotIds> = KeyStore::default();
455 let mut ctx = store.context_mut();
456
457 let user_key_old =
458 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
459 let user_key_new =
460 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
461
462 let folder = bitwarden_vault::FolderView {
464 id: None,
465 name: "Test Folder".to_string(),
466 revision_date: Utc::now(),
467 };
468 let encrypted_folder = folder.encrypt_composite(&mut ctx, user_key_old).unwrap();
469
470 let folders = vec![encrypted_folder];
472 let reencrypted_folders =
473 super::reencrypt_folders(folders.as_slice(), user_key_old, user_key_new, &mut ctx)
474 .unwrap();
475
476 let decrypted_folder = reencrypted_folders[0]
478 .decrypt(&mut ctx, user_key_new)
479 .unwrap();
480 assert_eq!(folder, decrypted_folder);
481 }
482
483 #[test]
484 fn test_sends() {
485 let store: KeyStore<KeySlotIds> = KeyStore::default();
486 let mut ctx = store.context_mut();
487
488 let user_key_old =
489 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
490 let user_key_new =
491 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
492
493 let send = bitwarden_send::SendView {
495 id: None,
496 access_id: None,
497 name: "Test Send".to_string(),
498 notes: Some("Some notes".to_string()),
499 key: Some("Pgui0FK85cNhBGWHAlBHBw".to_owned()),
500 text: Some(bitwarden_send::SendTextView {
501 text: Some("This is a test send".to_string()),
502 hidden: false,
503 }),
504 data: None,
505 r#type: bitwarden_send::SendType::Text,
506 max_access_count: None,
507 access_count: 0,
508 disabled: false,
509 hide_email: false,
510 revision_date: Utc::now(),
511 deletion_date: Utc::now(),
512 expiration_date: None,
513 new_password: None,
514 has_password: false,
515 file: None,
516 emails: vec![],
517 auth_type: bitwarden_send::AuthType::None,
518 };
519 let encrypted_send = send.encrypt_composite(&mut ctx, user_key_old).unwrap();
520
521 let sends = vec![encrypted_send];
523 let reencrypted_sends =
524 super::reencrypt_sends(sends.as_slice(), user_key_old, user_key_new, &mut ctx).unwrap();
525
526 let decrypted_send: SendView = reencrypted_sends[0]
528 .decrypt(&mut ctx, user_key_new)
529 .unwrap();
530
531 assert_eq!(send.key, decrypted_send.key);
533 }
534
535 #[test]
536 fn test_rotation_keyless_plain() {
537 let store: KeyStore<KeySlotIds> = KeyStore::default();
538 let mut ctx = store.context_mut();
539 let (old, new) = make_rotation_keys(&mut ctx);
540
541 let cipher = make_rotatable_cipher(&mut ctx, old, false, false, false);
542 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
543
544 assert_upgraded_to_blob(&mut ctx, &out[0], new);
545 }
546
547 #[test]
548 fn test_rotation_keyless_attachment() {
549 let store: KeyStore<KeySlotIds> = KeyStore::default();
550 let mut ctx = store.context_mut();
551 let (old, new) = make_rotation_keys(&mut ctx);
552
553 let cipher = make_rotatable_cipher(&mut ctx, old, false, true, false);
554 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
555
556 assert_upgraded_to_blob(&mut ctx, &out[0], new);
557 assert_attachment_key_decryptable(&mut ctx, &out[0], new);
558 }
559
560 #[test]
561 fn test_rotation_keyed_plain() {
562 let store: KeyStore<KeySlotIds> = KeyStore::default();
563 let mut ctx = store.context_mut();
564 let (old, new) = make_rotation_keys(&mut ctx);
565
566 let cipher = make_rotatable_cipher(&mut ctx, old, true, false, false);
567 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
568
569 assert_upgraded_to_blob(&mut ctx, &out[0], new);
570 }
571
572 #[test]
573 fn test_rotation_keyed_fido2() {
574 let store: KeyStore<KeySlotIds> = KeyStore::default();
575 let mut ctx = store.context_mut();
576 let (old, new) = make_rotation_keys(&mut ctx);
577
578 let cipher = make_rotatable_cipher(&mut ctx, old, true, false, true);
579 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
580
581 assert_upgraded_to_blob(&mut ctx, &out[0], new);
582 assert_fido2_decryptable(&mut ctx, &out[0], new);
583 }
584
585 #[test]
586 fn test_rotation_keyed_attachment() {
587 let store: KeyStore<KeySlotIds> = KeyStore::default();
588 let mut ctx = store.context_mut();
589 let (old, new) = make_rotation_keys(&mut ctx);
590
591 let cipher = make_rotatable_cipher(&mut ctx, old, true, true, false);
592 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
593
594 assert_upgraded_to_blob(&mut ctx, &out[0], new);
595 assert_attachment_key_decryptable(&mut ctx, &out[0], new);
596 }
597
598 #[test]
599 fn test_rotation_keyed_fido2_and_attachment() {
600 let store: KeyStore<KeySlotIds> = KeyStore::default();
601 let mut ctx = store.context_mut();
602 let (old, new) = make_rotation_keys(&mut ctx);
603
604 let cipher = make_rotatable_cipher(&mut ctx, old, true, true, true);
605 let out = super::reencrypt_ciphers(&[cipher], old, new, &mut ctx).unwrap();
606
607 assert_upgraded_to_blob(&mut ctx, &out[0], new);
608 assert_fido2_decryptable(&mut ctx, &out[0], new);
609 assert_attachment_key_decryptable(&mut ctx, &out[0], new);
610 }
611
612 fn make_rotation_keys(
616 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
617 ) -> (SymmetricKeySlotId, SymmetricKeySlotId) {
618 let old = ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);
619 let new =
620 ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::XChaCha20Poly1305);
621 (old, new)
622 }
623
624 fn make_rotatable_cipher(
627 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
628 user_key: SymmetricKeySlotId,
629 with_cipher_key: bool,
630 with_attachment: bool,
631 with_fido2: bool,
632 ) -> Cipher {
633 let mut view = make_cipher_view();
634 if with_fido2 {
635 let cred = Fido2CredentialView {
636 credential_id: "cred-123".to_string(),
637 key_type: "public-key".to_string(),
638 key_algorithm: "ECDSA".to_string(),
639 key_curve: "P-256".to_string(),
640 key_value: "key-value".to_string(),
641 rp_id: "example.com".to_string(),
642 user_handle: None,
643 user_name: None,
644 counter: "0".to_string(),
645 rp_name: None,
646 user_display_name: None,
647 discoverable: "true".to_string(),
648 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
649 };
650 view.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
651 }
652 let mut cipher = if with_cipher_key {
653 EncryptMode::Legacy(view)
654 .encrypt_composite(ctx, user_key)
655 .unwrap()
656 } else {
657 assert!(
658 !with_fido2,
659 "make_keyless_legacy_cipher drops FIDO2 credentials"
660 );
661 make_keyless_legacy_cipher(&view, user_key, ctx)
662 };
663 if with_attachment {
664 let cipher_key = match &cipher.key {
667 Some(key) => ctx.unwrap_symmetric_key(user_key, key).unwrap(),
668 None => user_key,
669 };
670 let content_key = ctx.generate_symmetric_key();
671 cipher.attachments = Some(vec![Attachment {
672 id: Some("att1".to_string()),
673 url: None,
674 size: None,
675 size_name: None,
676 file_name: Some("secret.txt".encrypt(ctx, cipher_key).unwrap()),
677 key: Some(ctx.wrap_symmetric_key(cipher_key, content_key).unwrap()),
678 }]);
679 }
680 cipher
681 }
682
683 fn assert_upgraded_to_blob(
686 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
687 rotated: &Cipher,
688 user_key: SymmetricKeySlotId,
689 ) {
690 assert!(rotated.is_blob_encrypted());
691 assert_decrypts_to(rotated, &make_cipher_view(), user_key, ctx);
692 }
693
694 fn assert_fido2_decryptable(
697 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
698 rotated: &Cipher,
699 user_key: SymmetricKeySlotId,
700 ) {
701 let dv: CipherView = rotated.decrypt(ctx, user_key).unwrap();
702 let creds = dv
703 .login
704 .as_ref()
705 .unwrap()
706 .fido2_credentials
707 .as_ref()
708 .unwrap();
709 assert_eq!(creds[0].credential_id, "cred-123");
710 assert_eq!(creds[0].key_value, "key-value");
711 }
712
713 fn assert_attachment_key_decryptable(
715 ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
716 rotated: &Cipher,
717 user_key: SymmetricKeySlotId,
718 ) {
719 let dv: CipherView = rotated.decrypt(ctx, user_key).unwrap();
720 let att = &dv.attachments.as_ref().unwrap()[0];
721 assert_eq!(att.file_name.as_deref(), Some("secret.txt"));
722 assert!(
723 att.key.is_some(),
724 "attachment key must be present after rotation"
725 );
726 }
727}