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