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#[cfg(feature = "wasm")]
23use tsify::Tsify;
24#[cfg(feature = "wasm")]
25use wasm_bindgen::prelude::wasm_bindgen;
26
27use super::{
28 attachment, bank_account,
29 bank_account::BankAccountListView,
30 blob::{decrypt_blob_cipher, encrypt_blob_cipher_with_wrapping_key, try_parse_blob},
31 card,
32 card::CardListView,
33 cipher_permissions::CipherPermissions,
34 drivers_license, field, identity,
35 local_data::{LocalData, LocalDataView},
36 login::{LoginListView, LoginUri, LoginUriView},
37 passport, secure_note, ssh_key,
38};
39use crate::{
40 DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView, FieldView, FolderId,
41 Login, LoginView, VaultParseError,
42 password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
43};
44
45uuid_newtype!(pub CipherId);
46
47#[allow(missing_docs)]
48#[bitwarden_error(flat)]
49#[derive(Debug, Error)]
50pub enum CipherError {
51 #[error(transparent)]
52 MissingField(#[from] MissingFieldError),
53 #[error(transparent)]
54 Crypto(#[from] CryptoError),
55 #[error(transparent)]
56 Decrypt(#[from] DecryptError),
57 #[error(transparent)]
58 Encrypt(#[from] EncryptError),
59 #[error(
60 "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
61 )]
62 AttachmentsWithoutKeys,
63 #[error("This cipher cannot be moved to the specified organization")]
64 OrganizationAlreadySet,
65 #[error(transparent)]
66 Repository(#[from] RepositoryError),
67 #[error(transparent)]
68 Chrono(#[from] chrono::ParseError),
69 #[error(transparent)]
70 SerdeJson(#[from] serde_json::Error),
71 #[error(transparent)]
72 Api(#[from] ApiError),
73}
74
75pub(super) trait CipherKind {
77 fn decrypt_subtitle(
79 &self,
80 ctx: &mut KeyStoreContext<KeySlotIds>,
81 key: SymmetricKeySlotId,
82 ) -> Result<String, CryptoError>;
83
84 fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
86}
87
88#[allow(missing_docs)]
89#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
90#[repr(u8)]
91#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
92#[cfg_attr(feature = "wasm", wasm_bindgen)]
93pub enum CipherType {
94 Login = 1,
95 SecureNote = 2,
96 Card = 3,
97 Identity = 4,
98 SshKey = 5,
99 BankAccount = 6,
100 DriversLicense = 7,
101 Passport = 8,
102}
103
104#[allow(missing_docs)]
105#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
106#[repr(u8)]
107#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
108#[cfg_attr(feature = "wasm", wasm_bindgen)]
109pub enum CipherRepromptType {
110 #[default]
111 None = 0,
112 Password = 1,
113}
114
115#[allow(missing_docs)]
116#[derive(Serialize, Deserialize, Debug, Clone)]
117#[serde(rename_all = "camelCase", deny_unknown_fields)]
118#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
119#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
120pub struct EncryptionContext {
121 pub encrypted_for: UserId,
124 #[serde(default)]
128 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
129 #[cfg_attr(feature = "wasm", tsify(optional))]
130 pub encrypted_by_key_id: Option<String>,
131 pub cipher: Cipher,
132}
133
134impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
135 type Error = CipherError;
136 fn try_from(
137 EncryptionContext {
138 cipher,
139 encrypted_for,
140 encrypted_by_key_id,
141 }: EncryptionContext,
142 ) -> Result<Self, Self::Error> {
143 Ok(Self {
144 id: require!(cipher.id).into(),
145 encrypted_for: Some(encrypted_for.into()),
146 encrypted_by_key_id,
147 r#type: Some(cipher.r#type.into()),
148 organization_id: cipher.organization_id.map(|o| o.to_string()),
149 is_organization_cipher: None,
150 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
151 favorite: cipher.favorite.into(),
152 reprompt: Some(cipher.reprompt.into()),
153 key: cipher.key.map(|k| k.to_string()),
154 name: cipher.name.as_ref().map(ToString::to_string),
155 notes: cipher.notes.map(|n| n.to_string()),
156 fields: Some(
157 cipher
158 .fields
159 .into_iter()
160 .flatten()
161 .map(Into::into)
162 .collect(),
163 ),
164 password_history: Some(
165 cipher
166 .password_history
167 .into_iter()
168 .flatten()
169 .map(Into::into)
170 .collect(),
171 ),
172 attachments: None,
173 attachments2: Some(
174 cipher
175 .attachments
176 .into_iter()
177 .flatten()
178 .filter_map(|a| {
179 a.id.map(|id| {
180 (
181 id,
182 bitwarden_api_api::models::CipherAttachmentModel {
183 file_name: a.file_name.map(|n| n.to_string()),
184 key: a.key.map(|k| k.to_string()),
185 },
186 )
187 })
188 })
189 .collect(),
190 ),
191 login: cipher.login.map(|l| Box::new(l.into())),
192 card: cipher.card.map(|c| Box::new(c.into())),
193 identity: cipher.identity.map(|i| Box::new(i.into())),
194 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
195 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
196 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
197 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
198 passport: cipher.passport.map(|p| Box::new(p.into())),
199 data: cipher.data,
200 last_known_revision_date: Some(
201 cipher
202 .revision_date
203 .to_rfc3339_opts(SecondsFormat::Millis, true),
204 ),
205 archived_date: cipher
206 .archived_date
207 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
208 })
209 }
210}
211
212impl From<EncryptionContext> for CipherRequestModel {
213 fn from(
214 EncryptionContext {
215 cipher,
216 encrypted_for,
217 encrypted_by_key_id,
218 }: EncryptionContext,
219 ) -> Self {
220 Self {
221 encrypted_for: Some(encrypted_for.into()),
222 encrypted_by_key_id,
223 r#type: Some(cipher.r#type.into()),
224 organization_id: cipher.organization_id.map(|o| o.to_string()),
225 is_organization_cipher: None,
226 folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
227 favorite: cipher.favorite.into(),
228 reprompt: Some(cipher.reprompt.into()),
229 key: cipher.key.map(|k| k.to_string()),
230 name: cipher.name.as_ref().map(ToString::to_string),
231 notes: cipher.notes.map(|n| n.to_string()),
232 fields: Some(
233 cipher
234 .fields
235 .into_iter()
236 .flatten()
237 .map(Into::into)
238 .collect(),
239 ),
240 password_history: Some(
241 cipher
242 .password_history
243 .into_iter()
244 .flatten()
245 .map(Into::into)
246 .collect(),
247 ),
248 attachments: None,
249 attachments2: Some(
250 cipher
251 .attachments
252 .into_iter()
253 .flatten()
254 .filter_map(|a| {
255 a.id.map(|id| {
256 (
257 id,
258 bitwarden_api_api::models::CipherAttachmentModel {
259 file_name: a.file_name.map(|n| n.to_string()),
260 key: a.key.map(|k| k.to_string()),
261 },
262 )
263 })
264 })
265 .collect(),
266 ),
267 login: cipher.login.map(|l| Box::new(l.into())),
268 card: cipher.card.map(|c| Box::new(c.into())),
269 identity: cipher.identity.map(|i| Box::new(i.into())),
270 secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
271 ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
272 bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
273 drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
274 passport: cipher.passport.map(|p| Box::new(p.into())),
275 data: cipher.data,
276 last_known_revision_date: Some(
277 cipher
278 .revision_date
279 .to_rfc3339_opts(SecondsFormat::Millis, true),
280 ),
281 archived_date: cipher
282 .archived_date
283 .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
284 }
285 }
286}
287
288#[allow(missing_docs)]
301#[derive(Serialize, Deserialize, Debug, Clone)]
302#[serde(rename_all = "camelCase", deny_unknown_fields)]
303#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
304#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
305pub struct Cipher {
306 pub id: Option<CipherId>,
307 pub organization_id: Option<OrganizationId>,
308 pub folder_id: Option<FolderId>,
309 pub collection_ids: Vec<CollectionId>,
310 pub key: Option<EncString>,
314
315 pub name: Option<EncString>,
319 pub notes: Option<EncString>,
321
322 pub r#type: CipherType,
323 pub login: Option<Login>,
325 pub identity: Option<identity::Identity>,
327 pub card: Option<card::Card>,
329 pub secure_note: Option<secure_note::SecureNote>,
331 pub ssh_key: Option<ssh_key::SshKey>,
333 pub bank_account: Option<bank_account::BankAccount>,
335 pub drivers_license: Option<drivers_license::DriversLicense>,
337 pub passport: Option<passport::Passport>,
339
340 pub favorite: bool,
341 pub reprompt: CipherRepromptType,
342 pub organization_use_totp: bool,
343 pub edit: bool,
344 pub permissions: Option<CipherPermissions>,
345 pub view_password: bool,
346 pub local_data: Option<LocalData>,
347
348 pub attachments: Option<Vec<attachment::Attachment>>,
349 pub fields: Option<Vec<field::Field>>,
351 pub password_history: Option<Vec<password_history::PasswordHistory>>,
353
354 pub creation_date: DateTime<Utc>,
355 pub deleted_date: Option<DateTime<Utc>>,
356 pub revision_date: DateTime<Utc>,
357 pub archived_date: Option<DateTime<Utc>>,
358 pub data: Option<String>,
361
362 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub partial_data: Option<String>,
368}
369
370pub enum CipherKeyRewrapError {
373 NoCipherKey,
374 DecryptionFailure,
375 EncryptionFailure,
376}
377
378impl Cipher {
379 pub fn rewrap_cipher_key(
384 &mut self,
385 old_key: SymmetricKeySlotId,
386 new_key: SymmetricKeySlotId,
387 ctx: &mut KeyStoreContext<KeySlotIds>,
388 ) -> Result<(), CipherKeyRewrapError> {
389 let new_cipher_key = self
390 .key
391 .as_ref()
392 .ok_or(CipherKeyRewrapError::NoCipherKey)
393 .and_then(|wrapped_cipher_key| {
394 ctx.unwrap_symmetric_key(old_key, wrapped_cipher_key)
395 .map_err(|_| CipherKeyRewrapError::DecryptionFailure)
396 })
397 .and_then(|cipher_key| {
398 ctx.wrap_symmetric_key(new_key, cipher_key)
399 .map_err(|_| CipherKeyRewrapError::EncryptionFailure)
400 })?;
401 self.key = Some(new_cipher_key);
402 Ok(())
403 }
404
405 pub fn is_blob_encrypted(&self) -> bool {
407 try_parse_blob(self).is_some()
408 }
409}
410
411bitwarden_state::register_repository_item!(CipherId => Cipher, "Cipher");
412
413impl TryFrom<Cipher> for CipherRequestModel {
414 type Error = CryptoError;
415
416 fn try_from(c: Cipher) -> Result<Self, Self::Error> {
423 let attachments2 = c
424 .attachments
425 .map(|list| {
426 list.into_iter()
427 .map(|a| {
428 let id = a.id.clone().ok_or(CryptoError::MissingField("id"))?;
429 Ok::<_, CryptoError>((id, a.into()))
430 })
431 .collect::<Result<_, _>>()
432 })
433 .transpose()?;
434
435 Ok(CipherRequestModel {
436 encrypted_for: None,
437 encrypted_by_key_id: None,
438 r#type: Some(c.r#type.into()),
439 organization_id: c.organization_id.map(|id| id.to_string()),
440 is_organization_cipher: None,
441 folder_id: c.folder_id.map(|id| id.to_string()),
442 favorite: Some(c.favorite),
443 reprompt: Some(c.reprompt.into()),
444 key: c.key.map(|k| k.to_string()),
445 name: c.name.as_ref().map(ToString::to_string),
446 notes: c.notes.map(|n| n.to_string()),
447 login: c.login.map(|v| Box::new(v.into())),
448 card: c.card.map(|v| Box::new(v.into())),
449 identity: c.identity.map(|v| Box::new(v.into())),
450 secure_note: c.secure_note.map(|v| Box::new(v.into())),
451 ssh_key: c.ssh_key.map(|v| Box::new(v.into())),
452 bank_account: c.bank_account.map(|v| Box::new(v.into())),
453 drivers_license: c.drivers_license.map(|v| Box::new(v.into())),
454 passport: c.passport.map(|v| Box::new(v.into())),
455 fields: c.fields.map(|f| f.into_iter().map(Into::into).collect()),
456 password_history: c
457 .password_history
458 .map(|h| h.into_iter().map(Into::into).collect()),
459 attachments: None,
460 attachments2,
461 last_known_revision_date: Some(
462 c.revision_date.to_rfc3339_opts(SecondsFormat::Secs, true),
463 ),
464 archived_date: c.archived_date.map(|d| d.to_rfc3339()),
465 data: c.data,
466 })
467 }
468}
469
470#[allow(missing_docs)]
471#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
472#[serde(rename_all = "camelCase", deny_unknown_fields)]
473#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
474#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
475pub struct CipherView {
476 pub id: Option<CipherId>,
477 pub organization_id: Option<OrganizationId>,
478 pub folder_id: Option<FolderId>,
479 pub collection_ids: Vec<CollectionId>,
480
481 #[cfg_attr(feature = "wasm", tsify(type = "SymmetricKey | undefined"))]
482 pub key: Option<SymmetricCryptoKey>,
483
484 pub name: String,
485 pub notes: Option<String>,
486
487 pub r#type: CipherType,
488 pub login: Option<LoginView>,
489 pub identity: Option<identity::IdentityView>,
490 pub card: Option<card::CardView>,
491 pub secure_note: Option<secure_note::SecureNoteView>,
492 pub ssh_key: Option<ssh_key::SshKeyView>,
493 pub bank_account: Option<bank_account::BankAccountView>,
494 pub drivers_license: Option<drivers_license::DriversLicenseView>,
495 pub passport: Option<passport::PassportView>,
496
497 pub favorite: bool,
498 pub reprompt: CipherRepromptType,
499 pub organization_use_totp: bool,
500 pub edit: bool,
501 pub permissions: Option<CipherPermissions>,
502 pub view_password: bool,
503 pub local_data: Option<LocalDataView>,
504
505 pub attachments: Option<Vec<attachment::AttachmentView>>,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub attachment_decryption_failures: Option<Vec<attachment::AttachmentView>>,
509 pub fields: Option<Vec<field::FieldView>>,
510 pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
511 pub creation_date: DateTime<Utc>,
512 pub deleted_date: Option<DateTime<Utc>>,
513 pub revision_date: DateTime<Utc>,
514 pub archived_date: Option<DateTime<Utc>>,
515
516 #[serde(default)]
523 pub partial: bool,
524}
525
526#[allow(missing_docs)]
527#[derive(Serialize, Deserialize, Debug, PartialEq)]
528#[serde(rename_all = "camelCase", deny_unknown_fields)]
529#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
530#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
531pub enum CipherListViewType {
532 Login(LoginListView),
533 SecureNote,
534 Card(CardListView),
535 Identity,
536 SshKey,
537 BankAccount(BankAccountListView),
538 Passport,
539 DriversLicense,
540}
541
542#[derive(Serialize, Deserialize, Debug, PartialEq)]
544#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
545#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
546pub enum CopyableCipherFields {
547 LoginUsername,
548 LoginPassword,
549 LoginTotp,
550 CardNumber,
551 CardSecurityCode,
552 IdentityUsername,
553 IdentityEmail,
554 IdentityPhone,
555 IdentityAddress,
556 SshKey,
557 SecureNotes,
558 BankAccountNameOnAccount,
559 BankAccountAccountNumber,
560 BankAccountRoutingNumber,
561 BankAccountBranchNumber,
562 BankAccountPin,
563 BankAccountIban,
564 BankAccountSwift,
565 PassportGivenName,
566 PassportSurname,
567 PassportPassportNumber,
568 PassportNationalIdentificationNumber,
569 DriversLicenseFirstName,
570 DriversLicenseMiddleName,
571 DriversLicenseLastName,
572 DriversLicenseLicenseNumber,
573}
574
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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 PartialCipher for CipherMiniResponseModel {
2213 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
2214 let cipher = cipher.as_ref();
2215 Ok(Cipher {
2216 partial_data: self.partial_data,
2217 id: self.id.map(CipherId::new),
2218 organization_id: self.organization_id.map(OrganizationId::new),
2219 key: EncString::try_from_optional(self.key)?,
2220 name: EncString::try_from_optional(self.name)?,
2221 notes: EncString::try_from_optional(self.notes)?,
2222 r#type: require!(self.r#type).try_into()?,
2223 login: self.login.map(|l| (*l).try_into()).transpose()?,
2224 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
2225 card: self.card.map(|c| (*c).try_into()).transpose()?,
2226 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
2227 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
2228 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
2229 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
2230 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
2231 reprompt: self
2232 .reprompt
2233 .map(|r| r.try_into())
2234 .transpose()?
2235 .unwrap_or(CipherRepromptType::None),
2236 organization_use_totp: self.organization_use_totp.unwrap_or(true),
2237 attachments: self
2238 .attachments
2239 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2240 .transpose()?,
2241 fields: self
2242 .fields
2243 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2244 .transpose()?,
2245 password_history: self
2246 .password_history
2247 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2248 .transpose()?,
2249 creation_date: require!(self.creation_date)
2250 .parse()
2251 .map_err(Into::<VaultParseError>::into)?,
2252 deleted_date: self
2253 .deleted_date
2254 .map(|d| d.parse())
2255 .transpose()
2256 .map_err(Into::<VaultParseError>::into)?,
2257 revision_date: require!(self.revision_date)
2258 .parse()
2259 .map_err(Into::<VaultParseError>::into)?,
2260 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
2261 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
2262 favorite: cipher.map_or(Default::default(), |c| c.favorite),
2263 edit: cipher.map_or(Default::default(), |c| c.edit),
2264 permissions: cipher.map_or(Default::default(), |c| c.permissions),
2265 view_password: cipher.is_none_or(|c| c.view_password),
2266 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
2267 data: self.data,
2268 collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
2269 })
2270 }
2271}
2272
2273impl PartialCipher for CipherMiniDetailsResponseModel {
2274 fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
2275 let cipher = cipher.as_ref();
2276 Ok(Cipher {
2277 partial_data: self.partial_data,
2278 id: self.id.map(CipherId::new),
2279 organization_id: self.organization_id.map(OrganizationId::new),
2280 key: EncString::try_from_optional(self.key)?,
2281 name: EncString::try_from_optional(self.name)?,
2282 notes: EncString::try_from_optional(self.notes)?,
2283 r#type: require!(self.r#type).try_into()?,
2284 login: self.login.map(|l| (*l).try_into()).transpose()?,
2285 identity: self.identity.map(|i| (*i).try_into()).transpose()?,
2286 card: self.card.map(|c| (*c).try_into()).transpose()?,
2287 secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
2288 ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
2289 bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
2290 drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
2291 passport: self.passport.map(|p| (*p).try_into()).transpose()?,
2292 reprompt: self
2293 .reprompt
2294 .map(|r| r.try_into())
2295 .transpose()?
2296 .unwrap_or(CipherRepromptType::None),
2297 organization_use_totp: self.organization_use_totp.unwrap_or(true),
2298 attachments: self
2299 .attachments
2300 .map(|a| a.into_iter().map(|a| a.try_into()).collect())
2301 .transpose()?,
2302 fields: self
2303 .fields
2304 .map(|f| f.into_iter().map(|f| f.try_into()).collect())
2305 .transpose()?,
2306 password_history: self
2307 .password_history
2308 .map(|p| p.into_iter().map(|p| p.try_into()).collect())
2309 .transpose()?,
2310 creation_date: require!(self.creation_date)
2311 .parse()
2312 .map_err(Into::<VaultParseError>::into)?,
2313 deleted_date: self
2314 .deleted_date
2315 .map(|d| d.parse())
2316 .transpose()
2317 .map_err(Into::<VaultParseError>::into)?,
2318 revision_date: require!(self.revision_date)
2319 .parse()
2320 .map_err(Into::<VaultParseError>::into)?,
2321 collection_ids: self
2322 .collection_ids
2323 .into_iter()
2324 .flatten()
2325 .map(CollectionId::new)
2326 .collect(),
2327 archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
2328 folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
2329 favorite: cipher.map_or(Default::default(), |c| c.favorite),
2330 edit: cipher.map_or(Default::default(), |c| c.edit),
2331 permissions: cipher.map_or(Default::default(), |c| c.permissions),
2332 view_password: cipher.is_none_or(|c: &Cipher| c.view_password),
2333 data: cipher.map_or(Default::default(), |c| c.data.clone()),
2334 local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
2335 })
2336 }
2337}
2338
2339#[cfg(test)]
2340mod tests {
2341
2342 use attachment::AttachmentView;
2343 use bitwarden_core::key_management::{
2344 create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
2345 };
2346 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, SymmetricKeyAlgorithm};
2347
2348 use super::*;
2349 use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
2350
2351 const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
2353 const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
2354 const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
2355 const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
2356 const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
2357 const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
2358 const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
2359
2360 const RESTRICTED_ORG_UUID: &str = "3cf0d3ba-3ded-4bf3-a51c-b03fd9ac6e07";
2362 const RESTRICTED_ORG_KEY_B64: &str =
2363 "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==";
2364
2365 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}]}"#;
2368 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="}"#;
2370 const RESTRICTED_SECURE_NOTE_ENVELOPE: &str = r#"{"name":"2.EwVjyRAgPmUvRpyT68lbjQ==|73O9id1+DZevAB3K+2fXnA==|OoJhN3p9UgjQ4yk55OUBZ4nYQMlFcf9wTHPxrOPhQVI="}"#;
2371 const RESTRICTED_CARD_ENVELOPE: &str = r#"{"name":"2.HF21EOZVqF3eyeZtEgxaCg==|zuChVgXPqxipFE6zOBUBXQ==|gxyvw0+gMf5Grxk8EAhpLCBeXdA0kvea2maJmpLIUIw="}"#;
2372
2373 fn restricted_test_key_store() -> (OrganizationId, KeyStore<KeySlotIds>) {
2376 let org: OrganizationId = RESTRICTED_ORG_UUID.parse().unwrap();
2377 let org_key: SymmetricCryptoKey = RESTRICTED_ORG_KEY_B64.to_string().try_into().unwrap();
2378 let key_store = create_test_crypto_with_user_and_org_key(
2379 SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
2380 org,
2381 org_key,
2382 );
2383 (org, key_store)
2384 }
2385
2386 fn generate_cipher() -> CipherView {
2387 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
2388 CipherView {
2389 partial: false,
2390 r#type: CipherType::Login,
2391 login: Some(LoginView {
2392 username: Some("test_username".to_string()),
2393 password: Some("test_password".to_string()),
2394 password_revision_date: None,
2395 uris: None,
2396 totp: None,
2397 autofill_on_page_load: None,
2398 fido2_credentials: None,
2399 }),
2400 id: Some(test_id),
2401 organization_id: None,
2402 folder_id: None,
2403 collection_ids: vec![],
2404 key: None,
2405 name: "My test login".to_string(),
2406 notes: None,
2407 identity: None,
2408 card: None,
2409 secure_note: None,
2410 ssh_key: None,
2411 bank_account: None,
2412 drivers_license: None,
2413 passport: None,
2414 favorite: false,
2415 reprompt: CipherRepromptType::None,
2416 organization_use_totp: true,
2417 edit: true,
2418 permissions: None,
2419 view_password: true,
2420 local_data: None,
2421 attachments: None,
2422 attachment_decryption_failures: None,
2423 fields: None,
2424 password_history: None,
2425 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2426 deleted_date: None,
2427 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2428 archived_date: None,
2429 }
2430 }
2431
2432 fn generate_fido2(
2433 ctx: &mut KeyStoreContext<KeySlotIds>,
2434 key: SymmetricKeySlotId,
2435 ) -> Fido2Credential {
2436 Fido2Credential {
2437 credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
2438 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
2439 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
2440 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
2441 key_value: "123".to_string().encrypt(ctx, key).unwrap(),
2442 rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
2443 user_handle: None,
2444 user_name: None,
2445 counter: "123".to_string().encrypt(ctx, key).unwrap(),
2446 rp_name: None,
2447 user_display_name: None,
2448 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
2449 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
2450 }
2451 }
2452
2453 fn generate_fido2_view() -> Fido2CredentialView {
2454 Fido2CredentialView {
2455 credential_id: "123".to_string(),
2456 key_type: "public-key".to_string(),
2457 key_algorithm: "ECDSA".to_string(),
2458 key_curve: "P-256".to_string(),
2459 key_value: "123".to_string(),
2460 rp_id: "123".to_string(),
2461 user_handle: None,
2462 user_name: None,
2463 counter: "123".to_string(),
2464 rp_name: None,
2465 user_display_name: None,
2466 discoverable: "true".to_string(),
2467 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
2468 }
2469 }
2470
2471 fn restricted_cipher(
2474 organization_id: OrganizationId,
2475 r#type: CipherType,
2476 partial_data: String,
2477 ) -> Cipher {
2478 Cipher {
2479 partial_data: Some(partial_data),
2480 id: Some(TEST_UUID.parse().unwrap()),
2481 organization_id: Some(organization_id),
2482 folder_id: None,
2483 collection_ids: vec![],
2484 key: None,
2485 name: None,
2486 notes: None,
2487 r#type,
2488 login: None,
2489 identity: None,
2490 card: None,
2491 secure_note: None,
2492 ssh_key: None,
2493 bank_account: None,
2494 drivers_license: None,
2495 passport: None,
2496 favorite: false,
2497 reprompt: CipherRepromptType::None,
2498 organization_use_totp: false,
2499 edit: true,
2500 permissions: None,
2501 view_password: true,
2502 local_data: None,
2503 attachments: None,
2504 fields: None,
2505 password_history: None,
2506 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2507 deleted_date: None,
2508 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2509 archived_date: None,
2510 data: None,
2511 }
2512 }
2513
2514 #[test]
2515 fn test_decrypt_restricted_cipher_list_view_login() {
2516 let (org, key_store) = restricted_test_key_store();
2517 let cipher = restricted_cipher(
2518 org,
2519 CipherType::Login,
2520 RESTRICTED_LOGIN_ENVELOPE.to_string(),
2521 );
2522 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
2523
2524 assert!(view.partial);
2525 assert_eq!(view.name, "Restricted Name".to_string());
2526 assert!(view.subtitle.is_empty());
2527 assert!(view.copyable_fields.is_empty());
2528 assert_eq!(view.attachments, 0);
2529 match view.r#type {
2530 CipherListViewType::Login(login) => {
2531 assert_eq!(
2532 login.uris.unwrap()[0].uri.as_deref(),
2533 Some("https://example.com")
2534 );
2535 assert_eq!(login.username, None);
2536 assert_eq!(login.totp, None);
2537 assert!(!login.has_fido2);
2538 }
2539 other => panic!("expected Login list view, got {other:?}"),
2540 }
2541 }
2542
2543 #[test]
2544 fn test_decrypt_restricted_cipher_view_login_omits_secrets() {
2545 let (org, key_store) = restricted_test_key_store();
2546 let cipher = restricted_cipher(
2549 org,
2550 CipherType::Login,
2551 RESTRICTED_LOGIN_ENVELOPE_WITH_PASSWORD.to_string(),
2552 );
2553 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2554
2555 assert!(view.partial);
2556 assert_eq!(view.name, "Restricted Name".to_string());
2557 let login = view
2558 .login
2559 .expect("login populated so the view can render a domain");
2560 assert_eq!(
2561 login.uris.unwrap()[0].uri.as_deref(),
2562 Some("https://example.com")
2563 );
2564 assert_eq!(login.username, None);
2565 assert_eq!(login.password, None);
2566 assert_eq!(login.totp, None);
2567 assert_eq!(view.notes, None);
2568 assert!(view.card.is_none());
2569 }
2570
2571 #[test]
2572 fn test_decrypt_restricted_non_login_name_only() {
2573 let (org, key_store) = restricted_test_key_store();
2574 let cipher = restricted_cipher(
2575 org,
2576 CipherType::SecureNote,
2577 RESTRICTED_SECURE_NOTE_ENVELOPE.to_string(),
2578 );
2579
2580 let list: CipherListView = key_store.decrypt(&cipher).unwrap();
2581 assert!(list.partial);
2582 assert_eq!(list.name, "Gated Note".to_string());
2583 assert_eq!(list.r#type, CipherListViewType::SecureNote);
2584
2585 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2586 assert!(view.partial);
2587 assert_eq!(view.name, "Gated Note".to_string());
2588 assert!(view.login.is_none());
2589 assert!(view.secure_note.is_none());
2590 }
2591
2592 #[test]
2593 fn test_decrypt_restricted_malformed_envelope_fails_closed() {
2594 let (org, key_store) = restricted_test_key_store();
2595 let cipher = restricted_cipher(org, CipherType::Login, "not valid json".to_string());
2597
2598 let list: CipherListView = key_store.decrypt(&cipher).unwrap();
2599 assert!(list.partial);
2600 assert!(list.name.is_empty());
2601
2602 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2603 assert!(view.partial);
2604 assert!(view.name.is_empty());
2605
2606 let strict_view: CipherView = key_store.decrypt(&StrictDecrypt(cipher.clone())).unwrap();
2609 assert!(strict_view.partial);
2610 assert!(strict_view.name.is_empty());
2611 }
2612
2613 #[test]
2614 fn test_decrypt_restricted_works_in_strict_mode() {
2615 let (org, key_store) = restricted_test_key_store();
2616 let cipher = restricted_cipher(org, CipherType::Card, RESTRICTED_CARD_ENVELOPE.to_string());
2619
2620 let list: CipherListView = key_store.decrypt(&StrictDecrypt(cipher.clone())).unwrap();
2621 assert!(list.partial);
2622 assert_eq!(list.name, "Restricted Card".to_string());
2623 assert_eq!(
2624 list.r#type,
2625 CipherListViewType::Card(CardListView { brand: None })
2626 );
2627
2628 let view: CipherView = key_store.decrypt(&StrictDecrypt(cipher)).unwrap();
2629 assert!(view.partial);
2630 assert!(view.card.is_none());
2631 }
2632
2633 #[test]
2634 fn test_decrypt_restricted_strict_propagates_field_decrypt_error() {
2635 let (org, key_store) = restricted_test_key_store();
2636 let wrong_key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2639 SymmetricKeyAlgorithm::Aes256CbcHmac,
2640 ));
2641 let bad_name = "boom"
2642 .to_string()
2643 .encrypt(&mut wrong_key_store.context(), SymmetricKeySlotId::User)
2644 .unwrap();
2645 let envelope = serde_json::json!({ "name": bad_name }).to_string();
2646 let cipher = restricted_cipher(org, CipherType::Login, envelope);
2647
2648 let view: CipherView = key_store.decrypt(&cipher).unwrap();
2650 assert!(view.partial);
2651 assert!(view.name.is_empty());
2652
2653 let view_result: Result<CipherView, CryptoError> =
2655 key_store.decrypt(&StrictDecrypt(cipher.clone()));
2656 assert!(view_result.is_err());
2657 let list_result: Result<CipherListView, CryptoError> =
2658 key_store.decrypt(&StrictDecrypt(cipher));
2659 assert!(list_result.is_err());
2660 }
2661
2662 #[test]
2663 fn test_decrypt_restricted_non_org_cipher_fails_closed() {
2664 let (org, key_store) = restricted_test_key_store();
2667 let cipher = Cipher {
2668 organization_id: None,
2669 ..restricted_cipher(
2670 org,
2671 CipherType::SecureNote,
2672 RESTRICTED_SECURE_NOTE_ENVELOPE.to_string(),
2673 )
2674 };
2675
2676 let view_result: Result<CipherView, CryptoError> = key_store.decrypt(&cipher);
2677 assert!(matches!(
2678 view_result.unwrap_err(),
2679 CryptoError::RestrictedCipherRequiresOrganization
2680 ));
2681
2682 let list_result: Result<CipherListView, CryptoError> = key_store.decrypt(&cipher);
2683 assert!(matches!(
2684 list_result.unwrap_err(),
2685 CryptoError::RestrictedCipherRequiresOrganization
2686 ));
2687 }
2688
2689 #[test]
2690 fn test_decrypt_cipher_list_view() {
2691 let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2692 let key_store = create_test_crypto_with_user_key(key);
2693
2694 let cipher = Cipher {
2695 partial_data: None,
2696 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2697 organization_id: None,
2698 folder_id: None,
2699 collection_ids: vec![],
2700 key: None,
2701 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
2702 notes: None,
2703 r#type: CipherType::Login,
2704 login: Some(Login {
2705 username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
2706 password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
2707 password_revision_date: None,
2708 uris: None,
2709 totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
2710 autofill_on_page_load: None,
2711 fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User)]),
2712 }),
2713 identity: None,
2714 card: None,
2715 secure_note: None,
2716 ssh_key: None,
2717 bank_account: None,
2718 drivers_license: None,
2719 passport: None,
2720 favorite: false,
2721 reprompt: CipherRepromptType::None,
2722 organization_use_totp: false,
2723 edit: true,
2724 permissions: Some(CipherPermissions {
2725 delete: false,
2726 restore: false
2727 }),
2728 view_password: true,
2729 local_data: None,
2730 attachments: None,
2731 fields: None,
2732 password_history: None,
2733 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2734 deleted_date: None,
2735 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2736 archived_date: None,
2737 data: None,
2738 };
2739
2740 let view: CipherListView = key_store.decrypt(&cipher).unwrap();
2741
2742 assert_eq!(
2743 view,
2744 CipherListView {
2745 partial: false,
2746 id: cipher.id,
2747 organization_id: cipher.organization_id,
2748 folder_id: cipher.folder_id,
2749 collection_ids: cipher.collection_ids,
2750 name: "My test login".to_string(),
2751 subtitle: "test_username".to_string(),
2752 r#type: CipherListViewType::Login(LoginListView {
2753 fido2_credentials: Some(vec![Fido2CredentialListView {
2754 credential_id: "123".to_string(),
2755 rp_id: "123".to_string(),
2756 user_handle: None,
2757 user_name: None,
2758 user_display_name: None,
2759 counter: "123".to_string(),
2760 }]),
2761 has_fido2: true,
2762 username: Some("test_username".to_string()),
2763 totp: cipher.login.as_ref().unwrap().totp.as_ref().map(|t| t
2764 .decrypt(&mut key_store.context(), SymmetricKeySlotId::User)
2765 .unwrap()),
2766 uris: None,
2767 }),
2768 favorite: cipher.favorite,
2769 reprompt: cipher.reprompt,
2770 organization_use_totp: cipher.organization_use_totp,
2771 edit: cipher.edit,
2772 permissions: cipher.permissions,
2773 view_password: cipher.view_password,
2774 attachments: 0,
2775 has_old_attachments: false,
2776 creation_date: cipher.creation_date,
2777 deleted_date: cipher.deleted_date,
2778 revision_date: cipher.revision_date,
2779 copyable_fields: vec![
2780 CopyableCipherFields::LoginUsername,
2781 CopyableCipherFields::LoginPassword,
2782 CopyableCipherFields::LoginTotp
2783 ],
2784 local_data: None,
2785 archived_date: cipher.archived_date,
2786 #[cfg(feature = "wasm")]
2787 notes: None,
2788 #[cfg(feature = "wasm")]
2789 fields: None,
2790 #[cfg(feature = "wasm")]
2791 attachment_names: None,
2792 }
2793 )
2794 }
2795
2796 fn blob_cipher() -> Cipher {
2797 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2798 SymmetricKeyAlgorithm::Aes256CbcHmac,
2799 ));
2800 let cipher: Cipher = key_store
2801 .encrypt(EncryptMode::Blob(generate_cipher()))
2802 .unwrap();
2803 assert!(cipher.data.is_some(), "expected a blob-shaped cipher");
2804 cipher
2805 }
2806
2807 #[test]
2808 fn test_encryption_context_to_cipher_with_id_request_preserves_data() {
2809 let cipher = blob_cipher();
2810 let expected = cipher.data.clone();
2811
2812 let request: CipherWithIdRequestModel = EncryptionContext {
2813 encrypted_for: UserId::new(TEST_UUID.parse().unwrap()),
2814 encrypted_by_key_id: Some("0102030405060708090a0b0c0d0e0f10".to_string()),
2815 cipher,
2816 }
2817 .try_into()
2818 .unwrap();
2819
2820 assert_eq!(request.data, expected);
2821 assert_eq!(
2822 request.encrypted_by_key_id.as_deref(),
2823 Some("0102030405060708090a0b0c0d0e0f10")
2824 );
2825 }
2826
2827 #[test]
2828 fn test_encryption_context_to_cipher_request_preserves_data() {
2829 let cipher = blob_cipher();
2830 let expected = cipher.data.clone();
2831
2832 let request: CipherRequestModel = EncryptionContext {
2833 encrypted_for: UserId::new(TEST_UUID.parse().unwrap()),
2834 encrypted_by_key_id: Some("0102030405060708090a0b0c0d0e0f10".to_string()),
2835 cipher,
2836 }
2837 .into();
2838
2839 assert_eq!(request.data, expected);
2840 assert_eq!(
2841 request.encrypted_by_key_id.as_deref(),
2842 Some("0102030405060708090a0b0c0d0e0f10")
2843 );
2844 }
2845
2846 #[test]
2849 fn test_encrypted_by_key_id_uses_user_key_for_personal_cipher() {
2850 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2851 let expected = user_key.key_id().unwrap().to_string();
2852 let key_store = create_test_crypto_with_user_key(user_key);
2853
2854 let view = generate_cipher();
2855 assert_eq!(view.key_identifier(), SymmetricKeySlotId::User);
2856
2857 let actual = key_store
2858 .context()
2859 .get_symmetric_key_id(view.key_identifier())
2860 .map(|id| id.to_string());
2861
2862 assert_eq!(actual.as_deref(), Some(expected.as_str()));
2863 assert_eq!(expected.len(), 32);
2865 assert!(expected.chars().all(|c| c.is_ascii_hexdigit()));
2866 assert_eq!(expected, expected.to_lowercase());
2867 }
2868
2869 #[test]
2871 fn test_encrypted_by_key_id_uses_org_key_for_org_cipher() {
2872 let org = OrganizationId::new_v4();
2873 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2874 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2875 let user_key_id = user_key.key_id().unwrap().to_string();
2876 let org_key_id = org_key.key_id().unwrap().to_string();
2877 assert_ne!(user_key_id, org_key_id);
2878
2879 let key_store = create_test_crypto_with_user_and_org_key(user_key, org, org_key);
2880
2881 let mut view = generate_cipher();
2882 view.organization_id = Some(org);
2883 assert_eq!(view.key_identifier(), SymmetricKeySlotId::Organization(org));
2884
2885 let actual = key_store
2886 .context()
2887 .get_symmetric_key_id(view.key_identifier())
2888 .map(|id| id.to_string());
2889
2890 assert_eq!(actual.as_deref(), Some(org_key_id.as_str()));
2891 }
2892
2893 #[test]
2896 fn test_encrypted_by_key_id_ignores_cipher_key() {
2897 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::XChaCha20Poly1305);
2898 let expected = user_key.key_id().unwrap().to_string();
2899 let key_store = create_test_crypto_with_user_key(user_key);
2900
2901 let mut view = generate_cipher();
2902 let _ = view.load_cipher_key_slot(&mut key_store.context()).unwrap();
2903 assert!(view.key.is_some());
2904
2905 let actual = key_store
2906 .context()
2907 .get_symmetric_key_id(view.key_identifier())
2908 .map(|id| id.to_string());
2909
2910 assert_eq!(actual.as_deref(), Some(expected.as_str()));
2911 }
2912
2913 #[test]
2916 fn test_encrypted_by_key_id_uses_derived_id_for_legacy_user_key() {
2917 let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
2918 let expected = user_key
2919 .key_id()
2920 .expect("an AES-CBC-HMAC key derives a key id")
2921 .to_string();
2922 let key_store = create_test_crypto_with_user_key(user_key);
2923
2924 let view = generate_cipher();
2925 let actual = key_store
2926 .context()
2927 .get_symmetric_key_id(view.key_identifier())
2928 .map(|id| id.to_string());
2929
2930 assert_eq!(actual.as_deref(), Some(expected.as_str()));
2931 }
2932
2933 #[test]
2934 fn test_decrypt_cipher_fails_with_invalid_name() {
2935 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2936 SymmetricKeyAlgorithm::Aes256CbcHmac,
2937 ));
2938
2939 let cipher = key_store
2941 .encrypt(EncryptMode::Legacy(generate_cipher()))
2942 .unwrap();
2943 let cipher = Cipher {
2944 name: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher
2946 };
2947
2948 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
2950 assert!(
2951 lenient_result.is_ok(),
2952 "Lenient decryption should succeed even when name is encrypted with a different key"
2953 );
2954 assert_eq!(
2955 lenient_result.unwrap().name,
2956 String::new(),
2957 "Lenient decryption should yield an empty name on error"
2958 );
2959
2960 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
2962 assert!(
2963 strict_result.is_err(),
2964 "Strict decryption should fail when name is encrypted with a different key"
2965 );
2966 }
2967
2968 #[test]
2969 fn test_decrypt_cipher_fails_with_invalid_login() {
2970 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
2971 SymmetricKeyAlgorithm::Aes256CbcHmac,
2972 ));
2973
2974 let cipher = key_store
2976 .encrypt(EncryptMode::Legacy(generate_cipher()))
2977 .unwrap();
2978 let cipher = Cipher {
2979 login: Some(Login {
2980 username: Some(TEST_CIPHER_NAME.parse().unwrap()), ..cipher.login.unwrap()
2982 }),
2983 ..cipher
2984 };
2985
2986 let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
2988 assert!(
2989 lenient_result.is_ok(),
2990 "Lenient decryption should succeed even when login username is encrypted with a different key"
2991 );
2992 let lenient_view = lenient_result.unwrap();
2993 assert!(
2994 lenient_view.login.is_some(),
2995 "Lenient decryption should still return the login object"
2996 );
2997 assert!(
2998 lenient_view.login.unwrap().username.is_none(),
2999 "Lenient decryption should null out the failing username field"
3000 );
3001
3002 let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
3004 assert!(
3005 strict_result.is_err(),
3006 "Strict decryption should fail when login username is encrypted with a different key"
3007 );
3008 }
3009
3010 #[test]
3011 fn test_encrypt_legacy_fails_closed_for_partial_view() {
3012 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3013 SymmetricKeyAlgorithm::Aes256CbcHmac,
3014 ));
3015
3016 let mut view = generate_cipher();
3017 view.partial = true;
3018
3019 let result = key_store.encrypt(EncryptMode::Legacy(view));
3020 assert!(matches!(result, Err(CryptoError::EncryptRestrictedView)));
3021 }
3022
3023 #[test]
3024 fn test_encrypt_blob_fails_closed_for_partial_view() {
3025 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3026 SymmetricKeyAlgorithm::Aes256CbcHmac,
3027 ));
3028
3029 let mut view = generate_cipher();
3030 view.partial = true;
3031
3032 let result = key_store.encrypt(EncryptMode::Blob(view));
3033 assert!(matches!(result, Err(CryptoError::EncryptRestrictedView)));
3034 }
3035
3036 #[test]
3037 fn test_encrypt_succeeds_for_non_partial_view() {
3038 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3039 SymmetricKeyAlgorithm::Aes256CbcHmac,
3040 ));
3041
3042 let view = generate_cipher();
3046 assert!(!view.partial);
3047
3048 let result = key_store.encrypt(EncryptMode::Legacy(view));
3049 assert!(result.is_ok());
3050 }
3051
3052 #[test]
3053 fn test_load_cipher_key_slot_ignores_attachments_without_key() {
3054 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3055 let key_store = create_test_crypto_with_user_key(key);
3056
3057 let mut cipher = generate_cipher();
3058 let attachment = AttachmentView {
3059 id: None,
3060 url: None,
3061 size: None,
3062 size_name: None,
3063 file_name: Some("Attachment test name".into()),
3064 key: None,
3065 };
3066 cipher.attachments = Some(vec![attachment]);
3067
3068 let _ = cipher
3069 .load_cipher_key_slot(&mut key_store.context())
3070 .unwrap();
3071 assert!(cipher.attachments.unwrap()[0].key.is_none());
3072 }
3073
3074 #[test]
3075 fn test_validate_attachment_keys_with_cipher_key() {
3076 let old_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3077 let key_store = create_test_crypto_with_user_key(old_key);
3078 let mut ctx = key_store.context_mut();
3079
3080 let mut cipher = generate_cipher();
3081 let _ = cipher.load_cipher_key_slot(&mut ctx).unwrap();
3082
3083 cipher.validate_attachment_keys().unwrap();
3084
3085 assert!(cipher.key.is_some());
3086 }
3087
3088 #[test]
3090 fn test_validate_attachment_keys_without_attachments() {
3091 let mut cipher = generate_cipher();
3092 assert!(cipher.key.is_none());
3093 assert!(cipher.attachments.is_none());
3094
3095 cipher.validate_attachment_keys().unwrap();
3096 }
3097
3098 #[test]
3099 fn test_move_user_cipher_to_org() {
3100 let org = OrganizationId::new_v4();
3101 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3102 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3103 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3104
3105 let mut cipher = generate_cipher();
3107 let _ = cipher
3108 .load_cipher_key_slot(&mut key_store.context())
3109 .unwrap();
3110
3111 cipher.move_to_organization(org).unwrap();
3112 assert_eq!(cipher.organization_id, Some(org));
3113
3114 let cipher_enc = key_store.encrypt(EncryptMode::Legacy(cipher)).unwrap();
3116 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
3117
3118 assert_eq!(cipher_dec.name, "My test login");
3119 }
3120
3121 #[test]
3122 fn test_move_user_cipher_to_org_manually() {
3123 let org = OrganizationId::new_v4();
3124 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3125 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3126 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3127
3128 let mut cipher = generate_cipher();
3130 let _ = cipher
3131 .load_cipher_key_slot(&mut key_store.context())
3132 .unwrap();
3133
3134 cipher.organization_id = Some(org);
3135
3136 let cipher_enc = key_store.encrypt(EncryptMode::Legacy(cipher)).unwrap();
3139 let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
3140 assert_eq!(cipher_dec.name, "My test login");
3141 }
3142
3143 #[test]
3144 fn test_move_user_cipher_with_attachment_without_key_to_org() {
3145 let org = OrganizationId::new_v4();
3146 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3147 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3148 let _key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3149
3150 let mut cipher = generate_cipher();
3151 let attachment = AttachmentView {
3152 id: None,
3153 url: None,
3154 size: None,
3155 size_name: None,
3156 file_name: Some("Attachment test name".into()),
3157 key: None,
3158 };
3159 cipher.attachments = Some(vec![attachment]);
3160
3161 assert!(cipher.move_to_organization(org).is_err());
3163 }
3164
3165 #[test]
3166 fn test_move_user_cipher_with_attachment_with_key_to_org() {
3167 let org = OrganizationId::new_v4();
3168 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3169 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3170 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3171
3172 let attachment_key_val = {
3174 let mut ctx = key_store.context();
3175 let attachment_key = ctx.generate_symmetric_key();
3176 #[allow(deprecated)]
3177 ctx.dangerous_get_symmetric_key(attachment_key)
3178 .unwrap()
3179 .clone()
3180 };
3181
3182 let mut cipher = generate_cipher();
3183 let attachment = AttachmentView {
3184 id: None,
3185 url: None,
3186 size: None,
3187 size_name: None,
3188 file_name: Some("Attachment test name".into()),
3189 key: Some(attachment_key_val.clone()),
3190 };
3191 cipher.attachments = Some(vec![attachment]);
3192 let cred = generate_fido2_view();
3193 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
3194
3195 cipher.move_to_organization(org).unwrap();
3196
3197 assert!(cipher.key.is_none());
3198
3199 assert_eq!(
3201 cipher.attachments.unwrap()[0].key.clone().unwrap(),
3202 attachment_key_val
3203 );
3204
3205 let cred2 = cipher
3206 .login
3207 .unwrap()
3208 .fido2_credentials
3209 .unwrap()
3210 .first()
3211 .unwrap()
3212 .clone();
3213
3214 assert_eq!(cred2.credential_id, "123");
3215 }
3216
3217 #[test]
3218 fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
3219 let org = OrganizationId::new_v4();
3220 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3221 let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
3222 let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
3223
3224 let mut ctx = key_store.context();
3225
3226 let cipher_key = ctx.generate_symmetric_key();
3227 #[allow(deprecated)]
3228 let cipher_key_raw = ctx.dangerous_get_symmetric_key(cipher_key).unwrap().clone();
3229
3230 let attachment_key = ctx.generate_symmetric_key();
3232 #[allow(deprecated)]
3233 let attachment_key_raw = ctx
3234 .dangerous_get_symmetric_key(attachment_key)
3235 .unwrap()
3236 .clone();
3237
3238 let mut cipher = generate_cipher();
3239 cipher.key = Some(cipher_key_raw.clone());
3240
3241 let attachment = AttachmentView {
3242 id: None,
3243 url: None,
3244 size: None,
3245 size_name: None,
3246 file_name: Some("Attachment test name".into()),
3247 key: Some(attachment_key_raw.clone()),
3248 };
3249 cipher.attachments = Some(vec![attachment]);
3250
3251 let cred = generate_fido2_view();
3252 cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
3253
3254 cipher.move_to_organization(org).unwrap();
3255
3256 assert_eq!(cipher.key.clone().unwrap(), cipher_key_raw);
3258
3259 assert_eq!(
3261 cipher.attachments.unwrap()[0].key.as_ref().unwrap(),
3262 &attachment_key_raw
3263 );
3264
3265 let cred2 = cipher
3266 .login
3267 .unwrap()
3268 .fido2_credentials
3269 .unwrap()
3270 .first()
3271 .unwrap()
3272 .clone();
3273
3274 assert_eq!(cred2.credential_id, cred.credential_id);
3275 }
3276
3277 #[test]
3278 fn test_password_history_on_password_change() {
3279 use chrono::Utc;
3280
3281 let original_cipher = generate_cipher();
3282 let mut new_cipher = generate_cipher();
3283
3284 if let Some(ref mut login) = new_cipher.login {
3286 login.password = Some("new_password123".to_string());
3287 }
3288
3289 let start = Utc::now();
3290 new_cipher.update_password_history(&original_cipher);
3291 let end = Utc::now();
3292
3293 assert!(new_cipher.password_history.is_some());
3294 let history = new_cipher.password_history.unwrap();
3295 assert_eq!(history.len(), 1);
3296 assert_eq!(history[0].password, "test_password");
3297 assert!(
3298 history[0].last_used_date >= start && history[0].last_used_date <= end,
3299 "last_used_date was not set properly"
3300 );
3301 }
3302
3303 #[test]
3304 fn test_password_history_on_unchanged_password() {
3305 let original_cipher = generate_cipher();
3306 let mut new_cipher = generate_cipher();
3307
3308 new_cipher.update_password_history(&original_cipher);
3309
3310 assert!(
3312 new_cipher.password_history.is_none()
3313 || new_cipher.password_history.as_ref().unwrap().is_empty()
3314 );
3315 }
3316
3317 #[test]
3318 fn test_password_history_is_preserved() {
3319 use chrono::TimeZone;
3320
3321 let mut original_cipher = generate_cipher();
3322 original_cipher.password_history = Some(
3323 (0..4)
3324 .map(|i| PasswordHistoryView {
3325 password: format!("old_password_{}", i),
3326 last_used_date: chrono::Utc
3327 .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
3328 .unwrap(),
3329 })
3330 .collect(),
3331 );
3332
3333 let mut new_cipher = generate_cipher();
3334
3335 new_cipher.update_password_history(&original_cipher);
3336
3337 assert!(new_cipher.password_history.is_some());
3338 let history = new_cipher.password_history.unwrap();
3339 assert_eq!(history.len(), 4);
3340
3341 assert_eq!(history[0].password, "old_password_0");
3342 assert_eq!(
3343 history[0].last_used_date,
3344 chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
3345 );
3346 assert_eq!(history[1].password, "old_password_1");
3347 assert_eq!(
3348 history[1].last_used_date,
3349 chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
3350 );
3351 assert_eq!(history[2].password, "old_password_2");
3352 assert_eq!(
3353 history[2].last_used_date,
3354 chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
3355 );
3356 assert_eq!(history[3].password, "old_password_3");
3357 assert_eq!(
3358 history[3].last_used_date,
3359 chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
3360 );
3361 }
3362
3363 #[test]
3364 fn test_populate_cipher_types_login_with_valid_data() {
3365 let mut cipher = Cipher {
3366 partial_data: None,
3367 id: Some(TEST_UUID.parse().unwrap()),
3368 organization_id: None,
3369 folder_id: None,
3370 collection_ids: vec![],
3371 key: None,
3372 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3373 notes: None,
3374 r#type: CipherType::Login,
3375 login: None,
3376 identity: None,
3377 card: None,
3378 secure_note: None,
3379 ssh_key: None,
3380 bank_account: None,
3381 drivers_license: None,
3382 passport: None,
3383 favorite: false,
3384 reprompt: CipherRepromptType::None,
3385 organization_use_totp: false,
3386 edit: true,
3387 view_password: true,
3388 permissions: None,
3389 local_data: None,
3390 attachments: None,
3391 fields: None,
3392 password_history: None,
3393 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3394 deleted_date: None,
3395 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3396 archived_date: None,
3397 data: Some(format!(
3398 r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3399 TEST_ENC_STRING_1, TEST_ENC_STRING_2
3400 )),
3401 };
3402
3403 cipher
3404 .populate_cipher_types()
3405 .expect("populate_cipher_types failed");
3406
3407 assert!(cipher.login.is_some());
3408 let login = cipher.login.unwrap();
3409 assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
3410 assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
3411 }
3412
3413 #[test]
3414 fn test_populate_cipher_types_secure_note() {
3415 let mut cipher = Cipher {
3416 partial_data: None,
3417 id: Some(TEST_UUID.parse().unwrap()),
3418 organization_id: None,
3419 folder_id: None,
3420 collection_ids: vec![],
3421 key: None,
3422 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3423 notes: None,
3424 r#type: CipherType::SecureNote,
3425 login: None,
3426 identity: None,
3427 card: None,
3428 secure_note: None,
3429 ssh_key: None,
3430 bank_account: None,
3431 drivers_license: None,
3432 passport: None,
3433 favorite: false,
3434 reprompt: CipherRepromptType::None,
3435 organization_use_totp: false,
3436 edit: true,
3437 view_password: true,
3438 permissions: None,
3439 local_data: None,
3440 attachments: None,
3441 fields: None,
3442 password_history: None,
3443 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3444 deleted_date: None,
3445 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3446 archived_date: None,
3447 data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
3448 };
3449
3450 cipher
3451 .populate_cipher_types()
3452 .expect("populate_cipher_types failed");
3453
3454 assert!(cipher.secure_note.is_some());
3455 }
3456
3457 #[test]
3458 fn test_populate_cipher_types_card() {
3459 let mut cipher = Cipher {
3460 partial_data: None,
3461 id: Some(TEST_UUID.parse().unwrap()),
3462 organization_id: None,
3463 folder_id: None,
3464 collection_ids: vec![],
3465 key: None,
3466 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3467 notes: None,
3468 r#type: CipherType::Card,
3469 login: None,
3470 identity: None,
3471 card: None,
3472 secure_note: None,
3473 ssh_key: None,
3474 bank_account: None,
3475 drivers_license: None,
3476 passport: None,
3477 favorite: false,
3478 reprompt: CipherRepromptType::None,
3479 organization_use_totp: false,
3480 edit: true,
3481 view_password: true,
3482 permissions: None,
3483 local_data: None,
3484 attachments: None,
3485 fields: None,
3486 password_history: None,
3487 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3488 deleted_date: None,
3489 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3490 archived_date: None,
3491 data: Some(format!(
3492 r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3493 TEST_ENC_STRING_1,
3494 TEST_ENC_STRING_2,
3495 TEST_ENC_STRING_3,
3496 TEST_ENC_STRING_4,
3497 TEST_ENC_STRING_5,
3498 TEST_ENC_STRING_1
3499 )),
3500 };
3501
3502 cipher
3503 .populate_cipher_types()
3504 .expect("populate_cipher_types failed");
3505
3506 assert!(cipher.card.is_some());
3507 let card = cipher.card.unwrap();
3508 assert_eq!(
3509 card.cardholder_name.as_ref().unwrap().to_string(),
3510 TEST_ENC_STRING_1
3511 );
3512 assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
3513 assert_eq!(
3514 card.exp_month.as_ref().unwrap().to_string(),
3515 TEST_ENC_STRING_3
3516 );
3517 assert_eq!(
3518 card.exp_year.as_ref().unwrap().to_string(),
3519 TEST_ENC_STRING_4
3520 );
3521 assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
3522 assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
3523 }
3524
3525 #[test]
3526 fn test_populate_cipher_types_identity() {
3527 let mut cipher = Cipher {
3528 partial_data: None,
3529 id: Some(TEST_UUID.parse().unwrap()),
3530 organization_id: None,
3531 folder_id: None,
3532 collection_ids: vec![],
3533 key: None,
3534 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3535 notes: None,
3536 r#type: CipherType::Identity,
3537 login: None,
3538 identity: None,
3539 card: None,
3540 secure_note: None,
3541 ssh_key: None,
3542 bank_account: None,
3543 drivers_license: None,
3544 passport: None,
3545 favorite: false,
3546 reprompt: CipherRepromptType::None,
3547 organization_use_totp: false,
3548 edit: true,
3549 view_password: true,
3550 permissions: None,
3551 local_data: None,
3552 attachments: None,
3553 fields: None,
3554 password_history: None,
3555 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3556 deleted_date: None,
3557 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3558 archived_date: None,
3559 data: Some(format!(
3560 r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
3561 TEST_ENC_STRING_1,
3562 TEST_ENC_STRING_2,
3563 TEST_ENC_STRING_3,
3564 TEST_ENC_STRING_4,
3565 TEST_ENC_STRING_5,
3566 TEST_ENC_STRING_1,
3567 TEST_ENC_STRING_2,
3568 TEST_ENC_STRING_3,
3569 TEST_ENC_STRING_4,
3570 TEST_ENC_STRING_5
3571 )),
3572 };
3573
3574 cipher
3575 .populate_cipher_types()
3576 .expect("populate_cipher_types failed");
3577
3578 assert!(cipher.identity.is_some());
3579 let identity = cipher.identity.unwrap();
3580 assert_eq!(
3581 identity.first_name.as_ref().unwrap().to_string(),
3582 TEST_ENC_STRING_1
3583 );
3584 assert_eq!(
3585 identity.last_name.as_ref().unwrap().to_string(),
3586 TEST_ENC_STRING_2
3587 );
3588 assert_eq!(
3589 identity.email.as_ref().unwrap().to_string(),
3590 TEST_ENC_STRING_3
3591 );
3592 assert_eq!(
3593 identity.phone.as_ref().unwrap().to_string(),
3594 TEST_ENC_STRING_4
3595 );
3596 assert_eq!(
3597 identity.company.as_ref().unwrap().to_string(),
3598 TEST_ENC_STRING_5
3599 );
3600 assert_eq!(
3601 identity.address1.as_ref().unwrap().to_string(),
3602 TEST_ENC_STRING_1
3603 );
3604 assert_eq!(
3605 identity.city.as_ref().unwrap().to_string(),
3606 TEST_ENC_STRING_2
3607 );
3608 assert_eq!(
3609 identity.state.as_ref().unwrap().to_string(),
3610 TEST_ENC_STRING_3
3611 );
3612 assert_eq!(
3613 identity.postal_code.as_ref().unwrap().to_string(),
3614 TEST_ENC_STRING_4
3615 );
3616 assert_eq!(
3617 identity.country.as_ref().unwrap().to_string(),
3618 TEST_ENC_STRING_5
3619 );
3620 }
3621
3622 #[test]
3623
3624 fn test_password_history_with_hidden_fields() {
3625 let mut original_cipher = generate_cipher();
3626 original_cipher.fields = Some(vec![FieldView {
3627 name: Some("Secret Key".to_string()),
3628 value: Some("old_secret_value".to_string()),
3629 r#type: crate::FieldType::Hidden,
3630 linked_id: None,
3631 }]);
3632
3633 let mut new_cipher = generate_cipher();
3634 new_cipher.fields = Some(vec![FieldView {
3635 name: Some("Secret Key".to_string()),
3636 value: Some("new_secret_value".to_string()),
3637 r#type: crate::FieldType::Hidden,
3638 linked_id: None,
3639 }]);
3640
3641 new_cipher.update_password_history(&original_cipher);
3642
3643 assert!(new_cipher.password_history.is_some());
3644 let history = new_cipher.password_history.unwrap();
3645 assert_eq!(history.len(), 1);
3646 assert_eq!(history[0].password, "Secret Key: old_secret_value");
3647 }
3648
3649 #[test]
3650 fn test_password_history_length_limit() {
3651 use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
3652
3653 let mut original_cipher = generate_cipher();
3654 original_cipher.password_history = Some(
3655 (0..10)
3656 .map(|i| PasswordHistoryView {
3657 password: format!("old_password_{}", i),
3658 last_used_date: chrono::Utc::now(),
3659 })
3660 .collect(),
3661 );
3662
3663 let mut new_cipher = original_cipher.clone();
3664 if let Some(ref mut login) = new_cipher.login {
3666 login.password = Some("brand_new_password".to_string());
3667 }
3668
3669 new_cipher.update_password_history(&original_cipher);
3670
3671 assert!(new_cipher.password_history.is_some());
3672 let history = new_cipher.password_history.unwrap();
3673
3674 assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
3676
3677 assert_eq!(history[0].password, "test_password");
3679 assert_eq!(history[1].password, "old_password_0");
3681 assert_eq!(history[2].password, "old_password_1");
3682 assert_eq!(history[3].password, "old_password_2");
3683 assert_eq!(history[4].password, "old_password_3");
3684 }
3685
3686 #[test]
3687 fn test_populate_cipher_types_ssh_key() {
3688 let mut cipher = Cipher {
3689 partial_data: None,
3690 id: Some(TEST_UUID.parse().unwrap()),
3691 organization_id: None,
3692 folder_id: None,
3693 collection_ids: vec![],
3694 key: None,
3695 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3696 notes: None,
3697 r#type: CipherType::SshKey,
3698 login: None,
3699 identity: None,
3700 card: None,
3701 secure_note: None,
3702 ssh_key: None,
3703 bank_account: None,
3704 drivers_license: None,
3705 passport: None,
3706 favorite: false,
3707 reprompt: CipherRepromptType::None,
3708 organization_use_totp: false,
3709 edit: true,
3710 view_password: true,
3711 permissions: None,
3712 local_data: None,
3713 attachments: None,
3714 fields: None,
3715 password_history: None,
3716 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3717 deleted_date: None,
3718 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3719 archived_date: None,
3720 data: Some(format!(
3721 r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
3722 TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
3723 )),
3724 };
3725
3726 cipher
3727 .populate_cipher_types()
3728 .expect("populate_cipher_types failed");
3729
3730 assert!(cipher.ssh_key.is_some());
3731 let ssh_key = cipher.ssh_key.unwrap();
3732 assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
3733 assert_eq!(ssh_key.public_key.unwrap().to_string(), TEST_ENC_STRING_2);
3734 assert_eq!(ssh_key.fingerprint.unwrap().to_string(), TEST_ENC_STRING_3);
3735 }
3736
3737 #[test]
3738 fn test_populate_cipher_types_with_null_data() {
3739 let mut cipher = Cipher {
3740 partial_data: None,
3741 id: Some(TEST_UUID.parse().unwrap()),
3742 organization_id: None,
3743 folder_id: None,
3744 collection_ids: vec![],
3745 key: None,
3746 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3747 notes: None,
3748 r#type: CipherType::Login,
3749 login: None,
3750 identity: None,
3751 card: None,
3752 secure_note: None,
3753 ssh_key: None,
3754 bank_account: None,
3755 drivers_license: None,
3756 passport: None,
3757 favorite: false,
3758 reprompt: CipherRepromptType::None,
3759 organization_use_totp: false,
3760 edit: true,
3761 view_password: true,
3762 permissions: None,
3763 local_data: None,
3764 attachments: None,
3765 fields: None,
3766 password_history: None,
3767 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3768 deleted_date: None,
3769 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3770 archived_date: None,
3771 data: None,
3772 };
3773
3774 let result = cipher.populate_cipher_types();
3775 assert!(matches!(
3776 result,
3777 Err(VaultParseError::MissingField(MissingFieldError("data")))
3778 ));
3779 }
3780
3781 #[test]
3782 fn test_populate_cipher_types_with_invalid_json() {
3783 let mut cipher = Cipher {
3784 partial_data: None,
3785 id: Some(TEST_UUID.parse().unwrap()),
3786 organization_id: None,
3787 folder_id: None,
3788 collection_ids: vec![],
3789 key: None,
3790 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3791 notes: None,
3792 r#type: CipherType::Login,
3793 login: None,
3794 identity: None,
3795 card: None,
3796 secure_note: None,
3797 ssh_key: None,
3798 bank_account: None,
3799 drivers_license: None,
3800 passport: None,
3801 favorite: false,
3802 reprompt: CipherRepromptType::None,
3803 organization_use_totp: false,
3804 edit: true,
3805 view_password: true,
3806 permissions: None,
3807 local_data: None,
3808 attachments: None,
3809 fields: None,
3810 password_history: None,
3811 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3812 deleted_date: None,
3813 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3814 archived_date: None,
3815 data: Some("invalid json".to_string()),
3816 };
3817
3818 let result = cipher.populate_cipher_types();
3819
3820 assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
3821 }
3822
3823 #[test]
3824 fn test_decrypt_cipher_with_mixed_attachments() {
3825 let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
3826 let key_store = create_test_crypto_with_user_key(user_key);
3827
3828 let mut ctx = key_store.context();
3830 let valid1 = "valid_file_1.txt"
3831 .encrypt(&mut ctx, SymmetricKeySlotId::User)
3832 .unwrap();
3833 let valid2 = "valid_file_2.txt"
3834 .encrypt(&mut ctx, SymmetricKeySlotId::User)
3835 .unwrap();
3836
3837 let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
3839 let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
3840 let mut wrong_ctx = wrong_key_store.context();
3841 let corrupted = "corrupted_file.txt"
3842 .encrypt(&mut wrong_ctx, SymmetricKeySlotId::User)
3843 .unwrap();
3844
3845 let cipher = Cipher {
3846 partial_data: None,
3847 id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
3848 organization_id: None,
3849 folder_id: None,
3850 collection_ids: vec![],
3851 key: None,
3852 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
3853 notes: None,
3854 r#type: CipherType::Login,
3855 login: None,
3856 identity: None,
3857 card: None,
3858 secure_note: None,
3859 ssh_key: None,
3860 bank_account: None,
3861 drivers_license: None,
3862 passport: None,
3863 favorite: false,
3864 reprompt: CipherRepromptType::None,
3865 organization_use_totp: false,
3866 edit: true,
3867 permissions: None,
3868 view_password: true,
3869 local_data: None,
3870 attachments: Some(vec![
3871 attachment::Attachment {
3873 id: Some("valid-attachment".to_string()),
3874 url: Some("https://example.com/valid".to_string()),
3875 size: Some("100".to_string()),
3876 size_name: Some("100 Bytes".to_string()),
3877 file_name: Some(valid1),
3878 key: None,
3879 },
3880 attachment::Attachment {
3882 id: Some("corrupted-attachment".to_string()),
3883 url: Some("https://example.com/corrupted".to_string()),
3884 size: Some("200".to_string()),
3885 size_name: Some("200 Bytes".to_string()),
3886 file_name: Some(corrupted),
3887 key: None,
3888 },
3889 attachment::Attachment {
3891 id: Some("valid-attachment-2".to_string()),
3892 url: Some("https://example.com/valid2".to_string()),
3893 size: Some("150".to_string()),
3894 size_name: Some("150 Bytes".to_string()),
3895 file_name: Some(valid2),
3896 key: None,
3897 },
3898 ]),
3899 fields: None,
3900 password_history: None,
3901 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3902 deleted_date: None,
3903 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
3904 archived_date: None,
3905 data: None,
3906 };
3907
3908 let view: CipherView = key_store.decrypt(&cipher).unwrap();
3909
3910 assert!(view.attachments.is_some());
3912 let successes = view.attachments.as_ref().unwrap();
3913 assert_eq!(successes.len(), 2);
3914 assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
3915 assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
3916
3917 assert!(view.attachment_decryption_failures.is_some());
3919 let failures = view.attachment_decryption_failures.as_ref().unwrap();
3920 assert_eq!(failures.len(), 1);
3921 assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
3922 assert_eq!(failures[0].file_name, None);
3923 }
3924
3925 #[test]
3926 fn test_decrypt_cipher_list_view_passport() {
3927 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3928 SymmetricKeyAlgorithm::Aes256CbcHmac,
3929 ));
3930
3931 let cipher_view = CipherView {
3932 r#type: CipherType::Passport,
3933 passport: Some(passport::PassportView {
3934 given_name: Some("Jane".to_string()),
3935 surname: Some("Doe".to_string()),
3936 passport_number: Some("P12345678".to_string()),
3937 ..Default::default()
3938 }),
3939 login: None,
3940 ..generate_cipher()
3941 };
3942
3943 let cipher: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
3944 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
3945
3946 assert_eq!(list_view.r#type, CipherListViewType::Passport);
3947 assert_eq!(list_view.subtitle, "Jane Doe");
3948 assert_eq!(
3949 list_view.copyable_fields,
3950 vec![
3951 CopyableCipherFields::PassportGivenName,
3952 CopyableCipherFields::PassportSurname,
3953 CopyableCipherFields::PassportPassportNumber,
3954 ]
3955 );
3956 }
3957
3958 #[test]
3959 fn test_decrypt_cipher_list_view_drivers_license() {
3960 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3961 SymmetricKeyAlgorithm::Aes256CbcHmac,
3962 ));
3963
3964 let cipher_view = CipherView {
3965 r#type: CipherType::DriversLicense,
3966 drivers_license: Some(drivers_license::DriversLicenseView {
3967 first_name: Some("John".to_string()),
3968 last_name: Some("Doe".to_string()),
3969 license_number: Some("DL-987654".to_string()),
3970 ..Default::default()
3971 }),
3972 login: None,
3973 ..generate_cipher()
3974 };
3975
3976 let cipher: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
3977 let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
3978
3979 assert_eq!(list_view.r#type, CipherListViewType::DriversLicense);
3980 assert_eq!(list_view.subtitle, "John Doe");
3981 assert_eq!(
3982 list_view.copyable_fields,
3983 vec![
3984 CopyableCipherFields::DriversLicenseFirstName,
3985 CopyableCipherFields::DriversLicenseLastName,
3986 CopyableCipherFields::DriversLicenseLicenseNumber,
3987 ]
3988 );
3989 }
3990
3991 #[test]
3992 fn test_cipher_view_encrypt_decrypt_passport() {
3993 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
3994 SymmetricKeyAlgorithm::Aes256CbcHmac,
3995 ));
3996
3997 let passport = passport::PassportView {
3998 given_name: Some("Jane".to_string()),
3999 surname: Some("Doe".to_string()),
4000 date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4001 sex: Some("F".to_string()),
4002 birth_place: Some("New York".to_string()),
4003 nationality: Some("American".to_string()),
4004 issuing_country: Some("US".to_string()),
4005 passport_number: Some("P12345678".to_string()),
4006 passport_type: Some("P".to_string()),
4007 national_identification_number: Some("123-45-6789".to_string()),
4008 issuing_authority: Some("US State Department".to_string()),
4009 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4010 expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
4011 };
4012
4013 let cipher_view = CipherView {
4014 r#type: CipherType::Passport,
4015 passport: Some(passport.clone()),
4016 login: None,
4017 ..generate_cipher()
4018 };
4019
4020 let encrypted: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
4021 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
4022
4023 assert_eq!(decrypted.r#type, CipherType::Passport);
4024 assert_eq!(decrypted.passport, Some(passport));
4025 assert!(decrypted.login.is_none());
4026 }
4027
4028 #[test]
4029 fn test_cipher_view_encrypt_decrypt_drivers_license() {
4030 let key_store = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4031 SymmetricKeyAlgorithm::Aes256CbcHmac,
4032 ));
4033
4034 let dl = drivers_license::DriversLicenseView {
4035 first_name: Some("John".to_string()),
4036 middle_name: Some("Michael".to_string()),
4037 last_name: Some("Doe".to_string()),
4038 date_of_birth: chrono::NaiveDate::from_ymd_opt(1985, 6, 15),
4039 license_number: Some("DL-987654".to_string()),
4040 issuing_country: Some("US".to_string()),
4041 issuing_state: Some("NY".to_string()),
4042 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4043 expiration_date: chrono::NaiveDate::from_ymd_opt(2028, 1, 1),
4044 issuing_authority: Some("NY DMV".to_string()),
4045 license_class: Some("D".to_string()),
4046 };
4047
4048 let cipher_view = CipherView {
4049 r#type: CipherType::DriversLicense,
4050 drivers_license: Some(dl.clone()),
4051 login: None,
4052 ..generate_cipher()
4053 };
4054
4055 let encrypted: Cipher = key_store.encrypt(EncryptMode::Legacy(cipher_view)).unwrap();
4056 let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
4057
4058 assert_eq!(decrypted.r#type, CipherType::DriversLicense);
4059 assert_eq!(decrypted.drivers_license, Some(dl));
4060 assert!(decrypted.login.is_none());
4061 }
4062
4063 #[test]
4064 fn test_mini_response_model_view_password_defaults_to_true() {
4065 use chrono::Utc;
4066
4067 let mini_response = CipherMiniResponseModel {
4070 id: Some(TEST_UUID.parse().unwrap()),
4071 name: Some(TEST_CIPHER_NAME.to_string()),
4072 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4073 creation_date: Some(Utc::now().to_rfc3339()),
4074 revision_date: Some(Utc::now().to_rfc3339()),
4075 ..Default::default()
4076 };
4077
4078 let cipher = mini_response.merge_with_cipher(None).unwrap();
4079 assert!(
4080 cipher.view_password,
4081 "view_password should default to true for CipherMiniResponseModel"
4082 );
4083
4084 let mini_details_response = CipherMiniDetailsResponseModel {
4086 id: Some(TEST_UUID.parse().unwrap()),
4087 name: Some(TEST_CIPHER_NAME.to_string()),
4088 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4089 creation_date: Some(Utc::now().to_rfc3339()),
4090 revision_date: Some(Utc::now().to_rfc3339()),
4091 ..Default::default()
4092 };
4093
4094 let cipher = mini_details_response.merge_with_cipher(None).unwrap();
4095 assert!(
4096 cipher.view_password,
4097 "view_password should default to true for CipherMiniDetailsResponseModel"
4098 );
4099 }
4100
4101 #[test]
4105 fn test_merge_takes_partial_data_from_response() {
4106 use chrono::Utc;
4107
4108 let org: OrganizationId = RESTRICTED_ORG_UUID.parse().unwrap();
4109 let local_restricted = || {
4111 Some(restricted_cipher(
4112 org,
4113 CipherType::Login,
4114 RESTRICTED_LOGIN_ENVELOPE.to_string(),
4115 ))
4116 };
4117 let now = Utc::now().to_rfc3339();
4118
4119 macro_rules! assert_gating_from_response {
4120 ($model:ident) => {{
4121 let base = $model {
4122 id: Some(TEST_UUID.parse().unwrap()),
4123 organization_id: Some(org.to_string().parse().unwrap()),
4124 r#type: Some(bitwarden_api_api::models::CipherType::Login),
4125 creation_date: Some(now.clone()),
4126 revision_date: Some(now.clone()),
4127 ..Default::default()
4128 };
4129
4130 assert_eq!(
4132 base.clone()
4133 .merge_with_cipher(local_restricted())
4134 .unwrap()
4135 .partial_data,
4136 None,
4137 concat!(stringify!($model), ": full response must un-gate"),
4138 );
4139
4140 let restricted = $model {
4142 partial_data: Some(RESTRICTED_LOGIN_ENVELOPE.to_string()),
4143 ..base
4144 };
4145 assert_eq!(
4146 restricted
4147 .merge_with_cipher(None)
4148 .unwrap()
4149 .partial_data
4150 .as_deref(),
4151 Some(RESTRICTED_LOGIN_ENVELOPE),
4152 concat!(stringify!($model), ": restricted response must gate"),
4153 );
4154 }};
4155 }
4156
4157 assert_gating_from_response!(CipherResponseModel);
4158 assert_gating_from_response!(CipherDetailsResponseModel);
4159 assert_gating_from_response!(CipherMiniResponseModel);
4160 assert_gating_from_response!(CipherMiniDetailsResponseModel);
4161 }
4162
4163 mod cipher_decrypt_dispatch {
4166 use bitwarden_crypto::KeyStore;
4167
4168 use super::*;
4169 use crate::{
4170 BankAccountView, CardView, DriversLicenseView, IdentityView, PassportView,
4171 SecureNoteType, SecureNoteView, SshKeyView, cipher::blob::encrypt_blob_cipher,
4172 };
4173
4174 fn make_key_store() -> KeyStore<KeySlotIds> {
4175 create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4176 SymmetricKeyAlgorithm::Aes256CbcHmac,
4177 ))
4178 }
4179
4180 fn encrypt_legacy(view: CipherView, key_store: &KeyStore<KeySlotIds>) -> Cipher {
4182 key_store.encrypt(EncryptMode::Legacy(view)).unwrap()
4183 }
4184
4185 fn encrypt_blob(mut view: CipherView, key_store: &KeyStore<KeySlotIds>) -> Cipher {
4187 let mut ctx = key_store.context_mut();
4188 encrypt_blob_cipher(&mut view, &mut ctx).unwrap()
4189 }
4190
4191 fn base_login_view() -> CipherView {
4192 let mut view = generate_cipher();
4193 view.name = "Test Login".to_string();
4194 view.login = Some(LoginView {
4195 username: Some("[email protected]".to_string()),
4196 password: Some("hunter2".to_string()),
4197 password_revision_date: None,
4198 uris: None,
4199 totp: Some("otpauth://totp/test?secret=SECRET".to_string()),
4200 autofill_on_page_load: None,
4201 fido2_credentials: None,
4202 });
4203 view
4204 }
4205
4206 #[test]
4208 fn dispatches_blob_to_cipher_view() {
4209 let key_store = make_key_store();
4210 let cipher = encrypt_blob(base_login_view(), &key_store);
4211
4212 let view: CipherView = key_store.decrypt(&cipher).unwrap();
4213
4214 assert_eq!(view.name, "Test Login");
4215 let login = view.login.expect("blob decrypt should restore login");
4216 assert_eq!(login.username.as_deref(), Some("[email protected]"));
4217 assert_eq!(login.password.as_deref(), Some("hunter2"));
4218 }
4219
4220 #[test]
4223 fn dispatches_legacy_to_cipher_view() {
4224 let key_store = make_key_store();
4225
4226 let cipher = encrypt_legacy(base_login_view(), &key_store);
4228 let view: CipherView = key_store.decrypt(&cipher).unwrap();
4229 assert_eq!(view.name, "Test Login");
4230 assert_eq!(
4231 view.login.unwrap().username.as_deref(),
4232 Some("[email protected]"),
4233 );
4234
4235 let cipher = encrypt_legacy(base_login_view(), &key_store);
4237 let view: CipherView = key_store.decrypt(&StrictDecrypt(cipher)).unwrap();
4238 assert_eq!(view.name, "Test Login");
4239 assert_eq!(
4240 view.login.unwrap().username.as_deref(),
4241 Some("[email protected]"),
4242 );
4243 }
4244
4245 #[test]
4250 fn blob_to_list_view_per_type() {
4251 let key_store = make_key_store();
4252
4253 {
4255 let list_view = decrypt_blob_list_view(&key_store, base_login_view());
4256 assert_eq!(list_view.name, "Test Login");
4257 assert_eq!(list_view.subtitle, "[email protected]");
4258 assert!(matches!(list_view.r#type, CipherListViewType::Login(_)));
4259 assert!(
4260 list_view
4261 .copyable_fields
4262 .contains(&CopyableCipherFields::LoginUsername)
4263 );
4264 assert!(
4265 list_view
4266 .copyable_fields
4267 .contains(&CopyableCipherFields::LoginPassword)
4268 );
4269 assert!(
4270 list_view
4271 .copyable_fields
4272 .contains(&CopyableCipherFields::LoginTotp)
4273 );
4274 }
4275
4276 {
4278 let mut view = generate_cipher();
4279 view.r#type = CipherType::Card;
4280 view.login = None;
4281 view.name = "My Card".to_string();
4282 view.card = Some(CardView {
4283 cardholder_name: Some("John Doe".to_string()),
4284 exp_month: Some("12".to_string()),
4285 exp_year: Some("2030".to_string()),
4286 code: Some("123".to_string()),
4287 brand: Some("Visa".to_string()),
4288 number: Some("4111111111111111".to_string()),
4289 });
4290 let list_view = decrypt_blob_list_view(&key_store, view);
4291 assert_eq!(list_view.name, "My Card");
4292 assert!(list_view.subtitle.contains("Visa"));
4293 assert!(list_view.subtitle.contains("1111"));
4294 match &list_view.r#type {
4295 CipherListViewType::Card(card) => {
4296 assert_eq!(card.brand.as_deref(), Some("Visa"))
4297 }
4298 other => panic!("expected Card, got {other:?}"),
4299 }
4300 assert!(
4301 list_view
4302 .copyable_fields
4303 .contains(&CopyableCipherFields::CardNumber)
4304 );
4305 assert!(
4306 list_view
4307 .copyable_fields
4308 .contains(&CopyableCipherFields::CardSecurityCode)
4309 );
4310 }
4311
4312 {
4314 let mut view = generate_cipher();
4315 view.r#type = CipherType::Identity;
4316 view.login = None;
4317 view.name = "My Identity".to_string();
4318 view.identity = Some(IdentityView {
4319 title: None,
4320 first_name: Some("Jane".to_string()),
4321 middle_name: None,
4322 last_name: Some("Doe".to_string()),
4323 address1: Some("123 Main St".to_string()),
4324 address2: None,
4325 address3: None,
4326 city: None,
4327 state: None,
4328 postal_code: None,
4329 country: None,
4330 company: None,
4331 email: Some("[email protected]".to_string()),
4332 phone: None,
4333 ssn: None,
4334 username: None,
4335 passport_number: None,
4336 license_number: None,
4337 });
4338 let list_view = decrypt_blob_list_view(&key_store, view);
4339 assert_eq!(list_view.name, "My Identity");
4340 assert!(list_view.subtitle.contains("Jane"));
4341 assert!(list_view.subtitle.contains("Doe"));
4342 assert!(matches!(list_view.r#type, CipherListViewType::Identity));
4343 assert!(
4344 list_view
4345 .copyable_fields
4346 .contains(&CopyableCipherFields::IdentityEmail)
4347 );
4348 assert!(
4349 list_view
4350 .copyable_fields
4351 .contains(&CopyableCipherFields::IdentityAddress)
4352 );
4353 }
4354
4355 {
4357 let mut view = generate_cipher();
4358 view.r#type = CipherType::SecureNote;
4359 view.login = None;
4360 view.name = "My Note".to_string();
4361 view.notes = Some("secret".to_string());
4362 view.secure_note = Some(SecureNoteView {
4363 r#type: SecureNoteType::Generic,
4364 });
4365 let list_view = decrypt_blob_list_view(&key_store, view);
4366 assert_eq!(list_view.name, "My Note");
4367 assert_eq!(list_view.subtitle, "");
4368 assert!(matches!(list_view.r#type, CipherListViewType::SecureNote));
4369 assert!(
4370 list_view
4371 .copyable_fields
4372 .contains(&CopyableCipherFields::SecureNotes)
4373 );
4374 }
4375
4376 {
4378 let mut view = generate_cipher();
4379 view.r#type = CipherType::SshKey;
4380 view.login = None;
4381 view.name = "My SSH".to_string();
4382 view.ssh_key = Some(SshKeyView {
4383 private_key: "-----BEGIN PRIVATE KEY-----".to_string(),
4384 public_key: "ssh-ed25519 AAAA".to_string(),
4385 fingerprint: "SHA256:abcdef".to_string(),
4386 });
4387 let list_view = decrypt_blob_list_view(&key_store, view);
4388 assert_eq!(list_view.name, "My SSH");
4389 assert_eq!(list_view.subtitle, "SHA256:abcdef");
4390 assert!(matches!(list_view.r#type, CipherListViewType::SshKey));
4391 assert!(
4392 list_view
4393 .copyable_fields
4394 .contains(&CopyableCipherFields::SshKey)
4395 );
4396 }
4397
4398 {
4400 let mut view = generate_cipher();
4401 view.r#type = CipherType::BankAccount;
4402 view.login = None;
4403 view.name = "My Bank Account".to_string();
4404 view.bank_account = Some(BankAccountView {
4405 bank_name: Some("Some Bank".to_string()),
4406 name_on_account: Some("Jane Doe".to_string()),
4407 account_number: Some("123456".to_string()),
4408 routing_number: Some("111000025".to_string()),
4409 branch_number: Some("001".to_string()),
4410 pin: Some("4321".to_string()),
4411 swift_code: Some("ABCDEF12".to_string()),
4412 iban: Some("DE89370400440532013000".to_string()),
4413 ..Default::default()
4414 });
4415 let list_view = decrypt_blob_list_view(&key_store, view);
4416 assert_eq!(list_view.name, "My Bank Account");
4417 assert_eq!(list_view.subtitle, "Some Bank");
4418 assert_eq!(
4419 list_view.r#type,
4420 CipherListViewType::BankAccount(BankAccountListView {
4421 account_number: Some("123456".to_string()),
4422 account_type: None,
4423 })
4424 );
4425 assert_eq!(
4426 list_view.copyable_fields,
4427 vec![
4428 CopyableCipherFields::BankAccountNameOnAccount,
4429 CopyableCipherFields::BankAccountAccountNumber,
4430 CopyableCipherFields::BankAccountRoutingNumber,
4431 CopyableCipherFields::BankAccountBranchNumber,
4432 CopyableCipherFields::BankAccountPin,
4433 CopyableCipherFields::BankAccountIban,
4434 CopyableCipherFields::BankAccountSwift,
4435 ]
4436 );
4437 }
4438 }
4439
4440 fn fully_populated_views() -> Vec<(&'static str, CipherView)> {
4447 let with_type = |r#type: CipherType, f: &dyn Fn(&mut CipherView)| {
4448 let mut view = generate_cipher();
4449 view.r#type = r#type;
4450 view.login = None;
4451 f(&mut view);
4452 view
4453 };
4454
4455 vec![
4456 ("Login", base_login_view()),
4457 (
4458 "Card",
4459 with_type(CipherType::Card, &|v| {
4460 v.card = Some(CardView {
4461 cardholder_name: Some("Jane Doe".to_string()),
4462 exp_month: Some("12".to_string()),
4463 exp_year: Some("2030".to_string()),
4464 code: Some("123".to_string()),
4465 brand: Some("Visa".to_string()),
4466 number: Some("4111111111111111".to_string()),
4467 });
4468 }),
4469 ),
4470 (
4471 "Identity",
4472 with_type(CipherType::Identity, &|v| {
4473 v.identity = Some(IdentityView {
4474 title: Some("Mx".to_string()),
4475 first_name: Some("Jane".to_string()),
4476 middle_name: Some("Q".to_string()),
4477 last_name: Some("Doe".to_string()),
4478 address1: Some("1 Main St".to_string()),
4479 address2: Some("Apt 2".to_string()),
4480 address3: Some("Floor 3".to_string()),
4481 city: Some("Anytown".to_string()),
4482 state: Some("CA".to_string()),
4483 postal_code: Some("90210".to_string()),
4484 country: Some("US".to_string()),
4485 company: Some("Acme".to_string()),
4486 email: Some("[email protected]".to_string()),
4487 phone: Some("555-0100".to_string()),
4488 ssn: Some("000-00-0000".to_string()),
4489 username: Some("jane".to_string()),
4490 passport_number: Some("X1234567".to_string()),
4491 license_number: Some("D1234567".to_string()),
4492 });
4493 }),
4494 ),
4495 (
4496 "SecureNote",
4497 with_type(CipherType::SecureNote, &|v| {
4498 v.notes = Some("a secret note".to_string());
4499 v.secure_note = Some(SecureNoteView {
4500 r#type: SecureNoteType::Generic,
4501 });
4502 }),
4503 ),
4504 (
4505 "SshKey",
4506 with_type(CipherType::SshKey, &|v| {
4507 v.ssh_key = Some(SshKeyView {
4508 private_key: "private".to_string(),
4509 public_key: "public".to_string(),
4510 fingerprint: "SHA256:abc".to_string(),
4511 });
4512 }),
4513 ),
4514 (
4515 "BankAccount",
4516 with_type(CipherType::BankAccount, &|v| {
4517 v.bank_account = Some(BankAccountView {
4518 bank_name: Some("Some Bank".to_string()),
4519 name_on_account: Some("Jane Doe".to_string()),
4520 account_type: Some("Checking".to_string()),
4521 account_number: Some("123456".to_string()),
4522 routing_number: Some("111000025".to_string()),
4523 branch_number: Some("001".to_string()),
4524 pin: Some("4321".to_string()),
4525 swift_code: Some("ABCDEF12".to_string()),
4526 iban: Some("DE89370400440532013000".to_string()),
4527 bank_contact_phone: Some("555-0199".to_string()),
4528 });
4529 }),
4530 ),
4531 (
4532 "DriversLicense",
4533 with_type(CipherType::DriversLicense, &|v| {
4534 v.drivers_license = Some(DriversLicenseView {
4535 first_name: Some("Jane".to_string()),
4536 middle_name: Some("Q".to_string()),
4537 last_name: Some("Doe".to_string()),
4538 date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4539 license_number: Some("D1234567".to_string()),
4540 issuing_country: Some("US".to_string()),
4541 issuing_state: Some("CA".to_string()),
4542 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4543 expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
4544 issuing_authority: Some("DMV".to_string()),
4545 license_class: Some("C".to_string()),
4546 });
4547 }),
4548 ),
4549 (
4550 "Passport",
4551 with_type(CipherType::Passport, &|v| {
4552 v.passport = Some(PassportView {
4553 surname: Some("Doe".to_string()),
4554 given_name: Some("Jane".to_string()),
4555 date_of_birth: chrono::NaiveDate::from_ymd_opt(1990, 1, 1),
4556 sex: Some("F".to_string()),
4557 birth_place: Some("Anytown".to_string()),
4558 nationality: Some("US".to_string()),
4559 issuing_country: Some("US".to_string()),
4560 passport_number: Some("X1234567".to_string()),
4561 passport_type: Some("P".to_string()),
4562 national_identification_number: Some("000-00-0000".to_string()),
4563 issuing_authority: Some("State Dept".to_string()),
4564 issue_date: chrono::NaiveDate::from_ymd_opt(2020, 1, 1),
4565 expiration_date: chrono::NaiveDate::from_ymd_opt(2030, 1, 1),
4566 });
4567 }),
4568 ),
4569 ]
4570 }
4571
4572 #[test]
4579 fn copyable_fields_parity_between_legacy_and_blob() {
4580 let key_store = make_key_store();
4581
4582 for (label, view) in fully_populated_views() {
4583 let legacy: CipherListView = key_store
4584 .decrypt(&encrypt_legacy(view.clone(), &key_store))
4585 .unwrap();
4586 let blob = decrypt_blob_list_view(&key_store, view);
4587
4588 assert_eq!(
4589 legacy.copyable_fields, blob.copyable_fields,
4590 "copyable_fields diverged between legacy and blob paths for {label}",
4591 );
4592 }
4593 }
4594
4595 #[test]
4599 fn login_list_view_preserves_totp_round_trip() {
4600 let key_store = make_key_store();
4601 let list_view = decrypt_blob_list_view(&key_store, base_login_view());
4602
4603 match &list_view.r#type {
4604 CipherListViewType::Login(login) => assert!(login.totp.is_some()),
4605 other => panic!("expected Login, got {other:?}"),
4606 }
4607 let totp = list_view.get_totp_key().unwrap();
4608 assert_eq!(totp.as_deref(), Some("otpauth://totp/test?secret=SECRET"));
4609 }
4610
4611 #[test]
4613 fn mixed_batch_decrypt_list() {
4614 let key_store = make_key_store();
4615 let blob = encrypt_blob(base_login_view(), &key_store);
4616 let legacy = encrypt_legacy(base_login_view(), &key_store);
4617
4618 let ciphers = vec![blob, legacy];
4619 let views: Vec<CipherListView> = key_store.decrypt_list(&ciphers).unwrap();
4620
4621 assert_eq!(views.len(), 2);
4622 for v in &views {
4623 assert_eq!(v.name, "Test Login");
4624 assert_eq!(v.subtitle, "[email protected]");
4625 }
4626 }
4627
4628 fn decrypt_blob_list_view(
4629 key_store: &KeyStore<KeySlotIds>,
4630 view: CipherView,
4631 ) -> CipherListView {
4632 let cipher = encrypt_blob(view, key_store);
4633 key_store.decrypt(&cipher).unwrap()
4634 }
4635
4636 fn failing_attachments() -> Vec<attachment::Attachment> {
4640 let wrong = create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4641 SymmetricKeyAlgorithm::Aes256CbcHmac,
4642 ));
4643 let mut ctx = wrong.context();
4644 let mut enc = |s: &str| s.encrypt(&mut ctx, SymmetricKeySlotId::User).unwrap();
4645 vec![
4646 attachment::Attachment {
4647 id: Some("a1".to_string()),
4648 url: None,
4649 size: None,
4650 size_name: None,
4651 file_name: Some(enc("a1.txt")),
4652 key: Some(enc("k1")),
4653 },
4654 attachment::Attachment {
4655 id: Some("a2-old".to_string()),
4656 url: None,
4657 size: None,
4658 size_name: None,
4659 file_name: Some(enc("a2.txt")),
4660 key: None,
4661 },
4662 attachment::Attachment {
4663 id: Some("a3".to_string()),
4664 url: None,
4665 size: None,
4666 size_name: None,
4667 file_name: Some(enc("a3.txt")),
4668 key: Some(enc("k3")),
4669 },
4670 ]
4671 }
4672
4673 #[test]
4680 fn attachment_metrics_parity_with_failing_attachments() {
4681 let key_store = make_key_store();
4682
4683 let mut legacy = encrypt_legacy(base_login_view(), &key_store);
4684 legacy.attachments = Some(failing_attachments());
4685 let legacy_list: CipherListView = key_store.decrypt(&legacy).unwrap();
4686
4687 let mut blob = encrypt_blob(base_login_view(), &key_store);
4688 blob.attachments = Some(failing_attachments());
4689 let blob_list: CipherListView = key_store.decrypt(&blob).unwrap();
4690
4691 assert_eq!(legacy_list.attachments, 3);
4692 assert!(legacy_list.has_old_attachments);
4693 assert_eq!(blob_list.attachments, legacy_list.attachments);
4694 assert_eq!(
4695 blob_list.has_old_attachments,
4696 legacy_list.has_old_attachments,
4697 );
4698 }
4699 }
4700
4701 mod encrypt_mode {
4704 use bitwarden_crypto::{IdentifyKey, KeyStore};
4705
4706 use super::*;
4707
4708 fn make_key_store() -> KeyStore<KeySlotIds> {
4709 create_test_crypto_with_user_key(SymmetricCryptoKey::make(
4710 SymmetricKeyAlgorithm::Aes256CbcHmac,
4711 ))
4712 }
4713
4714 fn base_login_view() -> CipherView {
4715 let mut view = generate_cipher();
4716 view.name = "Round Trip".to_string();
4717 view.login = Some(LoginView {
4718 username: Some("[email protected]".to_string()),
4719 password: Some("hunter2".to_string()),
4720 password_revision_date: None,
4721 uris: None,
4722 totp: None,
4723 autofill_on_page_load: None,
4724 fido2_credentials: None,
4725 });
4726 view
4727 }
4728
4729 #[test]
4732 fn blob_variant_produces_blob_shaped_cipher() {
4733 let key_store = make_key_store();
4734 let mode = EncryptMode::Blob(base_login_view());
4735
4736 let cipher: Cipher = key_store.encrypt(mode).unwrap();
4737
4738 assert!(try_parse_blob(&cipher).is_some());
4739 assert!(cipher.data.is_some());
4740 assert!(cipher.login.is_none());
4741 assert!(cipher.card.is_none());
4742 assert!(cipher.identity.is_none());
4743 assert!(cipher.secure_note.is_none());
4744 assert!(cipher.ssh_key.is_none());
4745 assert!(cipher.bank_account.is_none());
4746 assert!(cipher.fields.is_none());
4747 assert!(cipher.password_history.is_none());
4748 assert!(cipher.notes.is_none());
4749 }
4750
4751 #[test]
4754 fn legacy_variant_produces_legacy_shaped_cipher() {
4755 let key_store = make_key_store();
4756 let mode = EncryptMode::Legacy(base_login_view());
4757
4758 let cipher: Cipher = key_store.encrypt(mode).unwrap();
4759
4760 assert!(try_parse_blob(&cipher).is_none());
4761 assert!(cipher.data.is_none());
4762 assert!(cipher.login.is_some());
4763 assert!(
4764 cipher.key.is_some(),
4765 "legacy encrypt must always emit a cipher key"
4766 );
4767 }
4768
4769 #[test]
4773 fn legacy_variant_wraps_cipher_key_under_explicit_key() {
4774 let key_store = make_key_store();
4775 let view = base_login_view();
4776 let mut ctx = key_store.context();
4777
4778 let new_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make(
4779 SymmetricKeyAlgorithm::Aes256CbcHmac,
4780 ));
4781 let cipher = EncryptMode::Legacy(view.clone())
4782 .encrypt_composite(&mut ctx, new_key)
4783 .unwrap();
4784
4785 let decrypted = lenient_decrypt_cipher_view(&cipher, &mut ctx, new_key).unwrap();
4788 assert_eq!(decrypted.name, view.name);
4789 assert_eq!(
4790 decrypted.login.as_ref().unwrap().password,
4791 view.login.as_ref().unwrap().password
4792 );
4793
4794 assert!(
4796 lenient_decrypt_cipher_view(&cipher, &mut ctx, view.key_identifier()).is_err(),
4797 "cipher key must not be wrapped under the view's own slot"
4798 );
4799 }
4800
4801 #[test]
4803 fn legacy_variant_preserves_existing_cipher_key() {
4804 let key_store = make_key_store();
4805 let mut view = base_login_view();
4806 let mut ctx = key_store.context();
4807
4808 let _ = view.load_cipher_key_slot(&mut ctx).unwrap();
4809 let original_key = view
4810 .key
4811 .clone()
4812 .expect("load_cipher_key_slot stores the key");
4813
4814 let wrapping_key = ctx.add_local_symmetric_key(SymmetricCryptoKey::make(
4815 SymmetricKeyAlgorithm::Aes256CbcHmac,
4816 ));
4817 let cipher = EncryptMode::Legacy(view)
4818 .encrypt_composite(&mut ctx, wrapping_key)
4819 .unwrap();
4820
4821 let unwrapped = ctx
4822 .unwrap_symmetric_key(wrapping_key, &cipher.key.expect("cipher key is emitted"))
4823 .unwrap();
4824 #[allow(deprecated)]
4825 let unwrapped_raw = ctx.dangerous_get_symmetric_key(unwrapped).unwrap().clone();
4826
4827 assert_eq!(
4828 unwrapped_raw, original_key,
4829 "encryption must wrap the view's existing cipher key, not a new one"
4830 );
4831 }
4832
4833 #[test]
4835 fn blob_variant_round_trips_through_decrypt() {
4836 let key_store = make_key_store();
4837 let original = base_login_view();
4838 let mode = EncryptMode::Blob(original.clone());
4839
4840 let cipher: Cipher = key_store.encrypt(mode).unwrap();
4841 let restored: CipherView = key_store.decrypt(&cipher).unwrap();
4842
4843 assert_eq!(restored.name, original.name);
4844 let login = restored.login.expect("round-trip should restore login");
4845 assert_eq!(login.username, original.login.as_ref().unwrap().username);
4846 assert_eq!(login.password, original.login.as_ref().unwrap().password);
4847 }
4848
4849 #[test]
4852 fn key_identifier_delegates_to_inner_view() {
4853 let view = base_login_view();
4854 let expected = view.key_identifier();
4855 let mode = EncryptMode::Blob(view);
4856 assert_eq!(mode.key_identifier(), expected);
4857 }
4858
4859 #[test]
4862 fn mixed_batch_encrypt_list_preserves_per_item_shape() {
4863 let key_store = make_key_store();
4864 let mut legacy_view = base_login_view();
4865 legacy_view.name = "Legacy".to_string();
4866 let mut blob_view = base_login_view();
4867 blob_view.name = "Blob".to_string();
4868
4869 let modes = vec![
4870 EncryptMode::Legacy(legacy_view),
4871 EncryptMode::Blob(blob_view),
4872 ];
4873 let ciphers: Vec<Cipher> = key_store.encrypt_list(&modes).unwrap();
4874
4875 assert_eq!(ciphers.len(), 2);
4876 assert!(
4877 try_parse_blob(&ciphers[0]).is_none(),
4878 "first item should be legacy"
4879 );
4880 assert!(
4881 try_parse_blob(&ciphers[1]).is_some(),
4882 "second item should be blob"
4883 );
4884 assert!(ciphers[0].login.is_some());
4885 assert!(ciphers[1].login.is_none());
4886 }
4887 }
4888}