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