bitwarden_vault/cipher/
cipher.rs

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