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