bitwarden_vault/cipher/
cipher.rs

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