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, 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(
41 view: &CipherView,
42 ctx: &mut KeyStoreContext<KeySlotIds>,
43 wrapping_key: SymmetricKeySlotId,
44) -> Result<String, BlobEncryptionError> {
45 let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &view.key)?;
46 let blob = CipherBlobLatest::from_cipher_view(view, ctx, cipher_key)?;
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(
91 view: &mut CipherView,
92 ctx: &mut KeyStoreContext<KeySlotIds>,
93 wrapping_key: SymmetricKeySlotId,
94) -> Result<Cipher, BlobEncryptionError> {
95 if view.key.is_none() {
96 view.generate_cipher_key(ctx, wrapping_key)?;
97 }
98
99 let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &view.key)?;
100
101 let sealed_string = seal_cipher(view, ctx, wrapping_key)?;
102
103 let attachments = view.attachments.encrypt_composite(ctx, cipher_key)?;
104 let local_data = view.local_data.encrypt_composite(ctx, cipher_key)?;
105
106 Ok(Cipher {
107 id: view.id,
109 organization_id: view.organization_id,
110 folder_id: view.folder_id,
111 collection_ids: view.collection_ids.clone(),
112 key: view.key.clone(),
113 r#type: view.r#type,
114 favorite: view.favorite,
115 reprompt: view.reprompt,
116 organization_use_totp: view.organization_use_totp,
117 edit: view.edit,
118 permissions: view.permissions,
119 view_password: view.view_password,
120 creation_date: view.creation_date,
121 deleted_date: view.deleted_date,
122 revision_date: view.revision_date,
123 archived_date: view.archived_date,
124
125 data: Some(sealed_string),
127 attachments,
128 local_data,
129
130 name: None,
132 notes: None,
133 login: None,
134 identity: None,
135 card: None,
136 secure_note: None,
137 ssh_key: None,
138 bank_account: None,
139 drivers_license: None,
140 passport: None,
141 fields: None,
142 password_history: None,
143 })
144}
145
146#[instrument(err, fields(cipher_id = ?cipher.id, org_id = ?cipher.organization_id))]
153pub(crate) fn decrypt_blob_cipher(
154 cipher: &Cipher,
155 sealed: &SealedCipherBlob,
156 ctx: &mut KeyStoreContext<KeySlotIds>,
157 wrapping_key: SymmetricKeySlotId,
158) -> Result<CipherView, BlobEncryptionError> {
159 let cipher_key = Cipher::decrypt_cipher_key(ctx, wrapping_key, &cipher.key)?;
160
161 let CipherBlob::CipherBlobV1(blob) = sealed.unseal(&cipher_key, ctx)?;
162
163 let (attachments, attachment_decryption_failures) =
164 attachment::decrypt_attachments_with_failures(
165 cipher.attachments.as_deref().unwrap_or_default(),
166 ctx,
167 cipher_key,
168 );
169
170 let local_data = cipher.local_data.decrypt(ctx, cipher_key).ok().flatten();
171
172 let mut view = CipherView {
173 id: cipher.id,
175 organization_id: cipher.organization_id,
176 folder_id: cipher.folder_id,
177 collection_ids: cipher.collection_ids.clone(),
178 key: cipher.key.clone(),
179 r#type: cipher.r#type,
180 favorite: cipher.favorite,
181 reprompt: cipher.reprompt,
182 organization_use_totp: cipher.organization_use_totp,
183 edit: cipher.edit,
184 permissions: cipher.permissions,
185 view_password: cipher.view_password,
186 creation_date: cipher.creation_date,
187 deleted_date: cipher.deleted_date,
188 revision_date: cipher.revision_date,
189 archived_date: cipher.archived_date,
190
191 attachments: Some(attachments),
193 attachment_decryption_failures: Some(attachment_decryption_failures),
194 local_data,
195
196 name: String::new(),
198 notes: None,
199 login: None,
200 identity: None,
201 card: None,
202 secure_note: None,
203 ssh_key: None,
204 bank_account: None,
205 drivers_license: None,
206 passport: None,
207 fields: None,
208 password_history: None,
209 };
210
211 blob.apply_to_cipher_view(&mut view, ctx, cipher_key)?;
212
213 Ok(view)
214}
215
216#[cfg(test)]
217mod tests {
218 use bitwarden_crypto::{IdentifyKey, PrimitiveEncryptable};
219 use uuid::Uuid;
220
221 use super::*;
222 use crate::{
223 cipher::{
224 bank_account::BankAccountView,
225 blob::conversions::test_support::{create_shell_cipher_view, create_test_key_store},
226 card::CardView,
227 cipher::{CipherId, CipherRepromptType, CipherType},
228 field::{FieldType, FieldView},
229 identity::IdentityView,
230 login::LoginView,
231 secure_note::{SecureNoteType, SecureNoteView},
232 ssh_key::SshKeyView,
233 },
234 password_history::PasswordHistoryView,
235 };
236
237 fn make_test_cipher_with_data(
238 ctx: &mut KeyStoreContext<KeySlotIds>,
239 data: Option<String>,
240 ) -> Cipher {
241 let name = "test"
242 .encrypt(
243 ctx,
244 bitwarden_core::key_management::SymmetricKeySlotId::User,
245 )
246 .unwrap();
247 Cipher {
248 id: None,
249 organization_id: None,
250 folder_id: None,
251 collection_ids: vec![],
252 key: None,
253 name: Some(name),
254 notes: None,
255 r#type: CipherType::SecureNote,
256 login: None,
257 identity: None,
258 card: None,
259 secure_note: None,
260 ssh_key: None,
261 bank_account: None,
262 drivers_license: None,
263 passport: None,
264 favorite: false,
265 reprompt: CipherRepromptType::None,
266 organization_use_totp: false,
267 edit: true,
268 permissions: None,
269 view_password: true,
270 local_data: None,
271 attachments: None,
272 fields: None,
273 password_history: None,
274 creation_date: chrono::Utc::now(),
275 deleted_date: None,
276 revision_date: chrono::Utc::now(),
277 archived_date: None,
278 data,
279 }
280 }
281
282 #[test]
283 fn test_try_parse_blob_returns_some_after_encrypt() {
284 let (key_store, _) = create_test_key_store();
285 let mut ctx = key_store.context_mut();
286
287 let mut view = create_shell_cipher_view(CipherType::SecureNote);
288 view.name = "Blob Test".to_string();
289 view.secure_note = Some(SecureNoteView {
290 r#type: SecureNoteType::Generic,
291 });
292
293 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
294 assert!(try_parse_blob(&cipher).is_some());
295 }
296
297 #[test]
298 fn test_seal_unseal_round_trip() {
299 let (key_store, _) = create_test_key_store();
300 let mut ctx = key_store.context_mut();
301
302 let mut view = create_shell_cipher_view(CipherType::SecureNote);
303 view.name = "Round Trip".to_string();
304 view.notes = Some("Some notes".to_string());
305 view.secure_note = Some(SecureNoteView {
306 r#type: SecureNoteType::Generic,
307 });
308 view.generate_cipher_key(&mut ctx, view.key_identifier())
309 .unwrap();
310
311 let sealed_string = seal_cipher(&view, &mut ctx, view.key_identifier()).unwrap();
312
313 let mut cipher = make_test_cipher_with_data(&mut ctx, Some(sealed_string));
314 cipher.key = view.key.clone();
315
316 let view = decrypt_blob_cipher(
317 &cipher,
318 &try_parse_blob(&cipher).unwrap(),
319 &mut ctx,
320 cipher.key_identifier(),
321 )
322 .unwrap();
323 assert_eq!(view.name, "Round Trip");
324 assert_eq!(view.notes, Some("Some notes".to_string()));
325 }
326
327 #[test]
328 fn test_encrypt_blob_cipher_sets_data() {
329 let (key_store, _) = create_test_key_store();
330 let mut ctx = key_store.context_mut();
331
332 let mut view = create_shell_cipher_view(CipherType::SecureNote);
333 view.name = "Has Data".to_string();
334 view.secure_note = Some(SecureNoteView {
335 r#type: SecureNoteType::Generic,
336 });
337
338 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
339 assert!(cipher.data.is_some());
340 }
341
342 #[test]
343 fn test_encrypt_blob_cipher_clears_legacy_fields() {
344 let (key_store, _) = create_test_key_store();
345 let mut ctx = key_store.context_mut();
346
347 let mut view = create_shell_cipher_view(CipherType::Login);
348 view.name = "Login".to_string();
349 view.login = Some(LoginView {
350 username: Some("user".to_string()),
351 password: Some("pass".to_string()),
352 password_revision_date: None,
353 uris: None,
354 totp: None,
355 autofill_on_page_load: None,
356 fido2_credentials: None,
357 });
358
359 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
360 assert!(cipher.login.is_none());
361 assert!(cipher.card.is_none());
362 assert!(cipher.identity.is_none());
363 assert!(cipher.secure_note.is_none());
364 assert!(cipher.ssh_key.is_none());
365 assert!(cipher.bank_account.is_none());
366 assert!(cipher.notes.is_none());
367 assert!(cipher.fields.is_none());
368 assert!(cipher.password_history.is_none());
369 }
370
371 #[test]
372 fn test_encrypt_blob_cipher_generates_key() {
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.secure_note = Some(SecureNoteView {
378 r#type: SecureNoteType::Generic,
379 });
380 assert!(view.key.is_none());
381
382 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
383 assert!(cipher.key.is_some());
384 assert!(view.key.is_some());
385 }
386
387 #[test]
388 fn test_encrypt_blob_cipher_preserves_metadata() {
389 let (key_store, _) = create_test_key_store();
390 let mut ctx = key_store.context_mut();
391
392 let cipher_id = CipherId::new(Uuid::new_v4());
393 let mut view = create_shell_cipher_view(CipherType::SecureNote);
394 view.id = Some(cipher_id);
395 view.favorite = true;
396 view.reprompt = CipherRepromptType::Password;
397 view.name = "Metadata Test".to_string();
398 view.secure_note = Some(SecureNoteView {
399 r#type: SecureNoteType::Generic,
400 });
401
402 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
403 assert_eq!(cipher.id, Some(cipher_id));
404 assert!(cipher.favorite);
405 assert_eq!(cipher.reprompt, CipherRepromptType::Password);
406 assert_eq!(cipher.r#type, CipherType::SecureNote);
407 assert_eq!(cipher.creation_date, view.creation_date);
408 assert_eq!(cipher.revision_date, view.revision_date);
409 }
410
411 #[test]
412 fn test_encrypt_blob_cipher_each_type() {
413 let (key_store, _) = create_test_key_store();
414
415 {
417 let mut ctx = key_store.context_mut();
418 let mut view = create_shell_cipher_view(CipherType::Login);
419 view.name = "Login".to_string();
420 view.login = Some(LoginView {
421 username: Some("user".to_string()),
422 password: None,
423 password_revision_date: None,
424 uris: None,
425 totp: None,
426 autofill_on_page_load: None,
427 fido2_credentials: None,
428 });
429 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
430 }
431
432 {
434 let mut ctx = key_store.context_mut();
435 let mut view = create_shell_cipher_view(CipherType::Card);
436 view.name = "Card".to_string();
437 view.card = Some(CardView {
438 cardholder_name: Some("John".to_string()),
439 exp_month: None,
440 exp_year: None,
441 code: None,
442 brand: None,
443 number: None,
444 });
445 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
446 }
447
448 {
450 let mut ctx = key_store.context_mut();
451 let mut view = create_shell_cipher_view(CipherType::Identity);
452 view.name = "Identity".to_string();
453 view.identity = Some(IdentityView {
454 title: None,
455 first_name: Some("Jane".to_string()),
456 middle_name: None,
457 last_name: None,
458 address1: None,
459 address2: None,
460 address3: None,
461 city: None,
462 state: None,
463 postal_code: None,
464 country: None,
465 company: None,
466 email: None,
467 phone: None,
468 ssn: None,
469 username: None,
470 passport_number: None,
471 license_number: None,
472 });
473 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
474 }
475
476 {
478 let mut ctx = key_store.context_mut();
479 let mut view = create_shell_cipher_view(CipherType::SecureNote);
480 view.name = "Note".to_string();
481 view.secure_note = Some(SecureNoteView {
482 r#type: SecureNoteType::Generic,
483 });
484 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
485 }
486
487 {
489 let mut ctx = key_store.context_mut();
490 let mut view = create_shell_cipher_view(CipherType::SshKey);
491 view.name = "SSH".to_string();
492 view.ssh_key = Some(SshKeyView {
493 private_key: "private".to_string(),
494 public_key: "public".to_string(),
495 fingerprint: "fingerprint".to_string(),
496 });
497 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
498 }
499
500 {
502 let mut ctx = key_store.context_mut();
503 let mut view = create_shell_cipher_view(CipherType::BankAccount);
504 view.name = "Bank".to_string();
505 view.bank_account = Some(BankAccountView {
506 bank_name: Some("Bank".to_string()),
507 name_on_account: None,
508 account_type: None,
509 account_number: None,
510 routing_number: None,
511 branch_number: None,
512 pin: None,
513 swift_code: None,
514 iban: None,
515 bank_contact_phone: None,
516 });
517 assert!(encrypt_blob_cipher(&mut view, &mut ctx).is_ok());
518 }
519 }
520
521 #[test]
522 fn test_end_to_end_round_trip() {
523 let (key_store, _) = create_test_key_store();
524 let mut ctx = key_store.context_mut();
525
526 let mut view = create_shell_cipher_view(CipherType::Login);
527 view.name = "My Login".to_string();
528 view.notes = Some("Secret notes".to_string());
529 view.login = Some(LoginView {
530 username: Some("[email protected]".to_string()),
531 password: Some("p@ssw0rd".to_string()),
532 password_revision_date: None,
533 uris: None,
534 totp: None,
535 autofill_on_page_load: None,
536 fido2_credentials: None,
537 });
538 view.fields = Some(vec![FieldView {
539 name: Some("custom".to_string()),
540 value: Some("field-value".to_string()),
541 r#type: FieldType::Text,
542 linked_id: None,
543 }]);
544 let history_date = chrono::Utc::now();
545 view.password_history = Some(vec![PasswordHistoryView {
546 password: "old-p@ssw0rd".to_string(),
547 last_used_date: history_date,
548 }]);
549
550 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
551 assert!(try_parse_blob(&cipher).is_some());
552
553 let restored = decrypt_blob_cipher(
554 &cipher,
555 &try_parse_blob(&cipher).unwrap(),
556 &mut ctx,
557 cipher.key_identifier(),
558 )
559 .unwrap();
560
561 assert_eq!(restored.name, "My Login");
562 assert_eq!(restored.notes, Some("Secret notes".to_string()));
563 let login = restored.login.unwrap();
564 assert_eq!(login.username, Some("[email protected]".to_string()));
565 assert_eq!(login.password, Some("p@ssw0rd".to_string()));
566
567 let fields = restored.fields.unwrap();
568 assert_eq!(fields.len(), 1);
569 assert_eq!(fields[0].name, Some("custom".to_string()));
570 assert_eq!(fields[0].value, Some("field-value".to_string()));
571 assert_eq!(fields[0].r#type, FieldType::Text);
572
573 let history = restored.password_history.unwrap();
574 assert_eq!(history.len(), 1);
575 assert_eq!(history[0].password, "old-p@ssw0rd");
576 assert_eq!(history[0].last_used_date, history_date);
577 }
578
579 #[test]
580 fn test_decrypt_blob_cipher() {
581 let (key_store, _) = create_test_key_store();
582 let mut ctx = key_store.context_mut();
583
584 let mut view = create_shell_cipher_view(CipherType::Card);
585 view.name = "My Card".to_string();
586 view.notes = Some("Card notes".to_string());
587 view.card = Some(CardView {
588 cardholder_name: Some("John Doe".to_string()),
589 exp_month: Some("12".to_string()),
590 exp_year: Some("2030".to_string()),
591 code: Some("123".to_string()),
592 brand: Some("Visa".to_string()),
593 number: Some("4111111111111111".to_string()),
594 });
595
596 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
597 let restored = decrypt_blob_cipher(
598 &cipher,
599 &try_parse_blob(&cipher).unwrap(),
600 &mut ctx,
601 cipher.key_identifier(),
602 )
603 .unwrap();
604
605 assert_eq!(restored.name, "My Card");
606 assert_eq!(restored.notes, Some("Card notes".to_string()));
607 let card = restored.card.unwrap();
608 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
609 assert_eq!(card.number, Some("4111111111111111".to_string()));
610 assert_eq!(card.code, Some("123".to_string()));
611 assert_eq!(card.brand, Some("Visa".to_string()));
612 }
613
614 #[test]
615 fn test_decrypt_blob_cipher_preserves_metadata() {
616 let (key_store, _) = create_test_key_store();
617 let mut ctx = key_store.context_mut();
618
619 let cipher_id = CipherId::new(Uuid::new_v4());
620 let mut view = create_shell_cipher_view(CipherType::SecureNote);
621 view.id = Some(cipher_id);
622 view.favorite = true;
623 view.reprompt = CipherRepromptType::Password;
624 view.organization_use_totp = true;
625 view.edit = false;
626 view.view_password = false;
627 view.name = "Metadata".to_string();
628 view.secure_note = Some(SecureNoteView {
629 r#type: SecureNoteType::Generic,
630 });
631 let creation_date = view.creation_date;
632 let revision_date = view.revision_date;
633
634 let cipher = encrypt_blob_cipher(&mut view, &mut ctx).unwrap();
635 let restored = decrypt_blob_cipher(
636 &cipher,
637 &try_parse_blob(&cipher).unwrap(),
638 &mut ctx,
639 cipher.key_identifier(),
640 )
641 .unwrap();
642
643 assert_eq!(restored.id, Some(cipher_id));
644 assert!(restored.favorite);
645 assert_eq!(restored.reprompt, CipherRepromptType::Password);
646 assert!(restored.organization_use_totp);
647 assert!(!restored.edit);
648 assert!(!restored.view_password);
649 assert_eq!(restored.r#type, CipherType::SecureNote);
650 assert_eq!(restored.creation_date, creation_date);
651 assert_eq!(restored.revision_date, revision_date);
652 assert!(restored.key.is_some());
653 }
654
655 #[test]
656 fn test_try_parse_blob_returns_none_for_legacy() {
657 let (key_store, _) = create_test_key_store();
658 let mut ctx = key_store.context_mut();
659
660 let cipher = make_test_cipher_with_data(&mut ctx, None);
661 assert!(try_parse_blob(&cipher).is_none());
662
663 let cipher = make_test_cipher_with_data(&mut ctx, Some("not a blob".to_string()));
664 assert!(try_parse_blob(&cipher).is_none());
665 }
666}