1use bitwarden_api_api::models::{
2 CipherDetailsResponseModel, CipherMiniDetailsResponseModel, CipherMiniResponseModel,
3 CipherRequestModel, CipherResponseModel, CipherWithIdRequestModel,
4};
5use bitwarden_collections::collection::CollectionId;
6use bitwarden_core::{
7 ApiError, MissingFieldError, OrganizationId, UserId,
8 key_management::{KeySlotIds, MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SymmetricKeySlotId},
9 require,
10};
11use bitwarden_crypto::{
12 CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
13 PrimitiveEncryptable, SymmetricCryptoKey, SymmetricKeyAlgorithm,
14};
15use bitwarden_error::bitwarden_error;
16use bitwarden_state::repository::RepositoryError;
17use bitwarden_uuid::uuid_newtype;
18use chrono::{DateTime, SecondsFormat, Utc};
19use serde::{Deserialize, Serialize};
20use serde_repr::{Deserialize_repr, Serialize_repr};
21use thiserror::Error;
22
23use super::{
24 attachment, bank_account,
25 bank_account::BankAccountListView,
26 blob::{decrypt_blob_cipher, encrypt_blob_cipher_with_wrapping_key, try_parse_blob},
27 card,
28 card::CardListView,
29 cipher_permissions::CipherPermissions,
30 drivers_license, field, identity,
31 local_data::{LocalData, LocalDataView},
32 login::{LoginListView, LoginUri, LoginUriView},
33 passport, secure_note, ssh_key,
34};
35use crate::{
36 DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView, FieldView, FolderId,
37 Login, LoginView, VaultParseError,
38 password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
39};
40
41uuid_newtype!(pub CipherId);
42
43#[allow(missing_docs)]
44#[bitwarden_error(flat)]
45#[derive(Debug, Error)]
46pub enum CipherError {
47 #[error(transparent)]
48 MissingField(#[from] MissingFieldError),
49 #[error(transparent)]
50 Crypto(#[from] CryptoError),
51 #[error(transparent)]
52 Decrypt(#[from] DecryptError),
53 #[error(transparent)]
54 Encrypt(#[from] EncryptError),
55 #[error(
56 "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
57 )]
58 AttachmentsWithoutKeys,
59 #[error("This cipher cannot be moved to the specified organization")]
60 OrganizationAlreadySet,
61 #[error(transparent)]
62 Repository(#[from] RepositoryError),
63 #[error(transparent)]
64 Chrono(#[from] chrono::ParseError),
65 #[error(transparent)]
66 SerdeJson(#[from] serde_json::Error),
67 #[error(transparent)]
68 Api(#[from] ApiError),
69}
70
71pub(super) trait CipherKind {
73 fn decrypt_subtitle(
75 &self,
76 ctx: &mut KeyStoreContext<KeySlotIds>,
77 key: SymmetricKeySlotId,
78 ) -> Result<String, CryptoError>;
79
80 fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
82}
83
84#[allow(missing_docs)]
85#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
86#[repr(u8)]
87#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
88#[bitwarden_ffi::wasm_object]
89pub enum CipherType {
90 Login = 1,
91 SecureNote = 2,
92 Card = 3,
93 Identity = 4,
94 SshKey = 5,
95 BankAccount = 6,
96 DriversLicense = 7,
97 Passport = 8,
98}
99
100#[allow(missing_docs)]
101#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
102#[repr(u8)]
103#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
104#[bitwarden_ffi::wasm_object]
105pub enum CipherRepromptType {
106 #[default]
107 None = 0,
108 Password = 1,
109}
110
111#[serde_with::skip_serializing_none]
112#[allow(missing_docs)]
113#[derive(Serialize, Deserialize, Debug, Clone)]
114#[serde(rename_all = "camelCase", deny_unknown_fields)]
115#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
116#[bitwarden_ffi::wasm_record]
117pub struct EncryptionContext {
118 pub encrypted_for: UserId,
121 #[serde(default)]
125 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
126 #[cfg_attr(feature = "wasm", tsify(optional))]
127 pub encrypted_by_key_id: Option<String>,
128 pub cipher: Cipher,
129}
130
131impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
132 type Error = CipherError;
133 fn try_from(
134 EncryptionContext {
135 cipher,
136 encrypted_for,
137 encrypted_by_key_id,
138 }: EncryptionContext,
139 ) -> Result<Self, Self::Error> {
140 Ok(Self {
141 id: require!(cipher.id).into(),
142 encrypted_for: Some(encrypted_for.into()),
143 encrypted_by_key_id,
144 r#type: Some(cipher.r#type.into()),
145 organization_id: cipher.organization_id.map(|o| o.to_string()),
146 is_organization_cipher: None,
147 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
148 favorite: cipher.favorite.into(),
149 reprompt: Some(cipher.reprompt.into()),
150 key: cipher.key.map(|k| k.to_string()),
151 name: cipher.name.as_ref().map(ToString::to_string),
152 notes: cipher.notes.map(|n| n.to_string()),
153 fields: Some(
154 cipher
155 .fields
156 .into_iter()
157 .flatten()
158 .map(Into::into)
159 .collect(),
160 ),
161 password_history: Some(
162 cipher
163 .password_history
164 .into_iter()
165 .flatten()
166 .map(Into::into)
167 .collect(),
168 ),
169 attachments: None,
170 attachments2: Some(
171 cipher
172 .attachments
173 .into_iter()
174 .flatten()
175 .filter_map(|a| {
176 a.id.map(|id| {
177 (
178 id,
179 bitwarden_api_api::models::CipherAttachmentModel {
180 file_name: a.file_name.map(|n| n.to_string()),
181 key: a.key.map(|k| k.to_string()),
182 },
183 )
184 })
185 })
186 .collect(),
187 ),
188 login: cipher.login.map(|l| Box::new(l.into())),
189 card: cipher.card.map(|c| Box::new(c.into())),
190 identity: cipher.identity.map(|i| Box::new(i.into())),
191 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
192 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
193 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
194 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
195 passport: cipher.passport.map(|p| Box::new(p.into())),
196 data: cipher.data,
197 last_known_revision_date: Some(
198 cipher
199 .revision_date
200 .to_rfc3339_opts(SecondsFormat::Millis, true),
201 ),
202 archived_date: cipher
203 .archived_date
204 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
205 })
206 }
207}
208
209impl From<EncryptionContext> for CipherRequestModel {
210 fn from(
211 EncryptionContext {
212 cipher,
213 encrypted_for,
214 encrypted_by_key_id,
215 }: EncryptionContext,
216 ) -> Self {
217 Self {
218 encrypted_for: Some(encrypted_for.into()),
219 encrypted_by_key_id,
220 r#type: Some(cipher.r#type.into()),
221 organization_id: cipher.organization_id.map(|o| o.to_string()),
222 is_organization_cipher: None,
223 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
224 favorite: cipher.favorite.into(),
225 reprompt: Some(cipher.reprompt.into()),
226 key: cipher.key.map(|k| k.to_string()),
227 name: cipher.name.as_ref().map(ToString::to_string),
228 notes: cipher.notes.map(|n| n.to_string()),
229 fields: Some(
230 cipher
231 .fields
232 .into_iter()
233 .flatten()
234 .map(Into::into)
235 .collect(),
236 ),
237 password_history: Some(
238 cipher
239 .password_history
240 .into_iter()
241 .flatten()
242 .map(Into::into)
243 .collect(),
244 ),
245 attachments: None,
246 attachments2: Some(
247 cipher
248 .attachments
249 .into_iter()
250 .flatten()
251 .filter_map(|a| {
252 a.id.map(|id| {
253 (
254 id,
255 bitwarden_api_api::models::CipherAttachmentModel {
256 file_name: a.file_name.map(|n| n.to_string()),
257 key: a.key.map(|k| k.to_string()),
258 },
259 )
260 })
261 })
262 .collect(),
263 ),
264 login: cipher.login.map(|l| Box::new(l.into())),
265 card: cipher.card.map(|c| Box::new(c.into())),
266 identity: cipher.identity.map(|i| Box::new(i.into())),
267 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
268 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
269 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
270 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
271 passport: cipher.passport.map(|p| Box::new(p.into())),
272 data: cipher.data,
273 last_known_revision_date: Some(
274 cipher
275 .revision_date
276 .to_rfc3339_opts(SecondsFormat::Millis, true),
277 ),
278 archived_date: cipher
279 .archived_date
280 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
281 }
282 }
283}
284
285#[serde_with::skip_serializing_none]
298#[allow(missing_docs)]
299#[derive(Serialize, Deserialize, Debug, Clone)]
300#[serde(rename_all = "camelCase", deny_unknown_fields)]
301#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
302#[bitwarden_ffi::wasm_record]
303pub struct Cipher {
304 pub id: Option<CipherId>,
305 pub organization_id: Option<OrganizationId>,
306 pub folder_id: Option<FolderId>,
307 pub collection_ids: Vec<CollectionId>,
308 pub key: Option<EncString>,
312
313 pub name: Option<EncString>,
317 pub notes: Option<EncString>,
319
320 pub r#type: CipherType,
321 pub login: Option<Login>,
323 pub identity: Option<identity::Identity>,
325 pub card: Option<card::Card>,
327 pub secure_note: Option<secure_note::SecureNote>,
329 pub ssh_key: Option<ssh_key::SshKey>,
331 pub bank_account: Option<bank_account::BankAccount>,
333 pub drivers_license: Option<drivers_license::DriversLicense>,
335 pub passport: Option<passport::Passport>,
337
338 pub favorite: bool,
339 pub reprompt: CipherRepromptType,
340 pub organization_use_totp: bool,
341 pub edit: bool,
342 pub permissions: Option<CipherPermissions>,
343 pub view_password: bool,
344 pub local_data: Option<LocalData>,
345
346 pub attachments: Option<Vec<attachment::Attachment>>,
347 pub fields: Option<Vec<field::Field>>,
349 pub password_history: Option<Vec<password_history::PasswordHistory>>,
351
352 pub creation_date: DateTime<Utc>,
353 pub deleted_date: Option<DateTime<Utc>>,
354 pub revision_date: DateTime<Utc>,
355 pub archived_date: Option<DateTime<Utc>>,
356 pub data: Option<String>,
359
360 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub partial_data: Option<String>,
366}
367
368pub enum CipherKeyRewrapError {
371 NoCipherKey,
372 DecryptionFailure,
373 EncryptionFailure,
374}
375
376impl Cipher {
377 pub fn rewrap_cipher_key(
382 &mut self,
383 old_key: SymmetricKeySlotId,
384 new_key: SymmetricKeySlotId,
385 ctx: &mut KeyStoreContext<KeySlotIds>,
386 ) -> Result<(), CipherKeyRewrapError> {
387 let new_cipher_key = self
388 .key
389 .as_ref()
390 .ok_or(CipherKeyRewrapError::NoCipherKey)
391 .and_then(|wrapped_cipher_key| {
392 ctx.unwrap_symmetric_key(old_key, wrapped_cipher_key)
393 .map_err(|_| CipherKeyRewrapError::DecryptionFailure)
394 })
395 .and_then(|cipher_key| {
396 ctx.wrap_symmetric_key(new_key, cipher_key)
397 .map_err(|_| CipherKeyRewrapError::EncryptionFailure)
398 })?;
399 self.key = Some(new_cipher_key);
400 Ok(())
401 }
402
403 pub fn is_blob_encrypted(&self) -> bool {
405 try_parse_blob(self).is_some()
406 }
407}
408
409bitwarden_state::register_repository_item!(CipherId => Cipher, "Cipher");
410
411impl TryFrom<Cipher> for CipherRequestModel {
412 type Error = CryptoError;
413
414 fn try_from(c: Cipher) -> Result<Self, Self::Error> {
421 let attachments2 = c
422 .attachments
423 .map(|list| {
424 list.into_iter()
425 .map(|a| {
426 let id = a.id.clone().ok_or(CryptoError::MissingField("id"))?;
427 Ok::<_, CryptoError>((id, a.into()))
428 })
429 .collect::<Result<_, _>>()
430 })
431 .transpose()?;
432
433 Ok(CipherRequestModel {
434 encrypted_for: None,
435 encrypted_by_key_id: None,
436 r#type: Some(c.r#type.into()),
437 organization_id: c.organization_id.map(|id| id.to_string()),
438 is_organization_cipher: None,
439 folder_id: c.folder_id.map(|id| id.to_string()),
440 favorite: Some(c.favorite),
441 reprompt: Some(c.reprompt.into()),
442 key: c.key.map(|k| k.to_string()),
443 name: c.name.as_ref().map(ToString::to_string),
444 notes: c.notes.map(|n| n.to_string()),
445 login: c.login.map(|v| Box::new(v.into())),
446 card: c.card.map(|v| Box::new(v.into())),
447 identity: c.identity.map(|v| Box::new(v.into())),
448 secure_note: c.secure_note.map(|v| Box::new(v.into())),
449 ssh_key: c.ssh_key.map(|v| Box::new(v.into())),
450 bank_account: c.bank_account.map(|v| Box::new(v.into())),
451 drivers_license: c.drivers_license.map(|v| Box::new(v.into())),
452 passport: c.passport.map(|v| Box::new(v.into())),
453 fields: c.fields.map(|f| f.into_iter().map(Into::into).collect()),
454 password_history: c
455 .password_history
456 .map(|h| h.into_iter().map(Into::into).collect()),
457 attachments: None,
458 attachments2,
459 last_known_revision_date: Some(
460 c.revision_date.to_rfc3339_opts(SecondsFormat::Secs, true),
461 ),
462 archived_date: c.archived_date.map(|d| d.to_rfc3339()),
463 data: c.data,
464 })
465 }
466}
467
468#[serde_with::skip_serializing_none]
469#[allow(missing_docs)]
470#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
471#[serde(rename_all = "camelCase", deny_unknown_fields)]
472#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
473#[bitwarden_ffi::wasm_record]
474pub struct CipherView {
475 pub id: Option<CipherId>,
476 pub organization_id: Option<OrganizationId>,
477 pub folder_id: Option<FolderId>,
478 pub collection_ids: Vec<CollectionId>,
479
480 #[cfg_attr(feature = "wasm", tsify(type = "SymmetricKey | undefined"))]
481 pub key: Option<SymmetricCryptoKey>,
482
483 pub name: String,
484 pub notes: Option<String>,
485
486 pub r#type: CipherType,
487 pub login: Option<LoginView>,
488 pub identity: Option<identity::IdentityView>,
489 pub card: Option<card::CardView>,
490 pub secure_note: Option<secure_note::SecureNoteView>,
491 pub ssh_key: Option<ssh_key::SshKeyView>,
492 pub bank_account: Option<bank_account::BankAccountView>,
493 pub drivers_license: Option<drivers_license::DriversLicenseView>,
494 pub passport: Option<passport::PassportView>,
495
496 pub favorite: bool,
497 pub reprompt: CipherRepromptType,
498 pub organization_use_totp: bool,
499 pub edit: bool,
500 pub permissions: Option<CipherPermissions>,
501 pub view_password: bool,
502 pub local_data: Option<LocalDataView>,
503
504 pub attachments: Option<Vec<attachment::AttachmentView>>,
505 #[serde(skip_serializing_if = "Option::is_none")]
507 pub attachment_decryption_failures: Option<Vec<attachment::AttachmentView>>,
508 pub fields: Option<Vec<field::FieldView>>,
509 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
510 pub creation_date: DateTime<Utc>,
511 pub deleted_date: Option<DateTime<Utc>>,
512 pub revision_date: DateTime<Utc>,
513 pub archived_date: Option<DateTime<Utc>>,
514
515 #[serde(default)]
522 pub partial: bool,
523}
524
525#[allow(missing_docs)]
526#[derive(Serialize, Deserialize, Debug, PartialEq)]
527#[serde(rename_all = "camelCase", deny_unknown_fields)]
528#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
529#[bitwarden_ffi::wasm_record]
530pub enum CipherListViewType {
531 Login(LoginListView),
532 SecureNote,
533 Card(CardListView),
534 Identity,
535 SshKey,
536 BankAccount(BankAccountListView),
537 Passport,
538 DriversLicense,
539}
540
541#[derive(Serialize, Deserialize, Debug, PartialEq)]
543#[bitwarden_ffi::wasm_record]
544#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
545pub enum CopyableCipherFields {
546 LoginUsername,
547 LoginPassword,
548 LoginTotp,
549 CardNumber,
550 CardSecurityCode,
551 IdentityUsername,
552 IdentityEmail,
553 IdentityPhone,
554 IdentityAddress,
555 SshKey,
556 SecureNotes,
557 BankAccountNameOnAccount,
558 BankAccountAccountNumber,
559 BankAccountRoutingNumber,
560 BankAccountBranchNumber,
561 BankAccountPin,
562 BankAccountIban,
563 BankAccountSwift,
564 PassportGivenName,
565 PassportSurname,
566 PassportPassportNumber,
567 PassportNationalIdentificationNumber,
568 DriversLicenseFirstName,
569 DriversLicenseMiddleName,
570 DriversLicenseLastName,
571 DriversLicenseLicenseNumber,
572}
573
574#[serde_with::skip_serializing_none]
575#[allow(missing_docs)]
576#[derive(Serialize, Deserialize, Debug, PartialEq)]
577#[serde(rename_all = "camelCase", deny_unknown_fields)]
578#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
579#[bitwarden_ffi::wasm_record]
580pub struct CipherListView {
581 pub id: Option<CipherId>,
582 pub organization_id: Option<OrganizationId>,
583 pub folder_id: Option<FolderId>,
584 pub collection_ids: Vec<CollectionId>,
585
586 pub name: String,
587 pub subtitle: String,
588
589 pub r#type: CipherListViewType,
590
591 pub favorite: bool,
592 pub reprompt: CipherRepromptType,
593 pub organization_use_totp: bool,
594 pub edit: bool,
595 pub permissions: Option<CipherPermissions>,
596
597 pub view_password: bool,
598
599 pub attachments: u32,
601 pub has_old_attachments: bool,
603
604 pub creation_date: DateTime<Utc>,
605 pub deleted_date: Option<DateTime<Utc>>,
606 pub revision_date: DateTime<Utc>,
607 pub archived_date: Option<DateTime<Utc>>,
608
609 pub copyable_fields: Vec<CopyableCipherFields>,
611
612 pub local_data: Option<LocalDataView>,
613
614 #[serde(default)]
617 pub partial: bool,
618
619 #[cfg(feature = "wasm")]
621 pub notes: Option<String>,
622 #[cfg(feature = "wasm")]
625 pub fields: Option<Vec<field::FieldListView>>,
626 #[cfg(feature = "wasm")]
628 pub attachment_names: Option<Vec<String>>,
629}
630
631#[derive(Serialize, Deserialize, Debug)]
637#[serde(rename_all = "camelCase", deny_unknown_fields)]
638#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
639#[bitwarden_ffi::wasm_record]
640pub struct DecryptCipherListResult {
641 pub successes: Vec<CipherListView>,
643 pub failures: Vec<Cipher>,
645}
646
647#[derive(Serialize, Deserialize, Debug)]
653#[serde(rename_all = "camelCase", deny_unknown_fields)]
654#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
655#[bitwarden_ffi::wasm_record]
656pub struct DecryptCipherResult {
657 pub successes: Vec<CipherView>,
659 pub failures: Vec<Cipher>,
661}
662
663#[derive(Serialize, Deserialize, Debug)]
667#[serde(rename_all = "camelCase", deny_unknown_fields)]
668#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
669#[bitwarden_ffi::wasm_record]
670pub struct ListOrganizationCiphersResult {
671 pub ciphers: Vec<Cipher>,
673 pub list_views: Vec<CipherListView>,
675}
676
677impl CipherListView {
678 pub(crate) fn get_totp_key(self) -> Result<Option<String>, CryptoError> {
679 Ok(match self.r#type {
680 CipherListViewType::Login(LoginListView { totp, .. }) => totp,
681 _ => None,
682 })
683 }
684}
685
686impl CipherView {
687 fn encrypt_legacy_field_encryption(
688 &mut self,
689 ctx: &mut KeyStoreContext<KeySlotIds>,
690 key: SymmetricKeySlotId,
691 ) -> Result<Cipher, CryptoError> {
692 if self.partial {
696 return Err(CryptoError::EncryptRestrictedView);
697 }
698
699 let ciphers_key = self.load_cipher_key_slot(ctx)?;
700
701 self.generate_checksums();
702
703 Ok(Cipher {
704 partial_data: None,
705 id: self.id,
706 organization_id: self.organization_id,
707 folder_id: self.folder_id,
708 collection_ids: self.collection_ids.clone(),
709 key: Some(ctx.wrap_symmetric_key(key, ciphers_key)?),
710 name: Some(self.name.encrypt(ctx, ciphers_key)?),
711 notes: self.notes.encrypt(ctx, ciphers_key)?,
712 r#type: self.r#type,
713 login: self.login.encrypt_composite(ctx, ciphers_key)?,
714 identity: self.identity.encrypt_composite(ctx, ciphers_key)?,
715 card: self.card.encrypt_composite(ctx, ciphers_key)?,
716 secure_note: self.secure_note.encrypt_composite(ctx, ciphers_key)?,
717 ssh_key: self.ssh_key.encrypt_composite(ctx, ciphers_key)?,
718 bank_account: self.bank_account.encrypt_composite(ctx, ciphers_key)?,
719 drivers_license: self.drivers_license.encrypt_composite(ctx, ciphers_key)?,
720 passport: self.passport.encrypt_composite(ctx, ciphers_key)?,
721 favorite: self.favorite,
722 reprompt: self.reprompt,
723 organization_use_totp: self.organization_use_totp,
724 edit: self.edit,
725 view_password: self.view_password,
726 local_data: self.local_data.encrypt_composite(ctx, ciphers_key)?,
727 attachments: self.attachments.encrypt_composite(ctx, ciphers_key)?,
728 fields: self.fields.encrypt_composite(ctx, ciphers_key)?,
729 password_history: self.password_history.encrypt_composite(ctx, ciphers_key)?,
730 creation_date: self.creation_date,
731 deleted_date: self.deleted_date,
732 revision_date: self.revision_date,
733 permissions: self.permissions,
734 archived_date: self.archived_date,
735 data: None, })
737 }
738}
739
740pub(crate) fn lenient_decrypt_cipher_view(
746 cipher: &Cipher,
747 ctx: &mut KeyStoreContext<KeySlotIds>,
748 key: SymmetricKeySlotId,
749) -> Result<CipherView, CryptoError> {
750 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
751
752 let (attachments, attachment_decryption_failures) =
754 attachment::decrypt_attachments_with_failures(
755 cipher.attachments.as_deref().unwrap_or_default(),
756 ctx,
757 ciphers_key,
758 );
759
760 let mut view = CipherView {
761 partial: false,
762 id: cipher.id,
763 organization_id: cipher.organization_id,
764 folder_id: cipher.folder_id,
765 collection_ids: cipher.collection_ids.clone(),
766 key: if cipher.key.is_some() {
767 #[allow(deprecated)]
768 Some(ctx.dangerous_get_symmetric_key(ciphers_key)?.clone())
769 } else {
770 None
771 },
772 name: cipher
773 .name
774 .as_ref()
775 .and_then(|n| n.decrypt(ctx, ciphers_key).ok())
776 .unwrap_or_default(),
777 notes: cipher.notes.decrypt(ctx, ciphers_key).ok().flatten(),
778 r#type: cipher.r#type,
779 login: cipher.login.decrypt(ctx, ciphers_key).ok().flatten(),
780 identity: cipher.identity.decrypt(ctx, ciphers_key).ok().flatten(),
781 card: cipher.card.decrypt(ctx, ciphers_key).ok().flatten(),
782 secure_note: cipher.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
783 ssh_key: cipher.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
784 bank_account: cipher.bank_account.decrypt(ctx, ciphers_key).ok().flatten(),
785 drivers_license: cipher
786 .drivers_license
787 .decrypt(ctx, ciphers_key)
788 .ok()
789 .flatten(),
790 passport: cipher.passport.decrypt(ctx, ciphers_key).ok().flatten(),
791 favorite: cipher.favorite,
792 reprompt: cipher.reprompt,
793 organization_use_totp: cipher.organization_use_totp,
794 edit: cipher.edit,
795 permissions: cipher.permissions,
796 view_password: cipher.view_password,
797 local_data: cipher.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
798 attachments: Some(attachments),
799 attachment_decryption_failures: Some(attachment_decryption_failures),
800 fields: cipher.fields.decrypt(ctx, ciphers_key).ok().flatten(),
801 password_history: cipher
802 .password_history
803 .decrypt(ctx, ciphers_key)
804 .ok()
805 .flatten(),
806 creation_date: cipher.creation_date,
807 deleted_date: cipher.deleted_date,
808 revision_date: cipher.revision_date,
809 archived_date: cipher.archived_date,
810 };
811
812 if view.key.is_some()
815 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
816 {
817 view.remove_invalid_checksums();
818 }
819
820 Ok(view)
821}
822
823impl Cipher {
824 #[bitwarden_logging::instrument(err)]
835 pub(crate) fn decrypt_cipher_key(
836 ctx: &mut KeyStoreContext<KeySlotIds>,
837 key: SymmetricKeySlotId,
838 ciphers_key: &Option<EncString>,
839 ) -> Result<SymmetricKeySlotId, CryptoError> {
840 match ciphers_key {
841 Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
842 None => Ok(key),
843 }
844 }
845
846 #[bitwarden_logging::instrument(err)]
854 pub(crate) fn make_attachment_material(
855 &self,
856 ctx: &mut KeyStoreContext<KeySlotIds>,
857 file_name: &str,
858 ) -> Result<attachment::AttachmentMaterial, CryptoError> {
859 let cipher_key = Self::decrypt_cipher_key(ctx, self.key_identifier(), &self.key)?;
860 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
861 let slot = ctx.add_local_symmetric_key(key.clone());
862 let wrapped_key = ctx.wrap_symmetric_key(cipher_key, slot)?;
863 let encrypted_file_name = file_name.encrypt(ctx, cipher_key)?;
864 Ok(attachment::AttachmentMaterial {
865 key,
866 wrapped_key,
867 encrypted_file_name,
868 })
869 }
870
871 fn get_kind(&self) -> Option<&dyn CipherKind> {
873 match self.r#type {
874 CipherType::Login => self.login.as_ref().map(|v| v as _),
875 CipherType::Card => self.card.as_ref().map(|v| v as _),
876 CipherType::Identity => self.identity.as_ref().map(|v| v as _),
877 CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
878 CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
879 CipherType::BankAccount => self.bank_account.as_ref().map(|v| v as _),
880 CipherType::DriversLicense => self.drivers_license.as_ref().map(|v| v as _),
881 CipherType::Passport => self.passport.as_ref().map(|v| v as _),
882 }
883 }
884
885 fn decrypt_subtitle(
887 &self,
888 ctx: &mut KeyStoreContext<KeySlotIds>,
889 key: SymmetricKeySlotId,
890 ) -> Result<String, CryptoError> {
891 self.get_kind()
892 .map(|sub| sub.decrypt_subtitle(ctx, key))
893 .unwrap_or_else(|| Ok(String::new()))
894 }
895
896 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
899 self.get_kind()
900 .map(|kind| kind.get_copyable_fields(Some(self)))
901 .unwrap_or_default()
902 }
903
904 #[allow(unused)] pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
910 let data = self
911 .data
912 .as_ref()
913 .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
914
915 match &self.r#type {
916 crate::CipherType::Login => self.login = serde_json::from_str(data)?,
917 crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
918 crate::CipherType::Card => self.card = serde_json::from_str(data)?,
919 crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
920 crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
921 crate::CipherType::BankAccount => self.bank_account = serde_json::from_str(data)?,
922 crate::CipherType::DriversLicense => self.drivers_license = serde_json::from_str(data)?,
923 crate::CipherType::Passport => self.passport = serde_json::from_str(data)?,
924 }
925 Ok(())
926 }
927
928 pub(crate) fn soft_delete(&mut self) {
930 self.deleted_date = Some(Utc::now());
931 }
932}
933impl CipherView {
934 pub fn load_cipher_key_slot(
937 &mut self,
938 ctx: &mut KeyStoreContext<KeySlotIds>,
939 ) -> Result<SymmetricKeySlotId, CryptoError> {
940 if let Some(key) = &self.key {
941 return Ok(ctx.add_local_symmetric_key(key.clone()));
942 }
943
944 let new_key = ctx.generate_symmetric_key();
945 #[allow(deprecated)]
946 let new_key_raw = ctx.dangerous_get_symmetric_key(new_key)?.clone();
947 self.key = Some(new_key_raw);
948 Ok(new_key)
949 }
950
951 #[allow(missing_docs)]
952 pub fn generate_checksums(&mut self) {
953 if let Some(l) = self.login.as_mut() {
954 l.generate_checksums();
955 }
956 }
957
958 #[allow(missing_docs)]
959 pub fn remove_invalid_checksums(&mut self) {
960 if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
961 uris.retain(|u| u.is_checksum_valid());
962 }
963 }
964
965 #[allow(missing_docs)]
966 pub fn get_fido2_credentials(&self) -> Vec<Fido2CredentialView> {
967 self.login
968 .as_ref()
969 .and_then(|l| l.fido2_credentials.as_ref())
970 .cloned()
971 .unwrap_or_default()
972 }
973
974 pub fn move_to_organization(
979 &mut self,
980 organization_id: OrganizationId,
981 ) -> Result<(), CipherError> {
982 self.validate_attachment_keys()?;
983 self.organization_id = Some(organization_id);
984
985 Ok(())
986 }
987
988 pub fn validate_attachment_keys(&mut self) -> Result<(), CipherError> {
993 if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
994 return Err(CipherError::AttachmentsWithoutKeys);
995 }
996 Ok(())
997 }
998
999 #[allow(missing_docs)]
1000 pub fn set_new_fido2_credentials(
1001 &mut self,
1002 creds: Vec<Fido2CredentialFullView>,
1003 ) -> Result<(), CipherError> {
1004 require!(self.login.as_mut()).fido2_credentials =
1005 Some(creds.into_iter().map(Fido2CredentialView::from).collect());
1006 Ok(())
1007 }
1008
1009 pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
1010 let changes = self
1011 .login
1012 .as_mut()
1013 .map_or(vec![], |login| {
1014 login.detect_password_change(&original_cipher.login)
1015 })
1016 .into_iter()
1017 .chain(self.fields.as_deref().map_or(vec![], |fields| {
1018 FieldView::detect_hidden_field_changes(
1019 fields,
1020 original_cipher.fields.as_deref().unwrap_or(&[]),
1021 )
1022 }))
1023 .rev()
1024 .chain(original_cipher.password_history.iter().flatten().cloned())
1025 .take(MAX_PASSWORD_HISTORY_ENTRIES)
1026 .collect();
1027 self.password_history = Some(changes)
1028 }
1029
1030 pub(crate) fn to_list_view(&self) -> Result<CipherListView, CryptoError> {
1036 let all_attachments = || {
1037 self.attachments
1038 .iter()
1039 .flatten()
1040 .chain(self.attachment_decryption_failures.iter().flatten())
1041 };
1042 let attachments_count = all_attachments().count() as u32;
1043 let has_old_attachments = all_attachments().any(|att| att.key.is_none());
1044
1045 let list_type = match self.r#type {
1046 CipherType::Login => {
1047 let login = self
1048 .login
1049 .as_ref()
1050 .ok_or(CryptoError::MissingField("login"))?;
1051 CipherListViewType::Login(login.to_list_view())
1052 }
1053 CipherType::SecureNote => CipherListViewType::SecureNote,
1054 CipherType::Card => {
1055 let card = self
1056 .card
1057 .as_ref()
1058 .ok_or(CryptoError::MissingField("card"))?;
1059 CipherListViewType::Card(CardListView {
1060 brand: card.brand.clone(),
1061 })
1062 }
1063 CipherType::Identity => CipherListViewType::Identity,
1064 CipherType::SshKey => CipherListViewType::SshKey,
1065 CipherType::BankAccount => {
1066 let bank_account = self
1067 .bank_account
1068 .as_ref()
1069 .ok_or(CryptoError::MissingField("bank_account"))?;
1070 CipherListViewType::BankAccount(BankAccountListView {
1071 account_number: bank_account.account_number.clone(),
1072 account_type: bank_account.account_type.clone(),
1073 })
1074 }
1075 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1076 CipherType::Passport => CipherListViewType::Passport,
1077 };
1078
1079 Ok(CipherListView {
1080 partial: false,
1081 id: self.id,
1082 organization_id: self.organization_id,
1083 folder_id: self.folder_id,
1084 collection_ids: self.collection_ids.clone(),
1085 name: self.name.clone(),
1086 subtitle: self.subtitle(),
1087 r#type: list_type,
1088 favorite: self.favorite,
1089 reprompt: self.reprompt,
1090 organization_use_totp: self.organization_use_totp,
1091 edit: self.edit,
1092 permissions: self.permissions,
1093 view_password: self.view_password,
1094 attachments: attachments_count,
1095 has_old_attachments,
1096 creation_date: self.creation_date,
1097 deleted_date: self.deleted_date,
1098 revision_date: self.revision_date,
1099 archived_date: self.archived_date,
1100 copyable_fields: self.get_copyable_fields(),
1101 local_data: self.local_data.clone(),
1102 #[cfg(feature = "wasm")]
1103 notes: self.notes.clone(),
1104 #[cfg(feature = "wasm")]
1105 fields: self.fields.as_ref().map(|fields| {
1106 fields
1107 .iter()
1108 .cloned()
1109 .map(field::FieldListView::from)
1110 .collect()
1111 }),
1112 #[cfg(feature = "wasm")]
1113 attachment_names: self.attachments.as_ref().map(|attachments| {
1114 attachments
1115 .iter()
1116 .filter_map(|a| a.file_name.clone())
1117 .collect()
1118 }),
1119 })
1120 }
1121
1122 fn subtitle(&self) -> String {
1127 match self.r#type {
1128 CipherType::Login => self
1129 .login
1130 .as_ref()
1131 .and_then(|l| l.username.clone())
1132 .unwrap_or_default(),
1133 CipherType::Card => self
1134 .card
1135 .as_ref()
1136 .map(|c| card::build_subtitle_card(c.brand.clone(), c.number.clone()))
1137 .unwrap_or_default(),
1138 CipherType::Identity => self
1139 .identity
1140 .as_ref()
1141 .map(|i| {
1142 identity::build_subtitle_identity(i.first_name.clone(), i.last_name.clone())
1143 })
1144 .unwrap_or_default(),
1145 CipherType::SshKey => self
1146 .ssh_key
1147 .as_ref()
1148 .map(|s| s.fingerprint.clone())
1149 .unwrap_or_default(),
1150 CipherType::SecureNote => String::new(),
1151 CipherType::BankAccount => self
1152 .bank_account
1153 .as_ref()
1154 .map(|b| b.bank_name.clone().unwrap_or_default())
1155 .unwrap_or_default(),
1156 CipherType::DriversLicense => self
1157 .drivers_license
1158 .as_ref()
1159 .map(|d| {
1160 drivers_license::build_subtitle_drivers_license(
1161 d.first_name.clone(),
1162 d.last_name.clone(),
1163 d.issuing_state.clone(),
1164 )
1165 })
1166 .unwrap_or_default(),
1167 CipherType::Passport => self
1168 .passport
1169 .as_ref()
1170 .map(|p| {
1171 passport::build_subtitle_passport(
1172 p.given_name.clone(),
1173 p.surname.clone(),
1174 p.issuing_country.clone(),
1175 )
1176 })
1177 .unwrap_or_default(),
1178 }
1179 }
1180
1181 fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
1186 match self.r#type {
1187 CipherType::Login => self
1188 .login
1189 .as_ref()
1190 .map(|l| {
1191 [
1192 l.username
1193 .as_ref()
1194 .map(|_| CopyableCipherFields::LoginUsername),
1195 l.password
1196 .as_ref()
1197 .map(|_| CopyableCipherFields::LoginPassword),
1198 l.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
1199 ]
1200 .into_iter()
1201 .flatten()
1202 .collect()
1203 })
1204 .unwrap_or_default(),
1205 CipherType::Card => self
1206 .card
1207 .as_ref()
1208 .map(|c| {
1209 [
1210 c.number.as_ref().map(|_| CopyableCipherFields::CardNumber),
1211 c.code
1212 .as_ref()
1213 .map(|_| CopyableCipherFields::CardSecurityCode),
1214 ]
1215 .into_iter()
1216 .flatten()
1217 .collect()
1218 })
1219 .unwrap_or_default(),
1220 CipherType::Identity => self
1221 .identity
1222 .as_ref()
1223 .map(|i| {
1224 [
1225 i.username
1226 .as_ref()
1227 .map(|_| CopyableCipherFields::IdentityUsername),
1228 i.email
1229 .as_ref()
1230 .map(|_| CopyableCipherFields::IdentityEmail),
1231 i.phone
1232 .as_ref()
1233 .map(|_| CopyableCipherFields::IdentityPhone),
1234 i.address1
1235 .as_ref()
1236 .or(i.address2.as_ref())
1237 .or(i.address3.as_ref())
1238 .or(i.city.as_ref())
1239 .or(i.state.as_ref())
1240 .or(i.postal_code.as_ref())
1241 .map(|_| CopyableCipherFields::IdentityAddress),
1242 ]
1243 .into_iter()
1244 .flatten()
1245 .collect()
1246 })
1247 .unwrap_or_default(),
1248 CipherType::SshKey => vec![CopyableCipherFields::SshKey],
1249 CipherType::SecureNote => self
1250 .notes
1251 .as_ref()
1252 .map(|_| vec![CopyableCipherFields::SecureNotes])
1253 .unwrap_or_default(),
1254 CipherType::BankAccount => self
1255 .bank_account
1256 .as_ref()
1257 .map(|b| {
1258 [
1259 b.name_on_account
1260 .as_ref()
1261 .map(|_| CopyableCipherFields::BankAccountNameOnAccount),
1262 b.account_number
1263 .as_ref()
1264 .map(|_| CopyableCipherFields::BankAccountAccountNumber),
1265 b.routing_number
1266 .as_ref()
1267 .map(|_| CopyableCipherFields::BankAccountRoutingNumber),
1268 b.branch_number
1269 .as_ref()
1270 .map(|_| CopyableCipherFields::BankAccountBranchNumber),
1271 b.pin.as_ref().map(|_| CopyableCipherFields::BankAccountPin),
1272 b.iban
1273 .as_ref()
1274 .map(|_| CopyableCipherFields::BankAccountIban),
1275 b.swift_code
1276 .as_ref()
1277 .map(|_| CopyableCipherFields::BankAccountSwift),
1278 ]
1279 .into_iter()
1280 .flatten()
1281 .collect()
1282 })
1283 .unwrap_or_default(),
1284 CipherType::DriversLicense => self
1285 .drivers_license
1286 .as_ref()
1287 .map(|d| {
1288 [
1289 d.first_name
1290 .as_ref()
1291 .map(|_| CopyableCipherFields::DriversLicenseFirstName),
1292 d.middle_name
1293 .as_ref()
1294 .map(|_| CopyableCipherFields::DriversLicenseMiddleName),
1295 d.last_name
1296 .as_ref()
1297 .map(|_| CopyableCipherFields::DriversLicenseLastName),
1298 d.license_number
1299 .as_ref()
1300 .map(|_| CopyableCipherFields::DriversLicenseLicenseNumber),
1301 ]
1302 .into_iter()
1303 .flatten()
1304 .collect()
1305 })
1306 .unwrap_or_default(),
1307 CipherType::Passport => self
1308 .passport
1309 .as_ref()
1310 .map(|p| {
1311 [
1312 p.given_name
1313 .as_ref()
1314 .map(|_| CopyableCipherFields::PassportGivenName),
1315 p.surname
1316 .as_ref()
1317 .map(|_| CopyableCipherFields::PassportSurname),
1318 p.passport_number
1319 .as_ref()
1320 .map(|_| CopyableCipherFields::PassportPassportNumber),
1321 p.national_identification_number
1322 .as_ref()
1323 .map(|_| CopyableCipherFields::PassportNationalIdentificationNumber),
1324 ]
1325 .into_iter()
1326 .flatten()
1327 .collect()
1328 })
1329 .unwrap_or_default(),
1330 }
1331 }
1332}
1333
1334pub(crate) fn lenient_decrypt_cipher_list_view(
1337 cipher: &Cipher,
1338 ctx: &mut KeyStoreContext<KeySlotIds>,
1339 key: SymmetricKeySlotId,
1340) -> Result<CipherListView, CryptoError> {
1341 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1342
1343 Ok(CipherListView {
1344 partial: false,
1345 id: cipher.id,
1346 organization_id: cipher.organization_id,
1347 folder_id: cipher.folder_id,
1348 collection_ids: cipher.collection_ids.clone(),
1349 name: cipher
1350 .name
1351 .as_ref()
1352 .and_then(|n| n.decrypt(ctx, ciphers_key).ok())
1353 .unwrap_or_default(),
1354 subtitle: cipher
1355 .decrypt_subtitle(ctx, ciphers_key)
1356 .ok()
1357 .unwrap_or_default(),
1358 r#type: match cipher.r#type {
1359 CipherType::Login => {
1360 let login = cipher
1361 .login
1362 .as_ref()
1363 .ok_or(CryptoError::MissingField("login"))?;
1364 CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
1365 }
1366 CipherType::SecureNote => CipherListViewType::SecureNote,
1367 CipherType::Card => {
1368 let card = cipher
1369 .card
1370 .as_ref()
1371 .ok_or(CryptoError::MissingField("card"))?;
1372 CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
1373 }
1374 CipherType::Identity => CipherListViewType::Identity,
1375 CipherType::SshKey => CipherListViewType::SshKey,
1376 CipherType::BankAccount => {
1377 let bank_account = cipher
1378 .bank_account
1379 .as_ref()
1380 .ok_or(CryptoError::MissingField("bank_account"))?;
1381 CipherListViewType::BankAccount(bank_account.decrypt(ctx, ciphers_key)?)
1382 }
1383 CipherType::Passport => CipherListViewType::Passport,
1384 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1385 },
1386 favorite: cipher.favorite,
1387 reprompt: cipher.reprompt,
1388 organization_use_totp: cipher.organization_use_totp,
1389 edit: cipher.edit,
1390 permissions: cipher.permissions,
1391 view_password: cipher.view_password,
1392 attachments: cipher
1393 .attachments
1394 .as_ref()
1395 .map(|a| a.len() as u32)
1396 .unwrap_or(0),
1397 has_old_attachments: cipher
1398 .attachments
1399 .as_ref()
1400 .map(|a| a.iter().any(|att| att.key.is_none()))
1401 .unwrap_or(false),
1402 creation_date: cipher.creation_date,
1403 deleted_date: cipher.deleted_date,
1404 revision_date: cipher.revision_date,
1405 copyable_fields: cipher.get_copyable_fields(),
1406 local_data: cipher.local_data.decrypt(ctx, ciphers_key)?,
1407 archived_date: cipher.archived_date,
1408 #[cfg(feature = "wasm")]
1409 notes: cipher.notes.decrypt(ctx, ciphers_key).ok().flatten(),
1410 #[cfg(feature = "wasm")]
1411 fields: cipher.fields.as_ref().map(|fields| {
1412 fields
1413 .iter()
1414 .filter_map(|f| {
1415 f.decrypt(ctx, ciphers_key)
1416 .ok()
1417 .map(field::FieldListView::from)
1418 })
1419 .collect()
1420 }),
1421 #[cfg(feature = "wasm")]
1422 attachment_names: cipher.attachments.as_ref().map(|attachments| {
1423 attachments
1424 .iter()
1425 .filter_map(|a| a.file_name.decrypt(ctx, ciphers_key).ok().flatten())
1426 .collect()
1427 }),
1428 })
1429}
1430
1431impl IdentifyKey<SymmetricKeySlotId> for Cipher {
1432 fn key_identifier(&self) -> SymmetricKeySlotId {
1433 match self.organization_id {
1434 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1435 None => SymmetricKeySlotId::User,
1436 }
1437 }
1438}
1439
1440#[derive(Deserialize, Default)]
1449#[serde(rename_all = "camelCase")]
1450struct RestrictedCipherData {
1451 name: Option<EncString>,
1452 uris: Option<Vec<LoginUri>>,
1453}
1454
1455fn decrypt_restricted_name(
1459 restricted: &RestrictedCipherData,
1460 ctx: &mut KeyStoreContext<KeySlotIds>,
1461 ciphers_key: SymmetricKeySlotId,
1462 strict: bool,
1463) -> Result<String, CryptoError> {
1464 let Some(name) = restricted.name.as_ref() else {
1465 return Ok(String::new());
1466 };
1467 if strict {
1468 name.decrypt(ctx, ciphers_key)
1469 } else {
1470 Ok(name.decrypt(ctx, ciphers_key).ok().unwrap_or_default())
1471 }
1472}
1473
1474fn decrypt_restricted_uris(
1477 restricted: &RestrictedCipherData,
1478 ctx: &mut KeyStoreContext<KeySlotIds>,
1479 ciphers_key: SymmetricKeySlotId,
1480 strict: bool,
1481) -> Result<Option<Vec<LoginUriView>>, CryptoError> {
1482 if strict {
1483 restricted.uris.decrypt(ctx, ciphers_key)
1484 } else {
1485 Ok(restricted.uris.decrypt(ctx, ciphers_key).ok().flatten())
1486 }
1487}
1488
1489fn decrypt_restricted_cipher_view(
1498 cipher: &Cipher,
1499 raw: &str,
1500 ctx: &mut KeyStoreContext<KeySlotIds>,
1501 key: SymmetricKeySlotId,
1502 strict: bool,
1503) -> Result<CipherView, CryptoError> {
1504 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1505 let restricted: RestrictedCipherData = serde_json::from_str(raw).unwrap_or_default();
1506
1507 let name = decrypt_restricted_name(&restricted, ctx, ciphers_key, strict)?;
1508
1509 let login = if matches!(cipher.r#type, CipherType::Login) {
1512 Some(LoginView {
1513 username: None,
1514 password: None,
1515 password_revision_date: None,
1516 uris: decrypt_restricted_uris(&restricted, ctx, ciphers_key, strict)?,
1517 totp: None,
1518 autofill_on_page_load: None,
1519 fido2_credentials: None,
1520 })
1521 } else {
1522 None
1523 };
1524
1525 let mut view = CipherView {
1526 id: cipher.id,
1527 organization_id: cipher.organization_id,
1528 folder_id: cipher.folder_id,
1529 collection_ids: cipher.collection_ids.clone(),
1530 key: None,
1533 name,
1534 notes: None,
1535 r#type: cipher.r#type,
1536 login,
1537 identity: None,
1538 card: None,
1539 secure_note: None,
1540 ssh_key: None,
1541 bank_account: None,
1542 drivers_license: None,
1543 passport: None,
1544 favorite: cipher.favorite,
1545 reprompt: cipher.reprompt,
1546 organization_use_totp: cipher.organization_use_totp,
1547 edit: cipher.edit,
1548 permissions: cipher.permissions,
1549 view_password: cipher.view_password,
1550 local_data: cipher.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
1551 attachments: None,
1552 attachment_decryption_failures: None,
1553 fields: None,
1554 password_history: None,
1555 creation_date: cipher.creation_date,
1556 deleted_date: cipher.deleted_date,
1557 revision_date: cipher.revision_date,
1558 archived_date: cipher.archived_date,
1559 partial: true,
1560 };
1561
1562 if cipher.key.is_some()
1565 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
1566 {
1567 view.remove_invalid_checksums();
1568 }
1569
1570 Ok(view)
1571}
1572
1573fn decrypt_restricted_cipher_list_view(
1578 cipher: &Cipher,
1579 raw: &str,
1580 ctx: &mut KeyStoreContext<KeySlotIds>,
1581 key: SymmetricKeySlotId,
1582 strict: bool,
1583) -> Result<CipherListView, CryptoError> {
1584 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1585 let restricted: RestrictedCipherData = serde_json::from_str(raw).unwrap_or_default();
1586
1587 let name = decrypt_restricted_name(&restricted, ctx, ciphers_key, strict)?;
1588
1589 let r#type = match cipher.r#type {
1590 CipherType::Login => CipherListViewType::Login(LoginListView {
1591 fido2_credentials: None,
1592 has_fido2: false,
1593 username: None,
1594 totp: None,
1595 uris: decrypt_restricted_uris(&restricted, ctx, ciphers_key, strict)?,
1596 }),
1597 CipherType::SecureNote => CipherListViewType::SecureNote,
1598 CipherType::Card => CipherListViewType::Card(CardListView { brand: None }),
1599 CipherType::Identity => CipherListViewType::Identity,
1600 CipherType::SshKey => CipherListViewType::SshKey,
1601 CipherType::BankAccount => CipherListViewType::BankAccount(BankAccountListView {
1602 account_number: None,
1603 account_type: None,
1604 }),
1605 CipherType::Passport => CipherListViewType::Passport,
1606 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1607 };
1608
1609 Ok(CipherListView {
1610 id: cipher.id,
1611 organization_id: cipher.organization_id,
1612 folder_id: cipher.folder_id,
1613 collection_ids: cipher.collection_ids.clone(),
1614 name,
1615 subtitle: String::new(),
1616 r#type,
1617 favorite: cipher.favorite,
1618 reprompt: cipher.reprompt,
1619 organization_use_totp: cipher.organization_use_totp,
1620 edit: cipher.edit,
1621 permissions: cipher.permissions,
1622 view_password: cipher.view_password,
1623 attachments: 0,
1624 has_old_attachments: false,
1625 creation_date: cipher.creation_date,
1626 deleted_date: cipher.deleted_date,
1627 revision_date: cipher.revision_date,
1628 archived_date: cipher.archived_date,
1629 copyable_fields: vec![],
1630 local_data: cipher.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
1631 partial: true,
1632 #[cfg(feature = "wasm")]
1633 notes: None,
1634 #[cfg(feature = "wasm")]
1635 fields: None,
1636 #[cfg(feature = "wasm")]
1637 attachment_names: None,
1638 })
1639}
1640
1641impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for Cipher {
1642 #[bitwarden_logging::instrument(err, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
1643 fn decrypt(
1644 &self,
1645 ctx: &mut KeyStoreContext<KeySlotIds>,
1646 key: SymmetricKeySlotId,
1647 ) -> Result<CipherView, CryptoError> {
1648 if let Some(raw) = &self.partial_data {
1649 if self.organization_id.is_none() {
1653 return Err(CryptoError::RestrictedCipherRequiresOrganization);
1654 }
1655 return decrypt_restricted_cipher_view(self, raw, ctx, key, false);
1656 }
1657 match try_parse_blob(self) {
1658 Some(sealed) => decrypt_blob_cipher(self, &sealed, ctx, key).map_err(CryptoError::from),
1659 None => lenient_decrypt_cipher_view(self, ctx, key),
1660 }
1661 }
1662}
1663
1664impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for Cipher {
1665 fn decrypt(
1666 &self,
1667 ctx: &mut KeyStoreContext<KeySlotIds>,
1668 key: SymmetricKeySlotId,
1669 ) -> Result<CipherListView, CryptoError> {
1670 if let Some(raw) = &self.partial_data {
1671 if self.organization_id.is_none() {
1675 return Err(CryptoError::RestrictedCipherRequiresOrganization);
1676 }
1677 return decrypt_restricted_cipher_list_view(self, raw, ctx, key, false);
1678 }
1679 match try_parse_blob(self) {
1680 Some(sealed) => decrypt_blob_cipher(self, &sealed, ctx, key)?.to_list_view(),
1681 None => lenient_decrypt_cipher_list_view(self, ctx, key),
1682 }
1683 }
1684}
1685
1686impl IdentifyKey<SymmetricKeySlotId> for CipherView {
1687 fn key_identifier(&self) -> SymmetricKeySlotId {
1688 match self.organization_id {
1689 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1690 None => SymmetricKeySlotId::User,
1691 }
1692 }
1693}
1694
1695impl IdentifyKey<SymmetricKeySlotId> for CipherListView {
1696 fn key_identifier(&self) -> SymmetricKeySlotId {
1697 match self.organization_id {
1698 Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1699 None => SymmetricKeySlotId::User,
1700 }
1701 }
1702}
1703
1704pub(crate) struct StrictDecrypt<T>(pub(crate) T);
1713
1714impl IdentifyKey<SymmetricKeySlotId> for StrictDecrypt<Cipher> {
1715 fn key_identifier(&self) -> SymmetricKeySlotId {
1716 self.0.key_identifier()
1717 }
1718}
1719
1720impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for StrictDecrypt<Cipher> {
1721 #[bitwarden_logging::instrument(err, fields(cipher_id = ?self.0.id, org_id = ?self.0.organization_id, kind = ?self.0.r#type))]
1722 fn decrypt(
1723 &self,
1724 ctx: &mut KeyStoreContext<KeySlotIds>,
1725 key: SymmetricKeySlotId,
1726 ) -> Result<CipherView, CryptoError> {
1727 if let Some(raw) = &self.0.partial_data {
1728 if self.0.organization_id.is_none() {
1730 return Err(CryptoError::RestrictedCipherRequiresOrganization);
1731 }
1732 return decrypt_restricted_cipher_view(&self.0, raw, ctx, key, true);
1733 }
1734 match try_parse_blob(&self.0) {
1735 Some(sealed) => {
1736 decrypt_blob_cipher(&self.0, &sealed, ctx, key).map_err(CryptoError::from)
1737 }
1738 None => strict_decrypt_cipher_view(&self.0, ctx, key),
1739 }
1740 }
1741}
1742
1743fn strict_decrypt_cipher_view(
1746 cipher: &Cipher,
1747 ctx: &mut KeyStoreContext<KeySlotIds>,
1748 key: SymmetricKeySlotId,
1749) -> Result<CipherView, CryptoError> {
1750 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1751
1752 let (attachments, attachment_decryption_failures) =
1754 attachment::decrypt_attachments_with_failures(
1755 cipher.attachments.as_deref().unwrap_or_default(),
1756 ctx,
1757 ciphers_key,
1758 );
1759
1760 let mut view = CipherView {
1761 partial: false,
1762 id: cipher.id,
1763 organization_id: cipher.organization_id,
1764 folder_id: cipher.folder_id,
1765 collection_ids: cipher.collection_ids.clone(),
1766 key: if cipher.key.is_some() {
1767 #[allow(deprecated)]
1768 Some(ctx.dangerous_get_symmetric_key(ciphers_key)?.clone())
1769 } else {
1770 None
1771 },
1772 name: cipher
1773 .name
1774 .as_ref()
1775 .ok_or(CryptoError::MissingField("name"))?
1776 .decrypt(ctx, ciphers_key)?,
1777 notes: cipher.notes.decrypt(ctx, ciphers_key)?,
1778 r#type: cipher.r#type,
1779 login: cipher
1780 .login
1781 .as_ref()
1782 .map(|l| StrictDecrypt(l).decrypt(ctx, ciphers_key))
1783 .transpose()?,
1784 identity: cipher
1785 .identity
1786 .as_ref()
1787 .map(|i| StrictDecrypt(i).decrypt(ctx, ciphers_key))
1788 .transpose()?,
1789 card: cipher
1790 .card
1791 .as_ref()
1792 .map(|c| StrictDecrypt(c).decrypt(ctx, ciphers_key))
1793 .transpose()?,
1794 secure_note: cipher.secure_note.decrypt(ctx, ciphers_key)?,
1795 ssh_key: cipher.ssh_key.decrypt(ctx, ciphers_key)?,
1796 bank_account: cipher.bank_account.decrypt(ctx, ciphers_key)?,
1797 drivers_license: cipher.drivers_license.decrypt(ctx, ciphers_key)?,
1798 passport: cipher.passport.decrypt(ctx, ciphers_key)?,
1799 favorite: cipher.favorite,
1800 reprompt: cipher.reprompt,
1801 organization_use_totp: cipher.organization_use_totp,
1802 edit: cipher.edit,
1803 permissions: cipher.permissions,
1804 view_password: cipher.view_password,
1805 local_data: cipher.local_data.decrypt(ctx, ciphers_key)?,
1806 attachments: Some(attachments),
1807 attachment_decryption_failures: Some(attachment_decryption_failures),
1808 fields: cipher
1809 .fields
1810 .as_ref()
1811 .map(|fields| {
1812 fields
1813 .iter()
1814 .map(|f| StrictDecrypt(f).decrypt(ctx, ciphers_key))
1815 .collect::<Result<Vec<_>, _>>()
1816 })
1817 .transpose()?,
1818 password_history: cipher.password_history.decrypt(ctx, ciphers_key)?,
1819 creation_date: cipher.creation_date,
1820 deleted_date: cipher.deleted_date,
1821 revision_date: cipher.revision_date,
1822 archived_date: cipher.archived_date,
1823 };
1824
1825 if view.key.is_some()
1828 || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
1829 {
1830 view.remove_invalid_checksums();
1831 }
1832
1833 Ok(view)
1834}
1835
1836impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for StrictDecrypt<Cipher> {
1837 fn decrypt(
1838 &self,
1839 ctx: &mut KeyStoreContext<KeySlotIds>,
1840 key: SymmetricKeySlotId,
1841 ) -> Result<CipherListView, CryptoError> {
1842 if let Some(raw) = &self.0.partial_data {
1843 if self.0.organization_id.is_none() {
1845 return Err(CryptoError::RestrictedCipherRequiresOrganization);
1846 }
1847 return decrypt_restricted_cipher_list_view(&self.0, raw, ctx, key, true);
1848 }
1849 match try_parse_blob(&self.0) {
1850 Some(sealed) => decrypt_blob_cipher(&self.0, &sealed, ctx, key)?.to_list_view(),
1851 None => strict_decrypt_cipher_list_view(&self.0, ctx, key),
1852 }
1853 }
1854}
1855
1856fn strict_decrypt_cipher_list_view(
1859 cipher: &Cipher,
1860 ctx: &mut KeyStoreContext<KeySlotIds>,
1861 key: SymmetricKeySlotId,
1862) -> Result<CipherListView, CryptoError> {
1863 let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &cipher.key)?;
1864
1865 Ok(CipherListView {
1866 partial: false,
1867 id: cipher.id,
1868 organization_id: cipher.organization_id,
1869 folder_id: cipher.folder_id,
1870 collection_ids: cipher.collection_ids.clone(),
1871 name: cipher
1872 .name
1873 .as_ref()
1874 .ok_or(CryptoError::MissingField("name"))?
1875 .decrypt(ctx, ciphers_key)?,
1876 subtitle: cipher.decrypt_subtitle(ctx, ciphers_key)?,
1877 r#type: match cipher.r#type {
1878 CipherType::Login => {
1879 let login = cipher
1880 .login
1881 .as_ref()
1882 .ok_or(CryptoError::MissingField("login"))?;
1883 CipherListViewType::Login(StrictDecrypt(login).decrypt(ctx, ciphers_key)?)
1884 }
1885 CipherType::SecureNote => CipherListViewType::SecureNote,
1886 CipherType::Card => {
1887 let card = cipher
1888 .card
1889 .as_ref()
1890 .ok_or(CryptoError::MissingField("card"))?;
1891 CipherListViewType::Card(StrictDecrypt(card).decrypt(ctx, ciphers_key)?)
1892 }
1893 CipherType::Identity => CipherListViewType::Identity,
1894 CipherType::SshKey => CipherListViewType::SshKey,
1895 CipherType::BankAccount => {
1896 let bank_account = cipher
1897 .bank_account
1898 .as_ref()
1899 .ok_or(CryptoError::MissingField("bank_account"))?;
1900 CipherListViewType::BankAccount(
1901 StrictDecrypt(bank_account).decrypt(ctx, ciphers_key)?,
1902 )
1903 }
1904 CipherType::Passport => CipherListViewType::Passport,
1905 CipherType::DriversLicense => CipherListViewType::DriversLicense,
1906 },
1907 favorite: cipher.favorite,
1908 reprompt: cipher.reprompt,
1909 organization_use_totp: cipher.organization_use_totp,
1910 edit: cipher.edit,
1911 permissions: cipher.permissions,
1912 view_password: cipher.view_password,
1913 attachments: cipher
1914 .attachments
1915 .as_ref()
1916 .map(|a| a.len() as u32)
1917 .unwrap_or(0),
1918 has_old_attachments: cipher
1919 .attachments
1920 .as_ref()
1921 .map(|a| a.iter().any(|att| att.key.is_none()))
1922 .unwrap_or(false),
1923 creation_date: cipher.creation_date,
1924 deleted_date: cipher.deleted_date,
1925 revision_date: cipher.revision_date,
1926 copyable_fields: cipher.get_copyable_fields(),
1927 local_data: cipher.local_data.decrypt(ctx, ciphers_key)?,
1928 archived_date: cipher.archived_date,
1929 #[cfg(feature = "wasm")]
1930 notes: cipher.notes.decrypt(ctx, ciphers_key)?,
1931 #[cfg(feature = "wasm")]
1932 fields: cipher
1933 .fields
1934 .as_ref()
1935 .map(|fields| {
1936 fields
1937 .iter()
1938 .map(|f| {
1939 StrictDecrypt(f)
1940 .decrypt(ctx, ciphers_key)
1941 .map(field::FieldListView::from)
1942 })
1943 .collect::<Result<Vec<_>, _>>()
1944 })
1945 .transpose()?,
1946 #[cfg(feature = "wasm")]
1947 attachment_names: cipher
1948 .attachments
1949 .as_ref()
1950 .map(|attachments| {
1951 attachments
1952 .iter()
1953 .map(|a| a.file_name.decrypt(ctx, ciphers_key))
1954 .collect::<Result<Vec<_>, _>>()
1955 })
1956 .transpose()?
1957 .map(|names| names.into_iter().flatten().collect()),
1958 })
1959}
1960
1961pub enum EncryptMode<T> {
1966 Blob(T),
1968 Legacy(T),
1970}
1971
1972impl<T> EncryptMode<T> {
1973 pub(crate) fn inner(&self) -> &T {
1974 match self {
1975 Self::Blob(t) | Self::Legacy(t) => t,
1976 }
1977 }
1978}
1979
1980impl<T> IdentifyKey<SymmetricKeySlotId> for EncryptMode<T>
1981where
1982 T: IdentifyKey<SymmetricKeySlotId>,
1983{
1984 fn key_identifier(&self) -> SymmetricKeySlotId {
1985 self.inner().key_identifier()
1986 }
1987}
1988
1989impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Cipher> for EncryptMode<CipherView> {
1990 fn encrypt_composite(
1991 &self,
1992 ctx: &mut KeyStoreContext<KeySlotIds>,
1993 key: SymmetricKeySlotId,
1994 ) -> Result<Cipher, CryptoError> {
1995 match self {
1996 Self::Blob(view) => {
2000 let mut owned = view.clone();
2001 encrypt_blob_cipher_with_wrapping_key(&mut owned, ctx, key)
2002 .map_err(CryptoError::from)
2003 }
2004 Self::Legacy(view) => {
2005 let mut owned = view.clone();
2006 owned.encrypt_legacy_field_encryption(ctx, key)
2007 }
2008 }
2009 }
2010}
2011
2012impl TryFrom<CipherDetailsResponseModel> for Cipher {
2013 type Error = VaultParseError;
2014
2015 fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
2016 Ok(Self {
2017 partial_data: cipher.partial_data,
2018 id: cipher.id.map(CipherId::new),
2019 organization_id: cipher.organization_id.map(OrganizationId::new),
2020 folder_id: cipher.folder_id.map(FolderId::new),
2021 collection_ids: cipher
2022 .collection_ids
2023 .unwrap_or_default()
2024 .into_iter()
2025 .map(CollectionId::new)
2026 .collect(),
2027 name: EncString::try_from_optional(cipher.name)?,
2028 notes: EncString::try_from_optional(cipher.notes)?,
2029 r#type: require!(cipher.r#type).try_into()?,
2030 login: cipher.login.map(|l| (*l).try_into()).transpose()?,
2031 identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
2032 card: cipher.card.map(|c| (*c).try_into()).transpose()?,
2033 secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
2034 ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
2035 bank_account: cipher.bank_account.map(|b| (*b).try_into()).transpose()?,
2036 drivers_license: cipher
2037 .drivers_license
2038 .map(|d| (*d).try_into())
2039 .transpose()?,
2040 passport: cipher.passport.map(|p| (*p).try_into()).transpose()?,
2041 favorite: cipher.favorite.unwrap_or(false),
2042 reprompt: cipher
2043 .reprompt
2044 .map(|r| r.try_into())
2045 .transpose()?
2046 .unwrap_or(CipherRepromptType::None),
2047 organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
2048 edit: cipher.edit.unwrap_or(true),
2049 permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
2050 view_password: cipher.view_password.unwrap_or(true),
2051 local_data: None, attachments: cipher
2053 .attachments
2054 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2055 .transpose()?,
2056 fields: cipher
2057 .fields
2058 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2059 .transpose()?,
2060 password_history: cipher
2061 .password_history
2062 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2063 .transpose()?,
2064 creation_date: require!(cipher.creation_date).parse()?,
2065 deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
2066 revision_date: require!(cipher.revision_date).parse()?,
2067 key: EncString::try_from_optional(cipher.key)?,
2068 archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
2069 data: cipher.data,
2070 })
2071 }
2072}
2073
2074impl PartialCipher for CipherDetailsResponseModel {
2075 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
2076 Ok(Cipher {
2077 local_data: cipher.and_then(|c| c.local_data),
2078 ..self.try_into()?
2079 })
2080 }
2081}
2082
2083impl TryFrom<bitwarden_api_api::models::CipherType> for CipherType {
2084 type Error = MissingFieldError;
2085
2086 fn try_from(t: bitwarden_api_api::models::CipherType) -> Result<Self, Self::Error> {
2087 Ok(match t {
2088 bitwarden_api_api::models::CipherType::Login => CipherType::Login,
2089 bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
2090 bitwarden_api_api::models::CipherType::Card => CipherType::Card,
2091 bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
2092 bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
2093 bitwarden_api_api::models::CipherType::BankAccount => CipherType::BankAccount,
2094 bitwarden_api_api::models::CipherType::Passport => CipherType::Passport,
2095 bitwarden_api_api::models::CipherType::DriversLicense => CipherType::DriversLicense,
2096 bitwarden_api_api::models::CipherType::__Unknown(_) => {
2097 return Err(MissingFieldError("type"));
2098 }
2099 })
2100 }
2101}
2102
2103impl TryFrom<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
2104 type Error = MissingFieldError;
2105
2106 fn try_from(t: bitwarden_api_api::models::CipherRepromptType) -> Result<Self, Self::Error> {
2107 Ok(match t {
2108 bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
2109 bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
2110 bitwarden_api_api::models::CipherRepromptType::__Unknown(_) => {
2111 return Err(MissingFieldError("reprompt"));
2112 }
2113 })
2114 }
2115}
2116
2117pub(crate) trait PartialCipher {
2121 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
2122}
2123
2124impl From<CipherType> for bitwarden_api_api::models::CipherType {
2125 fn from(t: CipherType) -> Self {
2126 match t {
2127 CipherType::Login => bitwarden_api_api::models::CipherType::Login,
2128 CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
2129 CipherType::Card => bitwarden_api_api::models::CipherType::Card,
2130 CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
2131 CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
2132 CipherType::BankAccount => bitwarden_api_api::models::CipherType::BankAccount,
2133 CipherType::Passport => bitwarden_api_api::models::CipherType::Passport,
2134 CipherType::DriversLicense => bitwarden_api_api::models::CipherType::DriversLicense,
2135 }
2136 }
2137}
2138
2139impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
2140 fn from(t: CipherRepromptType) -> Self {
2141 match t {
2142 CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
2143 CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
2144 }
2145 }
2146}
2147
2148impl PartialCipher for CipherResponseModel {
2149 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
2150 Ok(Cipher {
2151 partial_data: self.partial_data,
2152 collection_ids: cipher
2153 .as_ref()
2154 .map(|c| c.collection_ids.clone())
2155 .unwrap_or_default(),
2156 local_data: cipher.and_then(|c| c.local_data),
2157 id: self.id.map(CipherId::new),
2158 organization_id: self.organization_id.map(OrganizationId::new),
2159 folder_id: self.folder_id.map(FolderId::new),
2160 name: self.name.map(|n| n.parse()).transpose()?,
2161 notes: EncString::try_from_optional(self.notes)?,
2162 r#type: require!(self.r#type).try_into()?,
2163 login: self.login.map(|l| (*l).try_into()).transpose()?,
2164 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
2165 card: self.card.map(|c| (*c).try_into()).transpose()?,
2166 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
2167 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
2168 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
2169 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
2170 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
2171 favorite: self.favorite.unwrap_or(false),
2172 reprompt: self
2173 .reprompt
2174 .map(|r| r.try_into())
2175 .transpose()?
2176 .unwrap_or(CipherRepromptType::None),
2177 organization_use_totp: self.organization_use_totp.unwrap_or(false),
2178 edit: self.edit.unwrap_or(false),
2179 permissions: self.permissions.map(|p| (*p).try_into()).transpose()?,
2180 view_password: self.view_password.unwrap_or(true),
2181 attachments: self
2182 .attachments
2183 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2184 .transpose()?,
2185 fields: self
2186 .fields
2187 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2188 .transpose()?,
2189 password_history: self
2190 .password_history
2191 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2192 .transpose()?,
2193 creation_date: require!(self.creation_date).parse()?,
2194 deleted_date: self.deleted_date.map(|d| d.parse()).transpose()?,
2195 revision_date: require!(self.revision_date).parse()?,
2196 key: EncString::try_from_optional(self.key)?,
2197 archived_date: self.archived_date.map(|d| d.parse()).transpose()?,
2198 data: self.data,
2199 })
2200 }
2201}
2202
2203impl TryFrom<CipherResponseModel> for Cipher {
2205 type Error = VaultParseError;
2206
2207 fn try_from(cipher: CipherResponseModel) -> Result<Self, Self::Error> {
2208 cipher.merge_with_cipher(None)
2209 }
2210}
2211
2212impl TryFrom<CipherMiniResponseModel> for Cipher {
2213 type Error = VaultParseError;
2214
2215 fn try_from(cipher: CipherMiniResponseModel) -> Result<Self, Self::Error> {
2216 cipher.merge_with_cipher(None)
2217 }
2218}
2219
2220impl PartialCipher for CipherMiniResponseModel {
2221 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
2222 let cipher = cipher.as_ref();
2223 Ok(Cipher {
2224 partial_data: self.partial_data,
2225 id: self.id.map(CipherId::new),
2226 organization_id: self.organization_id.map(OrganizationId::new),
2227 key: EncString::try_from_optional(self.key)?,
2228 name: EncString::try_from_optional(self.name)?,
2229 notes: EncString::try_from_optional(self.notes)?,
2230 r#type: require!(self.r#type).try_into()?,
2231 login: self.login.map(|l| (*l).try_into()).transpose()?,
2232 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
2233 card: self.card.map(|c| (*c).try_into()).transpose()?,
2234 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
2235 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
2236 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
2237 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
2238 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
2239 reprompt: self
2240 .reprompt
2241 .map(|r| r.try_into())
2242 .transpose()?
2243 .unwrap_or(CipherRepromptType::None),
2244 organization_use_totp: self.organization_use_totp.unwrap_or(true),
2245 attachments: self
2246 .attachments
2247 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2248 .transpose()?,
2249 fields: self
2250 .fields
2251 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2252 .transpose()?,
2253 password_history: self
2254 .password_history
2255 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2256 .transpose()?,
2257 creation_date: require!(self.creation_date)
2258 .parse()
2259 .map_err(Into::<VaultParseError>::into)?,
2260 deleted_date: self
2261 .deleted_date
2262 .map(|d| d.parse())
2263 .transpose()
2264 .map_err(Into::<VaultParseError>::into)?,
2265 revision_date: require!(self.revision_date)
2266 .parse()
2267 .map_err(Into::<VaultParseError>::into)?,
2268 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
2269 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
2270 favorite: cipher.map_or(Default::default(), |c| c.favorite),
2271 edit: cipher.map_or(Default::default(), |c| c.edit),
2272 permissions: cipher.map_or(Default::default(), |c| c.permissions),
2273 view_password: cipher.is_none_or(|c| c.view_password),
2274 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
2275 data: self.data,
2276 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
2277 })
2278 }
2279}
2280
2281impl PartialCipher for CipherMiniDetailsResponseModel {
2282 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
2283 let cipher = cipher.as_ref();
2284 Ok(Cipher {
2285 partial_data: self.partial_data,
2286 id: self.id.map(CipherId::new),
2287 organization_id: self.organization_id.map(OrganizationId::new),
2288 key: EncString::try_from_optional(self.key)?,
2289 name: EncString::try_from_optional(self.name)?,
2290 notes: EncString::try_from_optional(self.notes)?,
2291 r#type: require!(self.r#type).try_into()?,
2292 login: self.login.map(|l| (*l).try_into()).transpose()?,
2293 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
2294 card: self.card.map(|c| (*c).try_into()).transpose()?,
2295 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
2296 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
2297 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
2298 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
2299 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
2300 reprompt: self
2301 .reprompt
2302 .map(|r| r.try_into())
2303 .transpose()?
2304 .unwrap_or(CipherRepromptType::None),
2305 organization_use_totp: self.organization_use_totp.unwrap_or(true),
2306 attachments: self
2307 .attachments
2308 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2309 .transpose()?,
2310 fields: self
2311 .fields
2312 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2313 .transpose()?,
2314 password_history: self
2315 .password_history
2316 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2317 .transpose()?,
2318 creation_date: require!(self.creation_date)
2319 .parse()
2320 .map_err(Into::<VaultParseError>::into)?,
2321 deleted_date: self
2322 .deleted_date
2323 .map(|d| d.parse())
2324 .transpose()
2325 .map_err(Into::<VaultParseError>::into)?,
2326 revision_date: require!(self.revision_date)
2327 .parse()
2328 .map_err(Into::<VaultParseError>::into)?,
2329 collection_ids: self
2330 .collection_ids
2331 .into_iter()
2332 .flatten()
2333 .map(CollectionId::new)
2334 .collect(),
2335 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
2336 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
2337 favorite: cipher.map_or(Default::default(), |c| c.favorite),
2338 edit: cipher.map_or(Default::default(), |c| c.edit),
2339 permissions: cipher.map_or(Default::default(), |c| c.permissions),
2340 view_password: cipher.is_none_or(|c: &Cipher| c.view_password),
2341 data: cipher.map_or(Default::default(), |c| c.data.clone()),
2342 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
2343 })
2344 }
2345}
2346
2347#[cfg(test)]
2348mod tests {
2349
2350 use attachment::AttachmentView;
2351 use bitwarden_core::key_management::{
2352 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
2353 };
2354 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, SymmetricKeyAlgorithm};
2355
2356 use super::*;
2357 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
2358
2359 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
2361 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
2362 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
2363 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
2364 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
2365 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
2366 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
2367
2368 const RESTRICTED_ORG_UUID: &str = "3cf0d3ba-3ded-4bf3-a51c-b03fd9ac6e07";
2370 const RESTRICTED_ORG_KEY_B64: &str =
2371 "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==";
2372
2373 const RESTRICTED_LOGIN_ENVELOPE: &str = r#"{"name":"2.qip4DSwdOzU2KwY3jgDjUg==|CsGRQgTwAzmszz+dkk5xIg==|rmW/mlnHq2MulR9uNKclD+1UBFLfOimedkq5tPRSLOc=","uris":[{"uri":"2.2na8mpfA1B1OBTUHkDz+fw==|yTWB1nEf3EHIZgsDINM8JnTYyxf7KVZvXraIGAVOiEg=|i2swsODSjEMRaYNnBHAigdphZBBUg2lkPNo763fX12w=","uriChecksum":null,"match":null}]}"#;
2376 const RESTRICTED_LOGIN_ENVELOPE_WITH_PASSWORD: &str = r#"{"name":"2.qip4DSwdOzU2KwY3jgDjUg==|CsGRQgTwAzmszz+dkk5xIg==|rmW/mlnHq2MulR9uNKclD+1UBFLfOimedkq5tPRSLOc=","uris":[{"uri":"2.2na8mpfA1B1OBTUHkDz+fw==|yTWB1nEf3EHIZgsDINM8JnTYyxf7KVZvXraIGAVOiEg=|i2swsODSjEMRaYNnBHAigdphZBBUg2lkPNo763fX12w=","uriChecksum":null,"match":null}],"password":"2.cKf+VYTb7KF2ITGLDmGzig==|zC66OfcYpUB8V6jLB6GQvQ==|hnDFyYCAf6RPD4lPmXZCzEWzXwniRyFnCVrO0KZMPlc="}"#;
2378 const RESTRICTED_SECURE_NOTE_ENVELOPE: &str = r#"{"name":"2.EwVjyRAgPmUvRpyT68lbjQ==|73O9id1+DZevAB3K+2fXnA==|OoJhN3p9UgjQ4yk55OUBZ4nYQMlFcf9wTHPxrOPhQVI="}"#;
2379 const RESTRICTED_CARD_ENVELOPE: &str = r#"{"name":"2.HF21EOZVqF3eyeZtEgxaCg==|zuChVgXPqxipFE6zOBUBXQ==|gxyvw0+gMf5Grxk8EAhpLCBeXdA0kvea2maJmpLIUIw="}"#;
2380
2381 fn restricted_test_key_store() -> (OrganizationId, KeyStore<KeySlotIds>) {
2384 let org: OrganizationId = RESTRICTED_ORG_UUID.parse().unwrap();
2385 let org_key: SymmetricCryptoKey = RESTRICTED_ORG_KEY_B64.to_string().try_into().unwrap();
2386 let key_store = create_test_crypto_with_user_and_org_key(
2387 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
2388 org,
2389 org_key,
2390 );
2391 (org, key_store)
2392 }
2393
2394 fn generate_cipher() -> CipherView {
2395 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
2396 CipherView {
2397 partial: false,
2398 r#type: CipherType::Login,
2399 login: Some(LoginView {
2400 username: Some("test_username".to_string()),
2401 password: Some("test_password".to_string()),
2402 password_revision_date: None,
2403 uris: None,
2404 totp: None,
2405 autofill_on_page_load: None,
2406 fido2_credentials: None,
2407 }),
2408 id: Some(test_id),
2409 organization_id: None,
2410 folder_id: None,
2411 collection_ids: vec![],
2412 key: None,
2413 name: "My test login".to_string(),
2414 notes: None,
2415 identity: None,
2416 card: None,
2417 secure_note: None,
2418 ssh_key: None,
2419 bank_account: None,
2420 drivers_license: None,
2421 passport: None,
2422 favorite: false,
2423 reprompt: CipherRepromptType::None,
2424 organization_use_totp: true,
2425 edit: true,
2426 permissions: None,
2427 view_password: true,
2428 local_data: None,
2429 attachments: None,
2430 attachment_decryption_failures: None,
2431 fields: None,
2432 password_history: None,
2433 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2434 deleted_date: None,
2435 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2436 archived_date: None,
2437 }
2438 }
2439
2440 fn generate_fido2(
2441 ctx: &mut KeyStoreContext<KeySlotIds>,
2442 key: SymmetricKeySlotId,
2443 ) -> Fido2Credential {
2444 Fido2Credential {
2445 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
2446 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
2447 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
2448 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
2449 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
2450 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
2451 user_handle: None,
2452 user_name: None,
2453 counter: "123".to_string().encrypt(ctx, key).unwrap(),
2454 rp_name: None,
2455 user_display_name: None,
2456 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
2457 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
2458 }
2459 }
2460
2461 fn generate_fido2_view() -> Fido2CredentialView {
2462 Fido2CredentialView {
2463 credential_id: "123".to_string(),
2464 key_type: "public-key".to_string(),
2465 key_algorithm: "ECDSA".to_string(),
2466 key_curve: "P-256".to_string(),
2467 key_value: "123".to_string(),
2468 rp_id: "123".to_string(),
2469 user_handle: None,
2470 user_name: None,
2471 counter: "123".to_string(),
2472 rp_name: None,
2473 user_display_name: None,
2474 discoverable: "true".to_string(),
2475 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
2476 }
2477 }
2478
2479 fn restricted_cipher(
2482 organization_id: OrganizationId,
2483 r#type: CipherType,
2484 partial_data: String,
2485 ) -> Cipher {
2486 Cipher {
2487 partial_data: Some(partial_data),
2488 id: Some(TEST_UUID.parse().unwrap()),
2489 organization_id: Some(organization_id),
2490 folder_id: None,
2491 collection_ids: vec![],
2492 key: None,
2493 name: None,
2494 notes: None,
2495 r#type,
2496 login: None,
2497 identity: None,
2498 card: None,
2499 secure_note: None,
2500 ssh_key: None,
2501 bank_account: None,
2502 drivers_license: None,
2503 passport: None,
2504 favorite: false,
2505 reprompt: CipherRepromptType::None,
2506 organization_use_totp: false,
2507 edit: true,
2508 permissions: None,
2509 view_password: true,
2510 local_data: None,
2511 attachments: None,
2512 fields: None,
2513 password_history: None,
2514 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2515 deleted_date: None,
2516 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2517 archived_date: None,
2518 data: None,
2519 }
2520 }
2521
2522 #[test]
2523 fn test_decrypt_restricted_cipher_list_view_login() {
2524 let (org, key_store) = restricted_test_key_store();
2525 let cipher = restricted_cipher(
2526 org,
2527 CipherType::Login,
2528 RESTRICTED_LOGIN_ENVELOPE.to_string(),
2529 );
2530 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
2531
2532 assert!(view.partial);
2533 assert_eq!(view.name, "Restricted Name".to_string());
2534 assert!(view.subtitle.is_empty());
2535 assert!(view.copyable_fields.is_empty());
2536 assert_eq!(view.attachments, 0);
2537 match view.r#type {
2538 CipherListViewType::Login(login) => {
2539 assert_eq!(
2540 login.uris.unwrap()[0].uri.as_deref(),
2541 Some("https://example.com")
2542 );
2543 assert_eq!(login.username, None);
2544 assert_eq!(login.totp, None);
2545 assert!(!login.has_fido2);
2546 }
2547 other => panic!("expected Login list view, got {other:?}"),
2548 }
2549 }
2550
2551 #[test]
2552 fn test_decrypt_restricted_cipher_view_login_omits_secrets() {
2553 let (org, key_store) = restricted_test_key_store();
2554 let cipher = restricted_cipher(
2557 org,
2558 CipherType::Login,
2559 RESTRICTED_LOGIN_ENVELOPE_WITH_PASSWORD.to_string(),
2560 );
2561 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2562
2563 assert!(view.partial);
2564 assert_eq!(view.name, "Restricted Name".to_string());
2565 let login = view
2566 .login
2567 .expect("login populated so the view can render a domain");
2568 assert_eq!(
2569 login.uris.unwrap()[0].uri.as_deref(),
2570 Some("https://example.com")
2571 );
2572 assert_eq!(login.username, None);
2573 assert_eq!(login.password, None);
2574 assert_eq!(login.totp, None);
2575 assert_eq!(view.notes, None);
2576 assert!(view.card.is_none());
2577 }
2578
2579 #[test]
2580 fn test_decrypt_restricted_non_login_name_only() {
2581 let (org, key_store) = restricted_test_key_store();
2582 let cipher = restricted_cipher(
2583 org,
2584 CipherType::SecureNote,
2585 RESTRICTED_SECURE_NOTE_ENVELOPE.to_string(),
2586 );
2587
2588 let list: CipherListView = key_store.decrypt(&cipher).unwrap();
2589 assert!(list.partial);
2590 assert_eq!(list.name, "Gated Note".to_string());
2591 assert_eq!(list.r#type, CipherListViewType::SecureNote);
2592
2593 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2594 assert!(view.partial);
2595 assert_eq!(view.name, "Gated Note".to_string());
2596 assert!(view.login.is_none());
2597 assert!(view.secure_note.is_none());
2598 }
2599
2600 #[test]
2601 fn test_decrypt_restricted_malformed_envelope_fails_closed() {
2602 let (org, key_store) = restricted_test_key_store();
2603 let cipher = restricted_cipher(org, CipherType::Login, "not valid json".to_string());
2605
2606 let list: CipherListView = key_store.decrypt(&cipher).unwrap();
2607 assert!(list.partial);
2608 assert!(list.name.is_empty());
2609
2610 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2611 assert!(view.partial);
2612 assert!(view.name.is_empty());
2613
2614 let strict_view: CipherView = key_store.decrypt(&StrictDecrypt(cipher.clone())).unwrap();
2617 assert!(strict_view.partial);
2618 assert!(strict_view.name.is_empty());
2619 }
2620
2621 #[test]
2622 fn test_decrypt_restricted_works_in_strict_mode() {
2623 let (org, key_store) = restricted_test_key_store();
2624 let cipher = restricted_cipher(org, CipherType::Card, RESTRICTED_CARD_ENVELOPE.to_string());
2627
2628 let list: CipherListView = key_store.decrypt(&StrictDecrypt(cipher.clone())).unwrap();
2629 assert!(list.partial);
2630 assert_eq!(list.name, "Restricted Card".to_string());
2631 assert_eq!(
2632 list.r#type,
2633 CipherListViewType::Card(CardListView { brand: None })
2634 );
2635
2636 let view: CipherView = key_store.decrypt(&StrictDecrypt(cipher)).unwrap();
2637 assert!(view.partial);
2638 assert!(view.card.is_none());
2639 }
2640
2641 #[test]
2642 fn test_decrypt_restricted_strict_propagates_field_decrypt_error() {
2643 let (org, key_store) = restricted_test_key_store();
2644 let wrong_key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2647 SymmetricKeyAlgorithm::Aes256CbcHmac,
2648 ));
2649 let bad_name = "boom"
2650 .to_string()
2651 .encrypt(&mut wrong_key_store.context(), SymmetricKeySlotId::User)
2652 .unwrap();
2653 let envelope = serde_json::json!({ "name": bad_name }).to_string();
2654 let cipher = restricted_cipher(org, CipherType::Login, envelope);
2655
2656 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2658 assert!(view.partial);
2659 assert!(view.name.is_empty());
2660
2661 let view_result: Result<CipherView, CryptoError> =
2663 key_store.decrypt(&StrictDecrypt(cipher.clone()));
2664 assert!(view_result.is_err());
2665 let list_result: Result<CipherListView, CryptoError> =
2666 key_store.decrypt(&StrictDecrypt(cipher));
2667 assert!(list_result.is_err());
2668 }
2669
2670 #[test]
2671 fn test_decrypt_restricted_non_org_cipher_fails_closed() {
2672 let (org, key_store) = restricted_test_key_store();
2675 let cipher = Cipher {
2676 organization_id: None,
2677 ..restricted_cipher(
2678 org,
2679 CipherType::SecureNote,
2680 RESTRICTED_SECURE_NOTE_ENVELOPE.to_string(),
2681 )
2682 };
2683
2684 let view_result: Result<CipherView, CryptoError> = key_store.decrypt(&cipher);
2685 assert!(matches!(
2686 view_result.unwrap_err(),
2687 CryptoError::RestrictedCipherRequiresOrganization
2688 ));
2689
2690 let list_result: Result<CipherListView, CryptoError> = key_store.decrypt(&cipher);
2691 assert!(matches!(
2692 list_result.unwrap_err(),
2693 CryptoError::RestrictedCipherRequiresOrganization
2694 ));
2695 }
2696
2697 #[test]
2698 fn test_decrypt_cipher_list_view() {
2699 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2700 let key_store = create_test_crypto_with_user_key(key);
2701
2702 let cipher = Cipher {
2703 partial_data: None,
2704 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2705 organization_id: None,
2706 folder_id: None,
2707 collection_ids: vec![],
2708 key: None,
2709 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
2710 notes: None,
2711 r#type: CipherType::Login,
2712 login: Some(Login {
2713 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
2714 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
2715 password_revision_date: None,
2716 uris: None,
2717 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
2718 autofill_on_page_load: None,
2719 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User)]),
2720 }),
2721 identity: None,
2722 card: None,
2723 secure_note: None,
2724 ssh_key: None,
2725 bank_account: None,
2726 drivers_license: None,
2727 passport: None,
2728 favorite: false,
2729 reprompt: CipherRepromptType::None,
2730 organization_use_totp: false,
2731 edit: true,
2732 permissions: Some(CipherPermissions {
2733 delete: false,
2734 restore: false
2735 }),
2736 view_password: true,
2737 local_data: None,
2738 attachments: None,
2739 fields: None,
2740 password_history: None,
2741 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2742 deleted_date: None,
2743 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2744 archived_date: None,
2745 data: None,
2746 };
2747
2748 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
2749
2750 assert_eq!(
2751 view,
2752 CipherListView {
2753 partial: false,
2754 id: cipher.id,
2755 organization_id: cipher.organization_id,
2756 folder_id: cipher.folder_id,
2757 collection_ids: cipher.collection_ids,
2758 name: "My test login".to_string(),
2759 subtitle: "test_username".to_string(),
2760 r#type: CipherListViewType::Login(LoginListView {
2761 fido2_credentials: Some(vec![Fido2CredentialListView {
2762 credential_id: "123".to_string(),
2763 rp_id: "123".to_string(),
2764 user_handle: None,
2765 user_name: None,
2766 user_display_name: None,
2767 counter: "123".to_string(),
2768 }]),
2769 has_fido2: true,
2770 username: Some("test_username".to_string()),
2771 totp: cipher.login.as_ref().unwrap().totp.as_ref().map(|t| t
2772 .decrypt(&mut key_store.context(), SymmetricKeySlotId::User)
2773 .unwrap()),
2774 uris: None,
2775 }),
2776 favorite: cipher.favorite,
2777 reprompt: cipher.reprompt,
2778 organization_use_totp: cipher.organization_use_totp,
2779 edit: cipher.edit,
2780 permissions: cipher.permissions,
2781 view_password: cipher.view_password,
2782 attachments: 0,
2783 has_old_attachments: false,
2784 creation_date: cipher.creation_date,
2785 deleted_date: cipher.deleted_date,
2786 revision_date: cipher.revision_date,
2787 copyable_fields: vec![
2788 CopyableCipherFields::LoginUsername,
2789 CopyableCipherFields::LoginPassword,
2790 CopyableCipherFields::LoginTotp
2791 ],
2792 local_data: None,
2793 archived_date: cipher.archived_date,
2794 #[cfg(feature = "wasm")]
2795 notes: None,
2796 #[cfg(feature = "wasm")]
2797 fields: None,
2798 #[cfg(feature = "wasm")]
2799 attachment_names: None,
2800 }
2801 )
2802 }
2803
2804 fn blob_cipher() -> Cipher {
2805 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2806 SymmetricKeyAlgorithm::Aes256CbcHmac,
2807 ));
2808 let cipher: Cipher = key_store
2809 .encrypt(EncryptMode::Blob(generate_cipher()))
2810 .unwrap();
2811 assert!(cipher.data.is_some(), "expected a blob-shaped cipher");
2812 cipher
2813 }
2814
2815 #[test]
2816 fn test_encryption_context_to_cipher_with_id_request_preserves_data() {
2817 let cipher = blob_cipher();
2818 let expected = cipher.data.clone();
2819
2820 let request: CipherWithIdRequestModel = EncryptionContext {
2821 encrypted_for: UserId::new(TEST_UUID.parse().unwrap()),
2822 encrypted_by_key_id: Some("0102030405060708090a0b0c0d0e0f10".to_string()),
2823 cipher,
2824 }
2825 .try_into()
2826 .unwrap();
2827
2828 assert_eq!(request.data, expected);
2829 assert_eq!(
2830 request.encrypted_by_key_id.as_deref(),
2831 Some("0102030405060708090a0b0c0d0e0f10")
2832 );
2833 }
2834
2835 #[test]
2836 fn test_encryption_context_to_cipher_request_preserves_data() {
2837 let cipher = blob_cipher();
2838 let expected = cipher.data.clone();
2839
2840 let request: CipherRequestModel = EncryptionContext {
2841 encrypted_for: UserId::new(TEST_UUID.parse().unwrap()),
2842 encrypted_by_key_id: Some("0102030405060708090a0b0c0d0e0f10".to_string()),
2843 cipher,
2844 }
2845 .into();
2846
2847 assert_eq!(request.data, expected);
2848 assert_eq!(
2849 request.encrypted_by_key_id.as_deref(),
2850 Some("0102030405060708090a0b0c0d0e0f10")
2851 );
2852 }
2853
2854 #[test]
2857 fn test_encrypted_by_key_id_uses_user_key_for_personal_cipher() {
2858 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2859 let expected = user_key.key_id().unwrap().to_string();
2860 let key_store = create_test_crypto_with_user_key(user_key);
2861
2862 let view = generate_cipher();
2863 assert_eq!(view.key_identifier(), SymmetricKeySlotId::User);
2864
2865 let actual = key_store
2866 .context()
2867 .get_symmetric_key_id(view.key_identifier())
2868 .map(|id| id.to_string());
2869
2870 assert_eq!(actual.as_deref(), Some(expected.as_str()));
2871 assert_eq!(expected.len(), 32);
2873 assert!(expected.chars().all(|c| c.is_ascii_hexdigit()));
2874 assert_eq!(expected, expected.to_lowercase());
2875 }
2876
2877 #[test]
2879 fn test_encrypted_by_key_id_uses_org_key_for_org_cipher() {
2880 let org = OrganizationId::new_v4();
2881 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2882 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2883 let user_key_id = user_key.key_id().unwrap().to_string();
2884 let org_key_id = org_key.key_id().unwrap().to_string();
2885 assert_ne!(user_key_id, org_key_id);
2886
2887 let key_store = create_test_crypto_with_user_and_org_key(user_key, org, org_key);
2888
2889 let mut view = generate_cipher();
2890 view.organization_id = Some(org);
2891 assert_eq!(view.key_identifier(), SymmetricKeySlotId::Organization(org));
2892
2893 let actual = key_store
2894 .context()
2895 .get_symmetric_key_id(view.key_identifier())
2896 .map(|id| id.to_string());
2897
2898 assert_eq!(actual.as_deref(), Some(org_key_id.as_str()));
2899 }
2900
2901 #[test]
2904 fn test_encrypted_by_key_id_ignores_cipher_key() {
2905 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2906 let expected = user_key.key_id().unwrap().to_string();
2907 let key_store = create_test_crypto_with_user_key(user_key);
2908
2909 let mut view = generate_cipher();
2910 let _ = view.load_cipher_key_slot(&mut key_store.context()).unwrap();
2911 assert!(view.key.is_some());
2912
2913 let actual = key_store
2914 .context()
2915 .get_symmetric_key_id(view.key_identifier())
2916 .map(|id| id.to_string());
2917
2918 assert_eq!(actual.as_deref(), Some(expected.as_str()));
2919 }
2920
2921 #[test]
2924 fn test_encrypted_by_key_id_uses_derived_id_for_legacy_user_key() {
2925 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2926 let expected = user_key
2927 .key_id()
2928 .expect("an AES-CBC-HMAC key derives a key id")
2929 .to_string();
2930 let key_store = create_test_crypto_with_user_key(user_key);
2931
2932 let view = generate_cipher();
2933 let actual = key_store
2934 .context()
2935 .get_symmetric_key_id(view.key_identifier())
2936 .map(|id| id.to_string());
2937
2938 assert_eq!(actual.as_deref(), Some(expected.as_str()));
2939 }
2940
2941 #[test]
2942 fn test_decrypt_cipher_fails_with_invalid_name() {
2943 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2944 SymmetricKeyAlgorithm::Aes256CbcHmac,
2945 ));
2946
2947 let cipher = key_store
2949 .encrypt(EncryptMode::Legacy(generate_cipher()))
2950 .unwrap();
2951 let cipher = Cipher {
2952 name: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher
2954 };
2955
2956 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
2958 assert!(
2959 lenient_result.is_ok(),
2960 "Lenient decryption should succeed even when name is encrypted with a different key"
2961 );
2962 assert_eq!(
2963 lenient_result.unwrap().name,
2964 String::new(),
2965 "Lenient decryption should yield an empty name on error"
2966 );
2967
2968 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
2970 assert!(
2971 strict_result.is_err(),
2972 "Strict decryption should fail when name is encrypted with a different key"
2973 );
2974 }
2975
2976 #[test]
2977 fn test_decrypt_cipher_fails_with_invalid_login() {
2978 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2979 SymmetricKeyAlgorithm::Aes256CbcHmac,
2980 ));
2981
2982 let cipher = key_store
2984 .encrypt(EncryptMode::Legacy(generate_cipher()))
2985 .unwrap();
2986 let cipher = Cipher {
2987 login: Some(Login {
2988 username: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher.login.unwrap()
2990 }),
2991 ..cipher
2992 };
2993
2994 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
2996 assert!(
2997 lenient_result.is_ok(),
2998 "Lenient decryption should succeed even when login username is encrypted with a different key"
2999 );
3000 let lenient_view = lenient_result.unwrap();
3001 assert!(
3002 lenient_view.login.is_some(),
3003 "Lenient decryption should still return the login object"
3004 );
3005 assert!(
3006 lenient_view.login.unwrap().username.is_none(),
3007 "Lenient decryption should null out the failing username field"
3008 );
3009
3010 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
3012 assert!(
3013 strict_result.is_err(),
3014 "Strict decryption should fail when login username is encrypted with a different key"
3015 );
3016 }
3017
3018 #[test]
3019 fn test_encrypt_legacy_fails_closed_for_partial_view() {
3020 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3021 SymmetricKeyAlgorithm::Aes256CbcHmac,
3022 ));
3023
3024 let mut view = generate_cipher();
3025 view.partial = true;
3026
3027 let result = key_store.encrypt(EncryptMode::Legacy(view));
3028 assert!(matches!(result, Err(CryptoError::EncryptRestrictedView)));
3029 }
3030
3031 #[test]
3032 fn test_encrypt_blob_fails_closed_for_partial_view() {
3033 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3034 SymmetricKeyAlgorithm::Aes256CbcHmac,
3035 ));
3036
3037 let mut view = generate_cipher();
3038 view.partial = true;
3039
3040 let result = key_store.encrypt(EncryptMode::Blob(view));
3041 assert!(matches!(result, Err(CryptoError::EncryptRestrictedView)));
3042 }
3043
3044 #[test]
3045 fn test_encrypt_succeeds_for_non_partial_view() {
3046 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3047 SymmetricKeyAlgorithm::Aes256CbcHmac,
3048 ));
3049
3050 let view = generate_cipher();
3054 assert!(!view.partial);
3055
3056 let result = key_store.encrypt(EncryptMode::Legacy(view));
3057 assert!(result.is_ok());
3058 }
3059
3060 #[test]
3061 fn test_load_cipher_key_slot_ignores_attachments_without_key() {
3062 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3063 let key_store = create_test_crypto_with_user_key(key);
3064
3065 let mut cipher = generate_cipher();
3066 let attachment = AttachmentView {
3067 id: None,
3068 url: None,
3069 size: None,
3070 size_name: None,
3071 file_name: Some("Attachment test name".into()),
3072 key: None,
3073 };
3074 cipher.attachments = Some(vec![attachment]);
3075
3076 let _ = cipher
3077 .load_cipher_key_slot(&mut key_store.context())
3078 .unwrap();
3079 assert!(cipher.attachments.unwrap()[0].key.is_none());
3080 }
3081
3082 #[test]
3083 fn test_validate_attachment_keys_with_cipher_key() {
3084 let old_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3085 let key_store = create_test_crypto_with_user_key(old_key);
3086 let mut ctx = key_store.context_mut();
3087
3088 let mut cipher = generate_cipher();
3089 let _ = cipher.load_cipher_key_slot(&mut ctx).unwrap();
3090
3091 cipher.validate_attachment_keys().unwrap();
3092
3093 assert!(cipher.key.is_some());
3094 }
3095
3096 #[test]
3098 fn test_validate_attachment_keys_without_attachments() {
3099 let mut cipher = generate_cipher();
3100 assert!(cipher.key.is_none());
3101 assert!(cipher.attachments.is_none());
3102
3103 cipher.validate_attachment_keys().unwrap();
3104 }
3105
3106 #[test]
3107 fn test_move_user_cipher_to_org() {
3108 let org = OrganizationId::new_v4();
3109 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3110 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3111 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3112
3113 let mut cipher = generate_cipher();
3115 let _ = cipher
3116 .load_cipher_key_slot(&mut key_store.context())
3117 .unwrap();
3118
3119 cipher.move_to_organization(org).unwrap();
3120 assert_eq!(cipher.organization_id, Some(org));
3121
3122 let cipher_enc = key_store.encrypt(EncryptMode::Legacy(cipher)).unwrap();
3124 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
3125
3126 assert_eq!(cipher_dec.name, "My test login");
3127 }
3128
3129 #[test]
3130 fn test_move_user_cipher_to_org_manually() {
3131 let org = OrganizationId::new_v4();
3132 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3133 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3134 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3135
3136 let mut cipher = generate_cipher();
3138 let _ = cipher
3139 .load_cipher_key_slot(&mut key_store.context())
3140 .unwrap();
3141
3142 cipher.organization_id = Some(org);
3143
3144 let cipher_enc = key_store.encrypt(EncryptMode::Legacy(cipher)).unwrap();
3147 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
3148 assert_eq!(cipher_dec.name, "My test login");
3149 }
3150
3151 #[test]
3152 fn test_move_user_cipher_with_attachment_without_key_to_org() {
3153 let org = OrganizationId::new_v4();
3154 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3155 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3156 let _key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3157
3158 let mut cipher = generate_cipher();
3159 let attachment = AttachmentView {
3160 id: None,
3161 url: None,
3162 size: None,
3163 size_name: None,
3164 file_name: Some("Attachment test name".into()),
3165 key: None,
3166 };
3167 cipher.attachments = Some(vec![attachment]);
3168
3169 assert!(cipher.move_to_organization(org).is_err());
3171 }
3172
3173 #[test]
3174 fn test_move_user_cipher_with_attachment_with_key_to_org() {
3175 let org = OrganizationId::new_v4();
3176 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3177 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3178 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3179
3180 let attachment_key_val = {
3182 let mut ctx = key_store.context();
3183 let attachment_key = ctx.generate_symmetric_key();
3184 #[allow(deprecated)]
3185 ctx.dangerous_get_symmetric_key(attachment_key)
3186 .unwrap()
3187 .clone()
3188 };
3189
3190 let mut cipher = generate_cipher();
3191 let attachment = AttachmentView {
3192 id: None,
3193 url: None,
3194 size: None,
3195 size_name: None,
3196 file_name: Some("Attachment test name".into()),
3197 key: Some(attachment_key_val.clone()),
3198 };
3199 cipher.attachments = Some(vec![attachment]);
3200 let cred = generate_fido2_view();
3201 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
3202
3203 cipher.move_to_organization(org).unwrap();
3204
3205 assert!(cipher.key.is_none());
3206
3207 assert_eq!(
3209 cipher.attachments.unwrap()[0].key.clone().unwrap(),
3210 attachment_key_val
3211 );
3212
3213 let cred2 = cipher
3214 .login
3215 .unwrap()
3216 .fido2_credentials
3217 .unwrap()
3218 .first()
3219 .unwrap()
3220 .clone();
3221
3222 assert_eq!(cred2.credential_id, "123");
3223 }
3224
3225 #[test]
3226 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
3227 let org = OrganizationId::new_v4();
3228 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3229 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3230 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3231
3232 let mut ctx = key_store.context();
3233
3234 let cipher_key = ctx.generate_symmetric_key();
3235 #[allow(deprecated)]
3236 let cipher_key_raw = ctx.dangerous_get_symmetric_key(cipher_key).unwrap().clone();
3237
3238 let attachment_key = ctx.generate_symmetric_key();
3240 #[allow(deprecated)]
3241 let attachment_key_raw = ctx
3242 .dangerous_get_symmetric_key(attachment_key)
3243 .unwrap()
3244 .clone();
3245
3246 let mut cipher = generate_cipher();
3247 cipher.key = Some(cipher_key_raw.clone());
3248
3249 let attachment = AttachmentView {
3250 id: None,
3251 url: None,
3252 size: None,
3253 size_name: None,
3254 file_name: Some("Attachment test name".into()),
3255 key: Some(attachment_key_raw.clone()),
3256 };
3257 cipher.attachments = Some(vec![attachment]);
3258
3259 let cred = generate_fido2_view();
3260 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
3261
3262 cipher.move_to_organization(org).unwrap();
3263
3264 assert_eq!(cipher.key.clone().unwrap(), cipher_key_raw);
3266
3267 assert_eq!(
3269 cipher.attachments.unwrap()[0].key.as_ref().unwrap(),
3270 &attachment_key_raw
3271 );
3272
3273 let cred2 = cipher
3274 .login
3275 .unwrap()
3276 .fido2_credentials
3277 .unwrap()
3278 .first()
3279 .unwrap()
3280 .clone();
3281
3282 assert_eq!(cred2.credential_id, cred.credential_id);
3283 }
3284
3285 #[test]
3286 fn test_password_history_on_password_change() {
3287 use chrono::Utc;
3288
3289 let original_cipher = generate_cipher();
3290 let mut new_cipher = generate_cipher();
3291
3292 if let Some(ref mut login) = new_cipher.login {
3294 login.password = Some("new_password123".to_string());
3295 }
3296
3297 let start = Utc::now();
3298 new_cipher.update_password_history(&original_cipher);
3299 let end = Utc::now();
3300
3301 assert!(new_cipher.password_history.is_some());
3302 let history = new_cipher.password_history.unwrap();
3303 assert_eq!(history.len(), 1);
3304 assert_eq!(history[0].password, "test_password");
3305 assert!(
3306 history[0].last_used_date >= start && history[0].last_used_date <= end,
3307 "last_used_date was not set properly"
3308 );
3309 }
3310
3311 #[test]
3312 fn test_password_history_on_unchanged_password() {
3313 let original_cipher = generate_cipher();
3314 let mut new_cipher = generate_cipher();
3315
3316 new_cipher.update_password_history(&original_cipher);
3317
3318 assert!(
3320 new_cipher.password_history.is_none()
3321 || new_cipher.password_history.as_ref().unwrap().is_empty()
3322 );
3323 }
3324
3325 #[test]
3326 fn test_password_history_is_preserved() {
3327 use chrono::TimeZone;
3328
3329 let mut original_cipher = generate_cipher();
3330 original_cipher.password_history = Some(
3331 (0..4)
3332 .map(|i| PasswordHistoryView {
3333 password: format!("old_password_{}", i),
3334 last_used_date: chrono::Utc
3335 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
3336 .unwrap(),
3337 })
3338 .collect(),
3339 );
3340
3341 let mut new_cipher = generate_cipher();
3342
3343 new_cipher.update_password_history(&original_cipher);
3344
3345 assert!(new_cipher.password_history.is_some());
3346 let history = new_cipher.password_history.unwrap();
3347 assert_eq!(history.len(), 4);
3348
3349 assert_eq!(history[0].password, "old_password_0");
3350 assert_eq!(
3351 history[0].last_used_date,
3352 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
3353 );
3354 assert_eq!(history[1].password, "old_password_1");
3355 assert_eq!(
3356 history[1].last_used_date,
3357 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
3358 );
3359 assert_eq!(history[2].password, "old_password_2");
3360 assert_eq!(
3361 history[2].last_used_date,
3362 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
3363 );
3364 assert_eq!(history[3].password, "old_password_3");
3365 assert_eq!(
3366 history[3].last_used_date,
3367 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
3368 );
3369 }
3370
3371 #[test]
3372 fn test_populate_cipher_types_login_with_valid_data() {
3373 let mut cipher = Cipher {
3374 partial_data: None,
3375 id: Some(TEST_UUID.parse().unwrap()),
3376 organization_id: None,
3377 folder_id: None,
3378 collection_ids: vec![],
3379 key: None,
3380 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3381 notes: None,
3382 r#type: CipherType::Login,
3383 login: None,
3384 identity: None,
3385 card: None,
3386 secure_note: None,
3387 ssh_key: None,
3388 bank_account: None,
3389 drivers_license: None,
3390 passport: None,
3391 favorite: false,
3392 reprompt: CipherRepromptType::None,
3393 organization_use_totp: false,
3394 edit: true,
3395 view_password: true,
3396 permissions: None,
3397 local_data: None,
3398 attachments: None,
3399 fields: None,
3400 password_history: None,
3401 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3402 deleted_date: None,
3403 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3404 archived_date: None,
3405 data: Some(format!(
3406 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3407 TEST_ENC_STRING_1, TEST_ENC_STRING_2
3408 )),
3409 };
3410
3411 cipher
3412 .populate_cipher_types()
3413 .expect("populate_cipher_types failed");
3414
3415 assert!(cipher.login.is_some());
3416 let login = cipher.login.unwrap();
3417 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
3418 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
3419 }
3420
3421 #[test]
3422 fn test_populate_cipher_types_secure_note() {
3423 let mut cipher = Cipher {
3424 partial_data: None,
3425 id: Some(TEST_UUID.parse().unwrap()),
3426 organization_id: None,
3427 folder_id: None,
3428 collection_ids: vec![],
3429 key: None,
3430 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3431 notes: None,
3432 r#type: CipherType::SecureNote,
3433 login: None,
3434 identity: None,
3435 card: None,
3436 secure_note: None,
3437 ssh_key: None,
3438 bank_account: None,
3439 drivers_license: None,
3440 passport: None,
3441 favorite: false,
3442 reprompt: CipherRepromptType::None,
3443 organization_use_totp: false,
3444 edit: true,
3445 view_password: true,
3446 permissions: None,
3447 local_data: None,
3448 attachments: None,
3449 fields: None,
3450 password_history: None,
3451 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3452 deleted_date: None,
3453 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3454 archived_date: None,
3455 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
3456 };
3457
3458 cipher
3459 .populate_cipher_types()
3460 .expect("populate_cipher_types failed");
3461
3462 assert!(cipher.secure_note.is_some());
3463 }
3464
3465 #[test]
3466 fn test_populate_cipher_types_card() {
3467 let mut cipher = Cipher {
3468 partial_data: None,
3469 id: Some(TEST_UUID.parse().unwrap()),
3470 organization_id: None,
3471 folder_id: None,
3472 collection_ids: vec![],
3473 key: None,
3474 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3475 notes: None,
3476 r#type: CipherType::Card,
3477 login: None,
3478 identity: None,
3479 card: None,
3480 secure_note: None,
3481 ssh_key: None,
3482 bank_account: None,
3483 drivers_license: None,
3484 passport: None,
3485 favorite: false,
3486 reprompt: CipherRepromptType::None,
3487 organization_use_totp: false,
3488 edit: true,
3489 view_password: true,
3490 permissions: None,
3491 local_data: None,
3492 attachments: None,
3493 fields: None,
3494 password_history: None,
3495 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3496 deleted_date: None,
3497 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3498 archived_date: None,
3499 data: Some(format!(
3500 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3501 TEST_ENC_STRING_1,
3502 TEST_ENC_STRING_2,
3503 TEST_ENC_STRING_3,
3504 TEST_ENC_STRING_4,
3505 TEST_ENC_STRING_5,
3506 TEST_ENC_STRING_1
3507 )),
3508 };
3509
3510 cipher
3511 .populate_cipher_types()
3512 .expect("populate_cipher_types failed");
3513
3514 assert!(cipher.card.is_some());
3515 let card = cipher.card.unwrap();
3516 assert_eq!(
3517 card.cardholder_name.as_ref().unwrap().to_string(),
3518 TEST_ENC_STRING_1
3519 );
3520 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
3521 assert_eq!(
3522 card.exp_month.as_ref().unwrap().to_string(),
3523 TEST_ENC_STRING_3
3524 );
3525 assert_eq!(
3526 card.exp_year.as_ref().unwrap().to_string(),
3527 TEST_ENC_STRING_4
3528 );
3529 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
3530 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
3531 }
3532
3533 #[test]
3534 fn test_populate_cipher_types_identity() {
3535 let mut cipher = Cipher {
3536 partial_data: None,
3537 id: Some(TEST_UUID.parse().unwrap()),
3538 organization_id: None,
3539 folder_id: None,
3540 collection_ids: vec![],
3541 key: None,
3542 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3543 notes: None,
3544 r#type: CipherType::Identity,
3545 login: None,
3546 identity: None,
3547 card: None,
3548 secure_note: None,
3549 ssh_key: None,
3550 bank_account: None,
3551 drivers_license: None,
3552 passport: None,
3553 favorite: false,
3554 reprompt: CipherRepromptType::None,
3555 organization_use_totp: false,
3556 edit: true,
3557 view_password: true,
3558 permissions: None,
3559 local_data: None,
3560 attachments: None,
3561 fields: None,
3562 password_history: None,
3563 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3564 deleted_date: None,
3565 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3566 archived_date: None,
3567 data: Some(format!(
3568 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
3569 TEST_ENC_STRING_1,
3570 TEST_ENC_STRING_2,
3571 TEST_ENC_STRING_3,
3572 TEST_ENC_STRING_4,
3573 TEST_ENC_STRING_5,
3574 TEST_ENC_STRING_1,
3575 TEST_ENC_STRING_2,
3576 TEST_ENC_STRING_3,
3577 TEST_ENC_STRING_4,
3578 TEST_ENC_STRING_5
3579 )),
3580 };
3581
3582 cipher
3583 .populate_cipher_types()
3584 .expect("populate_cipher_types failed");
3585
3586 assert!(cipher.identity.is_some());
3587 let identity = cipher.identity.unwrap();
3588 assert_eq!(
3589 identity.first_name.as_ref().unwrap().to_string(),
3590 TEST_ENC_STRING_1
3591 );
3592 assert_eq!(
3593 identity.last_name.as_ref().unwrap().to_string(),
3594 TEST_ENC_STRING_2
3595 );
3596 assert_eq!(
3597 identity.email.as_ref().unwrap().to_string(),
3598 TEST_ENC_STRING_3
3599 );
3600 assert_eq!(
3601 identity.phone.as_ref().unwrap().to_string(),
3602 TEST_ENC_STRING_4
3603 );
3604 assert_eq!(
3605 identity.company.as_ref().unwrap().to_string(),
3606 TEST_ENC_STRING_5
3607 );
3608 assert_eq!(
3609 identity.address1.as_ref().unwrap().to_string(),
3610 TEST_ENC_STRING_1
3611 );
3612 assert_eq!(
3613 identity.city.as_ref().unwrap().to_string(),
3614 TEST_ENC_STRING_2
3615 );
3616 assert_eq!(
3617 identity.state.as_ref().unwrap().to_string(),
3618 TEST_ENC_STRING_3
3619 );
3620 assert_eq!(
3621 identity.postal_code.as_ref().unwrap().to_string(),
3622 TEST_ENC_STRING_4
3623 );
3624 assert_eq!(
3625 identity.country.as_ref().unwrap().to_string(),
3626 TEST_ENC_STRING_5
3627 );
3628 }
3629
3630 #[test]
3631
3632 fn test_password_history_with_hidden_fields() {
3633 let mut original_cipher = generate_cipher();
3634 original_cipher.fields = Some(vec![FieldView {
3635 name: Some("Secret Key".to_string()),
3636 value: Some("old_secret_value".to_string()),
3637 r#type: crate::FieldType::Hidden,
3638 linked_id: None,
3639 }]);
3640
3641 let mut new_cipher = generate_cipher();
3642 new_cipher.fields = Some(vec![FieldView {
3643 name: Some("Secret Key".to_string()),
3644 value: Some("new_secret_value".to_string()),
3645 r#type: crate::FieldType::Hidden,
3646 linked_id: None,
3647 }]);
3648
3649 new_cipher.update_password_history(&original_cipher);
3650
3651 assert!(new_cipher.password_history.is_some());
3652 let history = new_cipher.password_history.unwrap();
3653 assert_eq!(history.len(), 1);
3654 assert_eq!(history[0].password, "Secret Key: old_secret_value");
3655 }
3656
3657 #[test]
3658 fn test_password_history_length_limit() {
3659 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
3660
3661 let mut original_cipher = generate_cipher();
3662 original_cipher.password_history = Some(
3663 (0..10)
3664 .map(|i| PasswordHistoryView {
3665 password: format!("old_password_{}", i),
3666 last_used_date: chrono::Utc::now(),
3667 })
3668 .collect(),
3669 );
3670
3671 let mut new_cipher = original_cipher.clone();
3672 if let Some(ref mut login) = new_cipher.login {
3674 login.password = Some("brand_new_password".to_string());
3675 }
3676
3677 new_cipher.update_password_history(&original_cipher);
3678
3679 assert!(new_cipher.password_history.is_some());
3680 let history = new_cipher.password_history.unwrap();
3681
3682 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
3684
3685 assert_eq!(history[0].password, "test_password");
3687 assert_eq!(history[1].password, "old_password_0");
3689 assert_eq!(history[2].password, "old_password_1");
3690 assert_eq!(history[3].password, "old_password_2");
3691 assert_eq!(history[4].password, "old_password_3");
3692 }
3693
3694 #[test]
3695 fn test_populate_cipher_types_ssh_key() {
3696 let mut cipher = Cipher {
3697 partial_data: None,
3698 id: Some(TEST_UUID.parse().unwrap()),
3699 organization_id: None,
3700 folder_id: None,
3701 collection_ids: vec![],
3702 key: None,
3703 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3704 notes: None,
3705 r#type: CipherType::SshKey,
3706 login: None,
3707 identity: None,
3708 card: None,
3709 secure_note: None,
3710 ssh_key: None,
3711 bank_account: None,
3712 drivers_license: None,
3713 passport: None,
3714 favorite: false,
3715 reprompt: CipherRepromptType::None,
3716 organization_use_totp: false,
3717 edit: true,
3718 view_password: true,
3719 permissions: None,
3720 local_data: None,
3721 attachments: None,
3722 fields: None,
3723 password_history: None,
3724 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3725 deleted_date: None,
3726 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3727 archived_date: None,
3728 data: Some(format!(
3729 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3730 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
3731 )),
3732 };
3733
3734 cipher
3735 .populate_cipher_types()
3736 .expect("populate_cipher_types failed");
3737
3738 assert!(cipher.ssh_key.is_some());
3739 let ssh_key = cipher.ssh_key.unwrap();
3740 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
3741 assert_eq!(ssh_key.public_key.unwrap().to_string(), TEST_ENC_STRING_2);
3742 assert_eq!(ssh_key.fingerprint.unwrap().to_string(), TEST_ENC_STRING_3);
3743 }
3744
3745 #[test]
3746 fn test_populate_cipher_types_with_null_data() {
3747 let mut cipher = Cipher {
3748 partial_data: None,
3749 id: Some(TEST_UUID.parse().unwrap()),
3750 organization_id: None,
3751 folder_id: None,
3752 collection_ids: vec![],
3753 key: None,
3754 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3755 notes: None,
3756 r#type: CipherType::Login,
3757 login: None,
3758 identity: None,
3759 card: None,
3760 secure_note: None,
3761 ssh_key: None,
3762 bank_account: None,
3763 drivers_license: None,
3764 passport: None,
3765 favorite: false,
3766 reprompt: CipherRepromptType::None,
3767 organization_use_totp: false,
3768 edit: true,
3769 view_password: true,
3770 permissions: None,
3771 local_data: None,
3772 attachments: None,
3773 fields: None,
3774 password_history: None,
3775 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3776 deleted_date: None,
3777 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3778 archived_date: None,
3779 data: None,
3780 };
3781
3782 let result = cipher.populate_cipher_types();
3783 assert!(matches!(
3784 result,
3785 Err(VaultParseError::MissingField(MissingFieldError("data")))
3786 ));
3787 }
3788
3789 #[test]
3790 fn test_populate_cipher_types_with_invalid_json() {
3791 let mut cipher = Cipher {
3792 partial_data: None,
3793 id: Some(TEST_UUID.parse().unwrap()),
3794 organization_id: None,
3795 folder_id: None,
3796 collection_ids: vec![],
3797 key: None,
3798 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3799 notes: None,
3800 r#type: CipherType::Login,
3801 login: None,
3802 identity: None,
3803 card: None,
3804 secure_note: None,
3805 ssh_key: None,
3806 bank_account: None,
3807 drivers_license: None,
3808 passport: None,
3809 favorite: false,
3810 reprompt: CipherRepromptType::None,
3811 organization_use_totp: false,
3812 edit: true,
3813 view_password: true,
3814 permissions: None,
3815 local_data: None,
3816 attachments: None,
3817 fields: None,
3818 password_history: None,
3819 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3820 deleted_date: None,
3821 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3822 archived_date: None,
3823 data: Some("invalid json".to_string()),
3824 };
3825
3826 let result = cipher.populate_cipher_types();
3827
3828 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
3829 }
3830
3831 #[test]
3832 fn test_decrypt_cipher_with_mixed_attachments() {
3833 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
3834 let key_store = create_test_crypto_with_user_key(user_key);
3835
3836 let mut ctx = key_store.context();
3838 let valid1 = "valid_file_1.txt"
3839 .encrypt(&mut ctx, SymmetricKeySlotId::User)
3840 .unwrap();
3841 let valid2 = "valid_file_2.txt"
3842 .encrypt(&mut ctx, SymmetricKeySlotId::User)
3843 .unwrap();
3844
3845 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
3847 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
3848 let mut wrong_ctx = wrong_key_store.context();
3849 let corrupted = "corrupted_file.txt"
3850 .encrypt(&mut wrong_ctx, SymmetricKeySlotId::User)
3851 .unwrap();
3852
3853 let cipher = Cipher {
3854 partial_data: None,
3855 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
3856 organization_id: None,
3857 folder_id: None,
3858 collection_ids: vec![],
3859 key: None,
3860 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3861 notes: None,
3862 r#type: CipherType::Login,
3863 login: None,
3864 identity: None,
3865 card: None,
3866 secure_note: None,
3867 ssh_key: None,
3868 bank_account: None,
3869 drivers_license: None,
3870 passport: None,
3871 favorite: false,
3872 reprompt: CipherRepromptType::None,
3873 organization_use_totp: false,
3874 edit: true,
3875 permissions: None,
3876 view_password: true,
3877 local_data: None,
3878 attachments: Some(vec![
3879 attachment::Attachment {
3881 id: Some("valid-attachment".to_string()),
3882 url: Some("https://example.com/valid".to_string()),
3883 size: Some("100".to_string()),
3884 size_name: Some("100 Bytes".to_string()),
3885 file_name: Some(valid1),
3886 key: None,
3887 },
3888 attachment::Attachment {
3890 id: Some("corrupted-attachment".to_string()),
3891 url: Some("https://example.com/corrupted".to_string()),
3892 size: Some("200".to_string()),
3893 size_name: Some("200 Bytes".to_string()),
3894 file_name: Some(corrupted),
3895 key: None,
3896 },
3897 attachment::Attachment {
3899 id: Some("valid-attachment-2".to_string()),
3900 url: Some("https://example.com/valid2".to_string()),
3901 size: Some("150".to_string()),
3902 size_name: Some("150 Bytes".to_string()),
3903 file_name: Some(valid2),
3904 key: None,
3905 },
3906 ]),
3907 fields: None,
3908 password_history: None,
3909 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3910 deleted_date: None,
3911 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3912 archived_date: None,
3913 data: None,
3914 };
3915
3916 let view: CipherView = key_store.decrypt(&cipher).unwrap();
3917
3918 assert!(view.attachments.is_some());
3920 let successes = view.attachments.as_ref().unwrap();
3921 assert_eq!(successes.len(), 2);
3922 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
3923 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
3924
3925 assert!(view.attachment_decryption_failures.is_some());
3927 let failures = view.attachment_decryption_failures.as_ref().unwrap();
3928 assert_eq!(failures.len(), 1);
3929 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
3930 assert_eq!(failures[0].file_name, None);
3931 }
3932
3933 #[test]
3934 fn test_decrypt_cipher_list_view_passport() {
3935 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3936 SymmetricKeyAlgorithm::Aes256CbcHmac,
3937 ));
3938
3939 let cipher_view = CipherView {
3940 r#type: CipherType::Passport,
3941 passport: Some(passport::PassportView {
3942 given_name: Some("Jane".to_string()),
3943 surname: Some("Doe".to_string()),
3944 passport_number: Some("P12345678".to_string()),
3945 ..Default::default()
3946 }),
3947 login: None,
3948 ..generate_cipher()
3949 };
3950
3951 let cipher: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
3952 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
3953
3954 assert_eq!(list_view.r#type, CipherListViewType::Passport);
3955 assert_eq!(list_view.subtitle, "Jane Doe");
3956 assert_eq!(
3957 list_view.copyable_fields,
3958 vec![
3959 CopyableCipherFields::PassportGivenName,
3960 CopyableCipherFields::PassportSurname,
3961 CopyableCipherFields::PassportPassportNumber,
3962 ]
3963 );
3964 }
3965
3966 #[test]
3967 fn test_decrypt_cipher_list_view_drivers_license() {
3968 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3969 SymmetricKeyAlgorithm::Aes256CbcHmac,
3970 ));
3971
3972 let cipher_view = CipherView {
3973 r#type: CipherType::DriversLicense,
3974 drivers_license: Some(drivers_license::DriversLicenseView {
3975 first_name: Some("John".to_string()),
3976 last_name: Some("Doe".to_string()),
3977 license_number: Some("DL-987654".to_string()),
3978 ..Default::default()
3979 }),
3980 login: None,
3981 ..generate_cipher()
3982 };
3983
3984 let cipher: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
3985 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
3986
3987 assert_eq!(list_view.r#type, CipherListViewType::DriversLicense);
3988 assert_eq!(list_view.subtitle, "John Doe");
3989 assert_eq!(
3990 list_view.copyable_fields,
3991 vec![
3992 CopyableCipherFields::DriversLicenseFirstName,
3993 CopyableCipherFields::DriversLicenseLastName,
3994 CopyableCipherFields::DriversLicenseLicenseNumber,
3995 ]
3996 );
3997 }
3998
3999 #[test]
4000 fn test_cipher_view_encrypt_decrypt_passport() {
4001 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4002 SymmetricKeyAlgorithm::Aes256CbcHmac,
4003 ));
4004
4005 let passport = passport::PassportView {
4006 given_name: Some("Jane".to_string()),
4007 surname: Some("Doe".to_string()),
4008 date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4009 sex: Some("F".to_string()),
4010 birth_place: Some("New York".to_string()),
4011 nationality: Some("American".to_string()),
4012 issuing_country: Some("US".to_string()),
4013 passport_number: Some("P12345678".to_string()),
4014 passport_type: Some("P".to_string()),
4015 national_identification_number: Some("123-45-6789".to_string()),
4016 issuing_authority: Some("US State Department".to_string()),
4017 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4018 expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
4019 };
4020
4021 let cipher_view = CipherView {
4022 r#type: CipherType::Passport,
4023 passport: Some(passport.clone()),
4024 login: None,
4025 ..generate_cipher()
4026 };
4027
4028 let encrypted: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
4029 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
4030
4031 assert_eq!(decrypted.r#type, CipherType::Passport);
4032 assert_eq!(decrypted.passport, Some(passport));
4033 assert!(decrypted.login.is_none());
4034 }
4035
4036 #[test]
4037 fn test_cipher_view_encrypt_decrypt_drivers_license() {
4038 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4039 SymmetricKeyAlgorithm::Aes256CbcHmac,
4040 ));
4041
4042 let dl = drivers_license::DriversLicenseView {
4043 first_name: Some("John".to_string()),
4044 middle_name: Some("Michael".to_string()),
4045 last_name: Some("Doe".to_string()),
4046 date_of_birth: chrono::NaiveDate::from_ymd_opt(1985, 6, 15),
4047 license_number: Some("DL-987654".to_string()),
4048 issuing_country: Some("US".to_string()),
4049 issuing_state: Some("NY".to_string()),
4050 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4051 expiration_date: chrono::NaiveDate::from_ymd_opt(2028, 1, 1),
4052 issuing_authority: Some("NY DMV".to_string()),
4053 license_class: Some("D".to_string()),
4054 };
4055
4056 let cipher_view = CipherView {
4057 r#type: CipherType::DriversLicense,
4058 drivers_license: Some(dl.clone()),
4059 login: None,
4060 ..generate_cipher()
4061 };
4062
4063 let encrypted: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
4064 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
4065
4066 assert_eq!(decrypted.r#type, CipherType::DriversLicense);
4067 assert_eq!(decrypted.drivers_license, Some(dl));
4068 assert!(decrypted.login.is_none());
4069 }
4070
4071 #[test]
4072 fn test_try_from_cipher_mini_response_model_login() {
4073 use bitwarden_api_api::models::CipherLoginModel;
4074 use chrono::Utc;
4075
4076 let mini_response = CipherMiniResponseModel {
4077 id: Some(TEST_UUID.parse().unwrap()),
4078 organization_id: Some(TEST_UUID.parse().unwrap()),
4079 name: Some(TEST_CIPHER_NAME.to_string()),
4080 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4081 login: Some(Box::new(CipherLoginModel {
4082 username: Some(TEST_CIPHER_NAME.to_string()),
4083 ..Default::default()
4084 })),
4085 creation_date: Some(Utc::now().to_rfc3339()),
4086 revision_date: Some(Utc::now().to_rfc3339()),
4087 ..Default::default()
4088 };
4089
4090 let cipher = Cipher::try_from(mini_response).unwrap();
4091
4092 assert_eq!(cipher.id, Some(TEST_UUID.parse().unwrap()));
4093 assert_eq!(
4094 cipher.organization_id,
4095 Some(TEST_UUID.parse::<OrganizationId>().unwrap())
4096 );
4097 assert_eq!(cipher.r#type, CipherType::Login);
4098 assert_eq!(cipher.name, Some(TEST_CIPHER_NAME.parse().unwrap()));
4099 assert_eq!(
4100 cipher.login.as_ref().unwrap().username,
4101 Some(TEST_CIPHER_NAME.parse().unwrap())
4102 );
4103 assert!(cipher.view_password);
4104 assert!(cipher.key.is_none());
4105 }
4106
4107 #[test]
4108 fn test_mini_response_model_view_password_defaults_to_true() {
4109 use chrono::Utc;
4110
4111 let mini_response = CipherMiniResponseModel {
4114 id: Some(TEST_UUID.parse().unwrap()),
4115 name: Some(TEST_CIPHER_NAME.to_string()),
4116 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4117 creation_date: Some(Utc::now().to_rfc3339()),
4118 revision_date: Some(Utc::now().to_rfc3339()),
4119 ..Default::default()
4120 };
4121
4122 let cipher = mini_response.merge_with_cipher(None).unwrap();
4123 assert!(
4124 cipher.view_password,
4125 "view_password should default to true for CipherMiniResponseModel"
4126 );
4127
4128 let mini_details_response = CipherMiniDetailsResponseModel {
4130 id: Some(TEST_UUID.parse().unwrap()),
4131 name: Some(TEST_CIPHER_NAME.to_string()),
4132 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4133 creation_date: Some(Utc::now().to_rfc3339()),
4134 revision_date: Some(Utc::now().to_rfc3339()),
4135 ..Default::default()
4136 };
4137
4138 let cipher = mini_details_response.merge_with_cipher(None).unwrap();
4139 assert!(
4140 cipher.view_password,
4141 "view_password should default to true for CipherMiniDetailsResponseModel"
4142 );
4143 }
4144
4145 #[test]
4149 fn test_merge_takes_partial_data_from_response() {
4150 use chrono::Utc;
4151
4152 let org: OrganizationId = RESTRICTED_ORG_UUID.parse().unwrap();
4153 let local_restricted = || {
4155 Some(restricted_cipher(
4156 org,
4157 CipherType::Login,
4158 RESTRICTED_LOGIN_ENVELOPE.to_string(),
4159 ))
4160 };
4161 let now = Utc::now().to_rfc3339();
4162
4163 macro_rules! assert_gating_from_response {
4164 ($model:ident) => {{
4165 let base = $model {
4166 id: Some(TEST_UUID.parse().unwrap()),
4167 organization_id: Some(org.to_string().parse().unwrap()),
4168 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4169 creation_date: Some(now.clone()),
4170 revision_date: Some(now.clone()),
4171 ..Default::default()
4172 };
4173
4174 assert_eq!(
4176 base.clone()
4177 .merge_with_cipher(local_restricted())
4178 .unwrap()
4179 .partial_data,
4180 None,
4181 concat!(stringify!($model), ": full response must un-gate"),
4182 );
4183
4184 let restricted = $model {
4186 partial_data: Some(RESTRICTED_LOGIN_ENVELOPE.to_string()),
4187 ..base
4188 };
4189 assert_eq!(
4190 restricted
4191 .merge_with_cipher(None)
4192 .unwrap()
4193 .partial_data
4194 .as_deref(),
4195 Some(RESTRICTED_LOGIN_ENVELOPE),
4196 concat!(stringify!($model), ": restricted response must gate"),
4197 );
4198 }};
4199 }
4200
4201 assert_gating_from_response!(CipherResponseModel);
4202 assert_gating_from_response!(CipherDetailsResponseModel);
4203 assert_gating_from_response!(CipherMiniResponseModel);
4204 assert_gating_from_response!(CipherMiniDetailsResponseModel);
4205 }
4206
4207 mod cipher_decrypt_dispatch {
4210 use bitwarden_crypto::KeyStore;
4211
4212 use super::*;
4213 use crate::{
4214 BankAccountView, CardView, DriversLicenseView, IdentityView, PassportView,
4215 SecureNoteType, SecureNoteView, SshKeyView, cipher::blob::encrypt_blob_cipher,
4216 };
4217
4218 fn make_key_store() -> KeyStore<KeySlotIds> {
4219 create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4220 SymmetricKeyAlgorithm::Aes256CbcHmac,
4221 ))
4222 }
4223
4224 fn encrypt_legacy(view: CipherView, key_store: &KeyStore<KeySlotIds>) -> Cipher {
4226 key_store.encrypt(EncryptMode::Legacy(view)).unwrap()
4227 }
4228
4229 fn encrypt_blob(mut view: CipherView, key_store: &KeyStore<KeySlotIds>) -> Cipher {
4231 let mut ctx = key_store.context_mut();
4232 encrypt_blob_cipher(&mut view, &mut ctx).unwrap()
4233 }
4234
4235 fn base_login_view() -> CipherView {
4236 let mut view = generate_cipher();
4237 view.name = "Test Login".to_string();
4238 view.login = Some(LoginView {
4239 username: Some("[email protected]".to_string()),
4240 password: Some("hunter2".to_string()),
4241 password_revision_date: None,
4242 uris: None,
4243 totp: Some("otpauth://totp/test?secret=SECRET".to_string()),
4244 autofill_on_page_load: None,
4245 fido2_credentials: None,
4246 });
4247 view
4248 }
4249
4250 #[test]
4252 fn dispatches_blob_to_cipher_view() {
4253 let key_store = make_key_store();
4254 let cipher = encrypt_blob(base_login_view(), &key_store);
4255
4256 let view: CipherView = key_store.decrypt(&cipher).unwrap();
4257
4258 assert_eq!(view.name, "Test Login");
4259 let login = view.login.expect("blob decrypt should restore login");
4260 assert_eq!(login.username.as_deref(), Some("[email protected]"));
4261 assert_eq!(login.password.as_deref(), Some("hunter2"));
4262 }
4263
4264 #[test]
4267 fn dispatches_legacy_to_cipher_view() {
4268 let key_store = make_key_store();
4269
4270 let cipher = encrypt_legacy(base_login_view(), &key_store);
4272 let view: CipherView = key_store.decrypt(&cipher).unwrap();
4273 assert_eq!(view.name, "Test Login");
4274 assert_eq!(
4275 view.login.unwrap().username.as_deref(),
4276 Some("[email protected]"),
4277 );
4278
4279 let cipher = encrypt_legacy(base_login_view(), &key_store);
4281 let view: CipherView = key_store.decrypt(&StrictDecrypt(cipher)).unwrap();
4282 assert_eq!(view.name, "Test Login");
4283 assert_eq!(
4284 view.login.unwrap().username.as_deref(),
4285 Some("[email protected]"),
4286 );
4287 }
4288
4289 #[test]
4294 fn blob_to_list_view_per_type() {
4295 let key_store = make_key_store();
4296
4297 {
4299 let list_view = decrypt_blob_list_view(&key_store, base_login_view());
4300 assert_eq!(list_view.name, "Test Login");
4301 assert_eq!(list_view.subtitle, "[email protected]");
4302 assert!(matches!(list_view.r#type, CipherListViewType::Login(_)));
4303 assert!(
4304 list_view
4305 .copyable_fields
4306 .contains(&CopyableCipherFields::LoginUsername)
4307 );
4308 assert!(
4309 list_view
4310 .copyable_fields
4311 .contains(&CopyableCipherFields::LoginPassword)
4312 );
4313 assert!(
4314 list_view
4315 .copyable_fields
4316 .contains(&CopyableCipherFields::LoginTotp)
4317 );
4318 }
4319
4320 {
4322 let mut view = generate_cipher();
4323 view.r#type = CipherType::Card;
4324 view.login = None;
4325 view.name = "My Card".to_string();
4326 view.card = Some(CardView {
4327 cardholder_name: Some("John Doe".to_string()),
4328 exp_month: Some("12".to_string()),
4329 exp_year: Some("2030".to_string()),
4330 code: Some("123".to_string()),
4331 brand: Some("Visa".to_string()),
4332 number: Some("4111111111111111".to_string()),
4333 });
4334 let list_view = decrypt_blob_list_view(&key_store, view);
4335 assert_eq!(list_view.name, "My Card");
4336 assert!(list_view.subtitle.contains("Visa"));
4337 assert!(list_view.subtitle.contains("1111"));
4338 match &list_view.r#type {
4339 CipherListViewType::Card(card) => {
4340 assert_eq!(card.brand.as_deref(), Some("Visa"))
4341 }
4342 other => panic!("expected Card, got {other:?}"),
4343 }
4344 assert!(
4345 list_view
4346 .copyable_fields
4347 .contains(&CopyableCipherFields::CardNumber)
4348 );
4349 assert!(
4350 list_view
4351 .copyable_fields
4352 .contains(&CopyableCipherFields::CardSecurityCode)
4353 );
4354 }
4355
4356 {
4358 let mut view = generate_cipher();
4359 view.r#type = CipherType::Identity;
4360 view.login = None;
4361 view.name = "My Identity".to_string();
4362 view.identity = Some(IdentityView {
4363 title: None,
4364 first_name: Some("Jane".to_string()),
4365 middle_name: None,
4366 last_name: Some("Doe".to_string()),
4367 address1: Some("123 Main St".to_string()),
4368 address2: None,
4369 address3: None,
4370 city: None,
4371 state: None,
4372 postal_code: None,
4373 country: None,
4374 company: None,
4375 email: Some("[email protected]".to_string()),
4376 phone: None,
4377 ssn: None,
4378 username: None,
4379 passport_number: None,
4380 license_number: None,
4381 });
4382 let list_view = decrypt_blob_list_view(&key_store, view);
4383 assert_eq!(list_view.name, "My Identity");
4384 assert!(list_view.subtitle.contains("Jane"));
4385 assert!(list_view.subtitle.contains("Doe"));
4386 assert!(matches!(list_view.r#type, CipherListViewType::Identity));
4387 assert!(
4388 list_view
4389 .copyable_fields
4390 .contains(&CopyableCipherFields::IdentityEmail)
4391 );
4392 assert!(
4393 list_view
4394 .copyable_fields
4395 .contains(&CopyableCipherFields::IdentityAddress)
4396 );
4397 }
4398
4399 {
4401 let mut view = generate_cipher();
4402 view.r#type = CipherType::SecureNote;
4403 view.login = None;
4404 view.name = "My Note".to_string();
4405 view.notes = Some("secret".to_string());
4406 view.secure_note = Some(SecureNoteView {
4407 r#type: SecureNoteType::Generic,
4408 });
4409 let list_view = decrypt_blob_list_view(&key_store, view);
4410 assert_eq!(list_view.name, "My Note");
4411 assert_eq!(list_view.subtitle, "");
4412 assert!(matches!(list_view.r#type, CipherListViewType::SecureNote));
4413 assert!(
4414 list_view
4415 .copyable_fields
4416 .contains(&CopyableCipherFields::SecureNotes)
4417 );
4418 }
4419
4420 {
4422 let mut view = generate_cipher();
4423 view.r#type = CipherType::SshKey;
4424 view.login = None;
4425 view.name = "My SSH".to_string();
4426 view.ssh_key = Some(SshKeyView {
4427 private_key: "-----BEGIN PRIVATE KEY-----".to_string(),
4428 public_key: "ssh-ed25519 AAAA".to_string(),
4429 fingerprint: "SHA256:abcdef".to_string(),
4430 });
4431 let list_view = decrypt_blob_list_view(&key_store, view);
4432 assert_eq!(list_view.name, "My SSH");
4433 assert_eq!(list_view.subtitle, "SHA256:abcdef");
4434 assert!(matches!(list_view.r#type, CipherListViewType::SshKey));
4435 assert!(
4436 list_view
4437 .copyable_fields
4438 .contains(&CopyableCipherFields::SshKey)
4439 );
4440 }
4441
4442 {
4444 let mut view = generate_cipher();
4445 view.r#type = CipherType::BankAccount;
4446 view.login = None;
4447 view.name = "My Bank Account".to_string();
4448 view.bank_account = Some(BankAccountView {
4449 bank_name: Some("Some Bank".to_string()),
4450 name_on_account: Some("Jane Doe".to_string()),
4451 account_number: Some("123456".to_string()),
4452 routing_number: Some("111000025".to_string()),
4453 branch_number: Some("001".to_string()),
4454 pin: Some("4321".to_string()),
4455 swift_code: Some("ABCDEF12".to_string()),
4456 iban: Some("DE89370400440532013000".to_string()),
4457 ..Default::default()
4458 });
4459 let list_view = decrypt_blob_list_view(&key_store, view);
4460 assert_eq!(list_view.name, "My Bank Account");
4461 assert_eq!(list_view.subtitle, "Some Bank");
4462 assert_eq!(
4463 list_view.r#type,
4464 CipherListViewType::BankAccount(BankAccountListView {
4465 account_number: Some("123456".to_string()),
4466 account_type: None,
4467 })
4468 );
4469 assert_eq!(
4470 list_view.copyable_fields,
4471 vec![
4472 CopyableCipherFields::BankAccountNameOnAccount,
4473 CopyableCipherFields::BankAccountAccountNumber,
4474 CopyableCipherFields::BankAccountRoutingNumber,
4475 CopyableCipherFields::BankAccountBranchNumber,
4476 CopyableCipherFields::BankAccountPin,
4477 CopyableCipherFields::BankAccountIban,
4478 CopyableCipherFields::BankAccountSwift,
4479 ]
4480 );
4481 }
4482 }
4483
4484 fn fully_populated_views() -> Vec<(&'static str, CipherView)> {
4491 let with_type = |r#type: CipherType, f: &dyn Fn(&mut CipherView)| {
4492 let mut view = generate_cipher();
4493 view.r#type = r#type;
4494 view.login = None;
4495 f(&mut view);
4496 view
4497 };
4498
4499 vec![
4500 ("Login", base_login_view()),
4501 (
4502 "Card",
4503 with_type(CipherType::Card, &|v| {
4504 v.card = Some(CardView {
4505 cardholder_name: Some("Jane Doe".to_string()),
4506 exp_month: Some("12".to_string()),
4507 exp_year: Some("2030".to_string()),
4508 code: Some("123".to_string()),
4509 brand: Some("Visa".to_string()),
4510 number: Some("4111111111111111".to_string()),
4511 });
4512 }),
4513 ),
4514 (
4515 "Identity",
4516 with_type(CipherType::Identity, &|v| {
4517 v.identity = Some(IdentityView {
4518 title: Some("Mx".to_string()),
4519 first_name: Some("Jane".to_string()),
4520 middle_name: Some("Q".to_string()),
4521 last_name: Some("Doe".to_string()),
4522 address1: Some("1 Main St".to_string()),
4523 address2: Some("Apt 2".to_string()),
4524 address3: Some("Floor 3".to_string()),
4525 city: Some("Anytown".to_string()),
4526 state: Some("CA".to_string()),
4527 postal_code: Some("90210".to_string()),
4528 country: Some("US".to_string()),
4529 company: Some("Acme".to_string()),
4530 email: Some("[email protected]".to_string()),
4531 phone: Some("555-0100".to_string()),
4532 ssn: Some("000-00-0000".to_string()),
4533 username: Some("jane".to_string()),
4534 passport_number: Some("X1234567".to_string()),
4535 license_number: Some("D1234567".to_string()),
4536 });
4537 }),
4538 ),
4539 (
4540 "SecureNote",
4541 with_type(CipherType::SecureNote, &|v| {
4542 v.notes = Some("a secret note".to_string());
4543 v.secure_note = Some(SecureNoteView {
4544 r#type: SecureNoteType::Generic,
4545 });
4546 }),
4547 ),
4548 (
4549 "SshKey",
4550 with_type(CipherType::SshKey, &|v| {
4551 v.ssh_key = Some(SshKeyView {
4552 private_key: "private".to_string(),
4553 public_key: "public".to_string(),
4554 fingerprint: "SHA256:abc".to_string(),
4555 });
4556 }),
4557 ),
4558 (
4559 "BankAccount",
4560 with_type(CipherType::BankAccount, &|v| {
4561 v.bank_account = Some(BankAccountView {
4562 bank_name: Some("Some Bank".to_string()),
4563 name_on_account: Some("Jane Doe".to_string()),
4564 account_type: Some("Checking".to_string()),
4565 account_number: Some("123456".to_string()),
4566 routing_number: Some("111000025".to_string()),
4567 branch_number: Some("001".to_string()),
4568 pin: Some("4321".to_string()),
4569 swift_code: Some("ABCDEF12".to_string()),
4570 iban: Some("DE89370400440532013000".to_string()),
4571 bank_contact_phone: Some("555-0199".to_string()),
4572 });
4573 }),
4574 ),
4575 (
4576 "DriversLicense",
4577 with_type(CipherType::DriversLicense, &|v| {
4578 v.drivers_license = Some(DriversLicenseView {
4579 first_name: Some("Jane".to_string()),
4580 middle_name: Some("Q".to_string()),
4581 last_name: Some("Doe".to_string()),
4582 date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4583 license_number: Some("D1234567".to_string()),
4584 issuing_country: Some("US".to_string()),
4585 issuing_state: Some("CA".to_string()),
4586 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4587 expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
4588 issuing_authority: Some("DMV".to_string()),
4589 license_class: Some("C".to_string()),
4590 });
4591 }),
4592 ),
4593 (
4594 "Passport",
4595 with_type(CipherType::Passport, &|v| {
4596 v.passport = Some(PassportView {
4597 surname: Some("Doe".to_string()),
4598 given_name: Some("Jane".to_string()),
4599 date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4600 sex: Some("F".to_string()),
4601 birth_place: Some("Anytown".to_string()),
4602 nationality: Some("US".to_string()),
4603 issuing_country: Some("US".to_string()),
4604 passport_number: Some("X1234567".to_string()),
4605 passport_type: Some("P".to_string()),
4606 national_identification_number: Some("000-00-0000".to_string()),
4607 issuing_authority: Some("State Dept".to_string()),
4608 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4609 expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
4610 });
4611 }),
4612 ),
4613 ]
4614 }
4615
4616 #[test]
4623 fn copyable_fields_parity_between_legacy_and_blob() {
4624 let key_store = make_key_store();
4625
4626 for (label, view) in fully_populated_views() {
4627 let legacy: CipherListView = key_store
4628 .decrypt(&encrypt_legacy(view.clone(), &key_store))
4629 .unwrap();
4630 let blob = decrypt_blob_list_view(&key_store, view);
4631
4632 assert_eq!(
4633 legacy.copyable_fields, blob.copyable_fields,
4634 "copyable_fields diverged between legacy and blob paths for {label}",
4635 );
4636 }
4637 }
4638
4639 #[test]
4643 fn login_list_view_preserves_totp_round_trip() {
4644 let key_store = make_key_store();
4645 let list_view = decrypt_blob_list_view(&key_store, base_login_view());
4646
4647 match &list_view.r#type {
4648 CipherListViewType::Login(login) => assert!(login.totp.is_some()),
4649 other => panic!("expected Login, got {other:?}"),
4650 }
4651 let totp = list_view.get_totp_key().unwrap();
4652 assert_eq!(totp.as_deref(), Some("otpauth://totp/test?secret=SECRET"));
4653 }
4654
4655 #[test]
4657 fn mixed_batch_decrypt_list() {
4658 let key_store = make_key_store();
4659 let blob = encrypt_blob(base_login_view(), &key_store);
4660 let legacy = encrypt_legacy(base_login_view(), &key_store);
4661
4662 let ciphers = vec![blob, legacy];
4663 let views: Vec<CipherListView> = key_store.decrypt_list(&ciphers).unwrap();
4664
4665 assert_eq!(views.len(), 2);
4666 for v in &views {
4667 assert_eq!(v.name, "Test Login");
4668 assert_eq!(v.subtitle, "[email protected]");
4669 }
4670 }
4671
4672 fn decrypt_blob_list_view(
4673 key_store: &KeyStore<KeySlotIds>,
4674 view: CipherView,
4675 ) -> CipherListView {
4676 let cipher = encrypt_blob(view, key_store);
4677 key_store.decrypt(&cipher).unwrap()
4678 }
4679
4680 fn failing_attachments() -> Vec<attachment::Attachment> {
4684 let wrong = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4685 SymmetricKeyAlgorithm::Aes256CbcHmac,
4686 ));
4687 let mut ctx = wrong.context();
4688 let mut enc = |s: &str| s.encrypt(&mut ctx, SymmetricKeySlotId::User).unwrap();
4689 vec![
4690 attachment::Attachment {
4691 id: Some("a1".to_string()),
4692 url: None,
4693 size: None,
4694 size_name: None,
4695 file_name: Some(enc("a1.txt")),
4696 key: Some(enc("k1")),
4697 },
4698 attachment::Attachment {
4699 id: Some("a2-old".to_string()),
4700 url: None,
4701 size: None,
4702 size_name: None,
4703 file_name: Some(enc("a2.txt")),
4704 key: None,
4705 },
4706 attachment::Attachment {
4707 id: Some("a3".to_string()),
4708 url: None,
4709 size: None,
4710 size_name: None,
4711 file_name: Some(enc("a3.txt")),
4712 key: Some(enc("k3")),
4713 },
4714 ]
4715 }
4716
4717 #[test]
4724 fn attachment_metrics_parity_with_failing_attachments() {
4725 let key_store = make_key_store();
4726
4727 let mut legacy = encrypt_legacy(base_login_view(), &key_store);
4728 legacy.attachments = Some(failing_attachments());
4729 let legacy_list: CipherListView = key_store.decrypt(&legacy).unwrap();
4730
4731 let mut blob = encrypt_blob(base_login_view(), &key_store);
4732 blob.attachments = Some(failing_attachments());
4733 let blob_list: CipherListView = key_store.decrypt(&blob).unwrap();
4734
4735 assert_eq!(legacy_list.attachments, 3);
4736 assert!(legacy_list.has_old_attachments);
4737 assert_eq!(blob_list.attachments, legacy_list.attachments);
4738 assert_eq!(
4739 blob_list.has_old_attachments,
4740 legacy_list.has_old_attachments,
4741 );
4742 }
4743 }
4744
4745 mod encrypt_mode {
4748 use bitwarden_crypto::{IdentifyKey, KeyStore};
4749
4750 use super::*;
4751
4752 fn make_key_store() -> KeyStore<KeySlotIds> {
4753 create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4754 SymmetricKeyAlgorithm::Aes256CbcHmac,
4755 ))
4756 }
4757
4758 fn base_login_view() -> CipherView {
4759 let mut view = generate_cipher();
4760 view.name = "Round Trip".to_string();
4761 view.login = Some(LoginView {
4762 username: Some("[email protected]".to_string()),
4763 password: Some("hunter2".to_string()),
4764 password_revision_date: None,
4765 uris: None,
4766 totp: None,
4767 autofill_on_page_load: None,
4768 fido2_credentials: None,
4769 });
4770 view
4771 }
4772
4773 #[test]
4776 fn blob_variant_produces_blob_shaped_cipher() {
4777 let key_store = make_key_store();
4778 let mode = EncryptMode::Blob(base_login_view());
4779
4780 let cipher: Cipher = key_store.encrypt(mode).unwrap();
4781
4782 assert!(try_parse_blob(&cipher).is_some());
4783 assert!(cipher.data.is_some());
4784 assert!(cipher.login.is_none());
4785 assert!(cipher.card.is_none());
4786 assert!(cipher.identity.is_none());
4787 assert!(cipher.secure_note.is_none());
4788 assert!(cipher.ssh_key.is_none());
4789 assert!(cipher.bank_account.is_none());
4790 assert!(cipher.fields.is_none());
4791 assert!(cipher.password_history.is_none());
4792 assert!(cipher.notes.is_none());
4793 }
4794
4795 #[test]
4798 fn legacy_variant_produces_legacy_shaped_cipher() {
4799 let key_store = make_key_store();
4800 let mode = EncryptMode::Legacy(base_login_view());
4801
4802 let cipher: Cipher = key_store.encrypt(mode).unwrap();
4803
4804 assert!(try_parse_blob(&cipher).is_none());
4805 assert!(cipher.data.is_none());
4806 assert!(cipher.login.is_some());
4807 assert!(
4808 cipher.key.is_some(),
4809 "legacy encrypt must always emit a cipher key"
4810 );
4811 }
4812
4813 #[test]
4817 fn legacy_variant_wraps_cipher_key_under_explicit_key() {
4818 let key_store = make_key_store();
4819 let view = base_login_view();
4820 let mut ctx = key_store.context();
4821
4822 let new_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make(
4823 SymmetricKeyAlgorithm::Aes256CbcHmac,
4824 ));
4825 let cipher = EncryptMode::Legacy(view.clone())
4826 .encrypt_composite(&mut ctx, new_key)
4827 .unwrap();
4828
4829 let decrypted = lenient_decrypt_cipher_view(&cipher, &mut ctx, new_key).unwrap();
4832 assert_eq!(decrypted.name, view.name);
4833 assert_eq!(
4834 decrypted.login.as_ref().unwrap().password,
4835 view.login.as_ref().unwrap().password
4836 );
4837
4838 assert!(
4840 lenient_decrypt_cipher_view(&cipher, &mut ctx, view.key_identifier()).is_err(),
4841 "cipher key must not be wrapped under the view's own slot"
4842 );
4843 }
4844
4845 #[test]
4847 fn legacy_variant_preserves_existing_cipher_key() {
4848 let key_store = make_key_store();
4849 let mut view = base_login_view();
4850 let mut ctx = key_store.context();
4851
4852 let _ = view.load_cipher_key_slot(&mut ctx).unwrap();
4853 let original_key = view
4854 .key
4855 .clone()
4856 .expect("load_cipher_key_slot stores the key");
4857
4858 let wrapping_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make(
4859 SymmetricKeyAlgorithm::Aes256CbcHmac,
4860 ));
4861 let cipher = EncryptMode::Legacy(view)
4862 .encrypt_composite(&mut ctx, wrapping_key)
4863 .unwrap();
4864
4865 let unwrapped = ctx
4866 .unwrap_symmetric_key(wrapping_key, &cipher.key.expect("cipher key is emitted"))
4867 .unwrap();
4868 #[allow(deprecated)]
4869 let unwrapped_raw = ctx.dangerous_get_symmetric_key(unwrapped).unwrap().clone();
4870
4871 assert_eq!(
4872 unwrapped_raw, original_key,
4873 "encryption must wrap the view's existing cipher key, not a new one"
4874 );
4875 }
4876
4877 #[test]
4879 fn blob_variant_round_trips_through_decrypt() {
4880 let key_store = make_key_store();
4881 let original = base_login_view();
4882 let mode = EncryptMode::Blob(original.clone());
4883
4884 let cipher: Cipher = key_store.encrypt(mode).unwrap();
4885 let restored: CipherView = key_store.decrypt(&cipher).unwrap();
4886
4887 assert_eq!(restored.name, original.name);
4888 let login = restored.login.expect("round-trip should restore login");
4889 assert_eq!(login.username, original.login.as_ref().unwrap().username);
4890 assert_eq!(login.password, original.login.as_ref().unwrap().password);
4891 }
4892
4893 #[test]
4896 fn key_identifier_delegates_to_inner_view() {
4897 let view = base_login_view();
4898 let expected = view.key_identifier();
4899 let mode = EncryptMode::Blob(view);
4900 assert_eq!(mode.key_identifier(), expected);
4901 }
4902
4903 #[test]
4906 fn mixed_batch_encrypt_list_preserves_per_item_shape() {
4907 let key_store = make_key_store();
4908 let mut legacy_view = base_login_view();
4909 legacy_view.name = "Legacy".to_string();
4910 let mut blob_view = base_login_view();
4911 blob_view.name = "Blob".to_string();
4912
4913 let modes = vec![
4914 EncryptMode::Legacy(legacy_view),
4915 EncryptMode::Blob(blob_view),
4916 ];
4917 let ciphers: Vec<Cipher> = key_store.encrypt_list(&modes).unwrap();
4918
4919 assert_eq!(ciphers.len(), 2);
4920 assert!(
4921 try_parse_blob(&ciphers[0]).is_none(),
4922 "first item should be legacy"
4923 );
4924 assert!(
4925 try_parse_blob(&ciphers[1]).is_some(),
4926 "second item should be blob"
4927 );
4928 assert!(ciphers[0].login.is_some());
4929 assert!(ciphers[1].login.is_none());
4930 }
4931 }
4932}