1use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
2use bitwarden_crypto::{
3 CompositeEncryptable, CryptoError, Decryptable, IdentifyKey, KeyStoreContext,
4};
5use bitwarden_logging::instrument;
6use thiserror::Error;
7
8use super::{CipherBlob, CipherBlobLatest, SealedCipherBlob, SealedCipherBlobError};
9use crate::cipher::{
10 attachment,
11 cipher::{Cipher, CipherRepromptType, CipherType, CipherView},
12};
13
14#[derive(Debug, Error)]
16pub enum BlobEncryptionError {
17 #[error(transparent)]
19 Crypto(#[from] CryptoError),
20 #[error(transparent)]
22 SealedBlob(#[from] SealedCipherBlobError),
23}
24
25impl From<BlobEncryptionError> for CryptoError {
29 fn from(err: BlobEncryptionError) -> Self {
30 tracing::warn!(error = %err, error_debug = ?err, "blob operation failed");
31 match err {
32 BlobEncryptionError::Crypto(c) => c,
33 BlobEncryptionError::SealedBlob(_) => CryptoError::Decrypt,
34 }
35 }
36}
37
38fn seal_cipher(
42 view: &CipherView,
43 ctx: &mut KeyStoreContext<KeySlotIds>,
44 cipher_key: SymmetricKeySlotId,
45) -> Result<String, BlobEncryptionError> {
46 let blob = CipherBlobLatest::from_cipher_view(view)?;
47 seal_blob_content(blob, cipher_key, ctx)
48}
49
50fn seal_blob_content(
54 blob: CipherBlobLatest,
55 cipher_key: SymmetricKeySlotId,
56 ctx: &mut KeyStoreContext<KeySlotIds>,
57) -> Result<String, BlobEncryptionError> {
58 let versioned: CipherBlob = blob.into();
59 let sealed = SealedCipherBlob::seal(versioned, &cipher_key, ctx)?;
60 Ok(sealed.to_opaque_string()?)
61}
62
63pub(crate) fn try_parse_blob(cipher: &Cipher) -> Option<SealedCipherBlob> {
66 let data = cipher.data.as_deref()?;
67 SealedCipherBlob::from_opaque_string(data).ok()
68}
69
70pub(crate) fn encrypt_blob_cipher(
78 view: &mut CipherView,
79 ctx: &mut KeyStoreContext<KeySlotIds>,
80) -> Result<Cipher, BlobEncryptionError> {
81 let wrapping_key = view.key_identifier();
82 encrypt_blob_cipher_with_wrapping_key(view, ctx, wrapping_key)
83}
84
85pub(crate) fn encrypt_blob_cipher_with_wrapping_key(
90 view: &mut CipherView,
91 ctx: &mut KeyStoreContext<KeySlotIds>,
92 wrapping_key: SymmetricKeySlotId,
93) -> Result<Cipher, BlobEncryptionError> {
94 if view.partial {
97 return Err(BlobEncryptionError::Crypto(
98 CryptoError::EncryptRestrictedView,
99 ));
100 }
101
102 let cipher_key = view.load_cipher_key_slot(ctx)?;
103
104 let sealed_string = seal_cipher(view, ctx, cipher_key)?;
105
106 let attachments = view.attachments.encrypt_composite(ctx, cipher_key)?;
107 let local_data = view.local_data.encrypt_composite(ctx, cipher_key)?;
108
109 Ok(Cipher {
110 partial_data: None,
111 id: view.id,
113 organization_id: view.organization_id,
114 folder_id: view.folder_id,
115 collection_ids: view.collection_ids.clone(),
116 key: Some(ctx.wrap_symmetric_key(wrapping_key, cipher_key)?),
117 r#type: view.r#type,
118 favorite: view.favorite,
119 reprompt: view.reprompt,
120 organization_use_totp: view.organization_use_totp,
121 edit: view.edit,
122 permissions: view.permissions,
123 view_password: view.view_password,
124 creation_date: view.creation_date,
125 deleted_date: view.deleted_date,
126 revision_date: view.revision_date,
127 archived_date: view.archived_date,
128
129 data: Some(sealed_string),
131 attachments,
132 local_data,
133
134 name: None,
136 notes: None,
137 login: None,
138 identity: None,
139 card: None,
140 secure_note: None,
141 ssh_key: None,
142 bank_account: None,
143 drivers_license: None,
144 passport: None,
145 fields: None,
146 password_history: None,
147 })
148}
149
150#[instrument(err, fields(cipher_id = ?cipher.id, org_id = ?cipher.organization_id))]
157pub(crate) fn decrypt_blob_cipher(
158 cipher: &Cipher,
159 sealed: &SealedCipherBlob,
160 ctx: &mut KeyStoreContext<KeySlotIds>,
161 wrapping_key: SymmetricKeySlotId,
162) -> Result<CipherView, BlobEncryptionError> {
163 let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &cipher.key)?;
164
165 let CipherBlob::CipherBlobV1(blob) = sealed.unseal(&cipher_key, ctx)?;
166
167 let (attachments, attachment_decryption_failures) =
168 attachment::decrypt_attachments_with_failures(
169 cipher.attachments.as_deref().unwrap_or_default(),
170 ctx,
171 cipher_key,
172 );
173
174 let local_data = cipher.local_data.decrypt(ctx, cipher_key).ok().flatten();
175
176 let mut view = CipherView {
177 partial: false,
178 id: cipher.id,
180 organization_id: cipher.organization_id,
181 folder_id: cipher.folder_id,
182 collection_ids: cipher.collection_ids.clone(),
183 key: if cipher.key.is_some() {
184 #[allow(deprecated)]
185 Some(ctx.dangerous_get_symmetric_key(cipher_key)?.clone())
186 } else {
187 None
188 },
189 r#type: cipher.r#type,
190 favorite: cipher.favorite,
191 reprompt: cipher.reprompt,
192 organization_use_totp: cipher.organization_use_totp,
193 edit: cipher.edit,
194 permissions: cipher.permissions,
195 view_password: cipher.view_password,
196 creation_date: cipher.creation_date,
197 deleted_date: cipher.deleted_date,
198 revision_date: cipher.revision_date,
199 archived_date: cipher.archived_date,
200
201 attachments: Some(attachments),
203 attachment_decryption_failures: Some(attachment_decryption_failures),
204 local_data,
205
206 name: String::new(),
208 notes: None,
209 login: None,
210 identity: None,
211 card: None,
212 secure_note: None,
213 ssh_key: None,
214 bank_account: None,
215 drivers_license: None,
216 passport: None,
217 fields: None,
218 password_history: None,
219 };
220
221 blob.apply_to_cipher_view(&mut view)?;
222
223 Ok(view)
224}
225
226impl CipherView {
235 pub fn seal_blob_for_item_sends(
237 &self,
238 ctx: &mut KeyStoreContext<KeySlotIds>,
239 key: SymmetricKeySlotId,
240 ) -> Result<String, CryptoError> {
241 if self.partial {
243 return Err(CryptoError::EncryptRestrictedView);
244 }
245
246 let blob = CipherBlobLatest::from_cipher_view(self)?;
247 Ok(seal_blob_content(blob, key, ctx)?)
248 }
249
250 pub fn unseal_blob_for_item_sends(
253 data: &str,
254 ctx: &mut KeyStoreContext<KeySlotIds>,
255 key: SymmetricKeySlotId,
256 ) -> Result<CipherView, CryptoError> {
257 let sealed =
258 SealedCipherBlob::from_opaque_string(data).map_err(BlobEncryptionError::from)?;
259 let CipherBlob::CipherBlobV1(blob) = sealed
260 .unseal(&key, ctx)
261 .map_err(BlobEncryptionError::from)?;
262
263 let mut view = CipherView {
264 partial: false,
265 id: None,
266 organization_id: None,
267 folder_id: None,
268 collection_ids: Vec::new(),
269 key: None,
270 name: String::new(),
271 notes: None,
272 r#type: CipherType::Login,
274 login: None,
275 identity: None,
276 card: None,
277 secure_note: None,
278 ssh_key: None,
279 bank_account: None,
280 drivers_license: None,
281 passport: None,
282 favorite: false,
283 reprompt: CipherRepromptType::None,
284 organization_use_totp: false,
285 edit: false,
286 permissions: None,
287 view_password: true,
288 local_data: None,
289 attachments: None,
290 attachment_decryption_failures: None,
291 fields: None,
292 password_history: None,
293 creation_date: Default::default(),
294 deleted_date: None,
295 revision_date: Default::default(),
296 archived_date: None,
297 };
298 blob.apply_to_cipher_view(&mut view)?;
299
300 Ok(view)
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use bitwarden_crypto::{IdentifyKey, PrimitiveEncryptable};
307 use uuid::Uuid;
308
309 use super::*;
310 use crate::{
311 cipher::{
312 bank_account::BankAccountView,
313 blob::conversions::test_support::{create_shell_cipher_view, create_test_key_store},
314 card::CardView,
315 cipher::{CipherId, CipherRepromptType, CipherType},
316 field::{FieldType, FieldView},
317 identity::IdentityView,
318 login::LoginView,
319 secure_note::{SecureNoteType, SecureNoteView},
320 ssh_key::SshKeyView,
321 },
322 password_history::PasswordHistoryView,
323 };
324
325 fn make_test_cipher_with_data(
326 ctx: &mut KeyStoreContext<KeySlotIds>,
327 data: Option<String>,
328 ) -> Cipher {
329 let name = "test"
330 .encrypt(
331 ctx,
332 bitwarden_core::key_management::SymmetricKeySlotId::User,
333 )
334 .unwrap();
335 Cipher {
336 partial_data: None,
337 id: None,
338 organization_id: None,
339 folder_id: None,
340 collection_ids: vec![],
341 key: None,
342 name: Some(name),
343 notes: None,
344 r#type: CipherType::SecureNote,
345 login: None,
346 identity: None,
347 card: None,
348 secure_note: None,
349 ssh_key: None,
350 bank_account: None,
351 drivers_license: None,
352 passport: None,
353 favorite: false,
354 reprompt: CipherRepromptType::None,
355 organization_use_totp: false,
356 edit: true,
357 permissions: None,
358 view_password: true,
359 local_data: None,
360 attachments: None,
361 fields: None,
362 password_history: None,
363 creation_date: chrono::Utc::now(),
364 deleted_date: None,
365 revision_date: chrono::Utc::now(),
366 archived_date: None,
367 data,
368 }
369 }
370
371 #[test]
372 fn test_try_parse_blob_returns_some_after_encrypt() {
373 let (key_store, _) = create_test_key_store();
374 let mut ctx = key_store.context_mut();
375
376 let mut view = create_shell_cipher_view(CipherType::SecureNote);
377 view.name = "Blob Test".to_string();
378 view.secure_note = Some(SecureNoteView {
379 r#type: SecureNoteType::Generic,
380 });
381
382 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
383 assert!(try_parse_blob(&cipher).is_some());
384 }
385
386 #[test]
387 fn test_seal_unseal_round_trip() {
388 let (key_store, _) = create_test_key_store();
389 let mut ctx = key_store.context_mut();
390
391 let mut view = create_shell_cipher_view(CipherType::SecureNote);
392 view.name = "Round Trip".to_string();
393 view.notes = Some("Some notes".to_string());
394 view.secure_note = Some(SecureNoteView {
395 r#type: SecureNoteType::Generic,
396 });
397 let cipher_key = view.load_cipher_key_slot(&mut ctx).unwrap();
398
399 let sealed_string = seal_cipher(&view, &mut ctx, cipher_key).unwrap();
400
401 let mut cipher = make_test_cipher_with_data(&mut ctx, Some(sealed_string));
402 if let Some(key) = &view.key {
403 let slot = ctx.add_local_symmetric_key(key.clone());
404 cipher.key = Some(ctx.wrap_symmetric_key(view.key_identifier(), slot).unwrap());
405 }
406
407 let view = decrypt_blob_cipher(
408 &cipher,
409 &try_parse_blob(&cipher).unwrap(),
410 &mut ctx,
411 cipher.key_identifier(),
412 )
413 .unwrap();
414 assert_eq!(view.name, "Round Trip");
415 assert_eq!(view.notes, Some("Some notes".to_string()));
416 }
417
418 #[test]
419 fn test_encrypt_blob_cipher_sets_data() {
420 let (key_store, _) = create_test_key_store();
421 let mut ctx = key_store.context_mut();
422
423 let mut view = create_shell_cipher_view(CipherType::SecureNote);
424 view.name = "Has Data".to_string();
425 view.secure_note = Some(SecureNoteView {
426 r#type: SecureNoteType::Generic,
427 });
428
429 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
430 assert!(cipher.data.is_some());
431 }
432
433 #[test]
434 fn test_encrypt_blob_cipher_clears_legacy_fields() {
435 let (key_store, _) = create_test_key_store();
436 let mut ctx = key_store.context_mut();
437
438 let mut view = create_shell_cipher_view(CipherType::Login);
439 view.name = "Login".to_string();
440 view.login = Some(LoginView {
441 username: Some("user".to_string()),
442 password: Some("pass".to_string()),
443 password_revision_date: None,
444 uris: None,
445 totp: None,
446 autofill_on_page_load: None,
447 fido2_credentials: None,
448 });
449
450 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
451 assert!(cipher.login.is_none());
452 assert!(cipher.card.is_none());
453 assert!(cipher.identity.is_none());
454 assert!(cipher.secure_note.is_none());
455 assert!(cipher.ssh_key.is_none());
456 assert!(cipher.bank_account.is_none());
457 assert!(cipher.notes.is_none());
458 assert!(cipher.fields.is_none());
459 assert!(cipher.password_history.is_none());
460 }
461
462 #[test]
463 fn test_encrypt_blob_cipher_generates_key() {
464 let (key_store, _) = create_test_key_store();
465 let mut ctx = key_store.context_mut();
466
467 let mut view = create_shell_cipher_view(CipherType::SecureNote);
468 view.secure_note = Some(SecureNoteView {
469 r#type: SecureNoteType::Generic,
470 });
471 assert!(view.key.is_none());
472
473 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
474 assert!(cipher.key.is_some());
475 assert!(view.key.is_some());
476 }
477
478 #[test]
479 fn test_encrypt_blob_cipher_preserves_metadata() {
480 let (key_store, _) = create_test_key_store();
481 let mut ctx = key_store.context_mut();
482
483 let cipher_id = CipherId::new(Uuid::new_v4());
484 let mut view = create_shell_cipher_view(CipherType::SecureNote);
485 view.id = Some(cipher_id);
486 view.favorite = true;
487 view.reprompt = CipherRepromptType::Password;
488 view.name = "Metadata Test".to_string();
489 view.secure_note = Some(SecureNoteView {
490 r#type: SecureNoteType::Generic,
491 });
492
493 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
494 assert_eq!(cipher.id, Some(cipher_id));
495 assert!(cipher.favorite);
496 assert_eq!(cipher.reprompt, CipherRepromptType::Password);
497 assert_eq!(cipher.r#type, CipherType::SecureNote);
498 assert_eq!(cipher.creation_date, view.creation_date);
499 assert_eq!(cipher.revision_date, view.revision_date);
500 }
501
502 #[test]
503 fn test_encrypt_blob_cipher_each_type() {
504 let (key_store, _) = create_test_key_store();
505
506 {
508 let mut ctx = key_store.context_mut();
509 let mut view = create_shell_cipher_view(CipherType::Login);
510 view.name = "Login".to_string();
511 view.login = Some(LoginView {
512 username: Some("user".to_string()),
513 password: None,
514 password_revision_date: None,
515 uris: None,
516 totp: None,
517 autofill_on_page_load: None,
518 fido2_credentials: None,
519 });
520 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
521 }
522
523 {
525 let mut ctx = key_store.context_mut();
526 let mut view = create_shell_cipher_view(CipherType::Card);
527 view.name = "Card".to_string();
528 view.card = Some(CardView {
529 cardholder_name: Some("John".to_string()),
530 exp_month: None,
531 exp_year: None,
532 code: None,
533 brand: None,
534 number: None,
535 });
536 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
537 }
538
539 {
541 let mut ctx = key_store.context_mut();
542 let mut view = create_shell_cipher_view(CipherType::Identity);
543 view.name = "Identity".to_string();
544 view.identity = Some(IdentityView {
545 title: None,
546 first_name: Some("Jane".to_string()),
547 middle_name: None,
548 last_name: None,
549 address1: None,
550 address2: None,
551 address3: None,
552 city: None,
553 state: None,
554 postal_code: None,
555 country: None,
556 company: None,
557 email: None,
558 phone: None,
559 ssn: None,
560 username: None,
561 passport_number: None,
562 license_number: None,
563 });
564 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
565 }
566
567 {
569 let mut ctx = key_store.context_mut();
570 let mut view = create_shell_cipher_view(CipherType::SecureNote);
571 view.name = "Note".to_string();
572 view.secure_note = Some(SecureNoteView {
573 r#type: SecureNoteType::Generic,
574 });
575 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
576 }
577
578 {
580 let mut ctx = key_store.context_mut();
581 let mut view = create_shell_cipher_view(CipherType::SshKey);
582 view.name = "SSH".to_string();
583 view.ssh_key = Some(SshKeyView {
584 private_key: "private".to_string(),
585 public_key: "public".to_string(),
586 fingerprint: "fingerprint".to_string(),
587 });
588 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
589 }
590
591 {
593 let mut ctx = key_store.context_mut();
594 let mut view = create_shell_cipher_view(CipherType::BankAccount);
595 view.name = "Bank".to_string();
596 view.bank_account = Some(BankAccountView {
597 bank_name: Some("Bank".to_string()),
598 name_on_account: None,
599 account_type: None,
600 account_number: None,
601 routing_number: None,
602 branch_number: None,
603 pin: None,
604 swift_code: None,
605 iban: None,
606 bank_contact_phone: None,
607 });
608 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
609 }
610 }
611
612 #[test]
613 fn test_end_to_end_round_trip() {
614 let (key_store, _) = create_test_key_store();
615 let mut ctx = key_store.context_mut();
616
617 let mut view = create_shell_cipher_view(CipherType::Login);
618 view.name = "My Login".to_string();
619 view.notes = Some("Secret notes".to_string());
620 view.login = Some(LoginView {
621 username: Some("[email protected]".to_string()),
622 password: Some("p@ssw0rd".to_string()),
623 password_revision_date: None,
624 uris: None,
625 totp: None,
626 autofill_on_page_load: None,
627 fido2_credentials: None,
628 });
629 view.fields = Some(vec![FieldView {
630 name: Some("custom".to_string()),
631 value: Some("field-value".to_string()),
632 r#type: FieldType::Text,
633 linked_id: None,
634 }]);
635 let history_date = chrono::Utc::now();
636 view.password_history = Some(vec![PasswordHistoryView {
637 password: "old-p@ssw0rd".to_string(),
638 last_used_date: history_date,
639 }]);
640
641 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
642 assert!(try_parse_blob(&cipher).is_some());
643
644 let restored = decrypt_blob_cipher(
645 &cipher,
646 &try_parse_blob(&cipher).unwrap(),
647 &mut ctx,
648 cipher.key_identifier(),
649 )
650 .unwrap();
651
652 assert_eq!(restored.name, "My Login");
653 assert_eq!(restored.notes, Some("Secret notes".to_string()));
654 let login = restored.login.unwrap();
655 assert_eq!(login.username, Some("[email protected]".to_string()));
656 assert_eq!(login.password, Some("p@ssw0rd".to_string()));
657
658 let fields = restored.fields.unwrap();
659 assert_eq!(fields.len(), 1);
660 assert_eq!(fields[0].name, Some("custom".to_string()));
661 assert_eq!(fields[0].value, Some("field-value".to_string()));
662 assert_eq!(fields[0].r#type, FieldType::Text);
663
664 let history = restored.password_history.unwrap();
665 assert_eq!(history.len(), 1);
666 assert_eq!(history[0].password, "old-p@ssw0rd");
667 assert_eq!(history[0].last_used_date, history_date);
668 }
669
670 #[test]
671 fn test_decrypt_blob_cipher() {
672 let (key_store, _) = create_test_key_store();
673 let mut ctx = key_store.context_mut();
674
675 let mut view = create_shell_cipher_view(CipherType::Card);
676 view.name = "My Card".to_string();
677 view.notes = Some("Card notes".to_string());
678 view.card = Some(CardView {
679 cardholder_name: Some("John Doe".to_string()),
680 exp_month: Some("12".to_string()),
681 exp_year: Some("2030".to_string()),
682 code: Some("123".to_string()),
683 brand: Some("Visa".to_string()),
684 number: Some("4111111111111111".to_string()),
685 });
686
687 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
688 let restored = decrypt_blob_cipher(
689 &cipher,
690 &try_parse_blob(&cipher).unwrap(),
691 &mut ctx,
692 cipher.key_identifier(),
693 )
694 .unwrap();
695
696 assert_eq!(restored.name, "My Card");
697 assert_eq!(restored.notes, Some("Card notes".to_string()));
698 let card = restored.card.unwrap();
699 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
700 assert_eq!(card.number, Some("4111111111111111".to_string()));
701 assert_eq!(card.code, Some("123".to_string()));
702 assert_eq!(card.brand, Some("Visa".to_string()));
703 }
704
705 #[test]
706 fn test_decrypt_blob_cipher_preserves_metadata() {
707 let (key_store, _) = create_test_key_store();
708 let mut ctx = key_store.context_mut();
709
710 let cipher_id = CipherId::new(Uuid::new_v4());
711 let mut view = create_shell_cipher_view(CipherType::SecureNote);
712 view.id = Some(cipher_id);
713 view.favorite = true;
714 view.reprompt = CipherRepromptType::Password;
715 view.organization_use_totp = true;
716 view.edit = false;
717 view.view_password = false;
718 view.name = "Metadata".to_string();
719 view.secure_note = Some(SecureNoteView {
720 r#type: SecureNoteType::Generic,
721 });
722 let creation_date = view.creation_date;
723 let revision_date = view.revision_date;
724
725 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
726 let restored = decrypt_blob_cipher(
727 &cipher,
728 &try_parse_blob(&cipher).unwrap(),
729 &mut ctx,
730 cipher.key_identifier(),
731 )
732 .unwrap();
733
734 assert_eq!(restored.id, Some(cipher_id));
735 assert!(restored.favorite);
736 assert_eq!(restored.reprompt, CipherRepromptType::Password);
737 assert!(restored.organization_use_totp);
738 assert!(!restored.edit);
739 assert!(!restored.view_password);
740 assert_eq!(restored.r#type, CipherType::SecureNote);
741 assert_eq!(restored.creation_date, creation_date);
742 assert_eq!(restored.revision_date, revision_date);
743 assert!(restored.key.is_some());
744 }
745
746 #[test]
747 fn test_try_parse_blob_returns_none_for_legacy() {
748 let (key_store, _) = create_test_key_store();
749 let mut ctx = key_store.context_mut();
750
751 let cipher = make_test_cipher_with_data(&mut ctx, None);
752 assert!(try_parse_blob(&cipher).is_none());
753
754 let cipher = make_test_cipher_with_data(&mut ctx, Some("not a blob".to_string()));
755 assert!(try_parse_blob(&cipher).is_none());
756 }
757}