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