bitwarden_vault/cipher/
cipher.rs

1use bitwarden_api_api::{
2    apis::ciphers_api::{PutShareError, PutShareManyError},
3    models::{
4        CipherDetailsResponseModel, CipherRequestModel, CipherResponseModel,
5        CipherWithIdRequestModel,
6    },
7};
8use bitwarden_collections::collection::CollectionId;
9use bitwarden_core::{
10    MissingFieldError, OrganizationId, UserId,
11    key_management::{KeyIds, MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SymmetricKeyId},
12    require,
13};
14use bitwarden_crypto::{
15    CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
16    PrimitiveEncryptable,
17};
18use bitwarden_error::bitwarden_error;
19use bitwarden_state::repository::RepositoryError;
20use bitwarden_uuid::uuid_newtype;
21use chrono::{DateTime, SecondsFormat, Utc};
22use serde::{Deserialize, Serialize};
23use serde_repr::{Deserialize_repr, Serialize_repr};
24use thiserror::Error;
25#[cfg(feature = "wasm")]
26use tsify::Tsify;
27#[cfg(feature = "wasm")]
28use wasm_bindgen::prelude::wasm_bindgen;
29
30use super::{
31    attachment, card,
32    card::CardListView,
33    cipher_permissions::CipherPermissions,
34    field, identity,
35    local_data::{LocalData, LocalDataView},
36    login::LoginListView,
37    secure_note, ssh_key,
38};
39use crate::{
40    AttachmentView, DecryptError, EncryptError, Fido2CredentialFullView, Fido2CredentialView,
41    FieldView, FolderId, Login, LoginView, VaultParseError,
42    password_history::{self, MAX_PASSWORD_HISTORY_ENTRIES},
43};
44
45uuid_newtype!(pub CipherId);
46
47#[allow(missing_docs)]
48#[bitwarden_error(flat)]
49#[derive(Debug, Error)]
50pub enum CipherError {
51    #[error(transparent)]
52    MissingField(#[from] MissingFieldError),
53    #[error(transparent)]
54    Crypto(#[from] CryptoError),
55    #[error(transparent)]
56    Decrypt(#[from] DecryptError),
57    #[error(transparent)]
58    Encrypt(#[from] EncryptError),
59    #[error(
60        "This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation"
61    )]
62    AttachmentsWithoutKeys,
63    #[error("This cipher cannot be moved to the specified organization")]
64    OrganizationAlreadySet,
65    #[error(transparent)]
66    PutShare(#[from] bitwarden_api_api::apis::Error<PutShareError>),
67    #[error(transparent)]
68    PutShareMany(#[from] bitwarden_api_api::apis::Error<PutShareManyError>),
69    #[error(transparent)]
70    Repository(#[from] RepositoryError),
71    #[error(transparent)]
72    Chrono(#[from] chrono::ParseError),
73    #[error(transparent)]
74    SerdeJson(#[from] serde_json::Error),
75}
76
77/// Helper trait for operations on cipher types.
78pub(super) trait CipherKind {
79    /// Returns the item's subtitle.
80    fn decrypt_subtitle(
81        &self,
82        ctx: &mut KeyStoreContext<KeyIds>,
83        key: SymmetricKeyId,
84    ) -> Result<String, CryptoError>;
85
86    /// Returns a list of populated fields for the cipher.
87    fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
88}
89
90#[allow(missing_docs)]
91#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
92#[repr(u8)]
93#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
94#[cfg_attr(feature = "wasm", wasm_bindgen)]
95pub enum CipherType {
96    Login = 1,
97    SecureNote = 2,
98    Card = 3,
99    Identity = 4,
100    SshKey = 5,
101}
102
103#[allow(missing_docs)]
104#[derive(Clone, Copy, Default, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
105#[repr(u8)]
106#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
107#[cfg_attr(feature = "wasm", wasm_bindgen)]
108pub enum CipherRepromptType {
109    #[default]
110    None = 0,
111    Password = 1,
112}
113
114#[allow(missing_docs)]
115#[derive(Serialize, Deserialize, Debug, Clone)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
118#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
119pub struct EncryptionContext {
120    /// The Id of the user that encrypted the cipher. It should always represent a UserId, even for
121    /// Organization-owned ciphers
122    pub encrypted_for: UserId,
123    pub cipher: Cipher,
124}
125
126impl TryFrom<EncryptionContext> for CipherWithIdRequestModel {
127    type Error = CipherError;
128    fn try_from(
129        EncryptionContext {
130            cipher,
131            encrypted_for,
132        }: EncryptionContext,
133    ) -> Result<Self, Self::Error> {
134        Ok(Self {
135            id: require!(cipher.id).into(),
136            encrypted_for: Some(encrypted_for.into()),
137            r#type: Some(cipher.r#type.into()),
138            organization_id: cipher.organization_id.map(|o| o.to_string()),
139            folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
140            favorite: cipher.favorite.into(),
141            reprompt: Some(cipher.reprompt.into()),
142            key: cipher.key.map(|k| k.to_string()),
143            name: cipher.name.to_string(),
144            notes: cipher.notes.map(|n| n.to_string()),
145            fields: Some(
146                cipher
147                    .fields
148                    .into_iter()
149                    .flatten()
150                    .map(Into::into)
151                    .collect(),
152            ),
153            password_history: Some(
154                cipher
155                    .password_history
156                    .into_iter()
157                    .flatten()
158                    .map(Into::into)
159                    .collect(),
160            ),
161            attachments: None,
162            attachments2: Some(
163                cipher
164                    .attachments
165                    .into_iter()
166                    .flatten()
167                    .filter_map(|a| {
168                        a.id.map(|id| {
169                            (
170                                id,
171                                bitwarden_api_api::models::CipherAttachmentModel {
172                                    file_name: a.file_name.map(|n| n.to_string()),
173                                    key: a.key.map(|k| k.to_string()),
174                                },
175                            )
176                        })
177                    })
178                    .collect(),
179            ),
180            login: cipher.login.map(|l| Box::new(l.into())),
181            card: cipher.card.map(|c| Box::new(c.into())),
182            identity: cipher.identity.map(|i| Box::new(i.into())),
183            secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
184            ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
185            data: None, // TODO: Consume this instead of the individual fields above.
186            last_known_revision_date: Some(
187                cipher
188                    .revision_date
189                    .to_rfc3339_opts(SecondsFormat::Millis, true),
190            ),
191            archived_date: cipher
192                .archived_date
193                .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
194        })
195    }
196}
197
198impl From<EncryptionContext> for CipherRequestModel {
199    fn from(
200        EncryptionContext {
201            cipher,
202            encrypted_for,
203        }: EncryptionContext,
204    ) -> Self {
205        Self {
206            encrypted_for: Some(encrypted_for.into()),
207            r#type: Some(cipher.r#type.into()),
208            organization_id: cipher.organization_id.map(|o| o.to_string()),
209            folder_id: cipher.folder_id.as_ref().map(ToString::to_string),
210            favorite: cipher.favorite.into(),
211            reprompt: Some(cipher.reprompt.into()),
212            key: cipher.key.map(|k| k.to_string()),
213            name: cipher.name.to_string(),
214            notes: cipher.notes.map(|n| n.to_string()),
215            fields: Some(
216                cipher
217                    .fields
218                    .into_iter()
219                    .flatten()
220                    .map(Into::into)
221                    .collect(),
222            ),
223            password_history: Some(
224                cipher
225                    .password_history
226                    .into_iter()
227                    .flatten()
228                    .map(Into::into)
229                    .collect(),
230            ),
231            attachments: None,
232            attachments2: Some(
233                cipher
234                    .attachments
235                    .into_iter()
236                    .flatten()
237                    .filter_map(|a| {
238                        a.id.map(|id| {
239                            (
240                                id,
241                                bitwarden_api_api::models::CipherAttachmentModel {
242                                    file_name: a.file_name.map(|n| n.to_string()),
243                                    key: a.key.map(|k| k.to_string()),
244                                },
245                            )
246                        })
247                    })
248                    .collect(),
249            ),
250            login: cipher.login.map(|l| Box::new(l.into())),
251            card: cipher.card.map(|c| Box::new(c.into())),
252            identity: cipher.identity.map(|i| Box::new(i.into())),
253            secure_note: cipher.secure_note.map(|s| Box::new(s.into())),
254            ssh_key: cipher.ssh_key.map(|s| Box::new(s.into())),
255            data: None, // TODO: Consume this instead of the individual fields above.
256            last_known_revision_date: Some(
257                cipher
258                    .revision_date
259                    .to_rfc3339_opts(SecondsFormat::Millis, true),
260            ),
261            archived_date: cipher
262                .archived_date
263                .map(|d| d.to_rfc3339_opts(SecondsFormat::Millis, true)),
264        }
265    }
266}
267
268#[allow(missing_docs)]
269#[derive(Serialize, Deserialize, Debug, Clone)]
270#[serde(rename_all = "camelCase", deny_unknown_fields)]
271#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
272#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
273pub struct Cipher {
274    pub id: Option<CipherId>,
275    pub organization_id: Option<OrganizationId>,
276    pub folder_id: Option<FolderId>,
277    pub collection_ids: Vec<CollectionId>,
278    /// More recent ciphers uses individual encryption keys to encrypt the other fields of the
279    /// Cipher.
280    pub key: Option<EncString>,
281
282    pub name: EncString,
283    pub notes: Option<EncString>,
284
285    pub r#type: CipherType,
286    pub login: Option<Login>,
287    pub identity: Option<identity::Identity>,
288    pub card: Option<card::Card>,
289    pub secure_note: Option<secure_note::SecureNote>,
290    pub ssh_key: Option<ssh_key::SshKey>,
291
292    pub favorite: bool,
293    pub reprompt: CipherRepromptType,
294    pub organization_use_totp: bool,
295    pub edit: bool,
296    pub permissions: Option<CipherPermissions>,
297    pub view_password: bool,
298    pub local_data: Option<LocalData>,
299
300    pub attachments: Option<Vec<attachment::Attachment>>,
301    pub fields: Option<Vec<field::Field>>,
302    pub password_history: Option<Vec<password_history::PasswordHistory>>,
303
304    pub creation_date: DateTime<Utc>,
305    pub deleted_date: Option<DateTime<Utc>>,
306    pub revision_date: DateTime<Utc>,
307    pub archived_date: Option<DateTime<Utc>>,
308    pub data: Option<String>,
309}
310
311bitwarden_state::register_repository_item!(Cipher, "Cipher");
312
313#[allow(missing_docs)]
314#[derive(Serialize, Deserialize, Debug, Clone)]
315#[serde(rename_all = "camelCase", deny_unknown_fields)]
316#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
317#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
318pub struct CipherView {
319    pub id: Option<CipherId>,
320    pub organization_id: Option<OrganizationId>,
321    pub folder_id: Option<FolderId>,
322    pub collection_ids: Vec<CollectionId>,
323
324    /// Temporary, required to support re-encrypting existing items.
325    pub key: Option<EncString>,
326
327    pub name: String,
328    pub notes: Option<String>,
329
330    pub r#type: CipherType,
331    pub login: Option<LoginView>,
332    pub identity: Option<identity::IdentityView>,
333    pub card: Option<card::CardView>,
334    pub secure_note: Option<secure_note::SecureNoteView>,
335    pub ssh_key: Option<ssh_key::SshKeyView>,
336
337    pub favorite: bool,
338    pub reprompt: CipherRepromptType,
339    pub organization_use_totp: bool,
340    pub edit: bool,
341    pub permissions: Option<CipherPermissions>,
342    pub view_password: bool,
343    pub local_data: Option<LocalDataView>,
344
345    pub attachments: Option<Vec<attachment::AttachmentView>>,
346    pub fields: Option<Vec<field::FieldView>>,
347    pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
348
349    pub creation_date: DateTime<Utc>,
350    pub deleted_date: Option<DateTime<Utc>>,
351    pub revision_date: DateTime<Utc>,
352    pub archived_date: Option<DateTime<Utc>>,
353}
354
355#[allow(missing_docs)]
356#[derive(Serialize, Deserialize, Debug, PartialEq)]
357#[serde(rename_all = "camelCase", deny_unknown_fields)]
358#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
359#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
360pub enum CipherListViewType {
361    Login(LoginListView),
362    SecureNote,
363    Card(CardListView),
364    Identity,
365    SshKey,
366}
367
368/// Available fields on a cipher and can be copied from a the list view in the UI.
369#[derive(Serialize, Deserialize, Debug, PartialEq)]
370#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
371#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
372pub enum CopyableCipherFields {
373    LoginUsername,
374    LoginPassword,
375    LoginTotp,
376    CardNumber,
377    CardSecurityCode,
378    IdentityUsername,
379    IdentityEmail,
380    IdentityPhone,
381    IdentityAddress,
382    SshKey,
383    SecureNotes,
384}
385
386#[allow(missing_docs)]
387#[derive(Serialize, Deserialize, Debug, PartialEq)]
388#[serde(rename_all = "camelCase", deny_unknown_fields)]
389#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
390#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
391pub struct CipherListView {
392    pub id: Option<CipherId>,
393    pub organization_id: Option<OrganizationId>,
394    pub folder_id: Option<FolderId>,
395    pub collection_ids: Vec<CollectionId>,
396
397    /// Temporary, required to support calculating TOTP from CipherListView.
398    pub key: Option<EncString>,
399
400    pub name: String,
401    pub subtitle: String,
402
403    pub r#type: CipherListViewType,
404
405    pub favorite: bool,
406    pub reprompt: CipherRepromptType,
407    pub organization_use_totp: bool,
408    pub edit: bool,
409    pub permissions: Option<CipherPermissions>,
410
411    pub view_password: bool,
412
413    /// The number of attachments
414    pub attachments: u32,
415    /// Indicates if the cipher has old attachments that need to be re-uploaded
416    pub has_old_attachments: bool,
417
418    pub creation_date: DateTime<Utc>,
419    pub deleted_date: Option<DateTime<Utc>>,
420    pub revision_date: DateTime<Utc>,
421    pub archived_date: Option<DateTime<Utc>>,
422
423    /// Hints for the presentation layer for which fields can be copied.
424    pub copyable_fields: Vec<CopyableCipherFields>,
425
426    pub local_data: Option<LocalDataView>,
427}
428
429/// Represents the result of decrypting a list of ciphers.
430///
431/// This struct contains two vectors: `successes` and `failures`.
432/// `successes` contains the decrypted `CipherListView` objects,
433/// while `failures` contains the original `Cipher` objects that failed to decrypt.
434#[derive(Serialize, Deserialize, Debug)]
435#[serde(rename_all = "camelCase", deny_unknown_fields)]
436#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
437#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
438pub struct DecryptCipherListResult {
439    /// The decrypted `CipherListView` objects.
440    pub successes: Vec<CipherListView>,
441    /// The original `Cipher` objects that failed to decrypt.
442    pub failures: Vec<Cipher>,
443}
444
445impl CipherListView {
446    pub(crate) fn get_totp_key(
447        self,
448        ctx: &mut KeyStoreContext<KeyIds>,
449    ) -> Result<Option<String>, CryptoError> {
450        let key = self.key_identifier();
451        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
452
453        let totp = match self.r#type {
454            CipherListViewType::Login(LoginListView { totp, .. }) => {
455                totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
456            }
457            _ => None,
458        };
459
460        Ok(totp)
461    }
462}
463
464impl CompositeEncryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
465    fn encrypt_composite(
466        &self,
467        ctx: &mut KeyStoreContext<KeyIds>,
468        key: SymmetricKeyId,
469    ) -> Result<Cipher, CryptoError> {
470        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
471
472        let mut cipher_view = self.clone();
473        cipher_view.generate_checksums();
474
475        Ok(Cipher {
476            id: cipher_view.id,
477            organization_id: cipher_view.organization_id,
478            folder_id: cipher_view.folder_id,
479            collection_ids: cipher_view.collection_ids,
480            key: cipher_view.key,
481            name: cipher_view.name.encrypt(ctx, ciphers_key)?,
482            notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
483            r#type: cipher_view.r#type,
484            login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
485            identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
486            card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
487            secure_note: cipher_view
488                .secure_note
489                .encrypt_composite(ctx, ciphers_key)?,
490            ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
491            favorite: cipher_view.favorite,
492            reprompt: cipher_view.reprompt,
493            organization_use_totp: cipher_view.organization_use_totp,
494            edit: cipher_view.edit,
495            view_password: cipher_view.view_password,
496            local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
497            attachments: cipher_view
498                .attachments
499                .encrypt_composite(ctx, ciphers_key)?,
500            fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
501            password_history: cipher_view
502                .password_history
503                .encrypt_composite(ctx, ciphers_key)?,
504            creation_date: cipher_view.creation_date,
505            deleted_date: cipher_view.deleted_date,
506            revision_date: cipher_view.revision_date,
507            permissions: cipher_view.permissions,
508            archived_date: cipher_view.archived_date,
509            data: None, // TODO: Do we need to repopulate this on this on the cipher?
510        })
511    }
512}
513
514impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
515    fn decrypt(
516        &self,
517        ctx: &mut KeyStoreContext<KeyIds>,
518        key: SymmetricKeyId,
519    ) -> Result<CipherView, CryptoError> {
520        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
521
522        let mut cipher = CipherView {
523            id: self.id,
524            organization_id: self.organization_id,
525            folder_id: self.folder_id,
526            collection_ids: self.collection_ids.clone(),
527            key: self.key.clone(),
528            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
529            notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
530            r#type: self.r#type,
531            login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
532            identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
533            card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
534            secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
535            ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
536            favorite: self.favorite,
537            reprompt: self.reprompt,
538            organization_use_totp: self.organization_use_totp,
539            edit: self.edit,
540            permissions: self.permissions,
541            view_password: self.view_password,
542            local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
543            attachments: self.attachments.decrypt(ctx, ciphers_key).ok().flatten(),
544            fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
545            password_history: self
546                .password_history
547                .decrypt(ctx, ciphers_key)
548                .ok()
549                .flatten(),
550            creation_date: self.creation_date,
551            deleted_date: self.deleted_date,
552            revision_date: self.revision_date,
553            archived_date: self.archived_date,
554        };
555
556        // For compatibility we only remove URLs with invalid checksums if the cipher has a key
557        // or the user is on Crypto V2
558        if cipher.key.is_some()
559            || ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
560        {
561            cipher.remove_invalid_checksums();
562        }
563
564        Ok(cipher)
565    }
566}
567
568impl Cipher {
569    /// Decrypt the individual encryption key for this cipher into the provided [KeyStoreContext]
570    /// and return it's identifier. Note that some ciphers do not have individual encryption
571    /// keys, in which case this will return the provided key identifier instead
572    ///
573    /// # Arguments
574    ///
575    /// * `ctx` - The key store context where the cipher key will be decrypted, if it exists
576    /// * `key` - The key to use to decrypt the cipher key, this should be the user or organization
577    ///   key
578    /// * `ciphers_key` - The encrypted cipher key
579    pub(super) fn decrypt_cipher_key(
580        ctx: &mut KeyStoreContext<KeyIds>,
581        key: SymmetricKeyId,
582        ciphers_key: &Option<EncString>,
583    ) -> Result<SymmetricKeyId, CryptoError> {
584        match ciphers_key {
585            Some(ciphers_key) => ctx.unwrap_symmetric_key(key, ciphers_key),
586            None => Ok(key),
587        }
588    }
589
590    /// Temporary helper to return a [CipherKind] instance based on the cipher type.
591    fn get_kind(&self) -> Option<&dyn CipherKind> {
592        match self.r#type {
593            CipherType::Login => self.login.as_ref().map(|v| v as _),
594            CipherType::Card => self.card.as_ref().map(|v| v as _),
595            CipherType::Identity => self.identity.as_ref().map(|v| v as _),
596            CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
597            CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
598        }
599    }
600
601    /// Returns the decrypted subtitle for the cipher, if applicable.
602    fn decrypt_subtitle(
603        &self,
604        ctx: &mut KeyStoreContext<KeyIds>,
605        key: SymmetricKeyId,
606    ) -> Result<String, CryptoError> {
607        self.get_kind()
608            .map(|sub| sub.decrypt_subtitle(ctx, key))
609            .unwrap_or_else(|| Ok(String::new()))
610    }
611
612    /// Returns a list of copyable field names for this cipher,
613    /// based on the cipher type and populated properties.
614    fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
615        self.get_kind()
616            .map(|kind| kind.get_copyable_fields(Some(self)))
617            .unwrap_or_default()
618    }
619
620    /// This replaces the values provided by the API in the `login`, `secure_note`, `card`,
621    /// `identity`, and `ssh_key` fields, relying instead on client-side parsing of the
622    /// `data` field.
623    #[allow(unused)] // Will be used by future changes to support cipher versioning.
624    pub(crate) fn populate_cipher_types(&mut self) -> Result<(), VaultParseError> {
625        let data = self
626            .data
627            .as_ref()
628            .ok_or(VaultParseError::MissingField(MissingFieldError("data")))?;
629
630        match &self.r#type {
631            crate::CipherType::Login => self.login = serde_json::from_str(data)?,
632            crate::CipherType::SecureNote => self.secure_note = serde_json::from_str(data)?,
633            crate::CipherType::Card => self.card = serde_json::from_str(data)?,
634            crate::CipherType::Identity => self.identity = serde_json::from_str(data)?,
635            crate::CipherType::SshKey => self.ssh_key = serde_json::from_str(data)?,
636        }
637        Ok(())
638    }
639}
640impl CipherView {
641    #[allow(missing_docs)]
642    pub fn generate_cipher_key(
643        &mut self,
644        ctx: &mut KeyStoreContext<KeyIds>,
645        key: SymmetricKeyId,
646    ) -> Result<(), CryptoError> {
647        let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
648
649        let new_key = ctx.generate_symmetric_key();
650
651        self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
652        self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
653
654        self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
655        Ok(())
656    }
657
658    #[allow(missing_docs)]
659    pub fn generate_checksums(&mut self) {
660        if let Some(l) = self.login.as_mut() {
661            l.generate_checksums();
662        }
663    }
664
665    #[allow(missing_docs)]
666    pub fn remove_invalid_checksums(&mut self) {
667        if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
668            uris.retain(|u| u.is_checksum_valid());
669        }
670    }
671
672    fn reencrypt_attachment_keys(
673        &mut self,
674        ctx: &mut KeyStoreContext<KeyIds>,
675        old_key: SymmetricKeyId,
676        new_key: SymmetricKeyId,
677    ) -> Result<(), CryptoError> {
678        if let Some(attachments) = &mut self.attachments {
679            AttachmentView::reencrypt_keys(attachments, ctx, old_key, new_key)?;
680        }
681        Ok(())
682    }
683
684    #[allow(missing_docs)]
685    pub fn decrypt_fido2_credentials(
686        &self,
687        ctx: &mut KeyStoreContext<KeyIds>,
688    ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
689        let key = self.key_identifier();
690        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
691
692        Ok(self
693            .login
694            .as_ref()
695            .and_then(|l| l.fido2_credentials.as_ref())
696            .map(|f| f.decrypt(ctx, ciphers_key))
697            .transpose()?
698            .unwrap_or_default())
699    }
700
701    fn reencrypt_fido2_credentials(
702        &mut self,
703        ctx: &mut KeyStoreContext<KeyIds>,
704        old_key: SymmetricKeyId,
705        new_key: SymmetricKeyId,
706    ) -> Result<(), CryptoError> {
707        if let Some(login) = self.login.as_mut() {
708            login.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
709        }
710        Ok(())
711    }
712
713    /// Moves the cipher to an organization by re-encrypting the cipher keys with the organization
714    /// key and assigning the organization ID to the cipher.
715    ///
716    /// # Arguments
717    /// * `ctx` - The key store context where the cipher keys will be re-encrypted
718    /// * `organization_id` - The ID of the organization to move the cipher to
719    pub fn move_to_organization(
720        &mut self,
721        ctx: &mut KeyStoreContext<KeyIds>,
722        organization_id: OrganizationId,
723    ) -> Result<(), CipherError> {
724        let new_key = SymmetricKeyId::Organization(organization_id);
725
726        self.reencrypt_cipher_keys(ctx, new_key)?;
727        self.organization_id = Some(organization_id);
728
729        Ok(())
730    }
731
732    /// Re-encrypt the cipher key(s) using a new wrapping key.
733    ///
734    /// If the cipher has a cipher key, it will be re-encrypted with the new wrapping key.
735    /// Otherwise, the cipher will re-encrypt all attachment keys and FIDO2 credential keys
736    pub fn reencrypt_cipher_keys(
737        &mut self,
738        ctx: &mut KeyStoreContext<KeyIds>,
739        new_wrapping_key: SymmetricKeyId,
740    ) -> Result<(), CipherError> {
741        let old_key = self.key_identifier();
742
743        // If any attachment is missing a key we can't reencrypt the attachment keys
744        if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
745            return Err(CipherError::AttachmentsWithoutKeys);
746        }
747
748        // If the cipher has a key, reencrypt it with the new wrapping key
749        if self.key.is_some() {
750            // Decrypt the current cipher key using the existing wrapping key
751            let cipher_key = Cipher::decrypt_cipher_key(ctx, old_key, &self.key)?;
752
753            // Wrap the cipher key with the new wrapping key
754            self.key = Some(ctx.wrap_symmetric_key(new_wrapping_key, cipher_key)?);
755        } else {
756            // The cipher does not have a key, we must reencrypt all attachment keys and FIDO2
757            // credentials individually
758            self.reencrypt_attachment_keys(ctx, old_key, new_wrapping_key)?;
759            self.reencrypt_fido2_credentials(ctx, old_key, new_wrapping_key)?;
760        }
761
762        Ok(())
763    }
764
765    #[allow(missing_docs)]
766    pub fn set_new_fido2_credentials(
767        &mut self,
768        ctx: &mut KeyStoreContext<KeyIds>,
769        creds: Vec<Fido2CredentialFullView>,
770    ) -> Result<(), CipherError> {
771        let key = self.key_identifier();
772
773        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
774
775        require!(self.login.as_mut()).fido2_credentials =
776            Some(creds.encrypt_composite(ctx, ciphers_key)?);
777
778        Ok(())
779    }
780
781    #[allow(missing_docs)]
782    pub fn get_fido2_credentials(
783        &self,
784        ctx: &mut KeyStoreContext<KeyIds>,
785    ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
786        let key = self.key_identifier();
787
788        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
789
790        let login = require!(self.login.as_ref());
791        let creds = require!(login.fido2_credentials.as_ref());
792        let res = creds.decrypt(ctx, ciphers_key)?;
793        Ok(res)
794    }
795
796    #[allow(missing_docs)]
797    pub fn decrypt_fido2_private_key(
798        &self,
799        ctx: &mut KeyStoreContext<KeyIds>,
800    ) -> Result<String, CipherError> {
801        let fido2_credential = self.get_fido2_credentials(ctx)?;
802
803        Ok(fido2_credential[0].key_value.clone())
804    }
805
806    pub(crate) fn update_password_history(&mut self, original_cipher: &CipherView) {
807        let changes = self
808            .login
809            .as_mut()
810            .map_or(vec![], |login| {
811                login.detect_password_change(&original_cipher.login)
812            })
813            .into_iter()
814            .chain(self.fields.as_deref().map_or(vec![], |fields| {
815                FieldView::detect_hidden_field_changes(
816                    fields,
817                    original_cipher.fields.as_deref().unwrap_or(&[]),
818                )
819            }))
820            .rev()
821            .chain(original_cipher.password_history.iter().flatten().cloned())
822            .take(MAX_PASSWORD_HISTORY_ENTRIES)
823            .collect();
824        self.password_history = Some(changes)
825    }
826}
827
828impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
829    fn decrypt(
830        &self,
831        ctx: &mut KeyStoreContext<KeyIds>,
832        key: SymmetricKeyId,
833    ) -> Result<CipherListView, CryptoError> {
834        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
835
836        Ok(CipherListView {
837            id: self.id,
838            organization_id: self.organization_id,
839            folder_id: self.folder_id,
840            collection_ids: self.collection_ids.clone(),
841            key: self.key.clone(),
842            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
843            subtitle: self
844                .decrypt_subtitle(ctx, ciphers_key)
845                .ok()
846                .unwrap_or_default(),
847            r#type: match self.r#type {
848                CipherType::Login => {
849                    let login = self
850                        .login
851                        .as_ref()
852                        .ok_or(CryptoError::MissingField("login"))?;
853                    CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
854                }
855                CipherType::SecureNote => CipherListViewType::SecureNote,
856                CipherType::Card => {
857                    let card = self
858                        .card
859                        .as_ref()
860                        .ok_or(CryptoError::MissingField("card"))?;
861                    CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
862                }
863                CipherType::Identity => CipherListViewType::Identity,
864                CipherType::SshKey => CipherListViewType::SshKey,
865            },
866            favorite: self.favorite,
867            reprompt: self.reprompt,
868            organization_use_totp: self.organization_use_totp,
869            edit: self.edit,
870            permissions: self.permissions,
871            view_password: self.view_password,
872            attachments: self
873                .attachments
874                .as_ref()
875                .map(|a| a.len() as u32)
876                .unwrap_or(0),
877            has_old_attachments: self
878                .attachments
879                .as_ref()
880                .map(|a| a.iter().any(|att| att.key.is_none()))
881                .unwrap_or(false),
882            creation_date: self.creation_date,
883            deleted_date: self.deleted_date,
884            revision_date: self.revision_date,
885            copyable_fields: self.get_copyable_fields(),
886            local_data: self.local_data.decrypt(ctx, ciphers_key)?,
887            archived_date: self.archived_date,
888        })
889    }
890}
891
892impl IdentifyKey<SymmetricKeyId> for Cipher {
893    fn key_identifier(&self) -> SymmetricKeyId {
894        match self.organization_id {
895            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
896            None => SymmetricKeyId::User,
897        }
898    }
899}
900
901impl IdentifyKey<SymmetricKeyId> for CipherView {
902    fn key_identifier(&self) -> SymmetricKeyId {
903        match self.organization_id {
904            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
905            None => SymmetricKeyId::User,
906        }
907    }
908}
909
910impl IdentifyKey<SymmetricKeyId> for CipherListView {
911    fn key_identifier(&self) -> SymmetricKeyId {
912        match self.organization_id {
913            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
914            None => SymmetricKeyId::User,
915        }
916    }
917}
918
919impl TryFrom<CipherDetailsResponseModel> for Cipher {
920    type Error = VaultParseError;
921
922    fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
923        Ok(Self {
924            id: cipher.id.map(CipherId::new),
925            organization_id: cipher.organization_id.map(OrganizationId::new),
926            folder_id: cipher.folder_id.map(FolderId::new),
927            collection_ids: cipher
928                .collection_ids
929                .unwrap_or_default()
930                .into_iter()
931                .map(CollectionId::new)
932                .collect(),
933            name: require!(EncString::try_from_optional(cipher.name)?),
934            notes: EncString::try_from_optional(cipher.notes)?,
935            r#type: require!(cipher.r#type).into(),
936            login: cipher.login.map(|l| (*l).try_into()).transpose()?,
937            identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
938            card: cipher.card.map(|c| (*c).try_into()).transpose()?,
939            secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
940            ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
941            favorite: cipher.favorite.unwrap_or(false),
942            reprompt: cipher
943                .reprompt
944                .map(|r| r.into())
945                .unwrap_or(CipherRepromptType::None),
946            organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
947            edit: cipher.edit.unwrap_or(true),
948            permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
949            view_password: cipher.view_password.unwrap_or(true),
950            local_data: None, // Not sent from server
951            attachments: cipher
952                .attachments
953                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
954                .transpose()?,
955            fields: cipher
956                .fields
957                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
958                .transpose()?,
959            password_history: cipher
960                .password_history
961                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
962                .transpose()?,
963            creation_date: require!(cipher.creation_date).parse()?,
964            deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
965            revision_date: require!(cipher.revision_date).parse()?,
966            key: EncString::try_from_optional(cipher.key)?,
967            archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
968            data: cipher.data,
969        })
970    }
971}
972
973impl From<bitwarden_api_api::models::CipherType> for CipherType {
974    fn from(t: bitwarden_api_api::models::CipherType) -> Self {
975        match t {
976            bitwarden_api_api::models::CipherType::Login => CipherType::Login,
977            bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
978            bitwarden_api_api::models::CipherType::Card => CipherType::Card,
979            bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
980            bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
981        }
982    }
983}
984
985impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
986    fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self {
987        match t {
988            bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
989            bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
990        }
991    }
992}
993
994impl From<CipherType> for bitwarden_api_api::models::CipherType {
995    fn from(t: CipherType) -> Self {
996        match t {
997            CipherType::Login => bitwarden_api_api::models::CipherType::Login,
998            CipherType::SecureNote => bitwarden_api_api::models::CipherType::SecureNote,
999            CipherType::Card => bitwarden_api_api::models::CipherType::Card,
1000            CipherType::Identity => bitwarden_api_api::models::CipherType::Identity,
1001            CipherType::SshKey => bitwarden_api_api::models::CipherType::SSHKey,
1002        }
1003    }
1004}
1005
1006impl From<CipherRepromptType> for bitwarden_api_api::models::CipherRepromptType {
1007    fn from(t: CipherRepromptType) -> Self {
1008        match t {
1009            CipherRepromptType::None => bitwarden_api_api::models::CipherRepromptType::None,
1010            CipherRepromptType::Password => bitwarden_api_api::models::CipherRepromptType::Password,
1011        }
1012    }
1013}
1014
1015impl TryFrom<CipherResponseModel> for Cipher {
1016    type Error = VaultParseError;
1017
1018    fn try_from(cipher: CipherResponseModel) -> Result<Self, Self::Error> {
1019        Ok(Self {
1020            id: cipher.id.map(CipherId::new),
1021            organization_id: cipher.organization_id.map(OrganizationId::new),
1022            folder_id: cipher.folder_id.map(FolderId::new),
1023            collection_ids: vec![], // CipherResponseModel doesn't include collection_ids
1024            name: require!(cipher.name).parse()?,
1025            notes: EncString::try_from_optional(cipher.notes)?,
1026            r#type: require!(cipher.r#type).into(),
1027            login: cipher.login.map(|l| (*l).try_into()).transpose()?,
1028            identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
1029            card: cipher.card.map(|c| (*c).try_into()).transpose()?,
1030            secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
1031            ssh_key: cipher.ssh_key.map(|s| (*s).try_into()).transpose()?,
1032            favorite: cipher.favorite.unwrap_or(false),
1033            reprompt: cipher
1034                .reprompt
1035                .map(|r| r.into())
1036                .unwrap_or(CipherRepromptType::None),
1037            organization_use_totp: cipher.organization_use_totp.unwrap_or(false),
1038            edit: cipher.edit.unwrap_or(false),
1039            permissions: cipher.permissions.map(|p| (*p).try_into()).transpose()?,
1040            view_password: cipher.view_password.unwrap_or(true),
1041            local_data: None, // Not sent from server
1042            attachments: cipher
1043                .attachments
1044                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
1045                .transpose()?,
1046            fields: cipher
1047                .fields
1048                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
1049                .transpose()?,
1050            password_history: cipher
1051                .password_history
1052                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
1053                .transpose()?,
1054            creation_date: require!(cipher.creation_date).parse()?,
1055            deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
1056            revision_date: require!(cipher.revision_date).parse()?,
1057            key: EncString::try_from_optional(cipher.key)?,
1058            archived_date: cipher.archived_date.map(|d| d.parse()).transpose()?,
1059            data: cipher.data,
1060        })
1061    }
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066
1067    use attachment::AttachmentView;
1068    use bitwarden_core::key_management::{
1069        create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
1070    };
1071    use bitwarden_crypto::SymmetricCryptoKey;
1072
1073    use super::*;
1074    use crate::{Fido2Credential, PasswordHistoryView, login::Fido2CredentialListView};
1075
1076    // Test constants for encrypted strings
1077    const TEST_ENC_STRING_1: &str = "2.xzDCDWqRBpHm42EilUvyVw==|nIrWV3l/EeTbWTnAznrK0Q==|sUj8ol2OTgvvTvD86a9i9XUP58hmtCEBqhck7xT5YNk=";
1078    const TEST_ENC_STRING_2: &str = "2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=";
1079    const TEST_ENC_STRING_3: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1080    const TEST_ENC_STRING_4: &str = "2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=";
1081    const TEST_ENC_STRING_5: &str = "2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=";
1082    const TEST_CIPHER_NAME: &str = "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=";
1083    const TEST_UUID: &str = "fd411a1a-fec8-4070-985d-0e6560860e69";
1084
1085    fn generate_cipher() -> CipherView {
1086        let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
1087        CipherView {
1088            r#type: CipherType::Login,
1089            login: Some(LoginView {
1090                username: Some("test_username".to_string()),
1091                password: Some("test_password".to_string()),
1092                password_revision_date: None,
1093                uris: None,
1094                totp: None,
1095                autofill_on_page_load: None,
1096                fido2_credentials: None,
1097            }),
1098            id: Some(test_id),
1099            organization_id: None,
1100            folder_id: None,
1101            collection_ids: vec![],
1102            key: None,
1103            name: "My test login".to_string(),
1104            notes: None,
1105            identity: None,
1106            card: None,
1107            secure_note: None,
1108            ssh_key: None,
1109            favorite: false,
1110            reprompt: CipherRepromptType::None,
1111            organization_use_totp: true,
1112            edit: true,
1113            permissions: None,
1114            view_password: true,
1115            local_data: None,
1116            attachments: None,
1117            fields: None,
1118            password_history: None,
1119            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1120            deleted_date: None,
1121            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1122            archived_date: None,
1123        }
1124    }
1125
1126    fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
1127        Fido2Credential {
1128            credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
1129            key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
1130            key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
1131            key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
1132            key_value: "123".to_string().encrypt(ctx, key).unwrap(),
1133            rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
1134            user_handle: None,
1135            user_name: None,
1136            counter: "123".to_string().encrypt(ctx, key).unwrap(),
1137            rp_name: None,
1138            user_display_name: None,
1139            discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
1140            creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
1141        }
1142    }
1143
1144    #[test]
1145    fn test_decrypt_cipher_list_view() {
1146        let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
1147        let key_store = create_test_crypto_with_user_key(key);
1148
1149        let cipher = Cipher {
1150            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1151            organization_id: None,
1152            folder_id: None,
1153            collection_ids: vec![],
1154            key: None,
1155            name: TEST_CIPHER_NAME.parse().unwrap(),
1156            notes: None,
1157            r#type: CipherType::Login,
1158            login: Some(Login {
1159                username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
1160                password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
1161                password_revision_date: None,
1162                uris: None,
1163                totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
1164                autofill_on_page_load: None,
1165                fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
1166            }),
1167            identity: None,
1168            card: None,
1169            secure_note: None,
1170            ssh_key: None,
1171            favorite: false,
1172            reprompt: CipherRepromptType::None,
1173            organization_use_totp: false,
1174            edit: true,
1175            permissions: Some(CipherPermissions {
1176                delete: false,
1177                restore: false
1178            }),
1179            view_password: true,
1180            local_data: None,
1181            attachments: None,
1182            fields: None,
1183            password_history: None,
1184            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1185            deleted_date: None,
1186            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1187            archived_date: None,
1188            data: None,
1189        };
1190
1191        let view: CipherListView = key_store.decrypt(&cipher).unwrap();
1192
1193        assert_eq!(
1194            view,
1195            CipherListView {
1196                id: cipher.id,
1197                organization_id: cipher.organization_id,
1198                folder_id: cipher.folder_id,
1199                collection_ids: cipher.collection_ids,
1200                key: cipher.key,
1201                name: "My test login".to_string(),
1202                subtitle: "test_username".to_string(),
1203                r#type: CipherListViewType::Login(LoginListView {
1204                    fido2_credentials: Some(vec![Fido2CredentialListView {
1205                        credential_id: "123".to_string(),
1206                        rp_id: "123".to_string(),
1207                        user_handle: None,
1208                        user_name: None,
1209                        user_display_name: None,
1210                        counter: "123".to_string(),
1211                    }]),
1212                    has_fido2: true,
1213                    username: Some("test_username".to_string()),
1214                    totp: cipher.login.as_ref().unwrap().totp.clone(),
1215                    uris: None,
1216                }),
1217                favorite: cipher.favorite,
1218                reprompt: cipher.reprompt,
1219                organization_use_totp: cipher.organization_use_totp,
1220                edit: cipher.edit,
1221                permissions: cipher.permissions,
1222                view_password: cipher.view_password,
1223                attachments: 0,
1224                has_old_attachments: false,
1225                creation_date: cipher.creation_date,
1226                deleted_date: cipher.deleted_date,
1227                revision_date: cipher.revision_date,
1228                copyable_fields: vec![
1229                    CopyableCipherFields::LoginUsername,
1230                    CopyableCipherFields::LoginPassword,
1231                    CopyableCipherFields::LoginTotp
1232                ],
1233                local_data: None,
1234                archived_date: cipher.archived_date,
1235            }
1236        )
1237    }
1238
1239    #[test]
1240    fn test_generate_cipher_key() {
1241        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1242        let key_store = create_test_crypto_with_user_key(key);
1243
1244        let original_cipher = generate_cipher();
1245
1246        // Check that the cipher gets encrypted correctly without it's own key
1247        let cipher = generate_cipher();
1248        let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
1249        let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
1250        assert!(no_key_cipher_dec.key.is_none());
1251        assert_eq!(no_key_cipher_dec.name, original_cipher.name);
1252
1253        let mut cipher = generate_cipher();
1254        cipher
1255            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1256            .unwrap();
1257
1258        // Check that the cipher gets encrypted correctly when it's assigned it's own key
1259        let key_cipher_enc = key_store.encrypt(cipher).unwrap();
1260        let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
1261        assert!(key_cipher_dec.key.is_some());
1262        assert_eq!(key_cipher_dec.name, original_cipher.name);
1263    }
1264
1265    #[test]
1266    fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
1267        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1268        let key_store = create_test_crypto_with_user_key(key);
1269
1270        let mut original_cipher = generate_cipher();
1271        {
1272            let mut ctx = key_store.context();
1273            let cipher_key = ctx.generate_symmetric_key();
1274
1275            original_cipher.key = Some(
1276                ctx.wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1277                    .unwrap(),
1278            );
1279        }
1280
1281        original_cipher
1282            .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
1283            .unwrap();
1284
1285        // Make sure that the cipher key is decryptable
1286        let wrapped_key = original_cipher.key.unwrap();
1287        let mut ctx = key_store.context();
1288        ctx.unwrap_symmetric_key(SymmetricKeyId::User, &wrapped_key)
1289            .unwrap();
1290    }
1291
1292    #[test]
1293    fn test_generate_cipher_key_ignores_attachments_without_key() {
1294        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1295        let key_store = create_test_crypto_with_user_key(key);
1296
1297        let mut cipher = generate_cipher();
1298        let attachment = AttachmentView {
1299            id: None,
1300            url: None,
1301            size: None,
1302            size_name: None,
1303            file_name: Some("Attachment test name".into()),
1304            key: None,
1305            #[cfg(feature = "wasm")]
1306            decrypted_key: None,
1307        };
1308        cipher.attachments = Some(vec![attachment]);
1309
1310        cipher
1311            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1312            .unwrap();
1313        assert!(cipher.attachments.unwrap()[0].key.is_none());
1314    }
1315
1316    #[test]
1317    fn test_reencrypt_cipher_key() {
1318        let old_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1319        let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1320        let key_store = create_test_crypto_with_user_key(old_key);
1321        let mut ctx = key_store.context_mut();
1322
1323        let mut cipher = generate_cipher();
1324        cipher
1325            .generate_cipher_key(&mut ctx, cipher.key_identifier())
1326            .unwrap();
1327
1328        // Re-encrypt the cipher key with a new wrapping key
1329        let new_key_id = ctx.add_local_symmetric_key(new_key);
1330
1331        cipher.reencrypt_cipher_keys(&mut ctx, new_key_id).unwrap();
1332
1333        // Check that the cipher key can be unwrapped with the new key
1334        assert!(cipher.key.is_some());
1335        assert!(
1336            ctx.unwrap_symmetric_key(new_key_id, &cipher.key.unwrap())
1337                .is_ok()
1338        );
1339    }
1340
1341    #[test]
1342    fn test_reencrypt_cipher_key_ignores_missing_key() {
1343        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1344        let key_store = create_test_crypto_with_user_key(key);
1345        let mut ctx = key_store.context_mut();
1346        let mut cipher = generate_cipher();
1347
1348        // The cipher does not have a key, so re-encryption should not add one
1349        let new_cipher_key = ctx.generate_symmetric_key();
1350        cipher
1351            .reencrypt_cipher_keys(&mut ctx, new_cipher_key)
1352            .unwrap();
1353
1354        // Check that the cipher key is still None
1355        assert!(cipher.key.is_none());
1356    }
1357
1358    #[test]
1359    fn test_move_user_cipher_to_org() {
1360        let org = OrganizationId::new_v4();
1361        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1362        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1363        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1364
1365        // Create a cipher with a user key
1366        let mut cipher = generate_cipher();
1367        cipher
1368            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1369            .unwrap();
1370
1371        cipher
1372            .move_to_organization(&mut key_store.context(), org)
1373            .unwrap();
1374        assert_eq!(cipher.organization_id, Some(org));
1375
1376        // Check that the cipher can be encrypted/decrypted with the new org key
1377        let cipher_enc = key_store.encrypt(cipher).unwrap();
1378        let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
1379
1380        assert_eq!(cipher_dec.name, "My test login");
1381    }
1382
1383    #[test]
1384    fn test_move_user_cipher_to_org_manually() {
1385        let org = OrganizationId::new_v4();
1386        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1387        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1388        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1389
1390        // Create a cipher with a user key
1391        let mut cipher = generate_cipher();
1392        cipher
1393            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1394            .unwrap();
1395
1396        cipher.organization_id = Some(org);
1397
1398        // Check that the cipher can not be encrypted, as the
1399        // cipher key is tied to the user key and not the org key
1400        assert!(key_store.encrypt(cipher).is_err());
1401    }
1402
1403    #[test]
1404    fn test_move_user_cipher_with_attachment_without_key_to_org() {
1405        let org = OrganizationId::new_v4();
1406        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1407        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1408        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1409
1410        let mut cipher = generate_cipher();
1411        let attachment = AttachmentView {
1412            id: None,
1413            url: None,
1414            size: None,
1415            size_name: None,
1416            file_name: Some("Attachment test name".into()),
1417            key: None,
1418            #[cfg(feature = "wasm")]
1419            decrypted_key: None,
1420        };
1421        cipher.attachments = Some(vec![attachment]);
1422
1423        // Neither cipher nor attachment have keys, so the cipher can't be moved
1424        assert!(
1425            cipher
1426                .move_to_organization(&mut key_store.context(), org)
1427                .is_err()
1428        );
1429    }
1430
1431    #[test]
1432    fn test_move_user_cipher_with_attachment_with_key_to_org() {
1433        let org = OrganizationId::new_v4();
1434        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1435        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1436        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1437        let org_key = SymmetricKeyId::Organization(org);
1438
1439        // Attachment has a key that is encrypted with the user key, as the cipher has no key itself
1440        let (attachment_key_enc, attachment_key_val) = {
1441            let mut ctx = key_store.context();
1442            let attachment_key = ctx.generate_symmetric_key();
1443            let attachment_key_enc = ctx
1444                .wrap_symmetric_key(SymmetricKeyId::User, attachment_key)
1445                .unwrap();
1446            #[allow(deprecated)]
1447            let attachment_key_val = ctx
1448                .dangerous_get_symmetric_key(attachment_key)
1449                .unwrap()
1450                .clone();
1451
1452            (attachment_key_enc, attachment_key_val)
1453        };
1454
1455        let mut cipher = generate_cipher();
1456        let attachment = AttachmentView {
1457            id: None,
1458            url: None,
1459            size: None,
1460            size_name: None,
1461            file_name: Some("Attachment test name".into()),
1462            key: Some(attachment_key_enc),
1463            #[cfg(feature = "wasm")]
1464            decrypted_key: None,
1465        };
1466        cipher.attachments = Some(vec![attachment]);
1467        let cred = generate_fido2(&mut key_store.context(), SymmetricKeyId::User);
1468        cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
1469
1470        cipher
1471            .move_to_organization(&mut key_store.context(), org)
1472            .unwrap();
1473
1474        assert!(cipher.key.is_none());
1475
1476        // Check that the attachment key has been re-encrypted with the org key,
1477        // and the value matches with the original attachment key
1478        let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
1479        let mut ctx = key_store.context();
1480        let new_attachment_key_id = ctx
1481            .unwrap_symmetric_key(org_key, &new_attachment_key)
1482            .unwrap();
1483        #[allow(deprecated)]
1484        let new_attachment_key_dec = ctx
1485            .dangerous_get_symmetric_key(new_attachment_key_id)
1486            .unwrap();
1487
1488        assert_eq!(*new_attachment_key_dec, attachment_key_val);
1489
1490        let cred2: Fido2CredentialFullView = cipher
1491            .login
1492            .unwrap()
1493            .fido2_credentials
1494            .unwrap()
1495            .first()
1496            .unwrap()
1497            .decrypt(&mut key_store.context(), org_key)
1498            .unwrap();
1499
1500        assert_eq!(cred2.credential_id, "123");
1501    }
1502
1503    #[test]
1504    fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
1505        let org = OrganizationId::new_v4();
1506        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1507        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1508        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1509        let org_key = SymmetricKeyId::Organization(org);
1510
1511        let mut ctx = key_store.context();
1512
1513        let cipher_key = ctx.generate_symmetric_key();
1514        let cipher_key_enc = ctx
1515            .wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1516            .unwrap();
1517
1518        // Attachment has a key that is encrypted with the cipher key
1519        let attachment_key = ctx.generate_symmetric_key();
1520        let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
1521
1522        let mut cipher = generate_cipher();
1523        cipher.key = Some(cipher_key_enc);
1524
1525        let attachment = AttachmentView {
1526            id: None,
1527            url: None,
1528            size: None,
1529            size_name: None,
1530            file_name: Some("Attachment test name".into()),
1531            key: Some(attachment_key_enc.clone()),
1532            #[cfg(feature = "wasm")]
1533            decrypted_key: None,
1534        };
1535        cipher.attachments = Some(vec![attachment]);
1536
1537        let cred = generate_fido2(&mut ctx, cipher_key);
1538        cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
1539
1540        cipher.move_to_organization(&mut ctx, org).unwrap();
1541
1542        // Check that the cipher key has been re-encrypted with the org key,
1543        let wrapped_new_cipher_key = cipher.key.clone().unwrap();
1544        let new_cipher_key_dec = ctx
1545            .unwrap_symmetric_key(org_key, &wrapped_new_cipher_key)
1546            .unwrap();
1547        #[allow(deprecated)]
1548        let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
1549        #[allow(deprecated)]
1550        let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
1551
1552        assert_eq!(new_cipher_key_dec, cipher_key_val);
1553
1554        // Check that the attachment key hasn't changed
1555        assert_eq!(
1556            cipher.attachments.unwrap()[0]
1557                .key
1558                .as_ref()
1559                .unwrap()
1560                .to_string(),
1561            attachment_key_enc.to_string()
1562        );
1563
1564        let cred2: Fido2Credential = cipher
1565            .login
1566            .unwrap()
1567            .fido2_credentials
1568            .unwrap()
1569            .first()
1570            .unwrap()
1571            .clone();
1572
1573        assert_eq!(
1574            cred2.credential_id.to_string(),
1575            cred.credential_id.to_string()
1576        );
1577    }
1578
1579    #[test]
1580    fn test_decrypt_fido2_private_key() {
1581        let key_store =
1582            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1583        let mut ctx = key_store.context();
1584
1585        let mut cipher_view = generate_cipher();
1586        cipher_view
1587            .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1588            .unwrap();
1589
1590        let key_id = cipher_view.key_identifier();
1591        let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1592
1593        let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1594
1595        cipher_view.login.as_mut().unwrap().fido2_credentials =
1596            Some(vec![fido2_credential.clone()]);
1597
1598        let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1599        assert_eq!(decrypted_key_value, "123");
1600    }
1601
1602    #[test]
1603    fn test_password_history_on_password_change() {
1604        use chrono::Utc;
1605
1606        let original_cipher = generate_cipher();
1607        let mut new_cipher = generate_cipher();
1608
1609        // Change password
1610        if let Some(ref mut login) = new_cipher.login {
1611            login.password = Some("new_password123".to_string());
1612        }
1613
1614        let start = Utc::now();
1615        new_cipher.update_password_history(&original_cipher);
1616        let end = Utc::now();
1617
1618        assert!(new_cipher.password_history.is_some());
1619        let history = new_cipher.password_history.unwrap();
1620        assert_eq!(history.len(), 1);
1621        assert_eq!(history[0].password, "test_password");
1622        assert!(
1623            history[0].last_used_date >= start && history[0].last_used_date <= end,
1624            "last_used_date was not set properly"
1625        );
1626    }
1627
1628    #[test]
1629    fn test_password_history_on_unchanged_password() {
1630        let original_cipher = generate_cipher();
1631        let mut new_cipher = generate_cipher();
1632
1633        new_cipher.update_password_history(&original_cipher);
1634
1635        // Password history should be empty since password didn't change
1636        assert!(
1637            new_cipher.password_history.is_none()
1638                || new_cipher.password_history.as_ref().unwrap().is_empty()
1639        );
1640    }
1641
1642    #[test]
1643    fn test_password_history_is_preserved() {
1644        use chrono::TimeZone;
1645
1646        let mut original_cipher = generate_cipher();
1647        original_cipher.password_history = Some(
1648            (0..4)
1649                .map(|i| PasswordHistoryView {
1650                    password: format!("old_password_{}", i),
1651                    last_used_date: chrono::Utc
1652                        .with_ymd_and_hms(2025, i + 1, i + 1, i, i, i)
1653                        .unwrap(),
1654                })
1655                .collect(),
1656        );
1657
1658        let mut new_cipher = generate_cipher();
1659
1660        new_cipher.update_password_history(&original_cipher);
1661
1662        assert!(new_cipher.password_history.is_some());
1663        let history = new_cipher.password_history.unwrap();
1664        assert_eq!(history.len(), 4);
1665
1666        assert_eq!(history[0].password, "old_password_0");
1667        assert_eq!(
1668            history[0].last_used_date,
1669            chrono::Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap()
1670        );
1671        assert_eq!(history[1].password, "old_password_1");
1672        assert_eq!(
1673            history[1].last_used_date,
1674            chrono::Utc.with_ymd_and_hms(2025, 2, 2, 1, 1, 1).unwrap()
1675        );
1676        assert_eq!(history[2].password, "old_password_2");
1677        assert_eq!(
1678            history[2].last_used_date,
1679            chrono::Utc.with_ymd_and_hms(2025, 3, 3, 2, 2, 2).unwrap()
1680        );
1681        assert_eq!(history[3].password, "old_password_3");
1682        assert_eq!(
1683            history[3].last_used_date,
1684            chrono::Utc.with_ymd_and_hms(2025, 4, 4, 3, 3, 3).unwrap()
1685        );
1686    }
1687
1688    #[test]
1689    fn test_populate_cipher_types_login_with_valid_data() {
1690        let mut cipher = Cipher {
1691            id: Some(TEST_UUID.parse().unwrap()),
1692            organization_id: None,
1693            folder_id: None,
1694            collection_ids: vec![],
1695            key: None,
1696            name: TEST_CIPHER_NAME.parse().unwrap(),
1697            notes: None,
1698            r#type: CipherType::Login,
1699            login: None,
1700            identity: None,
1701            card: None,
1702            secure_note: None,
1703            ssh_key: None,
1704            favorite: false,
1705            reprompt: CipherRepromptType::None,
1706            organization_use_totp: false,
1707            edit: true,
1708            view_password: true,
1709            permissions: None,
1710            local_data: None,
1711            attachments: None,
1712            fields: None,
1713            password_history: None,
1714            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1715            deleted_date: None,
1716            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1717            archived_date: None,
1718            data: Some(format!(
1719                r#"{{"version": 2, "username": "{}", "password": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1720                TEST_ENC_STRING_1, TEST_ENC_STRING_2
1721            )),
1722        };
1723
1724        cipher
1725            .populate_cipher_types()
1726            .expect("populate_cipher_types failed");
1727
1728        assert!(cipher.login.is_some());
1729        let login = cipher.login.unwrap();
1730        assert_eq!(login.username.unwrap().to_string(), TEST_ENC_STRING_1);
1731        assert_eq!(login.password.unwrap().to_string(), TEST_ENC_STRING_2);
1732    }
1733
1734    #[test]
1735    fn test_populate_cipher_types_secure_note() {
1736        let mut cipher = Cipher {
1737            id: Some(TEST_UUID.parse().unwrap()),
1738            organization_id: None,
1739            folder_id: None,
1740            collection_ids: vec![],
1741            key: None,
1742            name: TEST_CIPHER_NAME.parse().unwrap(),
1743            notes: None,
1744            r#type: CipherType::SecureNote,
1745            login: None,
1746            identity: None,
1747            card: None,
1748            secure_note: None,
1749            ssh_key: None,
1750            favorite: false,
1751            reprompt: CipherRepromptType::None,
1752            organization_use_totp: false,
1753            edit: true,
1754            view_password: true,
1755            permissions: None,
1756            local_data: None,
1757            attachments: None,
1758            fields: None,
1759            password_history: None,
1760            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1761            deleted_date: None,
1762            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1763            archived_date: None,
1764            data: Some(r#"{"type": 0, "organizationUseTotp": false, "favorite": false, "deletedDate": null}"#.to_string()),
1765        };
1766
1767        cipher
1768            .populate_cipher_types()
1769            .expect("populate_cipher_types failed");
1770
1771        assert!(cipher.secure_note.is_some());
1772    }
1773
1774    #[test]
1775    fn test_populate_cipher_types_card() {
1776        let mut cipher = Cipher {
1777            id: Some(TEST_UUID.parse().unwrap()),
1778            organization_id: None,
1779            folder_id: None,
1780            collection_ids: vec![],
1781            key: None,
1782            name: TEST_CIPHER_NAME.parse().unwrap(),
1783            notes: None,
1784            r#type: CipherType::Card,
1785            login: None,
1786            identity: None,
1787            card: None,
1788            secure_note: None,
1789            ssh_key: None,
1790            favorite: false,
1791            reprompt: CipherRepromptType::None,
1792            organization_use_totp: false,
1793            edit: true,
1794            view_password: true,
1795            permissions: None,
1796            local_data: None,
1797            attachments: None,
1798            fields: None,
1799            password_history: None,
1800            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1801            deleted_date: None,
1802            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1803            archived_date: None,
1804            data: Some(format!(
1805                r#"{{"cardholderName": "{}", "number": "{}", "expMonth": "{}", "expYear": "{}", "code": "{}", "brand": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
1806                TEST_ENC_STRING_1,
1807                TEST_ENC_STRING_2,
1808                TEST_ENC_STRING_3,
1809                TEST_ENC_STRING_4,
1810                TEST_ENC_STRING_5,
1811                TEST_ENC_STRING_1
1812            )),
1813        };
1814
1815        cipher
1816            .populate_cipher_types()
1817            .expect("populate_cipher_types failed");
1818
1819        assert!(cipher.card.is_some());
1820        let card = cipher.card.unwrap();
1821        assert_eq!(
1822            card.cardholder_name.as_ref().unwrap().to_string(),
1823            TEST_ENC_STRING_1
1824        );
1825        assert_eq!(card.number.as_ref().unwrap().to_string(), TEST_ENC_STRING_2);
1826        assert_eq!(
1827            card.exp_month.as_ref().unwrap().to_string(),
1828            TEST_ENC_STRING_3
1829        );
1830        assert_eq!(
1831            card.exp_year.as_ref().unwrap().to_string(),
1832            TEST_ENC_STRING_4
1833        );
1834        assert_eq!(card.code.as_ref().unwrap().to_string(), TEST_ENC_STRING_5);
1835        assert_eq!(card.brand.as_ref().unwrap().to_string(), TEST_ENC_STRING_1);
1836    }
1837
1838    #[test]
1839    fn test_populate_cipher_types_identity() {
1840        let mut cipher = Cipher {
1841            id: Some(TEST_UUID.parse().unwrap()),
1842            organization_id: None,
1843            folder_id: None,
1844            collection_ids: vec![],
1845            key: None,
1846            name: TEST_CIPHER_NAME.parse().unwrap(),
1847            notes: None,
1848            r#type: CipherType::Identity,
1849            login: None,
1850            identity: None,
1851            card: None,
1852            secure_note: None,
1853            ssh_key: None,
1854            favorite: false,
1855            reprompt: CipherRepromptType::None,
1856            organization_use_totp: false,
1857            edit: true,
1858            view_password: true,
1859            permissions: None,
1860            local_data: None,
1861            attachments: None,
1862            fields: None,
1863            password_history: None,
1864            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1865            deleted_date: None,
1866            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
1867            archived_date: None,
1868            data: Some(format!(
1869                r#"{{"firstName": "{}", "lastName": "{}", "email": "{}", "phone": "{}", "company": "{}", "address1": "{}", "city": "{}", "state": "{}", "postalCode": "{}", "country": "{}", "organizationUseTotp": false, "favorite": true, "deletedDate": null}}"#,
1870                TEST_ENC_STRING_1,
1871                TEST_ENC_STRING_2,
1872                TEST_ENC_STRING_3,
1873                TEST_ENC_STRING_4,
1874                TEST_ENC_STRING_5,
1875                TEST_ENC_STRING_1,
1876                TEST_ENC_STRING_2,
1877                TEST_ENC_STRING_3,
1878                TEST_ENC_STRING_4,
1879                TEST_ENC_STRING_5
1880            )),
1881        };
1882
1883        cipher
1884            .populate_cipher_types()
1885            .expect("populate_cipher_types failed");
1886
1887        assert!(cipher.identity.is_some());
1888        let identity = cipher.identity.unwrap();
1889        assert_eq!(
1890            identity.first_name.as_ref().unwrap().to_string(),
1891            TEST_ENC_STRING_1
1892        );
1893        assert_eq!(
1894            identity.last_name.as_ref().unwrap().to_string(),
1895            TEST_ENC_STRING_2
1896        );
1897        assert_eq!(
1898            identity.email.as_ref().unwrap().to_string(),
1899            TEST_ENC_STRING_3
1900        );
1901        assert_eq!(
1902            identity.phone.as_ref().unwrap().to_string(),
1903            TEST_ENC_STRING_4
1904        );
1905        assert_eq!(
1906            identity.company.as_ref().unwrap().to_string(),
1907            TEST_ENC_STRING_5
1908        );
1909        assert_eq!(
1910            identity.address1.as_ref().unwrap().to_string(),
1911            TEST_ENC_STRING_1
1912        );
1913        assert_eq!(
1914            identity.city.as_ref().unwrap().to_string(),
1915            TEST_ENC_STRING_2
1916        );
1917        assert_eq!(
1918            identity.state.as_ref().unwrap().to_string(),
1919            TEST_ENC_STRING_3
1920        );
1921        assert_eq!(
1922            identity.postal_code.as_ref().unwrap().to_string(),
1923            TEST_ENC_STRING_4
1924        );
1925        assert_eq!(
1926            identity.country.as_ref().unwrap().to_string(),
1927            TEST_ENC_STRING_5
1928        );
1929    }
1930
1931    #[test]
1932
1933    fn test_password_history_with_hidden_fields() {
1934        let mut original_cipher = generate_cipher();
1935        original_cipher.fields = Some(vec![FieldView {
1936            name: Some("Secret Key".to_string()),
1937            value: Some("old_secret_value".to_string()),
1938            r#type: crate::FieldType::Hidden,
1939            linked_id: None,
1940        }]);
1941
1942        let mut new_cipher = generate_cipher();
1943        new_cipher.fields = Some(vec![FieldView {
1944            name: Some("Secret Key".to_string()),
1945            value: Some("new_secret_value".to_string()),
1946            r#type: crate::FieldType::Hidden,
1947            linked_id: None,
1948        }]);
1949
1950        new_cipher.update_password_history(&original_cipher);
1951
1952        assert!(new_cipher.password_history.is_some());
1953        let history = new_cipher.password_history.unwrap();
1954        assert_eq!(history.len(), 1);
1955        assert_eq!(history[0].password, "Secret Key: old_secret_value");
1956    }
1957
1958    #[test]
1959    fn test_password_history_length_limit() {
1960        use crate::password_history::MAX_PASSWORD_HISTORY_ENTRIES;
1961
1962        let mut original_cipher = generate_cipher();
1963        original_cipher.password_history = Some(
1964            (0..10)
1965                .map(|i| PasswordHistoryView {
1966                    password: format!("old_password_{}", i),
1967                    last_used_date: chrono::Utc::now(),
1968                })
1969                .collect(),
1970        );
1971
1972        let mut new_cipher = original_cipher.clone();
1973        // Change password
1974        if let Some(ref mut login) = new_cipher.login {
1975            login.password = Some("brand_new_password".to_string());
1976        }
1977
1978        new_cipher.update_password_history(&original_cipher);
1979
1980        assert!(new_cipher.password_history.is_some());
1981        let history = new_cipher.password_history.unwrap();
1982
1983        // Should be limited to MAX_PASSWORD_HISTORY_ENTRIES
1984        assert_eq!(history.len(), MAX_PASSWORD_HISTORY_ENTRIES);
1985
1986        // Most recent change (original password) should be first
1987        assert_eq!(history[0].password, "test_password");
1988        // Followed by the oldest entries from the existing history
1989        assert_eq!(history[1].password, "old_password_0");
1990        assert_eq!(history[2].password, "old_password_1");
1991        assert_eq!(history[3].password, "old_password_2");
1992        assert_eq!(history[4].password, "old_password_3");
1993    }
1994
1995    #[test]
1996    fn test_populate_cipher_types_ssh_key() {
1997        let mut cipher = Cipher {
1998            id: Some(TEST_UUID.parse().unwrap()),
1999            organization_id: None,
2000            folder_id: None,
2001            collection_ids: vec![],
2002            key: None,
2003            name: TEST_CIPHER_NAME.parse().unwrap(),
2004            notes: None,
2005            r#type: CipherType::SshKey,
2006            login: None,
2007            identity: None,
2008            card: None,
2009            secure_note: None,
2010            ssh_key: None,
2011            favorite: false,
2012            reprompt: CipherRepromptType::None,
2013            organization_use_totp: false,
2014            edit: true,
2015            view_password: true,
2016            permissions: None,
2017            local_data: None,
2018            attachments: None,
2019            fields: None,
2020            password_history: None,
2021            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2022            deleted_date: None,
2023            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2024            archived_date: None,
2025            data: Some(format!(
2026                r#"{{"privateKey": "{}", "publicKey": "{}", "fingerprint": "{}", "organizationUseTotp": true, "favorite": false, "deletedDate": null}}"#,
2027                TEST_ENC_STRING_1, TEST_ENC_STRING_2, TEST_ENC_STRING_3
2028            )),
2029        };
2030
2031        cipher
2032            .populate_cipher_types()
2033            .expect("populate_cipher_types failed");
2034
2035        assert!(cipher.ssh_key.is_some());
2036        let ssh_key = cipher.ssh_key.unwrap();
2037        assert_eq!(ssh_key.private_key.to_string(), TEST_ENC_STRING_1);
2038        assert_eq!(ssh_key.public_key.to_string(), TEST_ENC_STRING_2);
2039        assert_eq!(ssh_key.fingerprint.to_string(), TEST_ENC_STRING_3);
2040    }
2041
2042    #[test]
2043    fn test_populate_cipher_types_with_null_data() {
2044        let mut cipher = Cipher {
2045            id: Some(TEST_UUID.parse().unwrap()),
2046            organization_id: None,
2047            folder_id: None,
2048            collection_ids: vec![],
2049            key: None,
2050            name: TEST_CIPHER_NAME.parse().unwrap(),
2051            notes: None,
2052            r#type: CipherType::Login,
2053            login: None,
2054            identity: None,
2055            card: None,
2056            secure_note: None,
2057            ssh_key: None,
2058            favorite: false,
2059            reprompt: CipherRepromptType::None,
2060            organization_use_totp: false,
2061            edit: true,
2062            view_password: true,
2063            permissions: None,
2064            local_data: None,
2065            attachments: None,
2066            fields: None,
2067            password_history: None,
2068            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2069            deleted_date: None,
2070            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2071            archived_date: None,
2072            data: None,
2073        };
2074
2075        let result = cipher.populate_cipher_types();
2076        assert!(matches!(
2077            result,
2078            Err(VaultParseError::MissingField(MissingFieldError("data")))
2079        ));
2080    }
2081
2082    #[test]
2083    fn test_populate_cipher_types_with_invalid_json() {
2084        let mut cipher = Cipher {
2085            id: Some(TEST_UUID.parse().unwrap()),
2086            organization_id: None,
2087            folder_id: None,
2088            collection_ids: vec![],
2089            key: None,
2090            name: TEST_CIPHER_NAME.parse().unwrap(),
2091            notes: None,
2092            r#type: CipherType::Login,
2093            login: None,
2094            identity: None,
2095            card: None,
2096            secure_note: None,
2097            ssh_key: None,
2098            favorite: false,
2099            reprompt: CipherRepromptType::None,
2100            organization_use_totp: false,
2101            edit: true,
2102            view_password: true,
2103            permissions: None,
2104            local_data: None,
2105            attachments: None,
2106            fields: None,
2107            password_history: None,
2108            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2109            deleted_date: None,
2110            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
2111            archived_date: None,
2112            data: Some("invalid json".to_string()),
2113        };
2114
2115        let result = cipher.populate_cipher_types();
2116
2117        assert!(matches!(result, Err(VaultParseError::SerdeJson(_))));
2118    }
2119}