Skip to main content

bitwarden_vault/cipher/
cipher.rs

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,
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;
22use tracing::instrument;
23#[cfg(feature = "wasm")]
24use tsify::Tsify;
25#[cfg(feature = "wasm")]
26use wasm_bindgen::prelude::wasm_bindgen;
27
28use super::{
29    attachment, bank_account, card,
30    card::CardListView,
31    cipher_permissions::CipherPermissions,
32    drivers_license, field, identity,
33    local_data::{LocalData, LocalDataView},
34    login::LoginListView,
35    passport, secure_note, ssh_key,
36};
37use crate::{
38    AttachmentView, DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView,
39    FieldView, FolderId, Login, LoginView, VaultParseError,
40    password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
41};
42
43uuid_newtype!(pub CipherId);
44
45#[allow(missing_docs)]
46#[bitwarden_error(flat)]
47#[derive(Debug, Error)]
48pub enum CipherError {
49    #[error(transparent)]
50    MissingField(#[from] MissingFieldError),
51    #[error(transparent)]
52    Crypto(#[from] CryptoError),
53    #[error(transparent)]
54    Decrypt(#[from] DecryptError),
55    #[error(transparent)]
56    Encrypt(#[from] EncryptError),
57    #[error(
58        "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
59    )]
60    AttachmentsWithoutKeys,
61    #[error("This cipher cannot be moved to the specified organization")]
62    OrganizationAlreadySet,
63    #[error(transparent)]
64    Repository(#[from] RepositoryError),
65    #[error(transparent)]
66    Chrono(#[from] chrono::ParseError),
67    #[error(transparent)]
68    SerdeJson(#[from] serde_json::Error),
69    #[error(transparent)]
70    Api(#[from] ApiError),
71}
72
73impl<T> From<bitwarden_api_api::apis::Error<T>> for CipherError {
74    fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
75        Self::Api(value.into())
76    }
77}
78
79/// Helper trait for operations on cipher types.
80pub(super) trait CipherKind {
81    /// Returns the item's subtitle.
82    fn decrypt_subtitle(
83        &self,
84        ctx: &mut KeyStoreContext<KeySlotIds>,
85        key: SymmetricKeySlotId,
86    ) -> Result<String, CryptoError>;
87
88    /// Returns a list of populated fields for the cipher.
89    fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
90}
91
92#[allow(missing_docs)]
93#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
94#[repr(u8)]
95#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
96#[cfg_attr(feature = "wasm", wasm_bindgen)]
97pub enum CipherType {
98    Login = 1,
99    SecureNote = 2,
100    Card = 3,
101    Identity = 4,
102    SshKey = 5,
103    BankAccount = 6,
104    DriversLicense = 7,
105    Passport = 8,
106}
107
108#[allow(missing_docs)]
109#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
110#[repr(u8)]
111#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
112#[cfg_attr(feature = "wasm", wasm_bindgen)]
113pub enum CipherRepromptType {
114    #[default]
115    None = 0,
116    Password = 1,
117}
118
119#[allow(missing_docs)]
120#[derive(Serialize, Deserialize, Debug, Clone)]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
123#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
124pub struct EncryptionContext {
125    /// The Id of the user that encrypted the cipher. It should always represent a UserId, even for
126    /// Organization-owned ciphers
127    pub encrypted_for: UserId,
128    pub cipher: Cipher,
129}
130
131impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
132    type Error = CipherError;
133    fn try_from(
134        EncryptionContext {
135            cipher,
136            encrypted_for,
137        }: EncryptionContext,
138    ) -> Result<Self, Self::Error> {
139        Ok(Self {
140            id: require!(cipher.id).into(),
141            encrypted_for: Some(encrypted_for.into()),
142            r#type: Some(cipher.r#type.into()),
143            organization_id: cipher.organization_id.map(|o| o.to_string()),
144            folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
145            favorite: cipher.favorite.into(),
146            reprompt: Some(cipher.reprompt.into()),
147            key: cipher.key.map(|k| k.to_string()),
148            name: cipher.name.to_string(),
149            notes: cipher.notes.map(|n| n.to_string()),
150            fields: Some(
151                cipher
152                    .fields
153                    .into_iter()
154                    .flatten()
155                    .map(Into::into)
156                    .collect(),
157            ),
158            password_history: Some(
159                cipher
160                    .password_history
161                    .into_iter()
162                    .flatten()
163                    .map(Into::into)
164                    .collect(),
165            ),
166            attachments: None,
167            attachments2: Some(
168                cipher
169                    .attachments
170                    .into_iter()
171                    .flatten()
172                    .filter_map(|a| {
173                        a.id.map(|id| {
174                            (
175                                id,
176                                bitwarden_api_api::models::CipherAttachmentModel {
177                                    file_name: a.file_name.map(|n| n.to_string()),
178                                    key: a.key.map(|k| k.to_string()),
179                                },
180                            )
181                        })
182                    })
183                    .collect(),
184            ),
185            login: cipher.login.map(|l| Box::new(l.into())),
186            card: cipher.card.map(|c| Box::new(c.into())),
187            identity: cipher.identity.map(|i| Box::new(i.into())),
188            secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
189            ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
190            bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
191            drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
192            passport: cipher.passport.map(|p| Box::new(p.into())),
193            data: None, // TODO: Consume this instead of the individual fields above.
194            last_known_revision_date: Some(
195                cipher
196                    .revision_date
197                    .to_rfc3339_opts(SecondsFormat::Millis, true),
198            ),
199            archived_date: cipher
200                .archived_date
201                .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
202        })
203    }
204}
205
206impl From<EncryptionContext> for CipherRequestModel {
207    fn from(
208        EncryptionContext {
209            cipher,
210            encrypted_for,
211        }: EncryptionContext,
212    ) -> Self {
213        Self {
214            encrypted_for: Some(encrypted_for.into()),
215            r#type: Some(cipher.r#type.into()),
216            organization_id: cipher.organization_id.map(|o| o.to_string()),
217            folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
218            favorite: cipher.favorite.into(),
219            reprompt: Some(cipher.reprompt.into()),
220            key: cipher.key.map(|k| k.to_string()),
221            name: cipher.name.to_string(),
222            notes: cipher.notes.map(|n| n.to_string()),
223            fields: Some(
224                cipher
225                    .fields
226                    .into_iter()
227                    .flatten()
228                    .map(Into::into)
229                    .collect(),
230            ),
231            password_history: Some(
232                cipher
233                    .password_history
234                    .into_iter()
235                    .flatten()
236                    .map(Into::into)
237                    .collect(),
238            ),
239            attachments: None,
240            attachments2: Some(
241                cipher
242                    .attachments
243                    .into_iter()
244                    .flatten()
245                    .filter_map(|a| {
246                        a.id.map(|id| {
247                            (
248                                id,
249                                bitwarden_api_api::models::CipherAttachmentModel {
250                                    file_name: a.file_name.map(|n| n.to_string()),
251                                    key: a.key.map(|k| k.to_string()),
252                                },
253                            )
254                        })
255                    })
256                    .collect(),
257            ),
258            login: cipher.login.map(|l| Box::new(l.into())),
259            card: cipher.card.map(|c| Box::new(c.into())),
260            identity: cipher.identity.map(|i| Box::new(i.into())),
261            secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
262            ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
263            bank_account: cipher.bank_account.map(|b| Box::new(b.into())),
264            drivers_license: cipher.drivers_license.map(|d| Box::new(d.into())),
265            passport: cipher.passport.map(|p| Box::new(p.into())),
266            data: None, // TODO: Consume this instead of the individual fields above.
267            last_known_revision_date: Some(
268                cipher
269                    .revision_date
270                    .to_rfc3339_opts(SecondsFormat::Millis, true),
271            ),
272            archived_date: cipher
273                .archived_date
274                .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
275        }
276    }
277}
278
279#[allow(missing_docs)]
280#[derive(Serialize, Deserialize, Debug, Clone)]
281#[serde(rename_all = "camelCase", deny_unknown_fields)]
282#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
283#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
284pub struct Cipher {
285    pub id: Option<CipherId>,
286    pub organization_id: Option<OrganizationId>,
287    pub folder_id: Option<FolderId>,
288    pub collection_ids: Vec<CollectionId>,
289    /// More recent ciphers uses individual encryption keys to encrypt the other fields of the
290    /// Cipher.
291    pub key: Option<EncString>,
292
293    pub name: EncString,
294    pub notes: Option<EncString>,
295
296    pub r#type: CipherType,
297    pub login: Option<Login>,
298    pub identity: Option<identity::Identity>,
299    pub card: Option<card::Card>,
300    pub secure_note: Option<secure_note::SecureNote>,
301    pub ssh_key: Option<ssh_key::SshKey>,
302    pub bank_account: Option<bank_account::BankAccount>,
303    pub drivers_license: Option<drivers_license::DriversLicense>,
304    pub passport: Option<passport::Passport>,
305
306    pub favorite: bool,
307    pub reprompt: CipherRepromptType,
308    pub organization_use_totp: bool,
309    pub edit: bool,
310    pub permissions: Option<CipherPermissions>,
311    pub view_password: bool,
312    pub local_data: Option<LocalData>,
313
314    pub attachments: Option<Vec<attachment::Attachment>>,
315    pub fields: Option<Vec<field::Field>>,
316    pub password_history: Option<Vec<password_history::PasswordHistory>>,
317
318    pub creation_date: DateTime<Utc>,
319    pub deleted_date: Option<DateTime<Utc>>,
320    pub revision_date: DateTime<Utc>,
321    pub archived_date: Option<DateTime<Utc>>,
322    pub data: Option<String>,
323}
324
325/// Represents the result of re-wrapping a cipher key, which can be needed when changing the
326/// ownership of a cipher or rotating keys.
327pub enum CipherKeyRewrapError {
328    NoCipherKey,
329    DecryptionFailure,
330    EncryptionFailure,
331}
332
333impl Cipher {
334    /// Re-wraps the encrypted cipher-key. This should be done when moving the cipher to a new
335    /// ownership (user to org), or when rotating the owning key. This mutates the cipher's key
336    /// field if successful, otherwise returns an error. Data stays encrypted the same way and
337    /// does not need to be re-uploaded to the server.
338    pub fn rewrap_cipher_key(
339        &mut self,
340        old_key: SymmetricKeySlotId,
341        new_key: SymmetricKeySlotId,
342        ctx: &mut KeyStoreContext<KeySlotIds>,
343    ) -> Result<(), CipherKeyRewrapError> {
344        let new_cipher_key = self
345            .key
346            .as_ref()
347            .ok_or(CipherKeyRewrapError::NoCipherKey)
348            .and_then(|wrapped_cipher_key| {
349                ctx.unwrap_symmetric_key(old_key, wrapped_cipher_key)
350                    .map_err(|_| CipherKeyRewrapError::DecryptionFailure)
351            })
352            .and_then(|cipher_key| {
353                ctx.wrap_symmetric_key(new_key, cipher_key)
354                    .map_err(|_| CipherKeyRewrapError::EncryptionFailure)
355            })?;
356        self.key = Some(new_cipher_key);
357        Ok(())
358    }
359}
360
361bitwarden_state::register_repository_item!(CipherId => Cipher, "Cipher");
362
363impl TryFrom<Cipher> for CipherRequestModel {
364    type Error = CryptoError;
365
366    /// Structural mapping from an encrypted [`Cipher`] to the API's expected
367    /// [`CipherRequestModel`]. No crypto — all encryption happened upstream in
368    /// `CipherView::encrypt_composite`. Callers are responsible for setting
369    /// `encrypted_for` after the conversion.
370    ///
371    /// Fails with [`CryptoError::MissingField`] if any attachment has no `id`
372    fn try_from(c: Cipher) -> Result<Self, Self::Error> {
373        let attachments2 = c
374            .attachments
375            .map(|list| {
376                list.into_iter()
377                    .map(|a| {
378                        let id = a.id.clone().ok_or(CryptoError::MissingField("id"))?;
379                        Ok::<_, CryptoError>((id, a.into()))
380                    })
381                    .collect::<Result<_, _>>()
382            })
383            .transpose()?;
384
385        Ok(CipherRequestModel {
386            encrypted_for: None,
387            r#type: Some(c.r#type.into()),
388            organization_id: c.organization_id.map(|id| id.to_string()),
389            folder_id: c.folder_id.map(|id| id.to_string()),
390            favorite: Some(c.favorite),
391            reprompt: Some(c.reprompt.into()),
392            key: c.key.map(|k| k.to_string()),
393            name: c.name.to_string(),
394            notes: c.notes.map(|n| n.to_string()),
395            login: c.login.map(|v| Box::new(v.into())),
396            card: c.card.map(|v| Box::new(v.into())),
397            identity: c.identity.map(|v| Box::new(v.into())),
398            secure_note: c.secure_note.map(|v| Box::new(v.into())),
399            ssh_key: c.ssh_key.map(|v| Box::new(v.into())),
400            bank_account: c.bank_account.map(|v| Box::new(v.into())),
401            drivers_license: c.drivers_license.map(|v| Box::new(v.into())),
402            passport: c.passport.map(|v| Box::new(v.into())),
403            fields: c.fields.map(|f| f.into_iter().map(Into::into).collect()),
404            password_history: c
405                .password_history
406                .map(|h| h.into_iter().map(Into::into).collect()),
407            attachments: None,
408            attachments2,
409            last_known_revision_date: Some(
410                c.revision_date.to_rfc3339_opts(SecondsFormat::Secs, true),
411            ),
412            archived_date: c.archived_date.map(|d| d.to_rfc3339()),
413            data: c.data,
414        })
415    }
416}
417
418#[allow(missing_docs)]
419#[derive(Serialize, Deserialize, Debug, Clone)]
420#[serde(rename_all = "camelCase", deny_unknown_fields)]
421#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
422#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
423pub struct CipherView {
424    pub id: Option<CipherId>,
425    pub organization_id: Option<OrganizationId>,
426    pub folder_id: Option<FolderId>,
427    pub collection_ids: Vec<CollectionId>,
428
429    /// Temporary, required to support re-encrypting existing items.
430    pub key: Option<EncString>,
431
432    pub name: String,
433    pub notes: Option<String>,
434
435    pub r#type: CipherType,
436    pub login: Option<LoginView>,
437    pub identity: Option<identity::IdentityView>,
438    pub card: Option<card::CardView>,
439    pub secure_note: Option<secure_note::SecureNoteView>,
440    pub ssh_key: Option<ssh_key::SshKeyView>,
441    pub bank_account: Option<bank_account::BankAccountView>,
442    pub drivers_license: Option<drivers_license::DriversLicenseView>,
443    pub passport: Option<passport::PassportView>,
444
445    pub favorite: bool,
446    pub reprompt: CipherRepromptType,
447    pub organization_use_totp: bool,
448    pub edit: bool,
449    pub permissions: Option<CipherPermissions>,
450    pub view_password: bool,
451    pub local_data: Option<LocalDataView>,
452
453    pub attachments: Option<Vec<attachment::AttachmentView>>,
454    /// Attachments that failed to decrypt. Only present when there are decryption failures.
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub attachment_decryption_failures: Option<Vec<attachment::AttachmentView>>,
457    pub fields: Option<Vec<field::FieldView>>,
458    pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
459    pub creation_date: DateTime<Utc>,
460    pub deleted_date: Option<DateTime<Utc>>,
461    pub revision_date: DateTime<Utc>,
462    pub archived_date: Option<DateTime<Utc>>,
463}
464
465#[allow(missing_docs)]
466#[derive(Serialize, Deserialize, Debug, PartialEq)]
467#[serde(rename_all = "camelCase", deny_unknown_fields)]
468#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
469#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
470pub enum CipherListViewType {
471    Login(LoginListView),
472    SecureNote,
473    Card(CardListView),
474    Identity,
475    SshKey,
476    BankAccount,
477    Passport,
478    DriversLicense,
479}
480
481/// Available fields on a cipher and can be copied from a the list view in the UI.
482#[derive(Serialize, Deserialize, Debug, PartialEq)]
483#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
484#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
485pub enum CopyableCipherFields {
486    LoginUsername,
487    LoginPassword,
488    LoginTotp,
489    CardNumber,
490    CardSecurityCode,
491    IdentityUsername,
492    IdentityEmail,
493    IdentityPhone,
494    IdentityAddress,
495    SshKey,
496    SecureNotes,
497    BankAccountNameOnAccount,
498    BankAccountAccountNumber,
499    BankAccountRoutingNumber,
500    BankAccountBranchNumber,
501    BankAccountPin,
502    BankAccountIban,
503    BankAccountSwift,
504    PassportGivenName,
505    PassportSurname,
506    PassportPassportNumber,
507    PassportNationalIdentificationNumber,
508    DriversLicenseFirstName,
509    DriversLicenseMiddleName,
510    DriversLicenseLastName,
511    DriversLicenseLicenseNumber,
512}
513
514#[allow(missing_docs)]
515#[derive(Serialize, Deserialize, Debug, PartialEq)]
516#[serde(rename_all = "camelCase", deny_unknown_fields)]
517#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
518#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
519pub struct CipherListView {
520    pub id: Option<CipherId>,
521    pub organization_id: Option<OrganizationId>,
522    pub folder_id: Option<FolderId>,
523    pub collection_ids: Vec<CollectionId>,
524
525    /// Temporary, required to support calculating TOTP from CipherListView.
526    pub key: Option<EncString>,
527
528    pub name: String,
529    pub subtitle: String,
530
531    pub r#type: CipherListViewType,
532
533    pub favorite: bool,
534    pub reprompt: CipherRepromptType,
535    pub organization_use_totp: bool,
536    pub edit: bool,
537    pub permissions: Option<CipherPermissions>,
538
539    pub view_password: bool,
540
541    /// The number of attachments
542    pub attachments: u32,
543    /// Indicates if the cipher has old attachments that need to be re-uploaded
544    pub has_old_attachments: bool,
545
546    pub creation_date: DateTime<Utc>,
547    pub deleted_date: Option<DateTime<Utc>>,
548    pub revision_date: DateTime<Utc>,
549    pub archived_date: Option<DateTime<Utc>>,
550
551    /// Hints for the presentation layer for which fields can be copied.
552    pub copyable_fields: Vec<CopyableCipherFields>,
553
554    pub local_data: Option<LocalDataView>,
555
556    /// Decrypted cipher notes for search indexing.
557    #[cfg(feature = "wasm")]
558    pub notes: Option<String>,
559    /// Decrypted cipher fields for search indexing.
560    /// Only includes name and value (for text fields only).
561    #[cfg(feature = "wasm")]
562    pub fields: Option<Vec<field::FieldListView>>,
563    /// Decrypted attachment filenames for search indexing.
564    #[cfg(feature = "wasm")]
565    pub attachment_names: Option<Vec<String>>,
566}
567
568/// Represents the result of decrypting a list of ciphers.
569///
570/// This struct contains two vectors: `successes` and `failures`.
571/// `successes` contains the decrypted `CipherListView` objects,
572/// while `failures` contains the original `Cipher` objects that failed to decrypt.
573#[derive(Serialize, Deserialize, Debug)]
574#[serde(rename_all = "camelCase", deny_unknown_fields)]
575#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
576#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
577pub struct DecryptCipherListResult {
578    /// The decrypted `CipherListView` objects.
579    pub successes: Vec<CipherListView>,
580    /// The original `Cipher` objects that failed to decrypt.
581    pub failures: Vec<Cipher>,
582}
583
584/// Represents the result of decrypting a list of ciphers.
585///
586/// This struct contains two vectors: `successes` and `failures`.
587/// `successes` contains the decrypted `CipherView` objects,
588/// while `failures` contains the original `Cipher` objects that failed to decrypt.
589#[derive(Serialize, Deserialize, Debug)]
590#[serde(rename_all = "camelCase", deny_unknown_fields)]
591#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
592#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
593pub struct DecryptCipherResult {
594    /// The decrypted `CipherView` objects.
595    pub successes: Vec<CipherView>,
596    /// The original `Cipher` objects that failed to decrypt.
597    pub failures: Vec<Cipher>,
598}
599
600/// Represents the result of fetching and decrypting all ciphers for an organization.
601///
602/// Contains the encrypted ciphers from the API alongside their decrypted list views.
603#[derive(Serialize, Deserialize, Debug)]
604#[serde(rename_all = "camelCase", deny_unknown_fields)]
605#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
606#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
607pub struct ListOrganizationCiphersResult {
608    /// All encrypted ciphers returned from the API.
609    pub ciphers: Vec<Cipher>,
610    /// Successfully decrypted `CipherListView` objects.
611    pub list_views: Vec<CipherListView>,
612}
613
614impl CipherListView {
615    pub(crate) fn get_totp_key(
616        self,
617        ctx: &mut KeyStoreContext<KeySlotIds>,
618    ) -> Result<Option<String>, CryptoError> {
619        let key = self.key_identifier();
620        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
621
622        let totp = match self.r#type {
623            CipherListViewType::Login(LoginListView { totp, .. }) => {
624                totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
625            }
626            _ => None,
627        };
628
629        Ok(totp)
630    }
631}
632
633impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Cipher> for CipherView {
634    fn encrypt_composite(
635        &self,
636        ctx: &mut KeyStoreContext<KeySlotIds>,
637        key: SymmetricKeySlotId,
638    ) -> Result<Cipher, CryptoError> {
639        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
640
641        let mut cipher_view = self.clone();
642        cipher_view.generate_checksums();
643
644        Ok(Cipher {
645            id: cipher_view.id,
646            organization_id: cipher_view.organization_id,
647            folder_id: cipher_view.folder_id,
648            collection_ids: cipher_view.collection_ids,
649            key: cipher_view.key,
650            name: cipher_view.name.encrypt(ctx, ciphers_key)?,
651            notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
652            r#type: cipher_view.r#type,
653            login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
654            identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
655            card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
656            secure_note: cipher_view
657                .secure_note
658                .encrypt_composite(ctx, ciphers_key)?,
659            ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
660            bank_account: cipher_view
661                .bank_account
662                .encrypt_composite(ctx, ciphers_key)?,
663            drivers_license: cipher_view
664                .drivers_license
665                .encrypt_composite(ctx, ciphers_key)?,
666            passport: cipher_view.passport.encrypt_composite(ctx, ciphers_key)?,
667            favorite: cipher_view.favorite,
668            reprompt: cipher_view.reprompt,
669            organization_use_totp: cipher_view.organization_use_totp,
670            edit: cipher_view.edit,
671            view_password: cipher_view.view_password,
672            local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
673            attachments: cipher_view
674                .attachments
675                .encrypt_composite(ctx, ciphers_key)?,
676            fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
677            password_history: cipher_view
678                .password_history
679                .encrypt_composite(ctx, ciphers_key)?,
680            creation_date: cipher_view.creation_date,
681            deleted_date: cipher_view.deleted_date,
682            revision_date: cipher_view.revision_date,
683            permissions: cipher_view.permissions,
684            archived_date: cipher_view.archived_date,
685            data: None, // TODO: Do we need to repopulate this on this on the cipher?
686        })
687    }
688}
689
690impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for Cipher {
691    #[instrument(err, skip_all, fields(cipher_id = ?self.id, org_id = ?self.organization_id, kind = ?self.r#type))]
692    fn decrypt(
693        &self,
694        ctx: &mut KeyStoreContext<KeySlotIds>,
695        key: SymmetricKeySlotId,
696    ) -> Result<CipherView, CryptoError> {
697        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
698
699        // Separate successful and failed attachment decryptions
700        let (attachments, attachment_decryption_failures) =
701            attachment::decrypt_attachments_with_failures(
702                self.attachments.as_deref().unwrap_or_default(),
703                ctx,
704                ciphers_key,
705            );
706
707        let mut cipher = CipherView {
708            id: self.id,
709            organization_id: self.organization_id,
710            folder_id: self.folder_id,
711            collection_ids: self.collection_ids.clone(),
712            key: self.key.clone(),
713            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
714            notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
715            r#type: self.r#type,
716            login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
717            identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
718            card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
719            secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
720            ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
721            bank_account: self.bank_account.decrypt(ctx, ciphers_key).ok().flatten(),
722            drivers_license: self
723                .drivers_license
724                .decrypt(ctx, ciphers_key)
725                .ok()
726                .flatten(),
727            passport: self.passport.decrypt(ctx, ciphers_key).ok().flatten(),
728            favorite: self.favorite,
729            reprompt: self.reprompt,
730            organization_use_totp: self.organization_use_totp,
731            edit: self.edit,
732            permissions: self.permissions,
733            view_password: self.view_password,
734            local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
735            attachments: Some(attachments),
736            attachment_decryption_failures: Some(attachment_decryption_failures),
737            fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
738            password_history: self
739                .password_history
740                .decrypt(ctx, ciphers_key)
741                .ok()
742                .flatten(),
743            creation_date: self.creation_date,
744            deleted_date: self.deleted_date,
745            revision_date: self.revision_date,
746            archived_date: self.archived_date,
747        };
748
749        // For compatibility we only remove URLs with invalid checksums if the cipher has a key
750        // or the user is on Crypto V2
751        if cipher.key.is_some()
752            || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
753        {
754            cipher.remove_invalid_checksums();
755        }
756
757        Ok(cipher)
758    }
759}
760
761impl Cipher {
762    /// Decrypt the individual encryption key for this cipher into the provided [KeyStoreContext]
763    /// and return it's identifier. Note that some ciphers do not have individual encryption
764    /// keys, in which case this will return the provided key identifier instead
765    ///
766    /// # Arguments
767    ///
768    /// * `ctx` - The key store context where the cipher key will be decrypted, if it exists
769    /// * `key` - The key to use to decrypt the cipher key, this should be the user or organization
770    ///   key
771    /// * `ciphers_key` - The encrypted cipher key
772    #[instrument(err, skip_all)]
773    pub(crate) fn decrypt_cipher_key(
774        ctx: &mut KeyStoreContext<KeySlotIds>,
775        key: SymmetricKeySlotId,
776        ciphers_key: &Option<EncString>,
777    ) -> Result<SymmetricKeySlotId, CryptoError> {
778        match ciphers_key {
779            Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
780            None => Ok(key),
781        }
782    }
783
784    /// Temporary helper to return a [CipherKind] instance based on the cipher type.
785    fn get_kind(&self) -> Option<&dyn CipherKind> {
786        match self.r#type {
787            CipherType::Login => self.login.as_ref().map(|v| v as _),
788            CipherType::Card => self.card.as_ref().map(|v| v as _),
789            CipherType::Identity => self.identity.as_ref().map(|v| v as _),
790            CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
791            CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
792            CipherType::BankAccount => self.bank_account.as_ref().map(|v| v as _),
793            CipherType::DriversLicense => self.drivers_license.as_ref().map(|v| v as _),
794            CipherType::Passport => self.passport.as_ref().map(|v| v as _),
795        }
796    }
797
798    /// Returns the decrypted subtitle for the cipher, if applicable.
799    fn decrypt_subtitle(
800        &self,
801        ctx: &mut KeyStoreContext<KeySlotIds>,
802        key: SymmetricKeySlotId,
803    ) -> Result<String, CryptoError> {
804        self.get_kind()
805            .map(|sub| sub.decrypt_subtitle(ctx, key))
806            .unwrap_or_else(|| Ok(String::new()))
807    }
808
809    /// Returns a list of copyable field names for this cipher,
810    /// based on the cipher type and populated properties.
811    fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
812        self.get_kind()
813            .map(|kind| kind.get_copyable_fields(Some(self)))
814            .unwrap_or_default()
815    }
816
817    /// This replaces the values provided by the API in the `login`, `secure_note`, `card`,
818    /// `identity`, `ssh_key`, `bank_account`, `passport`, and `drivers_license` fields,
819    /// relying instead on client-side parsing of the
820    /// `data` field.
821    #[allow(unused)] // Will be used by future changes to support cipher versioning.
822    pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
823        let data = self
824            .data
825            .as_ref()
826            .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
827
828        match &self.r#type {
829            crate::CipherType::Login => self.login = serde_json::from_str(data)?,
830            crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
831            crate::CipherType::Card => self.card = serde_json::from_str(data)?,
832            crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
833            crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
834            crate::CipherType::BankAccount => self.bank_account = serde_json::from_str(data)?,
835            crate::CipherType::DriversLicense => self.drivers_license = serde_json::from_str(data)?,
836            crate::CipherType::Passport => self.passport = serde_json::from_str(data)?,
837        }
838        Ok(())
839    }
840
841    /// Marks the cipher as soft deleted by setting `deletion_date` to now.
842    pub(crate) fn soft_delete(&mut self) {
843        self.deleted_date = Some(Utc::now());
844    }
845}
846impl CipherView {
847    #[allow(missing_docs)]
848    pub fn generate_cipher_key(
849        &mut self,
850        ctx: &mut KeyStoreContext<KeySlotIds>,
851        wrapping_key: SymmetricKeySlotId,
852    ) -> Result<(), CryptoError> {
853        let old_unwrapping_key = self.key_identifier();
854        let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, old_unwrapping_key, &self.key)?;
855
856        let new_key = ctx.generate_symmetric_key();
857
858        self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
859        self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
860
861        self.key = Some(ctx.wrap_symmetric_key(wrapping_key, new_key)?);
862        Ok(())
863    }
864
865    #[allow(missing_docs)]
866    pub fn generate_checksums(&mut self) {
867        if let Some(l) = self.login.as_mut() {
868            l.generate_checksums();
869        }
870    }
871
872    #[allow(missing_docs)]
873    pub fn remove_invalid_checksums(&mut self) {
874        if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
875            uris.retain(|u| u.is_checksum_valid());
876        }
877    }
878
879    fn reencrypt_attachment_keys(
880        &mut self,
881        ctx: &mut KeyStoreContext<KeySlotIds>,
882        old_key: SymmetricKeySlotId,
883        new_key: SymmetricKeySlotId,
884    ) -> Result<(), CryptoError> {
885        if let Some(attachments) = &mut self.attachments {
886            AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
887        }
888        Ok(())
889    }
890
891    #[allow(missing_docs)]
892    pub fn decrypt_fido2_credentials(
893        &self,
894        ctx: &mut KeyStoreContext<KeySlotIds>,
895    ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
896        let key = self.key_identifier();
897        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
898
899        Ok(self
900            .login
901            .as_ref()
902            .and_then(|l| l.fido2_credentials.as_ref())
903            .map(|f| f.decrypt(ctx, ciphers_key))
904            .transpose()?
905            .unwrap_or_default())
906    }
907
908    fn reencrypt_fido2_credentials(
909        &mut self,
910        ctx: &mut KeyStoreContext<KeySlotIds>,
911        old_key: SymmetricKeySlotId,
912        new_key: SymmetricKeySlotId,
913    ) -> Result<(), CryptoError> {
914        if let Some(login) = self.login.as_mut() {
915            login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
916        }
917        Ok(())
918    }
919
920    /// Moves the cipher to an organization by re-encrypting the cipher keys with the organization
921    /// key and assigning the organization ID to the cipher.
922    ///
923    /// # Arguments
924    /// * `ctx` - The key store context where the cipher keys will be re-encrypted
925    /// * `organization_id` - The ID of the organization to move the cipher to
926    pub fn move_to_organization(
927        &mut self,
928        ctx: &mut KeyStoreContext<KeySlotIds>,
929        organization_id: OrganizationId,
930    ) -> Result<(), CipherError> {
931        let new_key = SymmetricKeySlotId::Organization(organization_id);
932
933        self.reencrypt_cipher_keys(ctx, new_key)?;
934        self.organization_id = Some(organization_id);
935
936        Ok(())
937    }
938
939    /// Re-encrypt the cipher key(s) using a new wrapping key.
940    ///
941    /// If the cipher has a cipher key, it will be re-encrypted with the new wrapping key.
942    /// Otherwise, the cipher will re-encrypt all attachment keys and FIDO2 credential keys
943    pub fn reencrypt_cipher_keys(
944        &mut self,
945        ctx: &mut KeyStoreContext<KeySlotIds>,
946        new_wrapping_key: SymmetricKeySlotId,
947    ) -> Result<(), CipherError> {
948        let old_key = self.key_identifier();
949
950        // If any attachment is missing a key we can't reencrypt the attachment keys
951        if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
952            return Err(CipherError::AttachmentsWithoutKeys);
953        }
954
955        // If the cipher has a key, reencrypt it with the new wrapping key
956        if self.key.is_some() {
957            // Decrypt the current cipher key using the existing wrapping key
958            let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
959
960            // Wrap the cipher key with the new wrapping key
961            self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
962        } else {
963            // The cipher does not have a key, we must reencrypt all attachment keys and FIDO2
964            // credentials individually
965            self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
966            self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
967        }
968
969        Ok(())
970    }
971
972    #[allow(missing_docs)]
973    pub fn set_new_fido2_credentials(
974        &mut self,
975        ctx: &mut KeyStoreContext<KeySlotIds>,
976        creds: Vec<Fido2CredentialFullView>,
977    ) -> Result<(), CipherError> {
978        let key = self.key_identifier();
979
980        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
981
982        require!(self.login.as_mut()).fido2_credentials =
983            Some(creds.encrypt_composite(ctx, ciphers_key)?);
984
985        Ok(())
986    }
987
988    #[allow(missing_docs)]
989    pub fn get_fido2_credentials(
990        &self,
991        ctx: &mut KeyStoreContext<KeySlotIds>,
992    ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
993        let key = self.key_identifier();
994
995        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
996
997        let login = require!(self.login.as_ref());
998        let creds = require!(login.fido2_credentials.as_ref());
999        let res = creds.decrypt(ctx, ciphers_key)?;
1000        Ok(res)
1001    }
1002
1003    #[allow(missing_docs)]
1004    pub fn decrypt_fido2_private_key(
1005        &self,
1006        ctx: &mut KeyStoreContext<KeySlotIds>,
1007    ) -> Result<String, CipherError> {
1008        let fido2_credential = self.get_fido2_credentials(ctx)?;
1009
1010        Ok(fido2_credential[0].key_value.clone())
1011    }
1012
1013    pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
1014        let changes = self
1015            .login
1016            .as_mut()
1017            .map_or(vec![], |login| {
1018                login.detect_password_change(&original_cipher.login)
1019            })
1020            .into_iter()
1021            .chain(self.fields.as_deref().map_or(vec![], |fields| {
1022                FieldView::detect_hidden_field_changes(
1023                    fields,
1024                    original_cipher.fields.as_deref().unwrap_or(&[]),
1025                )
1026            }))
1027            .rev()
1028            .chain(original_cipher.password_history.iter().flatten().cloned())
1029            .take(MAX_PASSWORD_HISTORY_ENTRIES)
1030            .collect();
1031        self.password_history = Some(changes)
1032    }
1033}
1034
1035impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for Cipher {
1036    fn decrypt(
1037        &self,
1038        ctx: &mut KeyStoreContext<KeySlotIds>,
1039        key: SymmetricKeySlotId,
1040    ) -> Result<CipherListView, CryptoError> {
1041        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
1042
1043        Ok(CipherListView {
1044            id: self.id,
1045            organization_id: self.organization_id,
1046            folder_id: self.folder_id,
1047            collection_ids: self.collection_ids.clone(),
1048            key: self.key.clone(),
1049            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
1050            subtitle: self
1051                .decrypt_subtitle(ctx, ciphers_key)
1052                .ok()
1053                .unwrap_or_default(),
1054            r#type: match self.r#type {
1055                CipherType::Login => {
1056                    let login = self
1057                        .login
1058                        .as_ref()
1059                        .ok_or(CryptoError::MissingField("login"))?;
1060                    CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
1061                }
1062                CipherType::SecureNote => CipherListViewType::SecureNote,
1063                CipherType::Card => {
1064                    let card = self
1065                        .card
1066                        .as_ref()
1067                        .ok_or(CryptoError::MissingField("card"))?;
1068                    CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
1069                }
1070                CipherType::Identity => CipherListViewType::Identity,
1071                CipherType::SshKey => CipherListViewType::SshKey,
1072                CipherType::BankAccount => CipherListViewType::BankAccount,
1073                CipherType::Passport => CipherListViewType::Passport,
1074                CipherType::DriversLicense => CipherListViewType::DriversLicense,
1075            },
1076            favorite: self.favorite,
1077            reprompt: self.reprompt,
1078            organization_use_totp: self.organization_use_totp,
1079            edit: self.edit,
1080            permissions: self.permissions,
1081            view_password: self.view_password,
1082            attachments: self
1083                .attachments
1084                .as_ref()
1085                .map(|a| a.len() as u32)
1086                .unwrap_or(0),
1087            has_old_attachments: self
1088                .attachments
1089                .as_ref()
1090                .map(|a| a.iter().any(|att| att.key.is_none()))
1091                .unwrap_or(false),
1092            creation_date: self.creation_date,
1093            deleted_date: self.deleted_date,
1094            revision_date: self.revision_date,
1095            copyable_fields: self.get_copyable_fields(),
1096            local_data: self.local_data.decrypt(ctx, ciphers_key)?,
1097            archived_date: self.archived_date,
1098            #[cfg(feature = "wasm")]
1099            notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
1100            #[cfg(feature = "wasm")]
1101            fields: self.fields.as_ref().map(|fields| {
1102                fields
1103                    .iter()
1104                    .filter_map(|f| {
1105                        f.decrypt(ctx, ciphers_key)
1106                            .ok()
1107                            .map(field::FieldListView::from)
1108                    })
1109                    .collect()
1110            }),
1111            #[cfg(feature = "wasm")]
1112            attachment_names: self.attachments.as_ref().map(|attachments| {
1113                attachments
1114                    .iter()
1115                    .filter_map(|a| a.file_name.decrypt(ctx, ciphers_key).ok().flatten())
1116                    .collect()
1117            }),
1118        })
1119    }
1120}
1121
1122impl IdentifyKey<SymmetricKeySlotId> for Cipher {
1123    fn key_identifier(&self) -> SymmetricKeySlotId {
1124        match self.organization_id {
1125            Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1126            None => SymmetricKeySlotId::User,
1127        }
1128    }
1129}
1130
1131impl IdentifyKey<SymmetricKeySlotId> for CipherView {
1132    fn key_identifier(&self) -> SymmetricKeySlotId {
1133        match self.organization_id {
1134            Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1135            None => SymmetricKeySlotId::User,
1136        }
1137    }
1138}
1139
1140impl IdentifyKey<SymmetricKeySlotId> for CipherListView {
1141    fn key_identifier(&self) -> SymmetricKeySlotId {
1142        match self.organization_id {
1143            Some(organization_id) => SymmetricKeySlotId::Organization(organization_id),
1144            None => SymmetricKeySlotId::User,
1145        }
1146    }
1147}
1148
1149/// Generic wrapper that uses strict decryption: field decryption errors are propagated
1150/// instead of silently nulling out the affected fields.
1151///
1152/// This is a transitional type gated behind the `PM-34500-strict_cipher_decryption` feature flag.
1153/// It will eventually replace the default lenient [Decryptable] implementations.
1154///
1155/// TODO [PM-34531]: Remove StrictDecrypt and `PM-34500-strict_cipher_decryption` feature flag
1156/// after feature has fully rolled out.
1157pub(crate) struct StrictDecrypt<T>(pub(crate) T);
1158
1159impl IdentifyKey<SymmetricKeySlotId> for StrictDecrypt<Cipher> {
1160    fn key_identifier(&self) -> SymmetricKeySlotId {
1161        self.0.key_identifier()
1162    }
1163}
1164
1165impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherView> for StrictDecrypt<Cipher> {
1166    #[instrument(err, skip_all, fields(cipher_id = ?self.0.id, org_id = ?self.0.organization_id, kind = ?self.0.r#type))]
1167    fn decrypt(
1168        &self,
1169        ctx: &mut KeyStoreContext<KeySlotIds>,
1170        key: SymmetricKeySlotId,
1171    ) -> Result<CipherView, CryptoError> {
1172        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.0.key)?;
1173
1174        // Separate successful and failed attachment decryptions
1175        let (attachments, attachment_decryption_failures) =
1176            attachment::decrypt_attachments_with_failures(
1177                self.0.attachments.as_deref().unwrap_or_default(),
1178                ctx,
1179                ciphers_key,
1180            );
1181
1182        let mut cipher = CipherView {
1183            id: self.0.id,
1184            organization_id: self.0.organization_id,
1185            folder_id: self.0.folder_id,
1186            collection_ids: self.0.collection_ids.clone(),
1187            key: self.0.key.clone(),
1188            name: self.0.name.decrypt(ctx, ciphers_key)?,
1189            notes: self.0.notes.decrypt(ctx, ciphers_key)?,
1190            r#type: self.0.r#type,
1191            login: self
1192                .0
1193                .login
1194                .as_ref()
1195                .map(|l| StrictDecrypt(l).decrypt(ctx, ciphers_key))
1196                .transpose()?,
1197            identity: self
1198                .0
1199                .identity
1200                .as_ref()
1201                .map(|i| StrictDecrypt(i).decrypt(ctx, ciphers_key))
1202                .transpose()?,
1203            card: self
1204                .0
1205                .card
1206                .as_ref()
1207                .map(|c| StrictDecrypt(c).decrypt(ctx, ciphers_key))
1208                .transpose()?,
1209            secure_note: self.0.secure_note.decrypt(ctx, ciphers_key)?,
1210            ssh_key: self.0.ssh_key.decrypt(ctx, ciphers_key)?,
1211            bank_account: self.0.bank_account.decrypt(ctx, ciphers_key)?,
1212            drivers_license: self.0.drivers_license.decrypt(ctx, ciphers_key)?,
1213            passport: self.0.passport.decrypt(ctx, ciphers_key)?,
1214            favorite: self.0.favorite,
1215            reprompt: self.0.reprompt,
1216            organization_use_totp: self.0.organization_use_totp,
1217            edit: self.0.edit,
1218            permissions: self.0.permissions,
1219            view_password: self.0.view_password,
1220            local_data: self.0.local_data.decrypt(ctx, ciphers_key)?,
1221            attachments: Some(attachments),
1222            attachment_decryption_failures: Some(attachment_decryption_failures),
1223            fields: self
1224                .0
1225                .fields
1226                .as_ref()
1227                .map(|fields| {
1228                    fields
1229                        .iter()
1230                        .map(|f| StrictDecrypt(f).decrypt(ctx, ciphers_key))
1231                        .collect::<Result<Vec<_>, _>>()
1232                })
1233                .transpose()?,
1234            password_history: self.0.password_history.decrypt(ctx, ciphers_key)?,
1235            creation_date: self.0.creation_date,
1236            deleted_date: self.0.deleted_date,
1237            revision_date: self.0.revision_date,
1238            archived_date: self.0.archived_date,
1239        };
1240
1241        // For compatibility we only remove URLs with invalid checksums if the cipher has a key
1242        // or the user is on Crypto V2
1243        if cipher.key.is_some()
1244            || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
1245        {
1246            cipher.remove_invalid_checksums();
1247        }
1248
1249        Ok(cipher)
1250    }
1251}
1252
1253impl Decryptable<KeySlotIds, SymmetricKeySlotId, CipherListView> for StrictDecrypt<Cipher> {
1254    fn decrypt(
1255        &self,
1256        ctx: &mut KeyStoreContext<KeySlotIds>,
1257        key: SymmetricKeySlotId,
1258    ) -> Result<CipherListView, CryptoError> {
1259        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.0.key)?;
1260
1261        Ok(CipherListView {
1262            id: self.0.id,
1263            organization_id: self.0.organization_id,
1264            folder_id: self.0.folder_id,
1265            collection_ids: self.0.collection_ids.clone(),
1266            key: self.0.key.clone(),
1267            name: self.0.name.decrypt(ctx, ciphers_key)?,
1268            subtitle: self.0.decrypt_subtitle(ctx, ciphers_key)?,
1269            r#type: match self.0.r#type {
1270                CipherType::Login => {
1271                    let login = self
1272                        .0
1273                        .login
1274                        .as_ref()
1275                        .ok_or(CryptoError::MissingField("login"))?;
1276                    CipherListViewType::Login(StrictDecrypt(login).decrypt(ctx, ciphers_key)?)
1277                }
1278                CipherType::SecureNote => CipherListViewType::SecureNote,
1279                CipherType::Card => {
1280                    let card = self
1281                        .0
1282                        .card
1283                        .as_ref()
1284                        .ok_or(CryptoError::MissingField("card"))?;
1285                    CipherListViewType::Card(StrictDecrypt(card).decrypt(ctx, ciphers_key)?)
1286                }
1287                CipherType::Identity => CipherListViewType::Identity,
1288                CipherType::SshKey => CipherListViewType::SshKey,
1289                CipherType::BankAccount => CipherListViewType::BankAccount,
1290                CipherType::Passport => CipherListViewType::Passport,
1291                CipherType::DriversLicense => CipherListViewType::DriversLicense,
1292            },
1293            favorite: self.0.favorite,
1294            reprompt: self.0.reprompt,
1295            organization_use_totp: self.0.organization_use_totp,
1296            edit: self.0.edit,
1297            permissions: self.0.permissions,
1298            view_password: self.0.view_password,
1299            attachments: self
1300                .0
1301                .attachments
1302                .as_ref()
1303                .map(|a| a.len() as u32)
1304                .unwrap_or(0),
1305            has_old_attachments: self
1306                .0
1307                .attachments
1308                .as_ref()
1309                .map(|a| a.iter().any(|att| att.key.is_none()))
1310                .unwrap_or(false),
1311            creation_date: self.0.creation_date,
1312            deleted_date: self.0.deleted_date,
1313            revision_date: self.0.revision_date,
1314            copyable_fields: self.0.get_copyable_fields(),
1315            local_data: self.0.local_data.decrypt(ctx, ciphers_key)?,
1316            archived_date: self.0.archived_date,
1317            #[cfg(feature = "wasm")]
1318            notes: self.0.notes.decrypt(ctx, ciphers_key)?,
1319            #[cfg(feature = "wasm")]
1320            fields: self
1321                .0
1322                .fields
1323                .as_ref()
1324                .map(|fields| {
1325                    fields
1326                        .iter()
1327                        .map(|f| {
1328                            StrictDecrypt(f)
1329                                .decrypt(ctx, ciphers_key)
1330                                .map(field::FieldListView::from)
1331                        })
1332                        .collect::<Result<Vec<_>, _>>()
1333                })
1334                .transpose()?,
1335            #[cfg(feature = "wasm")]
1336            attachment_names: self
1337                .0
1338                .attachments
1339                .as_ref()
1340                .map(|attachments| {
1341                    attachments
1342                        .iter()
1343                        .map(|a| a.file_name.decrypt(ctx, ciphers_key))
1344                        .collect::<Result<Vec<_>, _>>()
1345                })
1346                .transpose()?
1347                .map(|names| names.into_iter().flatten().collect()),
1348        })
1349    }
1350}
1351
1352impl TryFrom<CipherDetailsResponseModel> for Cipher {
1353    type Error = VaultParseError;
1354
1355    fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
1356        Ok(Self {
1357            id: cipher.id.map(CipherId::new),
1358            organization_id: cipher.organization_id.map(OrganizationId::new),
1359            folder_id: cipher.folder_id.map(FolderId::new),
1360            collection_ids: cipher
1361                .collection_ids
1362                .unwrap_or_default()
1363                .into_iter()
1364                .map(CollectionId::new)
1365                .collect(),
1366            name: require!(EncString::try_from_optional(cipher.name)?),
1367            notes: EncString::try_from_optional(cipher.notes)?,
1368            r#type: require!(cipher.r#type).try_into()?,
1369            login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1370            identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1371            card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1372            secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1373            ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1374            bank_account: cipher.bank_account.map(|b| (*b).try_into()).transpose()?,
1375            drivers_license: cipher
1376                .drivers_license
1377                .map(|d| (*d).try_into())
1378                .transpose()?,
1379            passport: cipher.passport.map(|p| (*p).try_into()).transpose()?,
1380            favorite: cipher.favorite.unwrap_or(false),
1381            reprompt: cipher
1382                .reprompt
1383                .map(|r| r.try_into())
1384                .transpose()?
1385                .unwrap_or(CipherRepromptType::None),
1386            organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
1387            edit: cipher.edit.unwrap_or(true),
1388            permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1389            view_password: cipher.view_password.unwrap_or(true),
1390            local_data: None, // Not sent from server
1391            attachments: cipher
1392                .attachments
1393                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1394                .transpose()?,
1395            fields: cipher
1396                .fields
1397                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1398                .transpose()?,
1399            password_history: cipher
1400                .password_history
1401                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1402                .transpose()?,
1403            creation_date: require!(cipher.creation_date).parse()?,
1404            deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1405            revision_date: require!(cipher.revision_date).parse()?,
1406            key: EncString::try_from_optional(cipher.key)?,
1407            archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1408            data: cipher.data,
1409        })
1410    }
1411}
1412
1413impl PartialCipher for CipherDetailsResponseModel {
1414    fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1415        Ok(Cipher {
1416            local_data: cipher.and_then(|c| c.local_data),
1417            ..self.try_into()?
1418        })
1419    }
1420}
1421
1422impl TryFrom<bitwarden_api_api::models::CipherType> for CipherType {
1423    type Error = MissingFieldError;
1424
1425    fn try_from(t: bitwarden_api_api::models::CipherType) -> Result<Self, Self::Error> {
1426        Ok(match t {
1427            bitwarden_api_api::models::CipherType::Login => CipherType::Login,
1428            bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
1429            bitwarden_api_api::models::CipherType::Card => CipherType::Card,
1430            bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
1431            bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
1432            bitwarden_api_api::models::CipherType::BankAccount => CipherType::BankAccount,
1433            bitwarden_api_api::models::CipherType::Passport => CipherType::Passport,
1434            bitwarden_api_api::models::CipherType::DriversLicense => CipherType::DriversLicense,
1435            bitwarden_api_api::models::CipherType::__Unknown(_) => {
1436                return Err(MissingFieldError("type"));
1437            }
1438        })
1439    }
1440}
1441
1442impl TryFrom<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
1443    type Error = MissingFieldError;
1444
1445    fn try_from(t: bitwarden_api_api::models::CipherRepromptType) -> Result<Self, Self::Error> {
1446        Ok(match t {
1447            bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
1448            bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
1449            bitwarden_api_api::models::CipherRepromptType::__Unknown(_) => {
1450                return Err(MissingFieldError("reprompt"));
1451            }
1452        })
1453    }
1454}
1455
1456/// A trait for merging partial cipher data into a full cipher.
1457/// Used to convert from API response models to full Cipher structs,
1458/// without losing local data that may not be present in the API response.
1459pub(crate) trait PartialCipher {
1460    fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError>;
1461}
1462
1463impl From<CipherType> for bitwarden_api_api::models::CipherType {
1464    fn from(t: CipherType) -> Self {
1465        match t {
1466            CipherType::Login => bitwarden_api_api::models::CipherType::Login,
1467            CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
1468            CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1469            CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1470            CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1471            CipherType::BankAccount => bitwarden_api_api::models::CipherType::BankAccount,
1472            CipherType::Passport => bitwarden_api_api::models::CipherType::Passport,
1473            CipherType::DriversLicense => bitwarden_api_api::models::CipherType::DriversLicense,
1474        }
1475    }
1476}
1477
1478impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1479    fn from(t: CipherRepromptType) -> Self {
1480        match t {
1481            CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1482            CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1483        }
1484    }
1485}
1486
1487impl PartialCipher for CipherResponseModel {
1488    fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1489        Ok(Cipher {
1490            collection_ids: cipher
1491                .as_ref()
1492                .map(|c| c.collection_ids.clone())
1493                .unwrap_or_default(),
1494            local_data: cipher.and_then(|c| c.local_data),
1495            id: self.id.map(CipherId::new),
1496            organization_id: self.organization_id.map(OrganizationId::new),
1497            folder_id: self.folder_id.map(FolderId::new),
1498            name: require!(self.name).parse()?,
1499            notes: EncString::try_from_optional(self.notes)?,
1500            r#type: require!(self.r#type).try_into()?,
1501            login: self.login.map(|l| (*l).try_into()).transpose()?,
1502            identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1503            card: self.card.map(|c| (*c).try_into()).transpose()?,
1504            secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1505            ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1506            bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1507            drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1508            passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1509            favorite: self.favorite.unwrap_or(false),
1510            reprompt: self
1511                .reprompt
1512                .map(|r| r.try_into())
1513                .transpose()?
1514                .unwrap_or(CipherRepromptType::None),
1515            organization_use_totp: self.organization_use_totp.unwrap_or(false),
1516            edit: self.edit.unwrap_or(false),
1517            permissions: self.permissions.map(|p| (*p).try_into()).transpose()?,
1518            view_password: self.view_password.unwrap_or(true),
1519            attachments: self
1520                .attachments
1521                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1522                .transpose()?,
1523            fields: self
1524                .fields
1525                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1526                .transpose()?,
1527            password_history: self
1528                .password_history
1529                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1530                .transpose()?,
1531            creation_date: require!(self.creation_date).parse()?,
1532            deleted_date: self.deleted_date.map(|d| d.parse()).transpose()?,
1533            revision_date: require!(self.revision_date).parse()?,
1534            key: EncString::try_from_optional(self.key)?,
1535            archived_date: self.archived_date.map(|d| d.parse()).transpose()?,
1536            data: self.data,
1537        })
1538    }
1539}
1540
1541impl PartialCipher for CipherMiniResponseModel {
1542    fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1543        let cipher = cipher.as_ref();
1544        Ok(Cipher {
1545            id: self.id.map(CipherId::new),
1546            organization_id: self.organization_id.map(OrganizationId::new),
1547            key: EncString::try_from_optional(self.key)?,
1548            name: require!(EncString::try_from_optional(self.name)?),
1549            notes: EncString::try_from_optional(self.notes)?,
1550            r#type: require!(self.r#type).try_into()?,
1551            login: self.login.map(|l| (*l).try_into()).transpose()?,
1552            identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1553            card: self.card.map(|c| (*c).try_into()).transpose()?,
1554            secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1555            ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1556            bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1557            drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1558            passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1559            reprompt: self
1560                .reprompt
1561                .map(|r| r.try_into())
1562                .transpose()?
1563                .unwrap_or(CipherRepromptType::None),
1564            organization_use_totp: self.organization_use_totp.unwrap_or(true),
1565            attachments: self
1566                .attachments
1567                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1568                .transpose()?,
1569            fields: self
1570                .fields
1571                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1572                .transpose()?,
1573            password_history: self
1574                .password_history
1575                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1576                .transpose()?,
1577            creation_date: require!(self.creation_date)
1578                .parse()
1579                .map_err(Into::<VaultParseError>::into)?,
1580            deleted_date: self
1581                .deleted_date
1582                .map(|d| d.parse())
1583                .transpose()
1584                .map_err(Into::<VaultParseError>::into)?,
1585            revision_date: require!(self.revision_date)
1586                .parse()
1587                .map_err(Into::<VaultParseError>::into)?,
1588            archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1589            folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1590            favorite: cipher.map_or(Default::default(), |c| c.favorite),
1591            edit: cipher.map_or(Default::default(), |c| c.edit),
1592            permissions: cipher.map_or(Default::default(), |c| c.permissions),
1593            view_password: cipher.is_none_or(|c| c.view_password),
1594            local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1595            data: cipher.map_or(Default::default(), |c| c.data.clone()),
1596            collection_ids: cipher.map_or(Default::default(), |c| c.collection_ids.clone()),
1597        })
1598    }
1599}
1600
1601impl PartialCipher for CipherMiniDetailsResponseModel {
1602    fn merge_with_cipher(self, cipher: Option<Cipher>) -> Result<Cipher, VaultParseError> {
1603        let cipher = cipher.as_ref();
1604        Ok(Cipher {
1605            id: self.id.map(CipherId::new),
1606            organization_id: self.organization_id.map(OrganizationId::new),
1607            key: EncString::try_from_optional(self.key)?,
1608            name: require!(EncString::try_from_optional(self.name)?),
1609            notes: EncString::try_from_optional(self.notes)?,
1610            r#type: require!(self.r#type).try_into()?,
1611            login: self.login.map(|l| (*l).try_into()).transpose()?,
1612            identity: self.identity.map(|i| (*i).try_into()).transpose()?,
1613            card: self.card.map(|c| (*c).try_into()).transpose()?,
1614            secure_note: self.secure_note.map(|s| (*s).try_into()).transpose()?,
1615            ssh_key: self.ssh_key.map(|s| (*s).try_into()).transpose()?,
1616            bank_account: self.bank_account.map(|b| (*b).try_into()).transpose()?,
1617            drivers_license: self.drivers_license.map(|d| (*d).try_into()).transpose()?,
1618            passport: self.passport.map(|p| (*p).try_into()).transpose()?,
1619            reprompt: self
1620                .reprompt
1621                .map(|r| r.try_into())
1622                .transpose()?
1623                .unwrap_or(CipherRepromptType::None),
1624            organization_use_totp: self.organization_use_totp.unwrap_or(true),
1625            attachments: self
1626                .attachments
1627                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1628                .transpose()?,
1629            fields: self
1630                .fields
1631                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1632                .transpose()?,
1633            password_history: self
1634                .password_history
1635                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1636                .transpose()?,
1637            creation_date: require!(self.creation_date)
1638                .parse()
1639                .map_err(Into::<VaultParseError>::into)?,
1640            deleted_date: self
1641                .deleted_date
1642                .map(|d| d.parse())
1643                .transpose()
1644                .map_err(Into::<VaultParseError>::into)?,
1645            revision_date: require!(self.revision_date)
1646                .parse()
1647                .map_err(Into::<VaultParseError>::into)?,
1648            collection_ids: self
1649                .collection_ids
1650                .into_iter()
1651                .flatten()
1652                .map(CollectionId::new)
1653                .collect(),
1654            archived_date: cipher.map_or(Default::default(), |c| c.archived_date),
1655            folder_id: cipher.map_or(Default::default(), |c| c.folder_id),
1656            favorite: cipher.map_or(Default::default(), |c| c.favorite),
1657            edit: cipher.map_or(Default::default(), |c| c.edit),
1658            permissions: cipher.map_or(Default::default(), |c| c.permissions),
1659            view_password: cipher.is_none_or(|c: &Cipher| c.view_password),
1660            data: cipher.map_or(Default::default(), |c| c.data.clone()),
1661            local_data: cipher.map_or(Default::default(), |c| c.local_data.clone()),
1662        })
1663    }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668
1669    use attachment::AttachmentView;
1670    use bitwarden_core::key_management::{
1671        create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1672    };
1673    use bitwarden_crypto::SymmetricCryptoKey;
1674
1675    use super::*;
1676    use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1677
1678    // Test constants for encrypted strings
1679    const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1680    const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1681    const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1682    const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1683    const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1684    const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1685    const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1686
1687    fn generate_cipher() -> CipherView {
1688        let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1689        CipherView {
1690            r#type: CipherType::Login,
1691            login: Some(LoginView {
1692                username: Some("test_username".to_string()),
1693                password: Some("test_password".to_string()),
1694                password_revision_date: None,
1695                uris: None,
1696                totp: None,
1697                autofill_on_page_load: None,
1698                fido2_credentials: None,
1699            }),
1700            id: Some(test_id),
1701            organization_id: None,
1702            folder_id: None,
1703            collection_ids: vec![],
1704            key: None,
1705            name: "My test login".to_string(),
1706            notes: None,
1707            identity: None,
1708            card: None,
1709            secure_note: None,
1710            ssh_key: None,
1711            bank_account: None,
1712            drivers_license: None,
1713            passport: None,
1714            favorite: false,
1715            reprompt: CipherRepromptType::None,
1716            organization_use_totp: true,
1717            edit: true,
1718            permissions: None,
1719            view_password: true,
1720            local_data: None,
1721            attachments: None,
1722            attachment_decryption_failures: None,
1723            fields: None,
1724            password_history: None,
1725            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1726            deleted_date: None,
1727            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1728            archived_date: None,
1729        }
1730    }
1731
1732    fn generate_fido2(
1733        ctx: &mut KeyStoreContext<KeySlotIds>,
1734        key: SymmetricKeySlotId,
1735    ) -> Fido2Credential {
1736        Fido2Credential {
1737            credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1738            key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1739            key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1740            key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1741            key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1742            rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1743            user_handle: None,
1744            user_name: None,
1745            counter: "123".to_string().encrypt(ctx, key).unwrap(),
1746            rp_name: None,
1747            user_display_name: None,
1748            discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1749            creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1750        }
1751    }
1752
1753    #[test]
1754    fn test_decrypt_cipher_list_view() {
1755        let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1756        let key_store = create_test_crypto_with_user_key(key);
1757
1758        let cipher = Cipher {
1759            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1760            organization_id: None,
1761            folder_id: None,
1762            collection_ids: vec![],
1763            key: None,
1764            name: TEST_CIPHER_NAME.parse().unwrap(),
1765            notes: None,
1766            r#type: CipherType::Login,
1767            login: Some(Login {
1768                username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1769                password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1770                password_revision_date: None,
1771                uris: None,
1772                totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1773                autofill_on_page_load: None,
1774                fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User)]),
1775            }),
1776            identity: None,
1777            card: None,
1778            secure_note: None,
1779            ssh_key: None,
1780            bank_account: None,
1781            drivers_license: None,
1782            passport: None,
1783            favorite: false,
1784            reprompt: CipherRepromptType::None,
1785            organization_use_totp: false,
1786            edit: true,
1787            permissions: Some(CipherPermissions {
1788                delete: false,
1789                restore: false
1790            }),
1791            view_password: true,
1792            local_data: None,
1793            attachments: None,
1794            fields: None,
1795            password_history: None,
1796            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1797            deleted_date: None,
1798            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1799            archived_date: None,
1800            data: None,
1801        };
1802
1803        let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1804
1805        assert_eq!(
1806            view,
1807            CipherListView {
1808                id: cipher.id,
1809                organization_id: cipher.organization_id,
1810                folder_id: cipher.folder_id,
1811                collection_ids: cipher.collection_ids,
1812                key: cipher.key,
1813                name: "My test login".to_string(),
1814                subtitle: "test_username".to_string(),
1815                r#type: CipherListViewType::Login(LoginListView {
1816                    fido2_credentials: Some(vec![Fido2CredentialListView {
1817                        credential_id: "123".to_string(),
1818                        rp_id: "123".to_string(),
1819                        user_handle: None,
1820                        user_name: None,
1821                        user_display_name: None,
1822                        counter: "123".to_string(),
1823                    }]),
1824                    has_fido2: true,
1825                    username: Some("test_username".to_string()),
1826                    totp: cipher.login.as_ref().unwrap().totp.clone(),
1827                    uris: None,
1828                }),
1829                favorite: cipher.favorite,
1830                reprompt: cipher.reprompt,
1831                organization_use_totp: cipher.organization_use_totp,
1832                edit: cipher.edit,
1833                permissions: cipher.permissions,
1834                view_password: cipher.view_password,
1835                attachments: 0,
1836                has_old_attachments: false,
1837                creation_date: cipher.creation_date,
1838                deleted_date: cipher.deleted_date,
1839                revision_date: cipher.revision_date,
1840                copyable_fields: vec![
1841                    CopyableCipherFields::LoginUsername,
1842                    CopyableCipherFields::LoginPassword,
1843                    CopyableCipherFields::LoginTotp
1844                ],
1845                local_data: None,
1846                archived_date: cipher.archived_date,
1847                #[cfg(feature = "wasm")]
1848                notes: None,
1849                #[cfg(feature = "wasm")]
1850                fields: None,
1851                #[cfg(feature = "wasm")]
1852                attachment_names: None,
1853            }
1854        )
1855    }
1856
1857    #[test]
1858    fn test_decrypt_cipher_fails_with_invalid_name() {
1859        let key_store =
1860            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1861
1862        // Encrypt a valid cipher, then swap name with an EncString from a different key
1863        let cipher = key_store.encrypt(generate_cipher()).unwrap();
1864        let cipher = Cipher {
1865            name: TEST_CIPHER_NAME.parse().unwrap(), // encrypted with a different key
1866            ..cipher
1867        };
1868
1869        // Default (lenient) decryption swallows the error, yielding an empty name
1870        let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
1871        assert!(
1872            lenient_result.is_ok(),
1873            "Lenient decryption should succeed even when name is encrypted with a different key"
1874        );
1875        assert_eq!(
1876            lenient_result.unwrap().name,
1877            String::new(),
1878            "Lenient decryption should yield an empty name on error"
1879        );
1880
1881        // Strict decryption propagates the error
1882        let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
1883        assert!(
1884            strict_result.is_err(),
1885            "Strict decryption should fail when name is encrypted with a different key"
1886        );
1887    }
1888
1889    #[test]
1890    fn test_decrypt_cipher_fails_with_invalid_login() {
1891        let key_store =
1892            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1893
1894        // Encrypt a valid cipher, then corrupt the login username
1895        let cipher = key_store.encrypt(generate_cipher()).unwrap();
1896        let cipher = Cipher {
1897            login: Some(Login {
1898                username: Some(TEST_CIPHER_NAME.parse().unwrap()), // encrypted with a different key
1899                ..cipher.login.unwrap()
1900            }),
1901            ..cipher
1902        };
1903
1904        // Default (lenient) decryption swallows the error, yielding None for the username field
1905        let lenient_result: Result<CipherView, _> = key_store.decrypt(&cipher);
1906        assert!(
1907            lenient_result.is_ok(),
1908            "Lenient decryption should succeed even when login username is encrypted with a different key"
1909        );
1910        let lenient_view = lenient_result.unwrap();
1911        assert!(
1912            lenient_view.login.is_some(),
1913            "Lenient decryption should still return the login object"
1914        );
1915        assert!(
1916            lenient_view.login.unwrap().username.is_none(),
1917            "Lenient decryption should null out the failing username field"
1918        );
1919
1920        // Strict decryption propagates the error
1921        let strict_result: Result<CipherView, _> = key_store.decrypt(&StrictDecrypt(cipher));
1922        assert!(
1923            strict_result.is_err(),
1924            "Strict decryption should fail when login username is encrypted with a different key"
1925        );
1926    }
1927
1928    #[test]
1929    fn test_generate_cipher_key() {
1930        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1931        let key_store = create_test_crypto_with_user_key(key);
1932
1933        let original_cipher = generate_cipher();
1934
1935        // Check that the cipher gets encrypted correctly without it's own key
1936        let cipher = generate_cipher();
1937        let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1938        let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1939        assert!(no_key_cipher_dec.key.is_none());
1940        assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1941
1942        let mut cipher = generate_cipher();
1943        cipher
1944            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1945            .unwrap();
1946
1947        // Check that the cipher gets encrypted correctly when it's assigned it's own key
1948        let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1949        let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1950        assert!(key_cipher_dec.key.is_some());
1951        assert_eq!(key_cipher_dec.name, original_cipher.name);
1952    }
1953
1954    #[test]
1955    fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1956        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1957        let key_store = create_test_crypto_with_user_key(key);
1958
1959        let mut original_cipher = generate_cipher();
1960        {
1961            let mut ctx = key_store.context();
1962            let cipher_key = ctx.generate_symmetric_key();
1963
1964            original_cipher.key = Some(
1965                ctx.wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
1966                    .unwrap(),
1967            );
1968        }
1969
1970        original_cipher
1971            .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1972            .unwrap();
1973
1974        // Make sure that the cipher key is decryptable
1975        let wrapped_key = original_cipher.key.unwrap();
1976        let mut ctx = key_store.context();
1977        let _ = ctx
1978            .unwrap_symmetric_key(SymmetricKeySlotId::User, &wrapped_key)
1979            .unwrap();
1980    }
1981
1982    #[test]
1983    fn test_generate_cipher_key_ignores_attachments_without_key() {
1984        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1985        let key_store = create_test_crypto_with_user_key(key);
1986
1987        let mut cipher = generate_cipher();
1988        let attachment = AttachmentView {
1989            id: None,
1990            url: None,
1991            size: None,
1992            size_name: None,
1993            file_name: Some("Attachment test name".into()),
1994            key: None,
1995            #[cfg(feature = "wasm")]
1996            decrypted_key: None,
1997        };
1998        cipher.attachments = Some(vec![attachment]);
1999
2000        cipher
2001            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2002            .unwrap();
2003        assert!(cipher.attachments.unwrap()[0].key.is_none());
2004    }
2005
2006    #[test]
2007    fn test_reencrypt_cipher_key() {
2008        let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2009        let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2010        let key_store = create_test_crypto_with_user_key(old_key);
2011        let mut ctx = key_store.context_mut();
2012
2013        let mut cipher = generate_cipher();
2014        cipher
2015            .generate_cipher_key(&mut ctx, cipher.key_identifier())
2016            .unwrap();
2017
2018        // Re-encrypt the cipher key with a new wrapping key
2019        let new_key_id = ctx.add_local_symmetric_key(new_key);
2020
2021        cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
2022
2023        // Check that the cipher key can be unwrapped with the new key
2024        assert!(cipher.key.is_some());
2025        assert!(
2026            ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
2027                .is_ok()
2028        );
2029    }
2030
2031    #[test]
2032    fn test_reencrypt_cipher_key_ignores_missing_key() {
2033        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2034        let key_store = create_test_crypto_with_user_key(key);
2035        let mut ctx = key_store.context_mut();
2036        let mut cipher = generate_cipher();
2037
2038        // The cipher does not have a key, so re-encryption should not add one
2039        let new_cipher_key = ctx.generate_symmetric_key();
2040        cipher
2041            .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
2042            .unwrap();
2043
2044        // Check that the cipher key is still None
2045        assert!(cipher.key.is_none());
2046    }
2047
2048    #[test]
2049    fn test_move_user_cipher_to_org() {
2050        let org = OrganizationId::new_v4();
2051        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2052        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2053        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2054
2055        // Create a cipher with a user key
2056        let mut cipher = generate_cipher();
2057        cipher
2058            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2059            .unwrap();
2060
2061        cipher
2062            .move_to_organization(&mut key_store.context(), org)
2063            .unwrap();
2064        assert_eq!(cipher.organization_id, Some(org));
2065
2066        // Check that the cipher can be encrypted/decrypted with the new org key
2067        let cipher_enc = key_store.encrypt(cipher).unwrap();
2068        let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
2069
2070        assert_eq!(cipher_dec.name, "My test login");
2071    }
2072
2073    #[test]
2074    fn test_move_user_cipher_to_org_manually() {
2075        let org = OrganizationId::new_v4();
2076        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2077        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2078        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2079
2080        // Create a cipher with a user key
2081        let mut cipher = generate_cipher();
2082        cipher
2083            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
2084            .unwrap();
2085
2086        cipher.organization_id = Some(org);
2087
2088        // Check that the cipher can not be encrypted, as the
2089        // cipher key is tied to the user key and not the org key
2090        assert!(key_store.encrypt(cipher).is_err());
2091    }
2092
2093    #[test]
2094    fn test_move_user_cipher_with_attachment_without_key_to_org() {
2095        let org = OrganizationId::new_v4();
2096        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2097        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2098        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2099
2100        let mut cipher = generate_cipher();
2101        let attachment = AttachmentView {
2102            id: None,
2103            url: None,
2104            size: None,
2105            size_name: None,
2106            file_name: Some("Attachment test name".into()),
2107            key: None,
2108            #[cfg(feature = "wasm")]
2109            decrypted_key: None,
2110        };
2111        cipher.attachments = Some(vec![attachment]);
2112
2113        // Neither cipher nor attachment have keys, so the cipher can't be moved
2114        assert!(
2115            cipher
2116                .move_to_organization(&mut key_store.context(), org)
2117                .is_err()
2118        );
2119    }
2120
2121    #[test]
2122    fn test_move_user_cipher_with_attachment_with_key_to_org() {
2123        let org = OrganizationId::new_v4();
2124        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2125        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2126        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2127        let org_key = SymmetricKeySlotId::Organization(org);
2128
2129        // Attachment has a key that is encrypted with the user key, as the cipher has no key itself
2130        let (attachment_key_enc, attachment_key_val) = {
2131            let mut ctx = key_store.context();
2132            let attachment_key = ctx.generate_symmetric_key();
2133            let attachment_key_enc = ctx
2134                .wrap_symmetric_key(SymmetricKeySlotId::User, attachment_key)
2135                .unwrap();
2136            #[allow(deprecated)]
2137            let attachment_key_val = ctx
2138                .dangerous_get_symmetric_key(attachment_key)
2139                .unwrap()
2140                .clone();
2141
2142            (attachment_key_enc, attachment_key_val)
2143        };
2144
2145        let mut cipher = generate_cipher();
2146        let attachment = AttachmentView {
2147            id: None,
2148            url: None,
2149            size: None,
2150            size_name: None,
2151            file_name: Some("Attachment test name".into()),
2152            key: Some(attachment_key_enc),
2153            #[cfg(feature = "wasm")]
2154            decrypted_key: None,
2155        };
2156        cipher.attachments = Some(vec![attachment]);
2157        let cred = generate_fido2(&mut key_store.context(), SymmetricKeySlotId::User);
2158        cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
2159
2160        cipher
2161            .move_to_organization(&mut key_store.context(), org)
2162            .unwrap();
2163
2164        assert!(cipher.key.is_none());
2165
2166        // Check that the attachment key has been re-encrypted with the org key,
2167        // and the value matches with the original attachment key
2168        let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
2169        let mut ctx = key_store.context();
2170        let new_attachment_key_id = ctx
2171            .unwrap_symmetric_key(org_key, &new_attachment_key)
2172            .unwrap();
2173        #[allow(deprecated)]
2174        let new_attachment_key_dec = ctx
2175            .dangerous_get_symmetric_key(new_attachment_key_id)
2176            .unwrap();
2177
2178        assert_eq!(*new_attachment_key_dec, attachment_key_val);
2179
2180        let cred2: Fido2CredentialFullView = cipher
2181            .login
2182            .unwrap()
2183            .fido2_credentials
2184            .unwrap()
2185            .first()
2186            .unwrap()
2187            .decrypt(&mut key_store.context(), org_key)
2188            .unwrap();
2189
2190        assert_eq!(cred2.credential_id, "123");
2191    }
2192
2193    #[test]
2194    fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
2195        let org = OrganizationId::new_v4();
2196        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2197        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
2198        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
2199        let org_key = SymmetricKeySlotId::Organization(org);
2200
2201        let mut ctx = key_store.context();
2202
2203        let cipher_key = ctx.generate_symmetric_key();
2204        let cipher_key_enc = ctx
2205            .wrap_symmetric_key(SymmetricKeySlotId::User, cipher_key)
2206            .unwrap();
2207
2208        // Attachment has a key that is encrypted with the cipher key
2209        let attachment_key = ctx.generate_symmetric_key();
2210        let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
2211
2212        let mut cipher = generate_cipher();
2213        cipher.key = Some(cipher_key_enc);
2214
2215        let attachment = AttachmentView {
2216            id: None,
2217            url: None,
2218            size: None,
2219            size_name: None,
2220            file_name: Some("Attachment test name".into()),
2221            key: Some(attachment_key_enc.clone()),
2222            #[cfg(feature = "wasm")]
2223            decrypted_key: None,
2224        };
2225        cipher.attachments = Some(vec![attachment]);
2226
2227        let cred = generate_fido2(&mut ctx, cipher_key);
2228        cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
2229
2230        cipher.move_to_organization(&mut ctx, org).unwrap();
2231
2232        // Check that the cipher key has been re-encrypted with the org key,
2233        let wrapped_new_cipher_key = cipher.key.clone().unwrap();
2234        let new_cipher_key_dec = ctx
2235            .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
2236            .unwrap();
2237        #[allow(deprecated)]
2238        let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
2239        #[allow(deprecated)]
2240        let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
2241
2242        assert_eq!(new_cipher_key_dec, cipher_key_val);
2243
2244        // Check that the attachment key hasn't changed
2245        assert_eq!(
2246            cipher.attachments.unwrap()[0]
2247                .key
2248                .as_ref()
2249                .unwrap()
2250                .to_string(),
2251            attachment_key_enc.to_string()
2252        );
2253
2254        let cred2: Fido2Credential = cipher
2255            .login
2256            .unwrap()
2257            .fido2_credentials
2258            .unwrap()
2259            .first()
2260            .unwrap()
2261            .clone();
2262
2263        assert_eq!(
2264            cred2.credential_id.to_string(),
2265            cred.credential_id.to_string()
2266        );
2267    }
2268
2269    #[test]
2270    fn test_decrypt_fido2_private_key() {
2271        let key_store =
2272            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2273        let mut ctx = key_store.context();
2274
2275        let mut cipher_view = generate_cipher();
2276        cipher_view
2277            .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
2278            .unwrap();
2279
2280        let key_id = cipher_view.key_identifier();
2281        let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
2282
2283        let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
2284
2285        cipher_view.login.as_mut().unwrap().fido2_credentials =
2286            Some(vec![fido2_credential.clone()]);
2287
2288        let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
2289        assert_eq!(decrypted_key_value, "123");
2290    }
2291
2292    #[test]
2293    fn test_password_history_on_password_change() {
2294        use chrono::Utc;
2295
2296        let original_cipher = generate_cipher();
2297        let mut new_cipher = generate_cipher();
2298
2299        // Change password
2300        if let Some(ref mut login) = new_cipher.login {
2301            login.password = Some("new_password123".to_string());
2302        }
2303
2304        let start = Utc::now();
2305        new_cipher.update_password_history(&original_cipher);
2306        let end = Utc::now();
2307
2308        assert!(new_cipher.password_history.is_some());
2309        let history = new_cipher.password_history.unwrap();
2310        assert_eq!(history.len(), 1);
2311        assert_eq!(history[0].password, "test_password");
2312        assert!(
2313            history[0].last_used_date >= start && history[0].last_used_date <= end,
2314            "last_used_date was not set properly"
2315        );
2316    }
2317
2318    #[test]
2319    fn test_password_history_on_unchanged_password() {
2320        let original_cipher = generate_cipher();
2321        let mut new_cipher = generate_cipher();
2322
2323        new_cipher.update_password_history(&original_cipher);
2324
2325        // Password history should be empty since password didn't change
2326        assert!(
2327            new_cipher.password_history.is_none()
2328                || new_cipher.password_history.as_ref().unwrap().is_empty()
2329        );
2330    }
2331
2332    #[test]
2333    fn test_password_history_is_preserved() {
2334        use chrono::TimeZone;
2335
2336        let mut original_cipher = generate_cipher();
2337        original_cipher.password_history = Some(
2338            (0..4)
2339                .map(|i| PasswordHistoryView {
2340                    password: format!("old_password_{}", i),
2341                    last_used_date: chrono::Utc
2342                        .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
2343                        .unwrap(),
2344                })
2345                .collect(),
2346        );
2347
2348        let mut new_cipher = generate_cipher();
2349
2350        new_cipher.update_password_history(&original_cipher);
2351
2352        assert!(new_cipher.password_history.is_some());
2353        let history = new_cipher.password_history.unwrap();
2354        assert_eq!(history.len(), 4);
2355
2356        assert_eq!(history[0].password, "old_password_0");
2357        assert_eq!(
2358            history[0].last_used_date,
2359            chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
2360        );
2361        assert_eq!(history[1].password, "old_password_1");
2362        assert_eq!(
2363            history[1].last_used_date,
2364            chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
2365        );
2366        assert_eq!(history[2].password, "old_password_2");
2367        assert_eq!(
2368            history[2].last_used_date,
2369            chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
2370        );
2371        assert_eq!(history[3].password, "old_password_3");
2372        assert_eq!(
2373            history[3].last_used_date,
2374            chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
2375        );
2376    }
2377
2378    #[test]
2379    fn test_populate_cipher_types_login_with_valid_data() {
2380        let mut cipher = Cipher {
2381            id: Some(TEST_UUID.parse().unwrap()),
2382            organization_id: None,
2383            folder_id: None,
2384            collection_ids: vec![],
2385            key: None,
2386            name: TEST_CIPHER_NAME.parse().unwrap(),
2387            notes: None,
2388            r#type: CipherType::Login,
2389            login: None,
2390            identity: None,
2391            card: None,
2392            secure_note: None,
2393            ssh_key: None,
2394            bank_account: None,
2395            drivers_license: None,
2396            passport: None,
2397            favorite: false,
2398            reprompt: CipherRepromptType::None,
2399            organization_use_totp: false,
2400            edit: true,
2401            view_password: true,
2402            permissions: None,
2403            local_data: None,
2404            attachments: None,
2405            fields: None,
2406            password_history: None,
2407            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2408            deleted_date: None,
2409            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2410            archived_date: None,
2411            data: Some(format!(
2412                r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2413                TEST_ENC_STRING_1, TEST_ENC_STRING_2
2414            )),
2415        };
2416
2417        cipher
2418            .populate_cipher_types()
2419            .expect("populate_cipher_types failed");
2420
2421        assert!(cipher.login.is_some());
2422        let login = cipher.login.unwrap();
2423        assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
2424        assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
2425    }
2426
2427    #[test]
2428    fn test_populate_cipher_types_secure_note() {
2429        let mut cipher = Cipher {
2430            id: Some(TEST_UUID.parse().unwrap()),
2431            organization_id: None,
2432            folder_id: None,
2433            collection_ids: vec![],
2434            key: None,
2435            name: TEST_CIPHER_NAME.parse().unwrap(),
2436            notes: None,
2437            r#type: CipherType::SecureNote,
2438            login: None,
2439            identity: None,
2440            card: None,
2441            secure_note: None,
2442            ssh_key: None,
2443            bank_account: None,
2444            drivers_license: None,
2445            passport: None,
2446            favorite: false,
2447            reprompt: CipherRepromptType::None,
2448            organization_use_totp: false,
2449            edit: true,
2450            view_password: true,
2451            permissions: None,
2452            local_data: None,
2453            attachments: None,
2454            fields: None,
2455            password_history: None,
2456            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2457            deleted_date: None,
2458            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2459            archived_date: None,
2460            data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
2461        };
2462
2463        cipher
2464            .populate_cipher_types()
2465            .expect("populate_cipher_types failed");
2466
2467        assert!(cipher.secure_note.is_some());
2468    }
2469
2470    #[test]
2471    fn test_populate_cipher_types_card() {
2472        let mut cipher = Cipher {
2473            id: Some(TEST_UUID.parse().unwrap()),
2474            organization_id: None,
2475            folder_id: None,
2476            collection_ids: vec![],
2477            key: None,
2478            name: TEST_CIPHER_NAME.parse().unwrap(),
2479            notes: None,
2480            r#type: CipherType::Card,
2481            login: None,
2482            identity: None,
2483            card: None,
2484            secure_note: None,
2485            ssh_key: None,
2486            bank_account: None,
2487            drivers_license: None,
2488            passport: None,
2489            favorite: false,
2490            reprompt: CipherRepromptType::None,
2491            organization_use_totp: false,
2492            edit: true,
2493            view_password: true,
2494            permissions: None,
2495            local_data: None,
2496            attachments: None,
2497            fields: None,
2498            password_history: None,
2499            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2500            deleted_date: None,
2501            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2502            archived_date: None,
2503            data: Some(format!(
2504                r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2505                TEST_ENC_STRING_1,
2506                TEST_ENC_STRING_2,
2507                TEST_ENC_STRING_3,
2508                TEST_ENC_STRING_4,
2509                TEST_ENC_STRING_5,
2510                TEST_ENC_STRING_1
2511            )),
2512        };
2513
2514        cipher
2515            .populate_cipher_types()
2516            .expect("populate_cipher_types failed");
2517
2518        assert!(cipher.card.is_some());
2519        let card = cipher.card.unwrap();
2520        assert_eq!(
2521            card.cardholder_name.as_ref().unwrap().to_string(),
2522            TEST_ENC_STRING_1
2523        );
2524        assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
2525        assert_eq!(
2526            card.exp_month.as_ref().unwrap().to_string(),
2527            TEST_ENC_STRING_3
2528        );
2529        assert_eq!(
2530            card.exp_year.as_ref().unwrap().to_string(),
2531            TEST_ENC_STRING_4
2532        );
2533        assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
2534        assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
2535    }
2536
2537    #[test]
2538    fn test_populate_cipher_types_identity() {
2539        let mut cipher = Cipher {
2540            id: Some(TEST_UUID.parse().unwrap()),
2541            organization_id: None,
2542            folder_id: None,
2543            collection_ids: vec![],
2544            key: None,
2545            name: TEST_CIPHER_NAME.parse().unwrap(),
2546            notes: None,
2547            r#type: CipherType::Identity,
2548            login: None,
2549            identity: None,
2550            card: None,
2551            secure_note: None,
2552            ssh_key: None,
2553            bank_account: None,
2554            drivers_license: None,
2555            passport: None,
2556            favorite: false,
2557            reprompt: CipherRepromptType::None,
2558            organization_use_totp: false,
2559            edit: true,
2560            view_password: true,
2561            permissions: None,
2562            local_data: None,
2563            attachments: None,
2564            fields: None,
2565            password_history: None,
2566            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2567            deleted_date: None,
2568            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2569            archived_date: None,
2570            data: Some(format!(
2571                r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
2572                TEST_ENC_STRING_1,
2573                TEST_ENC_STRING_2,
2574                TEST_ENC_STRING_3,
2575                TEST_ENC_STRING_4,
2576                TEST_ENC_STRING_5,
2577                TEST_ENC_STRING_1,
2578                TEST_ENC_STRING_2,
2579                TEST_ENC_STRING_3,
2580                TEST_ENC_STRING_4,
2581                TEST_ENC_STRING_5
2582            )),
2583        };
2584
2585        cipher
2586            .populate_cipher_types()
2587            .expect("populate_cipher_types failed");
2588
2589        assert!(cipher.identity.is_some());
2590        let identity = cipher.identity.unwrap();
2591        assert_eq!(
2592            identity.first_name.as_ref().unwrap().to_string(),
2593            TEST_ENC_STRING_1
2594        );
2595        assert_eq!(
2596            identity.last_name.as_ref().unwrap().to_string(),
2597            TEST_ENC_STRING_2
2598        );
2599        assert_eq!(
2600            identity.email.as_ref().unwrap().to_string(),
2601            TEST_ENC_STRING_3
2602        );
2603        assert_eq!(
2604            identity.phone.as_ref().unwrap().to_string(),
2605            TEST_ENC_STRING_4
2606        );
2607        assert_eq!(
2608            identity.company.as_ref().unwrap().to_string(),
2609            TEST_ENC_STRING_5
2610        );
2611        assert_eq!(
2612            identity.address1.as_ref().unwrap().to_string(),
2613            TEST_ENC_STRING_1
2614        );
2615        assert_eq!(
2616            identity.city.as_ref().unwrap().to_string(),
2617            TEST_ENC_STRING_2
2618        );
2619        assert_eq!(
2620            identity.state.as_ref().unwrap().to_string(),
2621            TEST_ENC_STRING_3
2622        );
2623        assert_eq!(
2624            identity.postal_code.as_ref().unwrap().to_string(),
2625            TEST_ENC_STRING_4
2626        );
2627        assert_eq!(
2628            identity.country.as_ref().unwrap().to_string(),
2629            TEST_ENC_STRING_5
2630        );
2631    }
2632
2633    #[test]
2634
2635    fn test_password_history_with_hidden_fields() {
2636        let mut original_cipher = generate_cipher();
2637        original_cipher.fields = Some(vec![FieldView {
2638            name: Some("Secret Key".to_string()),
2639            value: Some("old_secret_value".to_string()),
2640            r#type: crate::FieldType::Hidden,
2641            linked_id: None,
2642        }]);
2643
2644        let mut new_cipher = generate_cipher();
2645        new_cipher.fields = Some(vec![FieldView {
2646            name: Some("Secret Key".to_string()),
2647            value: Some("new_secret_value".to_string()),
2648            r#type: crate::FieldType::Hidden,
2649            linked_id: None,
2650        }]);
2651
2652        new_cipher.update_password_history(&original_cipher);
2653
2654        assert!(new_cipher.password_history.is_some());
2655        let history = new_cipher.password_history.unwrap();
2656        assert_eq!(history.len(), 1);
2657        assert_eq!(history[0].password, "Secret Key: old_secret_value");
2658    }
2659
2660    #[test]
2661    fn test_password_history_length_limit() {
2662        use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
2663
2664        let mut original_cipher = generate_cipher();
2665        original_cipher.password_history = Some(
2666            (0..10)
2667                .map(|i| PasswordHistoryView {
2668                    password: format!("old_password_{}", i),
2669                    last_used_date: chrono::Utc::now(),
2670                })
2671                .collect(),
2672        );
2673
2674        let mut new_cipher = original_cipher.clone();
2675        // Change password
2676        if let Some(ref mut login) = new_cipher.login {
2677            login.password = Some("brand_new_password".to_string());
2678        }
2679
2680        new_cipher.update_password_history(&original_cipher);
2681
2682        assert!(new_cipher.password_history.is_some());
2683        let history = new_cipher.password_history.unwrap();
2684
2685        // Should be limited to MAX_PASSWORD_HISTORY_ENTRIES
2686        assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
2687
2688        // Most recent change (original password) should be first
2689        assert_eq!(history[0].password, "test_password");
2690        // Followed by the oldest entries from the existing history
2691        assert_eq!(history[1].password, "old_password_0");
2692        assert_eq!(history[2].password, "old_password_1");
2693        assert_eq!(history[3].password, "old_password_2");
2694        assert_eq!(history[4].password, "old_password_3");
2695    }
2696
2697    #[test]
2698    fn test_populate_cipher_types_ssh_key() {
2699        let mut cipher = Cipher {
2700            id: Some(TEST_UUID.parse().unwrap()),
2701            organization_id: None,
2702            folder_id: None,
2703            collection_ids: vec![],
2704            key: None,
2705            name: TEST_CIPHER_NAME.parse().unwrap(),
2706            notes: None,
2707            r#type: CipherType::SshKey,
2708            login: None,
2709            identity: None,
2710            card: None,
2711            secure_note: None,
2712            ssh_key: None,
2713            bank_account: None,
2714            drivers_license: None,
2715            passport: None,
2716            favorite: false,
2717            reprompt: CipherRepromptType::None,
2718            organization_use_totp: false,
2719            edit: true,
2720            view_password: true,
2721            permissions: None,
2722            local_data: None,
2723            attachments: None,
2724            fields: None,
2725            password_history: None,
2726            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2727            deleted_date: None,
2728            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2729            archived_date: None,
2730            data: Some(format!(
2731                r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2732                TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2733            )),
2734        };
2735
2736        cipher
2737            .populate_cipher_types()
2738            .expect("populate_cipher_types failed");
2739
2740        assert!(cipher.ssh_key.is_some());
2741        let ssh_key = cipher.ssh_key.unwrap();
2742        assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2743        assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2744        assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2745    }
2746
2747    #[test]
2748    fn test_populate_cipher_types_with_null_data() {
2749        let mut cipher = Cipher {
2750            id: Some(TEST_UUID.parse().unwrap()),
2751            organization_id: None,
2752            folder_id: None,
2753            collection_ids: vec![],
2754            key: None,
2755            name: TEST_CIPHER_NAME.parse().unwrap(),
2756            notes: None,
2757            r#type: CipherType::Login,
2758            login: None,
2759            identity: None,
2760            card: None,
2761            secure_note: None,
2762            ssh_key: None,
2763            bank_account: None,
2764            drivers_license: None,
2765            passport: None,
2766            favorite: false,
2767            reprompt: CipherRepromptType::None,
2768            organization_use_totp: false,
2769            edit: true,
2770            view_password: true,
2771            permissions: None,
2772            local_data: None,
2773            attachments: None,
2774            fields: None,
2775            password_history: None,
2776            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2777            deleted_date: None,
2778            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2779            archived_date: None,
2780            data: None,
2781        };
2782
2783        let result = cipher.populate_cipher_types();
2784        assert!(matches!(
2785            result,
2786            Err(VaultParseError::MissingField(MissingFieldError("data")))
2787        ));
2788    }
2789
2790    #[test]
2791    fn test_populate_cipher_types_with_invalid_json() {
2792        let mut cipher = Cipher {
2793            id: Some(TEST_UUID.parse().unwrap()),
2794            organization_id: None,
2795            folder_id: None,
2796            collection_ids: vec![],
2797            key: None,
2798            name: TEST_CIPHER_NAME.parse().unwrap(),
2799            notes: None,
2800            r#type: CipherType::Login,
2801            login: None,
2802            identity: None,
2803            card: None,
2804            secure_note: None,
2805            ssh_key: None,
2806            bank_account: None,
2807            drivers_license: None,
2808            passport: None,
2809            favorite: false,
2810            reprompt: CipherRepromptType::None,
2811            organization_use_totp: false,
2812            edit: true,
2813            view_password: true,
2814            permissions: None,
2815            local_data: None,
2816            attachments: None,
2817            fields: None,
2818            password_history: None,
2819            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2820            deleted_date: None,
2821            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2822            archived_date: None,
2823            data: Some("invalid json".to_string()),
2824        };
2825
2826        let result = cipher.populate_cipher_types();
2827
2828        assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2829    }
2830
2831    #[test]
2832    fn test_decrypt_cipher_with_mixed_attachments() {
2833        let user_key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
2834        let key_store = create_test_crypto_with_user_key(user_key);
2835
2836        // Create properly encrypted attachments
2837        let mut ctx = key_store.context();
2838        let valid1 = "valid_file_1.txt"
2839            .encrypt(&mut ctx, SymmetricKeySlotId::User)
2840            .unwrap();
2841        let valid2 = "valid_file_2.txt"
2842            .encrypt(&mut ctx, SymmetricKeySlotId::User)
2843            .unwrap();
2844
2845        // Create corrupted attachment by encrypting with a random different key
2846        let wrong_key: SymmetricCryptoKey = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ==".to_string().try_into().unwrap();
2847        let wrong_key_store = create_test_crypto_with_user_key(wrong_key);
2848        let mut wrong_ctx = wrong_key_store.context();
2849        let corrupted = "corrupted_file.txt"
2850            .encrypt(&mut wrong_ctx, SymmetricKeySlotId::User)
2851            .unwrap();
2852
2853        let cipher = Cipher {
2854            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
2855            organization_id: None,
2856            folder_id: None,
2857            collection_ids: vec![],
2858            key: None,
2859            name: TEST_CIPHER_NAME.parse().unwrap(),
2860            notes: None,
2861            r#type: CipherType::Login,
2862            login: None,
2863            identity: None,
2864            card: None,
2865            secure_note: None,
2866            ssh_key: None,
2867            bank_account: None,
2868            drivers_license: None,
2869            passport: None,
2870            favorite: false,
2871            reprompt: CipherRepromptType::None,
2872            organization_use_totp: false,
2873            edit: true,
2874            permissions: None,
2875            view_password: true,
2876            local_data: None,
2877            attachments: Some(vec![
2878                // Valid attachment
2879                attachment::Attachment {
2880                    id: Some("valid-attachment".to_string()),
2881                    url: Some("https://example.com/valid".to_string()),
2882                    size: Some("100".to_string()),
2883                    size_name: Some("100 Bytes".to_string()),
2884                    file_name: Some(valid1),
2885                    key: None,
2886                },
2887                // Corrupted attachment
2888                attachment::Attachment {
2889                    id: Some("corrupted-attachment".to_string()),
2890                    url: Some("https://example.com/corrupted".to_string()),
2891                    size: Some("200".to_string()),
2892                    size_name: Some("200 Bytes".to_string()),
2893                    file_name: Some(corrupted),
2894                    key: None,
2895                },
2896                // Another valid attachment
2897                attachment::Attachment {
2898                    id: Some("valid-attachment-2".to_string()),
2899                    url: Some("https://example.com/valid2".to_string()),
2900                    size: Some("150".to_string()),
2901                    size_name: Some("150 Bytes".to_string()),
2902                    file_name: Some(valid2),
2903                    key: None,
2904                },
2905            ]),
2906            fields: None,
2907            password_history: None,
2908            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2909            deleted_date: None,
2910            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2911            archived_date: None,
2912            data: None,
2913        };
2914
2915        let view: CipherView = key_store.decrypt(&cipher).unwrap();
2916
2917        // Should have 2 successful attachments
2918        assert!(view.attachments.is_some());
2919        let successes = view.attachments.as_ref().unwrap();
2920        assert_eq!(successes.len(), 2);
2921        assert_eq!(successes[0].id, Some("valid-attachment".to_string()));
2922        assert_eq!(successes[1].id, Some("valid-attachment-2".to_string()));
2923
2924        // Should have 1 failed attachment
2925        assert!(view.attachment_decryption_failures.is_some());
2926        let failures = view.attachment_decryption_failures.as_ref().unwrap();
2927        assert_eq!(failures.len(), 1);
2928        assert_eq!(failures[0].id, Some("corrupted-attachment".to_string()));
2929        assert_eq!(failures[0].file_name, None);
2930    }
2931
2932    #[test]
2933    fn test_decrypt_cipher_list_view_passport() {
2934        let key_store =
2935            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2936
2937        let cipher_view = CipherView {
2938            r#type: CipherType::Passport,
2939            passport: Some(passport::PassportView {
2940                given_name: Some("Jane".to_string()),
2941                surname: Some("Doe".to_string()),
2942                passport_number: Some("P12345678".to_string()),
2943                ..Default::default()
2944            }),
2945            login: None,
2946            ..generate_cipher()
2947        };
2948
2949        let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
2950        let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
2951
2952        assert_eq!(list_view.r#type, CipherListViewType::Passport);
2953        assert_eq!(list_view.subtitle, "Jane Doe");
2954        assert_eq!(
2955            list_view.copyable_fields,
2956            vec![
2957                CopyableCipherFields::PassportGivenName,
2958                CopyableCipherFields::PassportSurname,
2959                CopyableCipherFields::PassportPassportNumber,
2960            ]
2961        );
2962    }
2963
2964    #[test]
2965    fn test_decrypt_cipher_list_view_drivers_license() {
2966        let key_store =
2967            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
2968
2969        let cipher_view = CipherView {
2970            r#type: CipherType::DriversLicense,
2971            drivers_license: Some(drivers_license::DriversLicenseView {
2972                first_name: Some("John".to_string()),
2973                last_name: Some("Doe".to_string()),
2974                license_number: Some("DL-987654".to_string()),
2975                ..Default::default()
2976            }),
2977            login: None,
2978            ..generate_cipher()
2979        };
2980
2981        let cipher: Cipher = key_store.encrypt(cipher_view).unwrap();
2982        let list_view: CipherListView = key_store.decrypt(&cipher).unwrap();
2983
2984        assert_eq!(list_view.r#type, CipherListViewType::DriversLicense);
2985        assert_eq!(list_view.subtitle, "John Doe");
2986        assert_eq!(
2987            list_view.copyable_fields,
2988            vec![
2989                CopyableCipherFields::DriversLicenseFirstName,
2990                CopyableCipherFields::DriversLicenseLastName,
2991                CopyableCipherFields::DriversLicenseLicenseNumber,
2992            ]
2993        );
2994    }
2995
2996    #[test]
2997    fn test_cipher_view_encrypt_decrypt_passport() {
2998        let key_store =
2999            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
3000
3001        let passport = passport::PassportView {
3002            given_name: Some("Jane".to_string()),
3003            surname: Some("Doe".to_string()),
3004            date_of_birth: Some("1990-01-01".to_string()),
3005            sex: Some("F".to_string()),
3006            birth_place: Some("New York".to_string()),
3007            nationality: Some("American".to_string()),
3008            issuing_country: Some("US".to_string()),
3009            passport_number: Some("P12345678".to_string()),
3010            passport_type: Some("P".to_string()),
3011            national_identification_number: Some("123-45-6789".to_string()),
3012            issuing_authority: Some("US State Department".to_string()),
3013            issue_date: Some("2020-01-01".to_string()),
3014            expiration_date: Some("2030-01-01".to_string()),
3015        };
3016
3017        let cipher_view = CipherView {
3018            r#type: CipherType::Passport,
3019            passport: Some(passport.clone()),
3020            login: None,
3021            ..generate_cipher()
3022        };
3023
3024        let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3025        let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3026
3027        assert_eq!(decrypted.r#type, CipherType::Passport);
3028        assert_eq!(decrypted.passport, Some(passport));
3029        assert!(decrypted.login.is_none());
3030    }
3031
3032    #[test]
3033    fn test_cipher_view_encrypt_decrypt_drivers_license() {
3034        let key_store =
3035            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
3036
3037        let dl = drivers_license::DriversLicenseView {
3038            first_name: Some("John".to_string()),
3039            middle_name: Some("Michael".to_string()),
3040            last_name: Some("Doe".to_string()),
3041            date_of_birth: Some("1985-06-15".to_string()),
3042            license_number: Some("DL-987654".to_string()),
3043            issuing_country: Some("US".to_string()),
3044            issuing_state: Some("NY".to_string()),
3045            issue_date: Some("2020-01-01".to_string()),
3046            expiration_date: Some("2028-01-01".to_string()),
3047            issuing_authority: Some("NY DMV".to_string()),
3048            license_class: Some("D".to_string()),
3049        };
3050
3051        let cipher_view = CipherView {
3052            r#type: CipherType::DriversLicense,
3053            drivers_license: Some(dl.clone()),
3054            login: None,
3055            ..generate_cipher()
3056        };
3057
3058        let encrypted: Cipher = key_store.encrypt(cipher_view).unwrap();
3059        let decrypted: CipherView = key_store.decrypt(&encrypted).unwrap();
3060
3061        assert_eq!(decrypted.r#type, CipherType::DriversLicense);
3062        assert_eq!(decrypted.drivers_license, Some(dl));
3063        assert!(decrypted.login.is_none());
3064    }
3065
3066    #[test]
3067    fn test_mini_response_model_view_password_defaults_to_true() {
3068        use chrono::Utc;
3069
3070        // CipherMiniResponseModel does not include view_password from the API,
3071        // so when merge_with_cipher is called with None, it should default to true
3072        let mini_response = CipherMiniResponseModel {
3073            id: Some(TEST_UUID.parse().unwrap()),
3074            name: Some(TEST_CIPHER_NAME.to_string()),
3075            r#type: Some(bitwarden_api_api::models::CipherType::Login),
3076            creation_date: Some(Utc::now().to_rfc3339()),
3077            revision_date: Some(Utc::now().to_rfc3339()),
3078            ..Default::default()
3079        };
3080
3081        let cipher = mini_response.merge_with_cipher(None).unwrap();
3082        assert!(
3083            cipher.view_password,
3084            "view_password should default to true for CipherMiniResponseModel"
3085        );
3086
3087        // CipherMiniDetailsResponseModel should also default to true
3088        let mini_details_response = CipherMiniDetailsResponseModel {
3089            id: Some(TEST_UUID.parse().unwrap()),
3090            name: Some(TEST_CIPHER_NAME.to_string()),
3091            r#type: Some(bitwarden_api_api::models::CipherType::Login),
3092            creation_date: Some(Utc::now().to_rfc3339()),
3093            revision_date: Some(Utc::now().to_rfc3339()),
3094            ..Default::default()
3095        };
3096
3097        let cipher = mini_details_response.merge_with_cipher(None).unwrap();
3098        assert!(
3099            cipher.view_password,
3100            "view_password should default to true for CipherMiniDetailsResponseModel"
3101        );
3102    }
3103}