bitwarden_vault/cipher/
cipher.rs

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