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