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