bitwarden_vault/cipher/
cipher.rs

1use bitwarden_api_api::models::CipherDetailsResponseModel;
2use bitwarden_core::{
3    key_management::{KeyIds, SymmetricKeyId},
4    require, MissingFieldError, VaultLockedError,
5};
6use bitwarden_crypto::{
7    CompositeEncryptable, CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext,
8    PrimitiveEncryptable,
9};
10use bitwarden_error::bitwarden_error;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14use thiserror::Error;
15#[cfg(feature = "wasm")]
16use tsify::Tsify;
17use uuid::Uuid;
18#[cfg(feature = "wasm")]
19use wasm_bindgen::prelude::wasm_bindgen;
20
21use super::{
22    attachment, card,
23    card::CardListView,
24    cipher_permissions::CipherPermissions,
25    field, identity,
26    local_data::{LocalData, LocalDataView},
27    login::LoginListView,
28    secure_note, ssh_key,
29};
30use crate::{
31    password_history, Fido2CredentialFullView, Fido2CredentialView, Login, LoginView,
32    VaultParseError,
33};
34
35#[allow(missing_docs)]
36#[bitwarden_error(flat)]
37#[derive(Debug, Error)]
38pub enum CipherError {
39    #[error(transparent)]
40    MissingFieldError(#[from] MissingFieldError),
41    #[error(transparent)]
42    VaultLocked(#[from] VaultLockedError),
43    #[error(transparent)]
44    CryptoError(#[from] CryptoError),
45    #[error("This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation")]
46    AttachmentsWithoutKeys,
47}
48
49/// Helper trait for operations on cipher types.
50pub(super) trait CipherKind {
51    /// Returns the item's subtitle.
52    fn decrypt_subtitle(
53        &self,
54        ctx: &mut KeyStoreContext<KeyIds>,
55        key: SymmetricKeyId,
56    ) -> Result<String, CryptoError>;
57
58    /// Returns a list of populated fields for the cipher.
59    fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
60}
61
62#[allow(missing_docs)]
63#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
64#[repr(u8)]
65#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
66#[cfg_attr(feature = "wasm", wasm_bindgen)]
67pub enum CipherType {
68    Login = 1,
69    SecureNote = 2,
70    Card = 3,
71    Identity = 4,
72    SshKey = 5,
73}
74
75#[allow(missing_docs)]
76#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
77#[repr(u8)]
78#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
79#[cfg_attr(feature = "wasm", wasm_bindgen)]
80pub enum CipherRepromptType {
81    None = 0,
82    Password = 1,
83}
84
85#[allow(missing_docs)]
86#[derive(Serialize, Deserialize, Debug, Clone)]
87#[serde(rename_all = "camelCase", deny_unknown_fields)]
88#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
89#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
90pub struct EncryptionContext {
91    /// The Id of the user that encrypted the cipher. It should always represent a UserId, even for
92    /// Organization-owned ciphers
93    pub encrypted_for: Uuid,
94    pub cipher: Cipher,
95}
96
97#[allow(missing_docs)]
98#[derive(Serialize, Deserialize, Debug, Clone)]
99#[serde(rename_all = "camelCase", deny_unknown_fields)]
100#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
101#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
102pub struct Cipher {
103    pub id: Option<Uuid>,
104    pub organization_id: Option<Uuid>,
105    pub folder_id: Option<Uuid>,
106    pub collection_ids: Vec<Uuid>,
107
108    /// More recent ciphers uses individual encryption keys to encrypt the other fields of the
109    /// Cipher.
110    pub key: Option<EncString>,
111
112    pub name: EncString,
113    pub notes: Option<EncString>,
114
115    pub r#type: CipherType,
116    pub login: Option<Login>,
117    pub identity: Option<identity::Identity>,
118    pub card: Option<card::Card>,
119    pub secure_note: Option<secure_note::SecureNote>,
120    pub ssh_key: Option<ssh_key::SshKey>,
121
122    pub favorite: bool,
123    pub reprompt: CipherRepromptType,
124    pub organization_use_totp: bool,
125    pub edit: bool,
126    pub permissions: Option<CipherPermissions>,
127    pub view_password: bool,
128    pub local_data: Option<LocalData>,
129
130    pub attachments: Option<Vec<attachment::Attachment>>,
131    pub fields: Option<Vec<field::Field>>,
132    pub password_history: Option<Vec<password_history::PasswordHistory>>,
133
134    pub creation_date: DateTime<Utc>,
135    pub deleted_date: Option<DateTime<Utc>>,
136    pub revision_date: DateTime<Utc>,
137}
138
139bitwarden_state::register_repository_item!(Cipher, "Cipher");
140
141#[allow(missing_docs)]
142#[derive(Serialize, Deserialize, Debug, Clone)]
143#[serde(rename_all = "camelCase", deny_unknown_fields)]
144#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
145#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
146pub struct CipherView {
147    pub id: Option<Uuid>,
148    pub organization_id: Option<Uuid>,
149    pub folder_id: Option<Uuid>,
150    pub collection_ids: Vec<Uuid>,
151
152    /// Temporary, required to support re-encrypting existing items.
153    pub key: Option<EncString>,
154
155    pub name: String,
156    pub notes: Option<String>,
157
158    pub r#type: CipherType,
159    pub login: Option<LoginView>,
160    pub identity: Option<identity::IdentityView>,
161    pub card: Option<card::CardView>,
162    pub secure_note: Option<secure_note::SecureNoteView>,
163    pub ssh_key: Option<ssh_key::SshKeyView>,
164
165    pub favorite: bool,
166    pub reprompt: CipherRepromptType,
167    pub organization_use_totp: bool,
168    pub edit: bool,
169    pub permissions: Option<CipherPermissions>,
170    pub view_password: bool,
171    pub local_data: Option<LocalDataView>,
172
173    pub attachments: Option<Vec<attachment::AttachmentView>>,
174    pub fields: Option<Vec<field::FieldView>>,
175    pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
176
177    pub creation_date: DateTime<Utc>,
178    pub deleted_date: Option<DateTime<Utc>>,
179    pub revision_date: DateTime<Utc>,
180}
181
182#[allow(missing_docs)]
183#[derive(Serialize, Deserialize, Debug, PartialEq)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
186#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
187pub enum CipherListViewType {
188    Login(LoginListView),
189    SecureNote,
190    Card(CardListView),
191    Identity,
192    SshKey,
193}
194
195/// Available fields on a cipher and can be copied from a the list view in the UI.
196#[derive(Serialize, Deserialize, Debug, PartialEq)]
197#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
198#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
199pub enum CopyableCipherFields {
200    LoginUsername,
201    LoginPassword,
202    LoginTotp,
203    CardNumber,
204    CardSecurityCode,
205    IdentityUsername,
206    IdentityEmail,
207    IdentityPhone,
208    IdentityAddress,
209    SshKey,
210    SecureNotes,
211}
212
213#[allow(missing_docs)]
214#[derive(Serialize, Deserialize, Debug, PartialEq)]
215#[serde(rename_all = "camelCase", deny_unknown_fields)]
216#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
217#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
218pub struct CipherListView {
219    pub id: Option<Uuid>,
220    pub organization_id: Option<Uuid>,
221    pub folder_id: Option<Uuid>,
222    pub collection_ids: Vec<Uuid>,
223
224    /// Temporary, required to support calculating TOTP from CipherListView.
225    pub key: Option<EncString>,
226
227    pub name: String,
228    pub subtitle: String,
229
230    pub r#type: CipherListViewType,
231
232    pub favorite: bool,
233    pub reprompt: CipherRepromptType,
234    pub organization_use_totp: bool,
235    pub edit: bool,
236    pub permissions: Option<CipherPermissions>,
237
238    pub view_password: bool,
239
240    /// The number of attachments
241    pub attachments: u32,
242    /// Indicates if the cipher has old attachments that need to be re-uploaded
243    pub has_old_attachments: bool,
244
245    pub creation_date: DateTime<Utc>,
246    pub deleted_date: Option<DateTime<Utc>>,
247    pub revision_date: DateTime<Utc>,
248
249    /// Hints for the presentation layer for which fields can be copied.
250    pub copyable_fields: Vec<CopyableCipherFields>,
251
252    pub local_data: Option<LocalDataView>,
253}
254
255/// Represents the result of decrypting a list of ciphers.
256///
257/// This struct contains two vectors: `successes` and `failures`.
258/// `successes` contains the decrypted `CipherListView` objects,
259/// while `failures` contains the original `Cipher` objects that failed to decrypt.
260#[derive(Serialize, Deserialize, Debug)]
261#[serde(rename_all = "camelCase", deny_unknown_fields)]
262#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
263#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
264pub struct DecryptCipherListResult {
265    /// The decrypted `CipherListView` objects.
266    pub successes: Vec<CipherListView>,
267    /// The original `Cipher` objects that failed to decrypt.
268    pub failures: Vec<Cipher>,
269}
270
271impl CipherListView {
272    pub(crate) fn get_totp_key(
273        self,
274        ctx: &mut KeyStoreContext<KeyIds>,
275    ) -> Result<Option<String>, CryptoError> {
276        let key = self.key_identifier();
277        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
278
279        let totp = match self.r#type {
280            CipherListViewType::Login(LoginListView { totp, .. }) => {
281                totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
282            }
283            _ => None,
284        };
285
286        Ok(totp)
287    }
288}
289
290impl CompositeEncryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
291    fn encrypt_composite(
292        &self,
293        ctx: &mut KeyStoreContext<KeyIds>,
294        key: SymmetricKeyId,
295    ) -> Result<Cipher, CryptoError> {
296        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
297
298        let mut cipher_view = self.clone();
299        cipher_view.generate_checksums();
300
301        Ok(Cipher {
302            id: cipher_view.id,
303            organization_id: cipher_view.organization_id,
304            folder_id: cipher_view.folder_id,
305            collection_ids: cipher_view.collection_ids,
306            key: cipher_view.key,
307            name: cipher_view.name.encrypt(ctx, ciphers_key)?,
308            notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
309            r#type: cipher_view.r#type,
310            login: cipher_view.login.encrypt_composite(ctx, ciphers_key)?,
311            identity: cipher_view.identity.encrypt_composite(ctx, ciphers_key)?,
312            card: cipher_view.card.encrypt_composite(ctx, ciphers_key)?,
313            secure_note: cipher_view
314                .secure_note
315                .encrypt_composite(ctx, ciphers_key)?,
316            ssh_key: cipher_view.ssh_key.encrypt_composite(ctx, ciphers_key)?,
317            favorite: cipher_view.favorite,
318            reprompt: cipher_view.reprompt,
319            organization_use_totp: cipher_view.organization_use_totp,
320            edit: cipher_view.edit,
321            view_password: cipher_view.view_password,
322            local_data: cipher_view.local_data.encrypt_composite(ctx, ciphers_key)?,
323            attachments: cipher_view
324                .attachments
325                .encrypt_composite(ctx, ciphers_key)?,
326            fields: cipher_view.fields.encrypt_composite(ctx, ciphers_key)?,
327            password_history: cipher_view
328                .password_history
329                .encrypt_composite(ctx, ciphers_key)?,
330            creation_date: cipher_view.creation_date,
331            deleted_date: cipher_view.deleted_date,
332            revision_date: cipher_view.revision_date,
333            permissions: cipher_view.permissions,
334        })
335    }
336}
337
338impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
339    fn decrypt(
340        &self,
341        ctx: &mut KeyStoreContext<KeyIds>,
342        key: SymmetricKeyId,
343    ) -> Result<CipherView, CryptoError> {
344        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
345
346        let mut cipher = CipherView {
347            id: self.id,
348            organization_id: self.organization_id,
349            folder_id: self.folder_id,
350            collection_ids: self.collection_ids.clone(),
351            key: self.key.clone(),
352            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
353            notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
354            r#type: self.r#type,
355            login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
356            identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
357            card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
358            secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
359            ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
360            favorite: self.favorite,
361            reprompt: self.reprompt,
362            organization_use_totp: self.organization_use_totp,
363            edit: self.edit,
364            permissions: self.permissions,
365            view_password: self.view_password,
366            local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
367            attachments: self.attachments.decrypt(ctx, ciphers_key).ok().flatten(),
368            fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
369            password_history: self
370                .password_history
371                .decrypt(ctx, ciphers_key)
372                .ok()
373                .flatten(),
374            creation_date: self.creation_date,
375            deleted_date: self.deleted_date,
376            revision_date: self.revision_date,
377        };
378
379        // For compatibility we only remove URLs with invalid checksums if the cipher has a key
380        if cipher.key.is_some() {
381            cipher.remove_invalid_checksums();
382        }
383
384        Ok(cipher)
385    }
386}
387
388impl Cipher {
389    /// Decrypt the individual encryption key for this cipher into the provided [KeyStoreContext]
390    /// and return it's identifier. Note that some ciphers do not have individual encryption
391    /// keys, in which case this will return the provided key identifier instead
392    ///
393    /// # Arguments
394    ///
395    /// * `ctx` - The key store context where the cipher key will be decrypted, if it exists
396    /// * `key` - The key to use to decrypt the cipher key, this should be the user or organization
397    ///   key
398    /// * `ciphers_key` - The encrypted cipher key
399    pub(super) fn decrypt_cipher_key(
400        ctx: &mut KeyStoreContext<KeyIds>,
401        key: SymmetricKeyId,
402        ciphers_key: &Option<EncString>,
403    ) -> Result<SymmetricKeyId, CryptoError> {
404        const CIPHER_KEY: SymmetricKeyId = SymmetricKeyId::Local("cipher_key");
405        match ciphers_key {
406            Some(ciphers_key) => ctx.unwrap_symmetric_key(key, CIPHER_KEY, ciphers_key),
407            None => Ok(key),
408        }
409    }
410
411    /// Temporary helper to return a [CipherKind] instance based on the cipher type.
412    fn get_kind(&self) -> Option<&dyn CipherKind> {
413        match self.r#type {
414            CipherType::Login => self.login.as_ref().map(|v| v as _),
415            CipherType::Card => self.card.as_ref().map(|v| v as _),
416            CipherType::Identity => self.identity.as_ref().map(|v| v as _),
417            CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
418            CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
419        }
420    }
421
422    /// Returns the decrypted subtitle for the cipher, if applicable.
423    fn decrypt_subtitle(
424        &self,
425        ctx: &mut KeyStoreContext<KeyIds>,
426        key: SymmetricKeyId,
427    ) -> Result<String, CryptoError> {
428        self.get_kind()
429            .map(|sub| sub.decrypt_subtitle(ctx, key))
430            .unwrap_or_else(|| Ok(String::new()))
431    }
432
433    /// Returns a list of copyable field names for this cipher,
434    /// based on the cipher type and populated properties.
435    fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
436        self.get_kind()
437            .map(|kind| kind.get_copyable_fields(Some(self)))
438            .unwrap_or_default()
439    }
440}
441
442impl CipherView {
443    #[allow(missing_docs)]
444    pub fn generate_cipher_key(
445        &mut self,
446        ctx: &mut KeyStoreContext<KeyIds>,
447        key: SymmetricKeyId,
448    ) -> Result<(), CryptoError> {
449        let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
450
451        const NEW_KEY: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key");
452
453        let new_key = ctx.generate_symmetric_key(NEW_KEY)?;
454
455        self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
456        self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
457
458        self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
459        Ok(())
460    }
461
462    #[allow(missing_docs)]
463    pub fn generate_checksums(&mut self) {
464        if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
465            for uri in uris {
466                uri.generate_checksum();
467            }
468        }
469    }
470
471    #[allow(missing_docs)]
472    pub fn remove_invalid_checksums(&mut self) {
473        if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
474            uris.retain(|u| u.is_checksum_valid());
475        }
476    }
477
478    fn reencrypt_attachment_keys(
479        &mut self,
480        ctx: &mut KeyStoreContext<KeyIds>,
481        old_key: SymmetricKeyId,
482        new_key: SymmetricKeyId,
483    ) -> Result<(), CryptoError> {
484        if let Some(attachments) = &mut self.attachments {
485            for attachment in attachments {
486                if let Some(attachment_key) = &mut attachment.key {
487                    let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key");
488                    ctx.unwrap_symmetric_key(old_key, tmp_attachment_key_id, attachment_key)?;
489                    *attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?;
490                }
491            }
492        }
493        Ok(())
494    }
495
496    #[allow(missing_docs)]
497    pub fn decrypt_fido2_credentials(
498        &self,
499        ctx: &mut KeyStoreContext<KeyIds>,
500    ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
501        let key = self.key_identifier();
502        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
503
504        Ok(self
505            .login
506            .as_ref()
507            .and_then(|l| l.fido2_credentials.as_ref())
508            .map(|f| f.decrypt(ctx, ciphers_key))
509            .transpose()?
510            .unwrap_or_default())
511    }
512
513    fn reencrypt_fido2_credentials(
514        &mut self,
515        ctx: &mut KeyStoreContext<KeyIds>,
516        old_key: SymmetricKeyId,
517        new_key: SymmetricKeyId,
518    ) -> Result<(), CryptoError> {
519        if let Some(login) = self.login.as_mut() {
520            if let Some(fido2_credentials) = &mut login.fido2_credentials {
521                let dec_fido2_credentials: Vec<Fido2CredentialFullView> =
522                    fido2_credentials.decrypt(ctx, old_key)?;
523                *fido2_credentials = dec_fido2_credentials.encrypt_composite(ctx, new_key)?;
524            }
525        }
526        Ok(())
527    }
528
529    #[allow(missing_docs)]
530    pub fn move_to_organization(
531        &mut self,
532        ctx: &mut KeyStoreContext<KeyIds>,
533        organization_id: Uuid,
534    ) -> Result<(), CipherError> {
535        let old_key = self.key_identifier();
536        let new_key = SymmetricKeyId::Organization(organization_id);
537
538        // If any attachment is missing a key we can't reencrypt the attachment keys
539        if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
540            return Err(CipherError::AttachmentsWithoutKeys);
541        }
542
543        // If the cipher has a key, we need to re-encrypt it with the new organization key
544        if let Some(cipher_key) = &mut self.key {
545            let tmp_cipher_key_id = SymmetricKeyId::Local("cipher_key");
546            ctx.unwrap_symmetric_key(old_key, tmp_cipher_key_id, cipher_key)?;
547            *cipher_key = ctx.wrap_symmetric_key(new_key, tmp_cipher_key_id)?;
548        } else {
549            // If the cipher does not have a key, we need to reencrypt all attachment keys
550            self.reencrypt_attachment_keys(ctx, old_key, new_key)?;
551            self.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
552        }
553
554        self.organization_id = Some(organization_id);
555        Ok(())
556    }
557
558    #[allow(missing_docs)]
559    pub fn set_new_fido2_credentials(
560        &mut self,
561        ctx: &mut KeyStoreContext<KeyIds>,
562        creds: Vec<Fido2CredentialFullView>,
563    ) -> Result<(), CipherError> {
564        let key = self.key_identifier();
565
566        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
567
568        require!(self.login.as_mut()).fido2_credentials =
569            Some(creds.encrypt_composite(ctx, ciphers_key)?);
570
571        Ok(())
572    }
573
574    #[allow(missing_docs)]
575    pub fn get_fido2_credentials(
576        &self,
577        ctx: &mut KeyStoreContext<KeyIds>,
578    ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
579        let key = self.key_identifier();
580
581        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
582
583        let login = require!(self.login.as_ref());
584        let creds = require!(login.fido2_credentials.as_ref());
585        let res = creds.decrypt(ctx, ciphers_key)?;
586        Ok(res)
587    }
588
589    #[allow(missing_docs)]
590    pub fn decrypt_fido2_private_key(
591        &self,
592        ctx: &mut KeyStoreContext<KeyIds>,
593    ) -> Result<String, CipherError> {
594        let fido2_credential = self.get_fido2_credentials(ctx)?;
595
596        Ok(fido2_credential[0].key_value.clone())
597    }
598}
599
600impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
601    fn decrypt(
602        &self,
603        ctx: &mut KeyStoreContext<KeyIds>,
604        key: SymmetricKeyId,
605    ) -> Result<CipherListView, CryptoError> {
606        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
607
608        Ok(CipherListView {
609            id: self.id,
610            organization_id: self.organization_id,
611            folder_id: self.folder_id,
612            collection_ids: self.collection_ids.clone(),
613            key: self.key.clone(),
614            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
615            subtitle: self
616                .decrypt_subtitle(ctx, ciphers_key)
617                .ok()
618                .unwrap_or_default(),
619            r#type: match self.r#type {
620                CipherType::Login => {
621                    let login = self
622                        .login
623                        .as_ref()
624                        .ok_or(CryptoError::MissingField("login"))?;
625                    CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
626                }
627                CipherType::SecureNote => CipherListViewType::SecureNote,
628                CipherType::Card => {
629                    let card = self
630                        .card
631                        .as_ref()
632                        .ok_or(CryptoError::MissingField("card"))?;
633                    CipherListViewType::Card(card.decrypt(ctx, ciphers_key)?)
634                }
635                CipherType::Identity => CipherListViewType::Identity,
636                CipherType::SshKey => CipherListViewType::SshKey,
637            },
638            favorite: self.favorite,
639            reprompt: self.reprompt,
640            organization_use_totp: self.organization_use_totp,
641            edit: self.edit,
642            permissions: self.permissions,
643            view_password: self.view_password,
644            attachments: self
645                .attachments
646                .as_ref()
647                .map(|a| a.len() as u32)
648                .unwrap_or(0),
649            has_old_attachments: self
650                .attachments
651                .as_ref()
652                .map(|a| a.iter().any(|att| att.key.is_none()))
653                .unwrap_or(false),
654            creation_date: self.creation_date,
655            deleted_date: self.deleted_date,
656            revision_date: self.revision_date,
657            copyable_fields: self.get_copyable_fields(),
658            local_data: self.local_data.decrypt(ctx, ciphers_key)?,
659        })
660    }
661}
662
663impl IdentifyKey<SymmetricKeyId> for Cipher {
664    fn key_identifier(&self) -> SymmetricKeyId {
665        match self.organization_id {
666            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
667            None => SymmetricKeyId::User,
668        }
669    }
670}
671
672impl IdentifyKey<SymmetricKeyId> for CipherView {
673    fn key_identifier(&self) -> SymmetricKeyId {
674        match self.organization_id {
675            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
676            None => SymmetricKeyId::User,
677        }
678    }
679}
680
681impl IdentifyKey<SymmetricKeyId> for CipherListView {
682    fn key_identifier(&self) -> SymmetricKeyId {
683        match self.organization_id {
684            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
685            None => SymmetricKeyId::User,
686        }
687    }
688}
689
690impl TryFrom<CipherDetailsResponseModel> for Cipher {
691    type Error = VaultParseError;
692
693    fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
694        Ok(Self {
695            id: cipher.id,
696            organization_id: cipher.organization_id,
697            folder_id: cipher.folder_id,
698            collection_ids: cipher.collection_ids.unwrap_or_default(),
699            name: require!(EncString::try_from_optional(cipher.name)?),
700            notes: EncString::try_from_optional(cipher.notes)?,
701            r#type: require!(cipher.r#type).into(),
702            login: cipher.login.map(|l| (*l).try_into()).transpose()?,
703            identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
704            card: cipher.card.map(|c| (*c).try_into()).transpose()?,
705            secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
706            // TODO: add ssh_key when api bindings have been updated
707            ssh_key: None,
708            favorite: cipher.favorite.unwrap_or(false),
709            reprompt: cipher
710                .reprompt
711                .map(|r| r.into())
712                .unwrap_or(CipherRepromptType::None),
713            organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
714            edit: cipher.edit.unwrap_or(true),
715            // TODO: add permissions when api bindings have been updated
716            permissions: None,
717            view_password: cipher.view_password.unwrap_or(true),
718            local_data: None, // Not sent from server
719            attachments: cipher
720                .attachments
721                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
722                .transpose()?,
723            fields: cipher
724                .fields
725                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
726                .transpose()?,
727            password_history: cipher
728                .password_history
729                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
730                .transpose()?,
731            creation_date: require!(cipher.creation_date).parse()?,
732            deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
733            revision_date: require!(cipher.revision_date).parse()?,
734            key: EncString::try_from_optional(cipher.key)?,
735        })
736    }
737}
738
739impl From<bitwarden_api_api::models::CipherType> for CipherType {
740    fn from(t: bitwarden_api_api::models::CipherType) -> Self {
741        match t {
742            bitwarden_api_api::models::CipherType::Login => CipherType::Login,
743            bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
744            bitwarden_api_api::models::CipherType::Card => CipherType::Card,
745            bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
746            bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
747        }
748    }
749}
750
751impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
752    fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self {
753        match t {
754            bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
755            bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
756        }
757    }
758}
759
760#[cfg(test)]
761mod tests {
762
763    use attachment::AttachmentView;
764    use bitwarden_core::key_management::{
765        create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
766    };
767    use bitwarden_crypto::SymmetricCryptoKey;
768
769    use super::*;
770    use crate::{login::Fido2CredentialListView, Fido2Credential};
771
772    fn generate_cipher() -> CipherView {
773        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
774        CipherView {
775            r#type: CipherType::Login,
776            login: Some(LoginView {
777                username: Some("test_username".to_string()),
778                password: Some("test_password".to_string()),
779                password_revision_date: None,
780                uris: None,
781                totp: None,
782                autofill_on_page_load: None,
783                fido2_credentials: None,
784            }),
785            id: Some(test_id),
786            organization_id: None,
787            folder_id: None,
788            collection_ids: vec![],
789            key: None,
790            name: "My test login".to_string(),
791            notes: None,
792            identity: None,
793            card: None,
794            secure_note: None,
795            ssh_key: None,
796            favorite: false,
797            reprompt: CipherRepromptType::None,
798            organization_use_totp: true,
799            edit: true,
800            permissions: None,
801            view_password: true,
802            local_data: None,
803            attachments: None,
804            fields: None,
805            password_history: None,
806            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
807            deleted_date: None,
808            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
809        }
810    }
811
812    fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
813        Fido2Credential {
814            credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
815            key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
816            key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
817            key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
818            key_value: "123".to_string().encrypt(ctx, key).unwrap(),
819            rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
820            user_handle: None,
821            user_name: None,
822            counter: "123".to_string().encrypt(ctx, key).unwrap(),
823            rp_name: None,
824            user_display_name: None,
825            discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
826            creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
827        }
828    }
829
830    #[test]
831    fn test_decrypt_cipher_list_view() {
832        let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
833        let key_store = create_test_crypto_with_user_key(key);
834
835        let cipher = Cipher {
836            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
837            organization_id: None,
838            folder_id: None,
839            collection_ids: vec![],
840            key: None,
841            name: "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=".parse().unwrap(),
842            notes: None,
843            r#type: CipherType::Login,
844            login: Some(Login {
845                username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
846                password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
847                password_revision_date: None,
848                uris: None,
849                totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
850                autofill_on_page_load: None,
851                fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
852            }),
853            identity: None,
854            card: None,
855            secure_note: None,
856            ssh_key: None,
857            favorite: false,
858            reprompt: CipherRepromptType::None,
859            organization_use_totp: false,
860            edit: true,
861            permissions: Some(CipherPermissions {
862                delete: false,
863                restore: false
864            }),
865            view_password: true,
866            local_data: None,
867            attachments: None,
868            fields: None,
869            password_history: None,
870            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
871            deleted_date: None,
872            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
873        };
874
875        let view: CipherListView = key_store.decrypt(&cipher).unwrap();
876
877        assert_eq!(
878            view,
879            CipherListView {
880                id: cipher.id,
881                organization_id: cipher.organization_id,
882                folder_id: cipher.folder_id,
883                collection_ids: cipher.collection_ids,
884                key: cipher.key,
885                name: "My test login".to_string(),
886                subtitle: "test_username".to_string(),
887                r#type: CipherListViewType::Login(LoginListView {
888                    fido2_credentials: Some(vec![Fido2CredentialListView {
889                        credential_id: "123".to_string(),
890                        rp_id: "123".to_string(),
891                        user_handle: None,
892                        user_name: None,
893                        user_display_name: None,
894                    }]),
895                    has_fido2: true,
896                    username: Some("test_username".to_string()),
897                    totp: cipher.login.as_ref().unwrap().totp.clone(),
898                    uris: None,
899                }),
900                favorite: cipher.favorite,
901                reprompt: cipher.reprompt,
902                organization_use_totp: cipher.organization_use_totp,
903                edit: cipher.edit,
904                permissions: cipher.permissions,
905                view_password: cipher.view_password,
906                attachments: 0,
907                has_old_attachments: false,
908                creation_date: cipher.creation_date,
909                deleted_date: cipher.deleted_date,
910                revision_date: cipher.revision_date,
911                copyable_fields: vec![
912                    CopyableCipherFields::LoginUsername,
913                    CopyableCipherFields::LoginPassword,
914                    CopyableCipherFields::LoginTotp
915                ],
916                local_data: None,
917            }
918        )
919    }
920
921    #[test]
922    fn test_generate_cipher_key() {
923        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
924        let key_store = create_test_crypto_with_user_key(key);
925
926        let original_cipher = generate_cipher();
927
928        // Check that the cipher gets encrypted correctly without it's own key
929        let cipher = generate_cipher();
930        let no_key_cipher_enc = key_store.encrypt(cipher).unwrap();
931        let no_key_cipher_dec: CipherView = key_store.decrypt(&no_key_cipher_enc).unwrap();
932        assert!(no_key_cipher_dec.key.is_none());
933        assert_eq!(no_key_cipher_dec.name, original_cipher.name);
934
935        let mut cipher = generate_cipher();
936        cipher
937            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
938            .unwrap();
939
940        // Check that the cipher gets encrypted correctly when it's assigned it's own key
941        let key_cipher_enc = key_store.encrypt(cipher).unwrap();
942        let key_cipher_dec: CipherView = key_store.decrypt(&key_cipher_enc).unwrap();
943        assert!(key_cipher_dec.key.is_some());
944        assert_eq!(key_cipher_dec.name, original_cipher.name);
945    }
946
947    #[test]
948    fn test_generate_cipher_key_when_a_cipher_key_already_exists() {
949        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
950        let key_store = create_test_crypto_with_user_key(key);
951
952        let mut original_cipher = generate_cipher();
953        {
954            const CIPHER_KEY: SymmetricKeyId = SymmetricKeyId::Local("test_cipher_key");
955            let mut ctx = key_store.context();
956            let cipher_key = ctx.generate_symmetric_key(CIPHER_KEY).unwrap();
957
958            original_cipher.key = Some(
959                ctx.wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
960                    .unwrap(),
961            );
962        }
963
964        original_cipher
965            .generate_cipher_key(&mut key_store.context(), original_cipher.key_identifier())
966            .unwrap();
967
968        // Make sure that the cipher key is decryptable
969        let wrapped_key = original_cipher.key.unwrap();
970        let mut ctx = key_store.context();
971        ctx.unwrap_symmetric_key(
972            SymmetricKeyId::User,
973            SymmetricKeyId::Local("test_cipher_key"),
974            &wrapped_key,
975        )
976        .unwrap();
977    }
978
979    #[test]
980    fn test_generate_cipher_key_ignores_attachments_without_key() {
981        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
982        let key_store = create_test_crypto_with_user_key(key);
983
984        let mut cipher = generate_cipher();
985        let attachment = AttachmentView {
986            id: None,
987            url: None,
988            size: None,
989            size_name: None,
990            file_name: Some("Attachment test name".into()),
991            key: None,
992            decrypted_key: None,
993        };
994        cipher.attachments = Some(vec![attachment]);
995
996        cipher
997            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
998            .unwrap();
999        assert!(cipher.attachments.unwrap()[0].key.is_none());
1000    }
1001
1002    #[test]
1003    fn test_move_user_cipher_to_org() {
1004        let org = uuid::Uuid::new_v4();
1005        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1006        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1007        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1008
1009        // Create a cipher with a user key
1010        let mut cipher = generate_cipher();
1011        cipher
1012            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1013            .unwrap();
1014
1015        cipher
1016            .move_to_organization(&mut key_store.context(), org)
1017            .unwrap();
1018        assert_eq!(cipher.organization_id, Some(org));
1019
1020        // Check that the cipher can be encrypted/decrypted with the new org key
1021        let cipher_enc = key_store.encrypt(cipher).unwrap();
1022        let cipher_dec: CipherView = key_store.decrypt(&cipher_enc).unwrap();
1023
1024        assert_eq!(cipher_dec.name, "My test login");
1025    }
1026
1027    #[test]
1028    fn test_move_user_cipher_to_org_manually() {
1029        let org = uuid::Uuid::new_v4();
1030        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1031        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1032        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1033
1034        // Create a cipher with a user key
1035        let mut cipher = generate_cipher();
1036        cipher
1037            .generate_cipher_key(&mut key_store.context(), cipher.key_identifier())
1038            .unwrap();
1039
1040        cipher.organization_id = Some(org);
1041
1042        // Check that the cipher can not be encrypted, as the
1043        // cipher key is tied to the user key and not the org key
1044        assert!(key_store.encrypt(cipher).is_err());
1045    }
1046
1047    #[test]
1048    fn test_move_user_cipher_with_attachment_without_key_to_org() {
1049        let org = uuid::Uuid::new_v4();
1050        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1051        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1052        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1053
1054        let mut cipher = generate_cipher();
1055        let attachment = AttachmentView {
1056            id: None,
1057            url: None,
1058            size: None,
1059            size_name: None,
1060            file_name: Some("Attachment test name".into()),
1061            key: None,
1062            decrypted_key: None,
1063        };
1064        cipher.attachments = Some(vec![attachment]);
1065
1066        // Neither cipher nor attachment have keys, so the cipher can't be moved
1067        assert!(cipher
1068            .move_to_organization(&mut key_store.context(), org)
1069            .is_err());
1070    }
1071
1072    #[test]
1073    fn test_move_user_cipher_with_attachment_with_key_to_org() {
1074        let org = uuid::Uuid::new_v4();
1075        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1076        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1077        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1078        let org_key = SymmetricKeyId::Organization(org);
1079
1080        // Attachment has a key that is encrypted with the user key, as the cipher has no key itself
1081        let (attachment_key_enc, attachment_key_val) = {
1082            let mut ctx = key_store.context();
1083            let attachment_key = ctx
1084                .generate_symmetric_key(SymmetricKeyId::Local("test_attachment_key"))
1085                .unwrap();
1086            let attachment_key_enc = ctx
1087                .wrap_symmetric_key(SymmetricKeyId::User, attachment_key)
1088                .unwrap();
1089            #[allow(deprecated)]
1090            let attachment_key_val = ctx
1091                .dangerous_get_symmetric_key(attachment_key)
1092                .unwrap()
1093                .clone();
1094
1095            (attachment_key_enc, attachment_key_val)
1096        };
1097
1098        let mut cipher = generate_cipher();
1099        let attachment = AttachmentView {
1100            id: None,
1101            url: None,
1102            size: None,
1103            size_name: None,
1104            file_name: Some("Attachment test name".into()),
1105            key: Some(attachment_key_enc),
1106            decrypted_key: None,
1107        };
1108        cipher.attachments = Some(vec![attachment]);
1109        let cred = generate_fido2(&mut key_store.context(), SymmetricKeyId::User);
1110        cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred]);
1111
1112        cipher
1113            .move_to_organization(&mut key_store.context(), org)
1114            .unwrap();
1115
1116        assert!(cipher.key.is_none());
1117
1118        // Check that the attachment key has been re-encrypted with the org key,
1119        // and the value matches with the original attachment key
1120        let new_attachment_key = cipher.attachments.unwrap()[0].key.clone().unwrap();
1121        let mut ctx = key_store.context();
1122        let new_attachment_key_id = ctx
1123            .unwrap_symmetric_key(
1124                org_key,
1125                SymmetricKeyId::Local("test_attachment_key"),
1126                &new_attachment_key,
1127            )
1128            .unwrap();
1129        #[allow(deprecated)]
1130        let new_attachment_key_dec = ctx
1131            .dangerous_get_symmetric_key(new_attachment_key_id)
1132            .unwrap();
1133
1134        assert_eq!(*new_attachment_key_dec, attachment_key_val);
1135
1136        let cred2: Fido2CredentialFullView = cipher
1137            .login
1138            .unwrap()
1139            .fido2_credentials
1140            .unwrap()
1141            .first()
1142            .unwrap()
1143            .decrypt(&mut key_store.context(), org_key)
1144            .unwrap();
1145
1146        assert_eq!(cred2.credential_id, "123");
1147    }
1148
1149    #[test]
1150    fn test_move_user_cipher_with_key_with_attachment_with_key_to_org() {
1151        let org = uuid::Uuid::new_v4();
1152        let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1153        let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
1154        let key_store = create_test_crypto_with_user_and_org_key(key, org, org_key);
1155        let org_key = SymmetricKeyId::Organization(org);
1156
1157        let mut ctx = key_store.context();
1158
1159        let cipher_key = ctx
1160            .generate_symmetric_key(SymmetricKeyId::Local("test_cipher_key"))
1161            .unwrap();
1162        let cipher_key_enc = ctx
1163            .wrap_symmetric_key(SymmetricKeyId::User, cipher_key)
1164            .unwrap();
1165
1166        // Attachment has a key that is encrypted with the cipher key
1167        let attachment_key = ctx
1168            .generate_symmetric_key(SymmetricKeyId::Local("test_attachment_key"))
1169            .unwrap();
1170        let attachment_key_enc = ctx.wrap_symmetric_key(cipher_key, attachment_key).unwrap();
1171
1172        let mut cipher = generate_cipher();
1173        cipher.key = Some(cipher_key_enc);
1174
1175        let attachment = AttachmentView {
1176            id: None,
1177            url: None,
1178            size: None,
1179            size_name: None,
1180            file_name: Some("Attachment test name".into()),
1181            key: Some(attachment_key_enc.clone()),
1182            decrypted_key: None,
1183        };
1184        cipher.attachments = Some(vec![attachment]);
1185
1186        let cred = generate_fido2(&mut ctx, cipher_key);
1187        cipher.login.as_mut().unwrap().fido2_credentials = Some(vec![cred.clone()]);
1188
1189        cipher.move_to_organization(&mut ctx, org).unwrap();
1190
1191        // Check that the cipher key has been re-encrypted with the org key,
1192        let wrapped_new_cipher_key = cipher.key.clone().unwrap();
1193        let new_cipher_key_dec = ctx
1194            .unwrap_symmetric_key(
1195                org_key,
1196                SymmetricKeyId::Local("test_cipher_key"),
1197                &wrapped_new_cipher_key,
1198            )
1199            .unwrap();
1200        #[allow(deprecated)]
1201        let new_cipher_key_dec = ctx.dangerous_get_symmetric_key(new_cipher_key_dec).unwrap();
1202        #[allow(deprecated)]
1203        let cipher_key_val = ctx.dangerous_get_symmetric_key(cipher_key).unwrap();
1204
1205        assert_eq!(new_cipher_key_dec, cipher_key_val);
1206
1207        // Check that the attachment key hasn't changed
1208        assert_eq!(
1209            cipher.attachments.unwrap()[0]
1210                .key
1211                .as_ref()
1212                .unwrap()
1213                .to_string(),
1214            attachment_key_enc.to_string()
1215        );
1216
1217        let cred2: Fido2Credential = cipher
1218            .login
1219            .unwrap()
1220            .fido2_credentials
1221            .unwrap()
1222            .first()
1223            .unwrap()
1224            .clone();
1225
1226        assert_eq!(
1227            cred2.credential_id.to_string(),
1228            cred.credential_id.to_string()
1229        );
1230    }
1231
1232    #[test]
1233    fn test_decrypt_fido2_private_key() {
1234        let key_store =
1235            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1236        let mut ctx = key_store.context();
1237
1238        let mut cipher_view = generate_cipher();
1239        cipher_view
1240            .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1241            .unwrap();
1242
1243        let key_id = cipher_view.key_identifier();
1244        let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1245
1246        let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1247
1248        cipher_view.login.as_mut().unwrap().fido2_credentials =
1249            Some(vec![fido2_credential.clone()]);
1250
1251        let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1252        assert_eq!(decrypted_key_value, "123");
1253    }
1254}