bitwarden_vault/cipher/
cipher.rs

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