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