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    cipher_permissions::CipherPermissions,
23    field, identity,
24    local_data::{LocalData, LocalDataView},
25    login::LoginListView,
26    secure_note, ssh_key,
27};
28use crate::{
29    password_history, Fido2CredentialFullView, Fido2CredentialView, Login, LoginView,
30    VaultParseError,
31};
32
33#[bitwarden_error(flat)]
34#[derive(Debug, Error)]
35pub enum CipherError {
36    #[error(transparent)]
37    MissingFieldError(#[from] MissingFieldError),
38    #[error(transparent)]
39    VaultLocked(#[from] VaultLockedError),
40    #[error(transparent)]
41    CryptoError(#[from] CryptoError),
42    #[error("This cipher contains attachments without keys. Those attachments will need to be reuploaded to complete the operation")]
43    AttachmentsWithoutKeys,
44}
45
46#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
47#[repr(u8)]
48#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
49#[cfg_attr(feature = "wasm", wasm_bindgen)]
50pub enum CipherType {
51    Login = 1,
52    SecureNote = 2,
53    Card = 3,
54    Identity = 4,
55    SshKey = 5,
56}
57
58#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
59#[repr(u8)]
60#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
61#[cfg_attr(feature = "wasm", wasm_bindgen)]
62pub enum CipherRepromptType {
63    None = 0,
64    Password = 1,
65}
66
67#[derive(Serialize, Deserialize, Debug, Clone)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
70#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
71pub struct Cipher {
72    pub id: Option<Uuid>,
73    pub organization_id: Option<Uuid>,
74    pub folder_id: Option<Uuid>,
75    pub collection_ids: Vec<Uuid>,
76
77    /// More recent ciphers uses individual encryption keys to encrypt the other fields of the
78    /// Cipher.
79    pub key: Option<EncString>,
80
81    pub name: EncString,
82    pub notes: Option<EncString>,
83
84    pub r#type: CipherType,
85    pub login: Option<Login>,
86    pub identity: Option<identity::Identity>,
87    pub card: Option<card::Card>,
88    pub secure_note: Option<secure_note::SecureNote>,
89    pub ssh_key: Option<ssh_key::SshKey>,
90
91    pub favorite: bool,
92    pub reprompt: CipherRepromptType,
93    pub organization_use_totp: bool,
94    pub edit: bool,
95    pub permissions: Option<CipherPermissions>,
96    pub view_password: bool,
97    pub local_data: Option<LocalData>,
98
99    pub attachments: Option<Vec<attachment::Attachment>>,
100    pub fields: Option<Vec<field::Field>>,
101    pub password_history: Option<Vec<password_history::PasswordHistory>>,
102
103    pub creation_date: DateTime<Utc>,
104    pub deleted_date: Option<DateTime<Utc>>,
105    pub revision_date: DateTime<Utc>,
106}
107
108#[derive(Serialize, Deserialize, Debug, Clone)]
109#[serde(rename_all = "camelCase", deny_unknown_fields)]
110#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
111#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
112pub struct CipherView {
113    pub id: Option<Uuid>,
114    pub organization_id: Option<Uuid>,
115    pub folder_id: Option<Uuid>,
116    pub collection_ids: Vec<Uuid>,
117
118    /// Temporary, required to support re-encrypting existing items.
119    pub key: Option<EncString>,
120
121    pub name: String,
122    pub notes: Option<String>,
123
124    pub r#type: CipherType,
125    pub login: Option<LoginView>,
126    pub identity: Option<identity::IdentityView>,
127    pub card: Option<card::CardView>,
128    pub secure_note: Option<secure_note::SecureNoteView>,
129    pub ssh_key: Option<ssh_key::SshKeyView>,
130
131    pub favorite: bool,
132    pub reprompt: CipherRepromptType,
133    pub organization_use_totp: bool,
134    pub edit: bool,
135    pub permissions: Option<CipherPermissions>,
136    pub view_password: bool,
137    pub local_data: Option<LocalDataView>,
138
139    pub attachments: Option<Vec<attachment::AttachmentView>>,
140    pub fields: Option<Vec<field::FieldView>>,
141    pub password_history: Option<Vec<password_history::PasswordHistoryView>>,
142
143    pub creation_date: DateTime<Utc>,
144    pub deleted_date: Option<DateTime<Utc>>,
145    pub revision_date: DateTime<Utc>,
146}
147
148#[derive(Serialize, Deserialize, Debug, PartialEq)]
149#[serde(rename_all = "camelCase", deny_unknown_fields)]
150#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
151#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
152pub enum CipherListViewType {
153    Login(LoginListView),
154    SecureNote,
155    Card,
156    Identity,
157    SshKey,
158}
159
160#[derive(Serialize, Deserialize, Debug, PartialEq)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
163#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
164pub struct CipherListView {
165    pub id: Option<Uuid>,
166    pub organization_id: Option<Uuid>,
167    pub folder_id: Option<Uuid>,
168    pub collection_ids: Vec<Uuid>,
169
170    /// Temporary, required to support calculating TOTP from CipherListView.
171    pub key: Option<EncString>,
172
173    pub name: String,
174    pub subtitle: String,
175
176    pub r#type: CipherListViewType,
177
178    pub favorite: bool,
179    pub reprompt: CipherRepromptType,
180    pub organization_use_totp: bool,
181    pub edit: bool,
182    pub permissions: Option<CipherPermissions>,
183
184    pub view_password: bool,
185
186    /// The number of attachments
187    pub attachments: u32,
188
189    pub creation_date: DateTime<Utc>,
190    pub deleted_date: Option<DateTime<Utc>>,
191    pub revision_date: DateTime<Utc>,
192}
193
194impl CipherListView {
195    pub(crate) fn get_totp_key(
196        self,
197        ctx: &mut KeyStoreContext<KeyIds>,
198    ) -> Result<Option<String>, CryptoError> {
199        let key = self.key_identifier();
200        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
201
202        let totp = match self.r#type {
203            CipherListViewType::Login(LoginListView { totp, .. }) => {
204                totp.map(|t| t.decrypt(ctx, ciphers_key)).transpose()?
205            }
206            _ => None,
207        };
208
209        Ok(totp)
210    }
211}
212
213impl Encryptable<KeyIds, SymmetricKeyId, Cipher> for CipherView {
214    fn encrypt(
215        &self,
216        ctx: &mut KeyStoreContext<KeyIds>,
217        key: SymmetricKeyId,
218    ) -> Result<Cipher, CryptoError> {
219        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
220
221        let mut cipher_view = self.clone();
222
223        // For compatibility reasons, we only create checksums for ciphers that have a key
224        if cipher_view.key.is_some() {
225            cipher_view.generate_checksums();
226        }
227
228        Ok(Cipher {
229            id: cipher_view.id,
230            organization_id: cipher_view.organization_id,
231            folder_id: cipher_view.folder_id,
232            collection_ids: cipher_view.collection_ids,
233            key: cipher_view.key,
234            name: cipher_view.name.encrypt(ctx, ciphers_key)?,
235            notes: cipher_view.notes.encrypt(ctx, ciphers_key)?,
236            r#type: cipher_view.r#type,
237            login: cipher_view.login.encrypt(ctx, ciphers_key)?,
238            identity: cipher_view.identity.encrypt(ctx, ciphers_key)?,
239            card: cipher_view.card.encrypt(ctx, ciphers_key)?,
240            secure_note: cipher_view.secure_note.encrypt(ctx, ciphers_key)?,
241            ssh_key: cipher_view.ssh_key.encrypt(ctx, ciphers_key)?,
242            favorite: cipher_view.favorite,
243            reprompt: cipher_view.reprompt,
244            organization_use_totp: cipher_view.organization_use_totp,
245            edit: cipher_view.edit,
246            view_password: cipher_view.view_password,
247            local_data: cipher_view.local_data.encrypt(ctx, ciphers_key)?,
248            attachments: cipher_view.attachments.encrypt(ctx, ciphers_key)?,
249            fields: cipher_view.fields.encrypt(ctx, ciphers_key)?,
250            password_history: cipher_view.password_history.encrypt(ctx, ciphers_key)?,
251            creation_date: cipher_view.creation_date,
252            deleted_date: cipher_view.deleted_date,
253            revision_date: cipher_view.revision_date,
254            permissions: cipher_view.permissions,
255        })
256    }
257}
258
259impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
260    fn decrypt(
261        &self,
262        ctx: &mut KeyStoreContext<KeyIds>,
263        key: SymmetricKeyId,
264    ) -> Result<CipherView, CryptoError> {
265        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
266
267        let mut cipher = CipherView {
268            id: self.id,
269            organization_id: self.organization_id,
270            folder_id: self.folder_id,
271            collection_ids: self.collection_ids.clone(),
272            key: self.key.clone(),
273            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
274            notes: self.notes.decrypt(ctx, ciphers_key).ok().flatten(),
275            r#type: self.r#type,
276            login: self.login.decrypt(ctx, ciphers_key).ok().flatten(),
277            identity: self.identity.decrypt(ctx, ciphers_key).ok().flatten(),
278            card: self.card.decrypt(ctx, ciphers_key).ok().flatten(),
279            secure_note: self.secure_note.decrypt(ctx, ciphers_key).ok().flatten(),
280            ssh_key: self.ssh_key.decrypt(ctx, ciphers_key).ok().flatten(),
281            favorite: self.favorite,
282            reprompt: self.reprompt,
283            organization_use_totp: self.organization_use_totp,
284            edit: self.edit,
285            permissions: self.permissions,
286            view_password: self.view_password,
287            local_data: self.local_data.decrypt(ctx, ciphers_key).ok().flatten(),
288            attachments: self.attachments.decrypt(ctx, ciphers_key).ok().flatten(),
289            fields: self.fields.decrypt(ctx, ciphers_key).ok().flatten(),
290            password_history: self
291                .password_history
292                .decrypt(ctx, ciphers_key)
293                .ok()
294                .flatten(),
295            creation_date: self.creation_date,
296            deleted_date: self.deleted_date,
297            revision_date: self.revision_date,
298        };
299
300        // For compatibility we only remove URLs with invalid checksums if the cipher has a key
301        if cipher.key.is_some() {
302            cipher.remove_invalid_checksums();
303        }
304
305        Ok(cipher)
306    }
307}
308
309impl Cipher {
310    /// Decrypt the individual encryption key for this cipher into the provided [KeyStoreContext]
311    /// and return it's identifier. Note that some ciphers do not have individual encryption
312    /// keys, in which case this will return the provided key identifier instead
313    ///
314    /// # Arguments
315    ///
316    /// * `ctx` - The key store context where the cipher key will be decrypted, if it exists
317    /// * `key` - The key to use to decrypt the cipher key, this should be the user or organization
318    ///   key
319    /// * `ciphers_key` - The encrypted cipher key
320    pub(super) fn decrypt_cipher_key(
321        ctx: &mut KeyStoreContext<KeyIds>,
322        key: SymmetricKeyId,
323        ciphers_key: &Option<EncString>,
324    ) -> Result<SymmetricKeyId, CryptoError> {
325        const CIPHER_KEY: SymmetricKeyId = SymmetricKeyId::Local("cipher_key");
326        match ciphers_key {
327            Some(ciphers_key) => ctx.unwrap_symmetric_key(key, CIPHER_KEY, ciphers_key),
328            None => Ok(key),
329        }
330    }
331
332    fn get_decrypted_subtitle(
333        &self,
334        ctx: &mut KeyStoreContext<KeyIds>,
335        key: SymmetricKeyId,
336    ) -> Result<String, CryptoError> {
337        Ok(match self.r#type {
338            CipherType::Login => {
339                let Some(login) = &self.login else {
340                    return Ok(String::new());
341                };
342                login.username.decrypt(ctx, key)?.unwrap_or_default()
343            }
344            CipherType::SecureNote => String::new(),
345            CipherType::Card => {
346                let Some(card) = &self.card else {
347                    return Ok(String::new());
348                };
349
350                build_subtitle_card(
351                    card.brand
352                        .as_ref()
353                        .map(|b| b.decrypt(ctx, key))
354                        .transpose()?,
355                    card.number
356                        .as_ref()
357                        .map(|n| n.decrypt(ctx, key))
358                        .transpose()?,
359                )
360            }
361            CipherType::Identity => {
362                let Some(identity) = &self.identity else {
363                    return Ok(String::new());
364                };
365
366                build_subtitle_identity(
367                    identity
368                        .first_name
369                        .as_ref()
370                        .map(|f| f.decrypt(ctx, key))
371                        .transpose()?,
372                    identity
373                        .last_name
374                        .as_ref()
375                        .map(|l| l.decrypt(ctx, key))
376                        .transpose()?,
377                )
378            }
379            CipherType::SshKey => {
380                let Some(ssh_key) = &self.ssh_key else {
381                    return Ok(String::new());
382                };
383
384                Some(ssh_key.fingerprint.clone())
385                    .as_ref()
386                    .map(|c| c.decrypt(ctx, key))
387                    .transpose()?
388                    .unwrap_or_default()
389            }
390        })
391    }
392}
393
394/// Builds the subtitle for a card cipher
395fn build_subtitle_card(brand: Option<String>, number: Option<String>) -> String {
396    // Attempt to pre-allocate the string with the expected max-size
397    let mut subtitle =
398        String::with_capacity(brand.as_ref().map(|b| b.len()).unwrap_or_default() + 8);
399
400    if let Some(brand) = brand {
401        subtitle.push_str(&brand);
402    }
403
404    if let Some(number) = number {
405        let number_len = number.len();
406        if number_len > 4 {
407            if !subtitle.is_empty() {
408                subtitle.push_str(", ");
409            }
410
411            // On AMEX cards we show 5 digits instead of 4
412            let digit_count = match &number[0..2] {
413                "34" | "37" => 5,
414                _ => 4,
415            };
416
417            subtitle.push('*');
418            subtitle.push_str(&number[(number_len - digit_count)..]);
419        }
420    }
421
422    subtitle
423}
424
425/// Builds the subtitle for a card cipher
426fn build_subtitle_identity(first_name: Option<String>, last_name: Option<String>) -> String {
427    let len = match (first_name.as_ref(), last_name.as_ref()) {
428        (Some(first_name), Some(last_name)) => first_name.len() + last_name.len() + 1,
429        (Some(first_name), None) => first_name.len(),
430        (None, Some(last_name)) => last_name.len(),
431        (None, None) => 0,
432    };
433
434    let mut subtitle = String::with_capacity(len);
435
436    if let Some(first_name) = &first_name {
437        subtitle.push_str(first_name);
438    }
439
440    if let Some(last_name) = &last_name {
441        if !subtitle.is_empty() {
442            subtitle.push(' ');
443        }
444        subtitle.push_str(last_name);
445    }
446
447    subtitle
448}
449
450impl CipherView {
451    pub fn generate_cipher_key(
452        &mut self,
453        ctx: &mut KeyStoreContext<KeyIds>,
454        key: SymmetricKeyId,
455    ) -> Result<(), CryptoError> {
456        let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
457
458        const NEW_KEY: SymmetricKeyId = SymmetricKeyId::Local("new_cipher_key");
459
460        let new_key = ctx.generate_symmetric_key(NEW_KEY)?;
461
462        self.reencrypt_attachment_keys(ctx, old_ciphers_key, new_key)?;
463        self.reencrypt_fido2_credentials(ctx, old_ciphers_key, new_key)?;
464
465        self.key = Some(ctx.wrap_symmetric_key(key, new_key)?);
466        Ok(())
467    }
468
469    pub fn generate_checksums(&mut self) {
470        if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
471            for uri in uris {
472                uri.generate_checksum();
473            }
474        }
475    }
476
477    pub fn remove_invalid_checksums(&mut self) {
478        if let Some(uris) = self.login.as_mut().and_then(|l| l.uris.as_mut()) {
479            uris.retain(|u| u.is_checksum_valid());
480        }
481    }
482
483    fn reencrypt_attachment_keys(
484        &mut self,
485        ctx: &mut KeyStoreContext<KeyIds>,
486        old_key: SymmetricKeyId,
487        new_key: SymmetricKeyId,
488    ) -> Result<(), CryptoError> {
489        if let Some(attachments) = &mut self.attachments {
490            for attachment in attachments {
491                if let Some(attachment_key) = &mut attachment.key {
492                    let dec_attachment_key: Vec<u8> = attachment_key.decrypt(ctx, old_key)?;
493                    *attachment_key = dec_attachment_key.encrypt(ctx, new_key)?;
494                }
495            }
496        }
497        Ok(())
498    }
499
500    pub fn decrypt_fido2_credentials(
501        &self,
502        ctx: &mut KeyStoreContext<KeyIds>,
503    ) -> Result<Vec<Fido2CredentialView>, CryptoError> {
504        let key = self.key_identifier();
505        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
506
507        Ok(self
508            .login
509            .as_ref()
510            .and_then(|l| l.fido2_credentials.as_ref())
511            .map(|f| f.decrypt(ctx, ciphers_key))
512            .transpose()?
513            .unwrap_or_default())
514    }
515
516    fn reencrypt_fido2_credentials(
517        &mut self,
518        ctx: &mut KeyStoreContext<KeyIds>,
519        old_key: SymmetricKeyId,
520        new_key: SymmetricKeyId,
521    ) -> Result<(), CryptoError> {
522        if let Some(login) = self.login.as_mut() {
523            if let Some(fido2_credentials) = &mut login.fido2_credentials {
524                let dec_fido2_credentials: Vec<Fido2CredentialFullView> =
525                    fido2_credentials.decrypt(ctx, old_key)?;
526                *fido2_credentials = dec_fido2_credentials.encrypt(ctx, new_key)?;
527            }
528        }
529        Ok(())
530    }
531
532    pub fn move_to_organization(
533        &mut self,
534        ctx: &mut KeyStoreContext<KeyIds>,
535        organization_id: Uuid,
536    ) -> Result<(), CipherError> {
537        let old_key = self.key_identifier();
538        let new_key = SymmetricKeyId::Organization(organization_id);
539
540        // If any attachment is missing a key we can't reencrypt the attachment keys
541        if self.attachments.iter().flatten().any(|a| a.key.is_none()) {
542            return Err(CipherError::AttachmentsWithoutKeys);
543        }
544
545        // If the cipher has a key, we need to re-encrypt it with the new organization key
546        if let Some(cipher_key) = &mut self.key {
547            let dec_cipher_key: Vec<u8> = cipher_key.decrypt(ctx, old_key)?;
548            *cipher_key = dec_cipher_key.encrypt(ctx, new_key)?;
549        } else {
550            // If the cipher does not have a key, we need to reencrypt all attachment keys
551            self.reencrypt_attachment_keys(ctx, old_key, new_key)?;
552            self.reencrypt_fido2_credentials(ctx, old_key, new_key)?;
553        }
554
555        self.organization_id = Some(organization_id);
556        Ok(())
557    }
558
559    pub fn set_new_fido2_credentials(
560        &mut self,
561        ctx: &mut KeyStoreContext<KeyIds>,
562        creds: Vec<Fido2CredentialFullView>,
563    ) -> Result<(), CipherError> {
564        let key = self.key_identifier();
565
566        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
567
568        require!(self.login.as_mut()).fido2_credentials = Some(creds.encrypt(ctx, ciphers_key)?);
569
570        Ok(())
571    }
572
573    pub fn get_fido2_credentials(
574        &self,
575        ctx: &mut KeyStoreContext<KeyIds>,
576    ) -> Result<Vec<Fido2CredentialFullView>, CipherError> {
577        let key = self.key_identifier();
578
579        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
580
581        let login = require!(self.login.as_ref());
582        let creds = require!(login.fido2_credentials.as_ref());
583        let res = creds.decrypt(ctx, ciphers_key)?;
584        Ok(res)
585    }
586
587    pub fn decrypt_fido2_private_key(
588        &self,
589        ctx: &mut KeyStoreContext<KeyIds>,
590    ) -> Result<String, CipherError> {
591        let fido2_credential = self.get_fido2_credentials(ctx)?;
592
593        Ok(fido2_credential[0].key_value.clone())
594    }
595}
596
597impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
598    fn decrypt(
599        &self,
600        ctx: &mut KeyStoreContext<KeyIds>,
601        key: SymmetricKeyId,
602    ) -> Result<CipherListView, CryptoError> {
603        let ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
604
605        Ok(CipherListView {
606            id: self.id,
607            organization_id: self.organization_id,
608            folder_id: self.folder_id,
609            collection_ids: self.collection_ids.clone(),
610            key: self.key.clone(),
611            name: self.name.decrypt(ctx, ciphers_key).ok().unwrap_or_default(),
612            subtitle: self
613                .get_decrypted_subtitle(ctx, ciphers_key)
614                .ok()
615                .unwrap_or_default(),
616            r#type: match self.r#type {
617                CipherType::Login => {
618                    let login = self
619                        .login
620                        .as_ref()
621                        .ok_or(CryptoError::MissingField("login"))?;
622                    CipherListViewType::Login(login.decrypt(ctx, ciphers_key)?)
623                }
624                CipherType::SecureNote => CipherListViewType::SecureNote,
625                CipherType::Card => CipherListViewType::Card,
626                CipherType::Identity => CipherListViewType::Identity,
627                CipherType::SshKey => CipherListViewType::SshKey,
628            },
629            favorite: self.favorite,
630            reprompt: self.reprompt,
631            organization_use_totp: self.organization_use_totp,
632            edit: self.edit,
633            permissions: self.permissions,
634            view_password: self.view_password,
635            attachments: self
636                .attachments
637                .as_ref()
638                .map(|a| a.len() as u32)
639                .unwrap_or(0),
640            creation_date: self.creation_date,
641            deleted_date: self.deleted_date,
642            revision_date: self.revision_date,
643        })
644    }
645}
646
647impl IdentifyKey<SymmetricKeyId> for Cipher {
648    fn key_identifier(&self) -> SymmetricKeyId {
649        match self.organization_id {
650            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
651            None => SymmetricKeyId::User,
652        }
653    }
654}
655
656impl IdentifyKey<SymmetricKeyId> for CipherView {
657    fn key_identifier(&self) -> SymmetricKeyId {
658        match self.organization_id {
659            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
660            None => SymmetricKeyId::User,
661        }
662    }
663}
664
665impl IdentifyKey<SymmetricKeyId> for CipherListView {
666    fn key_identifier(&self) -> SymmetricKeyId {
667        match self.organization_id {
668            Some(organization_id) => SymmetricKeyId::Organization(organization_id),
669            None => SymmetricKeyId::User,
670        }
671    }
672}
673
674impl TryFrom<CipherDetailsResponseModel> for Cipher {
675    type Error = VaultParseError;
676
677    fn try_from(cipher: CipherDetailsResponseModel) -> Result<Self, Self::Error> {
678        Ok(Self {
679            id: cipher.id,
680            organization_id: cipher.organization_id,
681            folder_id: cipher.folder_id,
682            collection_ids: cipher.collection_ids.unwrap_or_default(),
683            name: require!(EncString::try_from_optional(cipher.name)?),
684            notes: EncString::try_from_optional(cipher.notes)?,
685            r#type: require!(cipher.r#type).into(),
686            login: cipher.login.map(|l| (*l).try_into()).transpose()?,
687            identity: cipher.identity.map(|i| (*i).try_into()).transpose()?,
688            card: cipher.card.map(|c| (*c).try_into()).transpose()?,
689            secure_note: cipher.secure_note.map(|s| (*s).try_into()).transpose()?,
690            // TODO: add ssh_key when api bindings have been updated
691            ssh_key: None,
692            favorite: cipher.favorite.unwrap_or(false),
693            reprompt: cipher
694                .reprompt
695                .map(|r| r.into())
696                .unwrap_or(CipherRepromptType::None),
697            organization_use_totp: cipher.organization_use_totp.unwrap_or(true),
698            edit: cipher.edit.unwrap_or(true),
699            // TODO: add permissions when api bindings have been updated
700            permissions: None,
701            view_password: cipher.view_password.unwrap_or(true),
702            local_data: None, // Not sent from server
703            attachments: cipher
704                .attachments
705                .map(|a| a.into_iter().map(|a| a.try_into()).collect())
706                .transpose()?,
707            fields: cipher
708                .fields
709                .map(|f| f.into_iter().map(|f| f.try_into()).collect())
710                .transpose()?,
711            password_history: cipher
712                .password_history
713                .map(|p| p.into_iter().map(|p| p.try_into()).collect())
714                .transpose()?,
715            creation_date: require!(cipher.creation_date).parse()?,
716            deleted_date: cipher.deleted_date.map(|d| d.parse()).transpose()?,
717            revision_date: require!(cipher.revision_date).parse()?,
718            key: EncString::try_from_optional(cipher.key)?,
719        })
720    }
721}
722
723impl From<bitwarden_api_api::models::CipherType> for CipherType {
724    fn from(t: bitwarden_api_api::models::CipherType) -> Self {
725        match t {
726            bitwarden_api_api::models::CipherType::Login => CipherType::Login,
727            bitwarden_api_api::models::CipherType::SecureNote => CipherType::SecureNote,
728            bitwarden_api_api::models::CipherType::Card => CipherType::Card,
729            bitwarden_api_api::models::CipherType::Identity => CipherType::Identity,
730            bitwarden_api_api::models::CipherType::SSHKey => CipherType::SshKey,
731        }
732    }
733}
734
735impl From<bitwarden_api_api::models::CipherRepromptType> for CipherRepromptType {
736    fn from(t: bitwarden_api_api::models::CipherRepromptType) -> Self {
737        match t {
738            bitwarden_api_api::models::CipherRepromptType::None => CipherRepromptType::None,
739            bitwarden_api_api::models::CipherRepromptType::Password => CipherRepromptType::Password,
740        }
741    }
742}
743
744#[cfg(test)]
745mod tests {
746
747    use attachment::AttachmentView;
748    use bitwarden_core::key_management::{
749        create_test_crypto_with_user_and_org_key, create_test_crypto_with_user_key,
750    };
751    use bitwarden_crypto::SymmetricCryptoKey;
752    use ssh_key::SshKey;
753
754    use super::*;
755    use crate::{login::Fido2CredentialListView, Fido2Credential};
756
757    fn generate_cipher() -> CipherView {
758        let test_id: uuid::Uuid = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
759        CipherView {
760            r#type: CipherType::Login,
761            login: Some(LoginView {
762                username: Some("test_username".to_string()),
763                password: Some("test_password".to_string()),
764                password_revision_date: None,
765                uris: None,
766                totp: None,
767                autofill_on_page_load: None,
768                fido2_credentials: None,
769            }),
770            id: Some(test_id),
771            organization_id: None,
772            folder_id: None,
773            collection_ids: vec![],
774            key: None,
775            name: "My test login".to_string(),
776            notes: None,
777            identity: None,
778            card: None,
779            secure_note: None,
780            ssh_key: None,
781            favorite: false,
782            reprompt: CipherRepromptType::None,
783            organization_use_totp: true,
784            edit: true,
785            permissions: None,
786            view_password: true,
787            local_data: None,
788            attachments: None,
789            fields: None,
790            password_history: None,
791            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
792            deleted_date: None,
793            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
794        }
795    }
796
797    fn generate_fido2(ctx: &mut KeyStoreContext<KeyIds>, key: SymmetricKeyId) -> Fido2Credential {
798        Fido2Credential {
799            credential_id: "123".to_string().encrypt(ctx, key).unwrap(),
800            key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
801            key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
802            key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
803            key_value: "123".to_string().encrypt(ctx, key).unwrap(),
804            rp_id: "123".to_string().encrypt(ctx, key).unwrap(),
805            user_handle: None,
806            user_name: None,
807            counter: "123".to_string().encrypt(ctx, key).unwrap(),
808            rp_name: None,
809            user_display_name: None,
810            discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
811            creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
812        }
813    }
814
815    #[test]
816    fn test_decrypt_cipher_list_view() {
817        let key: SymmetricCryptoKey = "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q==".to_string().try_into().unwrap();
818        let key_store = create_test_crypto_with_user_key(key);
819
820        let cipher = Cipher {
821            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
822            organization_id: None,
823            folder_id: None,
824            collection_ids: vec![],
825            key: None,
826            name: "2.d3rzo0P8rxV9Hs1m1BmAjw==|JOwna6i0zs+K7ZghwrZRuw==|SJqKreLag1ID+g6H1OdmQr0T5zTrVWKzD6hGy3fDqB0=".parse().unwrap(),
827            notes: None,
828            r#type: CipherType::Login,
829            login: Some(Login {
830                username: Some("2.EBNGgnaMHeO/kYnI3A0jiA==|9YXlrgABP71ebZ5umurCJQ==|GDk5jxiqTYaU7e2AStCFGX+a1kgCIk8j0NEli7Jn0L4=".parse().unwrap()),
831                password: Some("2.M7ZJ7EuFDXCq66gDTIyRIg==|B1V+jroo6+m/dpHx6g8DxA==|PIXPBCwyJ1ady36a7jbcLg346pm/7N/06W4UZxc1TUo=".parse().unwrap()),
832                password_revision_date: None,
833                uris: None,
834                totp: Some("2.hqdioUAc81FsKQmO1XuLQg==|oDRdsJrQjoFu9NrFVy8tcJBAFKBx95gHaXZnWdXbKpsxWnOr2sKipIG43pKKUFuq|3gKZMiboceIB5SLVOULKg2iuyu6xzos22dfJbvx0EHk=".parse().unwrap()),
835                autofill_on_page_load: None,
836                fido2_credentials: Some(vec![generate_fido2(&mut key_store.context(), SymmetricKeyId::User)]),
837            }),
838            identity: None,
839            card: None,
840            secure_note: None,
841            ssh_key: None,
842            favorite: false,
843            reprompt: CipherRepromptType::None,
844            organization_use_totp: false,
845            edit: true,
846            permissions: Some(CipherPermissions {
847                delete: false,
848                restore: false
849            }),
850            view_password: true,
851            local_data: None,
852            attachments: None,
853            fields: None,
854            password_history: None,
855            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
856            deleted_date: None,
857            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
858        };
859
860        let view: CipherListView = key_store.decrypt(&cipher).unwrap();
861
862        assert_eq!(
863            view,
864            CipherListView {
865                id: cipher.id,
866                organization_id: cipher.organization_id,
867                folder_id: cipher.folder_id,
868                collection_ids: cipher.collection_ids,
869                key: cipher.key,
870                name: "My test login".to_string(),
871                subtitle: "test_username".to_string(),
872                r#type: CipherListViewType::Login(LoginListView {
873                    fido2_credentials: Some(vec![Fido2CredentialListView {
874                        credential_id: "123".to_string(),
875                        rp_id: "123".to_string(),
876                        user_handle: None,
877                        user_name: None,
878                        user_display_name: None,
879                    }]),
880                    has_fido2: true,
881                    username: Some("test_username".to_string()),
882                    totp: cipher.login.as_ref().unwrap().totp.clone(),
883                    uris: None,
884                }),
885                favorite: cipher.favorite,
886                reprompt: cipher.reprompt,
887                organization_use_totp: cipher.organization_use_totp,
888                edit: cipher.edit,
889                permissions: cipher.permissions,
890                view_password: cipher.view_password,
891                attachments: 0,
892                creation_date: cipher.creation_date,
893                deleted_date: cipher.deleted_date,
894                revision_date: cipher.revision_date
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_build_subtitle_card_visa() {
1196        let brand = Some("Visa".to_owned());
1197        let number = Some("4111111111111111".to_owned());
1198
1199        let subtitle = build_subtitle_card(brand, number);
1200        assert_eq!(subtitle, "Visa, *1111");
1201    }
1202
1203    #[test]
1204    fn test_build_subtitle_card_mastercard() {
1205        let brand = Some("Mastercard".to_owned());
1206        let number = Some("5555555555554444".to_owned());
1207
1208        let subtitle = build_subtitle_card(brand, number);
1209        assert_eq!(subtitle, "Mastercard, *4444");
1210    }
1211
1212    #[test]
1213    fn test_build_subtitle_card_amex() {
1214        let brand = Some("Amex".to_owned());
1215        let number = Some("378282246310005".to_owned());
1216
1217        let subtitle = build_subtitle_card(brand, number);
1218        assert_eq!(subtitle, "Amex, *10005");
1219    }
1220
1221    #[test]
1222    fn test_build_subtitle_card_underflow() {
1223        let brand = Some("Mastercard".to_owned());
1224        let number = Some("4".to_owned());
1225
1226        let subtitle = build_subtitle_card(brand, number);
1227        assert_eq!(subtitle, "Mastercard");
1228    }
1229
1230    #[test]
1231    fn test_build_subtitle_card_only_brand() {
1232        let brand = Some("Mastercard".to_owned());
1233        let number = None;
1234
1235        let subtitle = build_subtitle_card(brand, number);
1236        assert_eq!(subtitle, "Mastercard");
1237    }
1238
1239    #[test]
1240    fn test_build_subtitle_card_only_card() {
1241        let brand = None;
1242        let number = Some("5555555555554444".to_owned());
1243
1244        let subtitle = build_subtitle_card(brand, number);
1245        assert_eq!(subtitle, "*4444");
1246    }
1247
1248    #[test]
1249    fn test_build_subtitle_identity() {
1250        let first_name = Some("John".to_owned());
1251        let last_name = Some("Doe".to_owned());
1252
1253        let subtitle = build_subtitle_identity(first_name, last_name);
1254        assert_eq!(subtitle, "John Doe");
1255    }
1256
1257    #[test]
1258    fn test_build_subtitle_identity_only_first() {
1259        let first_name = Some("John".to_owned());
1260        let last_name = None;
1261
1262        let subtitle = build_subtitle_identity(first_name, last_name);
1263        assert_eq!(subtitle, "John");
1264    }
1265
1266    #[test]
1267    fn test_build_subtitle_identity_only_last() {
1268        let first_name = None;
1269        let last_name = Some("Doe".to_owned());
1270
1271        let subtitle = build_subtitle_identity(first_name, last_name);
1272        assert_eq!(subtitle, "Doe");
1273    }
1274
1275    #[test]
1276    fn test_build_subtitle_identity_none() {
1277        let first_name = None;
1278        let last_name = None;
1279
1280        let subtitle = build_subtitle_identity(first_name, last_name);
1281        assert_eq!(subtitle, "");
1282    }
1283
1284    #[test]
1285    fn test_subtitle_ssh_key() {
1286        let key = SymmetricCryptoKey::try_from("hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string()).unwrap();
1287        let key_store = create_test_crypto_with_user_key(key);
1288        let key = SymmetricKeyId::User;
1289        let mut ctx = key_store.context();
1290
1291        let original_subtitle = "SHA256:1JjFjvPRkj1Gbf2qRP1dgHiIzEuNAEvp+92x99jw3K0".to_string();
1292        let fingerprint_encrypted = original_subtitle.to_owned().encrypt(&mut ctx, key).unwrap();
1293        let private_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
1294        let public_key_encrypted = "".to_string().encrypt(&mut ctx, key).unwrap();
1295        let ssh_key_cipher = Cipher {
1296            id: Some("090c19ea-a61a-4df6-8963-262b97bc6266".parse().unwrap()),
1297            organization_id: None,
1298            folder_id: None,
1299            collection_ids: vec![],
1300            r#type: CipherType::SshKey,
1301            key: None,
1302            name: "My test ssh key"
1303                .to_string()
1304                .encrypt(&mut ctx, key)
1305                .unwrap(),
1306            notes: None,
1307            login: None,
1308            identity: None,
1309            card: None,
1310            secure_note: None,
1311            ssh_key: Some(SshKey {
1312                private_key: private_key_encrypted,
1313                public_key: public_key_encrypted,
1314                fingerprint: fingerprint_encrypted,
1315            }),
1316            favorite: false,
1317            reprompt: CipherRepromptType::None,
1318            organization_use_totp: false,
1319            edit: true,
1320            permissions: None,
1321            view_password: true,
1322            local_data: None,
1323            attachments: None,
1324            fields: None,
1325            password_history: None,
1326            creation_date: "2024-01-01T00:00:00.000Z".parse().unwrap(),
1327            deleted_date: None,
1328            revision_date: "2024-01-01T00:00:00.000Z".parse().unwrap(),
1329        };
1330        let subtitle = ssh_key_cipher
1331            .get_decrypted_subtitle(&mut ctx, key)
1332            .unwrap();
1333        assert_eq!(subtitle, original_subtitle);
1334    }
1335
1336    #[test]
1337    fn test_decrypt_fido2_private_key() {
1338        let key_store =
1339            create_test_crypto_with_user_key(SymmetricCryptoKey::make_aes256_cbc_hmac_key());
1340        let mut ctx = key_store.context();
1341
1342        let mut cipher_view = generate_cipher();
1343        cipher_view
1344            .generate_cipher_key(&mut ctx, cipher_view.key_identifier())
1345            .unwrap();
1346
1347        let key_id = cipher_view.key_identifier();
1348        let ciphers_key = Cipher::decrypt_cipher_key(&mut ctx, key_id, &cipher_view.key).unwrap();
1349
1350        let fido2_credential = generate_fido2(&mut ctx, ciphers_key);
1351
1352        cipher_view.login.as_mut().unwrap().fido2_credentials =
1353            Some(vec![fido2_credential.clone()]);
1354
1355        let decrypted_key_value = cipher_view.decrypt_fido2_private_key(&mut ctx).unwrap();
1356        assert_eq!(decrypted_key_value, "123");
1357    }
1358}