Skip to main content

bitwarden_vault/cipher/
login.rs

1use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel};
2use bitwarden_core::{
3    key_management::{KeyIds, SymmetricKeyId},
4    require,
5};
6use bitwarden_crypto::{
7    CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
8    PrimitiveEncryptable,
9};
10use bitwarden_encoding::B64;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14use subtle::ConstantTimeEq;
15#[cfg(feature = "wasm")]
16use tsify::Tsify;
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::wasm_bindgen;
19
20use super::cipher::CipherKind;
21use crate::{Cipher, PasswordHistoryView, VaultParseError, cipher::cipher::CopyableCipherFields};
22
23#[allow(missing_docs)]
24#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
25#[repr(u8)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28#[cfg_attr(feature = "wasm", wasm_bindgen)]
29pub enum UriMatchType {
30    Domain = 0,
31    Host = 1,
32    StartsWith = 2,
33    Exact = 3,
34    RegularExpression = 4,
35    Never = 5,
36}
37
38#[derive(Serialize, Deserialize, Debug, Clone)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
41#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
42pub struct LoginUri {
43    pub uri: Option<EncString>,
44    pub r#match: Option<UriMatchType>,
45    pub uri_checksum: Option<EncString>,
46}
47
48#[allow(missing_docs)]
49#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53pub struct LoginUriView {
54    pub uri: Option<String>,
55    pub r#match: Option<UriMatchType>,
56    pub uri_checksum: Option<String>,
57}
58
59impl LoginUriView {
60    pub(crate) fn is_checksum_valid(&self) -> bool {
61        let Some(uri) = &self.uri else {
62            return false;
63        };
64        let Some(cs) = &self.uri_checksum else {
65            return false;
66        };
67        let Ok(cs) = B64::try_from(cs.as_str()) else {
68            return false;
69        };
70
71        use sha2::Digest;
72        let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
73
74        uri_hash.as_slice().ct_eq(cs.as_bytes()).into()
75    }
76
77    pub(crate) fn generate_checksum(&mut self) {
78        if let Some(uri) = &self.uri {
79            use sha2::Digest;
80            let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
81            let uri_hash = B64::from(uri_hash.as_slice()).to_string();
82            self.uri_checksum = Some(uri_hash);
83        }
84    }
85}
86
87#[allow(missing_docs)]
88#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
91#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
92pub struct Fido2Credential {
93    pub credential_id: EncString,
94    pub key_type: EncString,
95    pub key_algorithm: EncString,
96    pub key_curve: EncString,
97    pub key_value: EncString,
98    pub rp_id: EncString,
99    pub user_handle: Option<EncString>,
100    pub user_name: Option<EncString>,
101    pub counter: EncString,
102    pub rp_name: Option<EncString>,
103    pub user_display_name: Option<EncString>,
104    pub discoverable: EncString,
105    pub creation_date: DateTime<Utc>,
106}
107
108#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
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 Fido2CredentialListView {
113    pub credential_id: String,
114    pub rp_id: String,
115    pub user_handle: Option<String>,
116    pub user_name: Option<String>,
117    pub user_display_name: Option<String>,
118    pub counter: String,
119}
120
121#[allow(missing_docs)]
122#[derive(Serialize, Deserialize, Debug, Clone)]
123#[serde(rename_all = "camelCase", deny_unknown_fields)]
124#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
125#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
126pub struct Fido2CredentialView {
127    pub credential_id: String,
128    pub key_type: String,
129    pub key_algorithm: String,
130    pub key_curve: String,
131    // This value doesn't need to be returned to the client
132    // so we keep it encrypted until we need it
133    pub key_value: EncString,
134    pub rp_id: String,
135    pub user_handle: Option<String>,
136    pub user_name: Option<String>,
137    pub counter: String,
138    pub rp_name: Option<String>,
139    pub user_display_name: Option<String>,
140    pub discoverable: String,
141    pub creation_date: DateTime<Utc>,
142}
143
144// This is mostly a copy of the Fido2CredentialView, but with the key exposed
145// Only meant to be used internally and not exposed to the outside world
146#[allow(missing_docs)]
147#[derive(Serialize, Deserialize, Debug, Clone)]
148#[serde(rename_all = "camelCase", deny_unknown_fields)]
149#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
150pub struct Fido2CredentialFullView {
151    pub credential_id: String,
152    pub key_type: String,
153    pub key_algorithm: String,
154    pub key_curve: String,
155    pub key_value: String,
156    pub rp_id: String,
157    pub user_handle: Option<String>,
158    pub user_name: Option<String>,
159    pub counter: String,
160    pub rp_name: Option<String>,
161    pub user_display_name: Option<String>,
162    pub discoverable: String,
163    pub creation_date: DateTime<Utc>,
164}
165
166// This is mostly a copy of the Fido2CredentialView, meant to be exposed to the clients
167// to let them select where to store the new credential. Note that it doesn't contain
168// the encrypted key as that is only filled when the cipher is selected
169#[allow(missing_docs)]
170#[derive(Serialize, Deserialize, Debug, Clone)]
171#[serde(rename_all = "camelCase", deny_unknown_fields)]
172#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
173#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
174pub struct Fido2CredentialNewView {
175    pub credential_id: String,
176    pub key_type: String,
177    pub key_algorithm: String,
178    pub key_curve: String,
179    pub rp_id: String,
180    pub user_handle: Option<String>,
181    pub user_name: Option<String>,
182    pub counter: String,
183    pub rp_name: Option<String>,
184    pub user_display_name: Option<String>,
185    pub creation_date: DateTime<Utc>,
186}
187
188impl From<Fido2CredentialFullView> for Fido2CredentialNewView {
189    fn from(value: Fido2CredentialFullView) -> Self {
190        Fido2CredentialNewView {
191            credential_id: value.credential_id,
192            key_type: value.key_type,
193            key_algorithm: value.key_algorithm,
194            key_curve: value.key_curve,
195            rp_id: value.rp_id,
196            user_handle: value.user_handle,
197            user_name: value.user_name,
198            counter: value.counter,
199            rp_name: value.rp_name,
200            user_display_name: value.user_display_name,
201            creation_date: value.creation_date,
202        }
203    }
204}
205
206impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialFullView {
207    fn encrypt_composite(
208        &self,
209        ctx: &mut KeyStoreContext<KeyIds>,
210        key: SymmetricKeyId,
211    ) -> Result<Fido2Credential, CryptoError> {
212        Ok(Fido2Credential {
213            credential_id: self.credential_id.encrypt(ctx, key)?,
214            key_type: self.key_type.encrypt(ctx, key)?,
215            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
216            key_curve: self.key_curve.encrypt(ctx, key)?,
217            key_value: self.key_value.encrypt(ctx, key)?,
218            rp_id: self.rp_id.encrypt(ctx, key)?,
219            user_handle: self
220                .user_handle
221                .as_ref()
222                .map(|h| h.encrypt(ctx, key))
223                .transpose()?,
224            user_name: self.user_name.encrypt(ctx, key)?,
225            counter: self.counter.encrypt(ctx, key)?,
226            rp_name: self.rp_name.encrypt(ctx, key)?,
227            user_display_name: self.user_display_name.encrypt(ctx, key)?,
228            discoverable: self.discoverable.encrypt(ctx, key)?,
229            creation_date: self.creation_date,
230        })
231    }
232}
233
234impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2Credential {
235    fn decrypt(
236        &self,
237        ctx: &mut KeyStoreContext<KeyIds>,
238        key: SymmetricKeyId,
239    ) -> Result<Fido2CredentialFullView, CryptoError> {
240        Ok(Fido2CredentialFullView {
241            credential_id: self.credential_id.decrypt(ctx, key)?,
242            key_type: self.key_type.decrypt(ctx, key)?,
243            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
244            key_curve: self.key_curve.decrypt(ctx, key)?,
245            key_value: self.key_value.decrypt(ctx, key)?,
246            rp_id: self.rp_id.decrypt(ctx, key)?,
247            user_handle: self.user_handle.decrypt(ctx, key)?,
248            user_name: self.user_name.decrypt(ctx, key)?,
249            counter: self.counter.decrypt(ctx, key)?,
250            rp_name: self.rp_name.decrypt(ctx, key)?,
251            user_display_name: self.user_display_name.decrypt(ctx, key)?,
252            discoverable: self.discoverable.decrypt(ctx, key)?,
253            creation_date: self.creation_date,
254        })
255    }
256}
257
258impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2CredentialView {
259    fn decrypt(
260        &self,
261        ctx: &mut KeyStoreContext<KeyIds>,
262        key: SymmetricKeyId,
263    ) -> Result<Fido2CredentialFullView, CryptoError> {
264        Ok(Fido2CredentialFullView {
265            credential_id: self.credential_id.clone(),
266            key_type: self.key_type.clone(),
267            key_algorithm: self.key_algorithm.clone(),
268            key_curve: self.key_curve.clone(),
269            key_value: self.key_value.decrypt(ctx, key)?,
270            rp_id: self.rp_id.clone(),
271            user_handle: self.user_handle.clone(),
272            user_name: self.user_name.clone(),
273            counter: self.counter.clone(),
274            rp_name: self.rp_name.clone(),
275            user_display_name: self.user_display_name.clone(),
276            discoverable: self.discoverable.clone(),
277            creation_date: self.creation_date,
278        })
279    }
280}
281
282#[allow(missing_docs)]
283#[derive(Serialize, Deserialize, Debug, Clone)]
284#[serde(rename_all = "camelCase")]
285#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
286#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
287pub struct Login {
288    pub username: Option<EncString>,
289    pub password: Option<EncString>,
290    pub password_revision_date: Option<DateTime<Utc>>,
291
292    pub uris: Option<Vec<LoginUri>>,
293    pub totp: Option<EncString>,
294    pub autofill_on_page_load: Option<bool>,
295
296    pub fido2_credentials: Option<Vec<Fido2Credential>>,
297}
298
299#[allow(missing_docs)]
300#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
301#[serde(rename_all = "camelCase", deny_unknown_fields)]
302#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
303#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
304pub struct LoginView {
305    pub username: Option<String>,
306    pub password: Option<String>,
307    pub password_revision_date: Option<DateTime<Utc>>,
308
309    pub uris: Option<Vec<LoginUriView>>,
310    pub totp: Option<String>,
311    pub autofill_on_page_load: Option<bool>,
312
313    // TODO: Remove this once the SDK supports state
314    pub fido2_credentials: Option<Vec<Fido2Credential>>,
315}
316
317impl LoginView {
318    /// Generate checksums for all URIs in the login view
319    pub fn generate_checksums(&mut self) {
320        if let Some(uris) = &mut self.uris {
321            for uri in uris {
322                uri.generate_checksum();
323            }
324        }
325    }
326
327    /// Re-encrypts the fido2 credentials with a new key, replacing the old encrypted values.
328    pub fn reencrypt_fido2_credentials(
329        &mut self,
330        ctx: &mut KeyStoreContext<KeyIds>,
331        old_key: SymmetricKeyId,
332        new_key: SymmetricKeyId,
333    ) -> Result<(), CryptoError> {
334        if let Some(creds) = &mut self.fido2_credentials {
335            let decrypted_creds: Vec<Fido2CredentialFullView> = creds.decrypt(ctx, old_key)?;
336            *creds = decrypted_creds.encrypt_composite(ctx, new_key)?;
337        }
338        Ok(())
339    }
340
341    /// Compares this LoginView to the original, and returns any new password history items.
342    pub(crate) fn detect_password_change(
343        &mut self,
344        original: &Option<LoginView>,
345    ) -> Vec<PasswordHistoryView> {
346        let Some(original_login) = original else {
347            return vec![];
348        };
349
350        let original_password = original_login.password.as_deref().unwrap_or("");
351        let current_password = self.password.as_deref().unwrap_or("");
352
353        if original_password.is_empty() {
354            // No original password - set revision date only if adding new password
355            if !current_password.is_empty() {
356                self.password_revision_date = Some(Utc::now());
357            }
358            vec![]
359        } else if original_password == current_password {
360            // Password unchanged - preserve original revision date
361            self.password_revision_date = original_login.password_revision_date;
362            vec![]
363        } else {
364            // Password changed - update revision date and track change
365            self.password_revision_date = Some(Utc::now());
366            vec![PasswordHistoryView::new_password(original_password)]
367        }
368    }
369}
370
371#[allow(missing_docs)]
372#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
373#[serde(rename_all = "camelCase", deny_unknown_fields)]
374#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
375#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
376pub struct LoginListView {
377    pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
378    pub has_fido2: bool,
379    pub username: Option<String>,
380    /// The TOTP key is not decrypted. Useable as is with [`crate::generate_totp_cipher_view`].
381    pub totp: Option<EncString>,
382    pub uris: Option<Vec<LoginUriView>>,
383}
384
385impl CompositeEncryptable<KeyIds, SymmetricKeyId, LoginUri> for LoginUriView {
386    fn encrypt_composite(
387        &self,
388        ctx: &mut KeyStoreContext<KeyIds>,
389        key: SymmetricKeyId,
390    ) -> Result<LoginUri, CryptoError> {
391        Ok(LoginUri {
392            uri: self.uri.encrypt(ctx, key)?,
393            r#match: self.r#match,
394            uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
395        })
396    }
397}
398
399impl CompositeEncryptable<KeyIds, SymmetricKeyId, Login> for LoginView {
400    fn encrypt_composite(
401        &self,
402        ctx: &mut KeyStoreContext<KeyIds>,
403        key: SymmetricKeyId,
404    ) -> Result<Login, CryptoError> {
405        Ok(Login {
406            username: self.username.encrypt(ctx, key)?,
407            password: self.password.encrypt(ctx, key)?,
408            password_revision_date: self.password_revision_date,
409            uris: self.uris.encrypt_composite(ctx, key)?,
410            totp: self
411                .totp
412                .clone()
413                .filter(|s| !s.is_empty())
414                .encrypt(ctx, key)?,
415            autofill_on_page_load: self.autofill_on_page_load,
416            fido2_credentials: self.fido2_credentials.clone(),
417        })
418    }
419}
420
421impl Decryptable<KeyIds, SymmetricKeyId, LoginUriView> for LoginUri {
422    fn decrypt(
423        &self,
424        ctx: &mut KeyStoreContext<KeyIds>,
425        key: SymmetricKeyId,
426    ) -> Result<LoginUriView, CryptoError> {
427        Ok(LoginUriView {
428            uri: self.uri.decrypt(ctx, key)?,
429            r#match: self.r#match,
430            uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
431        })
432    }
433}
434
435impl Decryptable<KeyIds, SymmetricKeyId, LoginView> for Login {
436    fn decrypt(
437        &self,
438        ctx: &mut KeyStoreContext<KeyIds>,
439        key: SymmetricKeyId,
440    ) -> Result<LoginView, CryptoError> {
441        Ok(LoginView {
442            username: self.username.decrypt(ctx, key)?,
443            password: self.password.decrypt(ctx, key)?,
444            password_revision_date: self.password_revision_date,
445            uris: self.uris.decrypt(ctx, key)?,
446            totp: self.totp.decrypt(ctx, key)?,
447            autofill_on_page_load: self.autofill_on_page_load,
448            fido2_credentials: self.fido2_credentials.clone(),
449        })
450    }
451}
452
453impl Decryptable<KeyIds, SymmetricKeyId, LoginListView> for Login {
454    fn decrypt(
455        &self,
456        ctx: &mut KeyStoreContext<KeyIds>,
457        key: SymmetricKeyId,
458    ) -> Result<LoginListView, CryptoError> {
459        Ok(LoginListView {
460            fido2_credentials: self
461                .fido2_credentials
462                .as_ref()
463                .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
464                .transpose()?,
465            has_fido2: self.fido2_credentials.is_some(),
466            username: self.username.decrypt(ctx, key)?,
467            totp: self.totp.clone(),
468            uris: self.uris.decrypt(ctx, key)?,
469        })
470    }
471}
472
473impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialView {
474    fn encrypt_composite(
475        &self,
476        ctx: &mut KeyStoreContext<KeyIds>,
477        key: SymmetricKeyId,
478    ) -> Result<Fido2Credential, CryptoError> {
479        Ok(Fido2Credential {
480            credential_id: self.credential_id.encrypt(ctx, key)?,
481            key_type: self.key_type.encrypt(ctx, key)?,
482            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
483            key_curve: self.key_curve.encrypt(ctx, key)?,
484            key_value: self.key_value.clone(),
485            rp_id: self.rp_id.encrypt(ctx, key)?,
486            user_handle: self
487                .user_handle
488                .as_ref()
489                .map(|h| h.encrypt(ctx, key))
490                .transpose()?,
491            user_name: self
492                .user_name
493                .as_ref()
494                .map(|n| n.encrypt(ctx, key))
495                .transpose()?,
496            counter: self.counter.encrypt(ctx, key)?,
497            rp_name: self.rp_name.encrypt(ctx, key)?,
498            user_display_name: self.user_display_name.encrypt(ctx, key)?,
499            discoverable: self.discoverable.encrypt(ctx, key)?,
500            creation_date: self.creation_date,
501        })
502    }
503}
504
505impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialView> for Fido2Credential {
506    fn decrypt(
507        &self,
508        ctx: &mut KeyStoreContext<KeyIds>,
509        key: SymmetricKeyId,
510    ) -> Result<Fido2CredentialView, CryptoError> {
511        Ok(Fido2CredentialView {
512            credential_id: self.credential_id.decrypt(ctx, key)?,
513            key_type: self.key_type.decrypt(ctx, key)?,
514            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
515            key_curve: self.key_curve.decrypt(ctx, key)?,
516            key_value: self.key_value.clone(),
517            rp_id: self.rp_id.decrypt(ctx, key)?,
518            user_handle: self.user_handle.decrypt(ctx, key)?,
519            user_name: self.user_name.decrypt(ctx, key)?,
520            counter: self.counter.decrypt(ctx, key)?,
521            rp_name: self.rp_name.decrypt(ctx, key)?,
522            user_display_name: self.user_display_name.decrypt(ctx, key)?,
523            discoverable: self.discoverable.decrypt(ctx, key)?,
524            creation_date: self.creation_date,
525        })
526    }
527}
528
529impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialListView> for Fido2Credential {
530    fn decrypt(
531        &self,
532        ctx: &mut KeyStoreContext<KeyIds>,
533        key: SymmetricKeyId,
534    ) -> Result<Fido2CredentialListView, CryptoError> {
535        Ok(Fido2CredentialListView {
536            credential_id: self.credential_id.decrypt(ctx, key)?,
537            rp_id: self.rp_id.decrypt(ctx, key)?,
538            user_handle: self.user_handle.decrypt(ctx, key)?,
539            user_name: self.user_name.decrypt(ctx, key)?,
540            user_display_name: self.user_display_name.decrypt(ctx, key)?,
541            counter: self.counter.decrypt(ctx, key)?,
542        })
543    }
544}
545
546impl TryFrom<CipherLoginModel> for Login {
547    type Error = VaultParseError;
548
549    fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
550        Ok(Self {
551            username: EncString::try_from_optional(login.username)?,
552            password: EncString::try_from_optional(login.password)?,
553            password_revision_date: login
554                .password_revision_date
555                .map(|d| d.parse())
556                .transpose()?,
557            uris: login
558                .uris
559                .map(|v| v.into_iter().map(|u| u.try_into()).collect())
560                .transpose()?,
561            totp: EncString::try_from_optional(login.totp)?,
562            autofill_on_page_load: login.autofill_on_page_load,
563            fido2_credentials: login
564                .fido2_credentials
565                .map(|v| v.into_iter().map(|c| c.try_into()).collect())
566                .transpose()?,
567        })
568    }
569}
570
571impl TryFrom<CipherLoginUriModel> for LoginUri {
572    type Error = VaultParseError;
573
574    fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
575        Ok(Self {
576            uri: EncString::try_from_optional(uri.uri)?,
577            r#match: uri.r#match.map(|m| m.try_into()).transpose()?,
578            uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
579        })
580    }
581}
582
583impl TryFrom<bitwarden_api_api::models::UriMatchType> for UriMatchType {
584    type Error = bitwarden_core::MissingFieldError;
585
586    fn try_from(value: bitwarden_api_api::models::UriMatchType) -> Result<Self, Self::Error> {
587        Ok(match value {
588            bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
589            bitwarden_api_api::models::UriMatchType::Host => Self::Host,
590            bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
591            bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
592            bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
593            bitwarden_api_api::models::UriMatchType::Never => Self::Never,
594            bitwarden_api_api::models::UriMatchType::__Unknown(_) => {
595                return Err(bitwarden_core::MissingFieldError("match"));
596            }
597        })
598    }
599}
600
601impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
602    type Error = VaultParseError;
603
604    fn try_from(
605        value: bitwarden_api_api::models::CipherFido2CredentialModel,
606    ) -> Result<Self, Self::Error> {
607        Ok(Self {
608            credential_id: require!(value.credential_id).parse()?,
609            key_type: require!(value.key_type).parse()?,
610            key_algorithm: require!(value.key_algorithm).parse()?,
611            key_curve: require!(value.key_curve).parse()?,
612            key_value: require!(value.key_value).parse()?,
613            rp_id: require!(value.rp_id).parse()?,
614            user_handle: EncString::try_from_optional(value.user_handle)
615                .ok()
616                .flatten(),
617            user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
618            counter: require!(value.counter).parse()?,
619            rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
620            user_display_name: EncString::try_from_optional(value.user_display_name)
621                .ok()
622                .flatten(),
623            discoverable: require!(value.discoverable).parse()?,
624            creation_date: value.creation_date.parse()?,
625        })
626    }
627}
628
629impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
630    fn from(uri: LoginUri) -> Self {
631        bitwarden_api_api::models::CipherLoginUriModel {
632            uri: uri.uri.map(|u| u.to_string()),
633            uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
634            r#match: uri.r#match.map(|m| m.into()),
635        }
636    }
637}
638
639impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
640    fn from(match_type: UriMatchType) -> Self {
641        match match_type {
642            UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
643            UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
644            UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
645            UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
646            UriMatchType::RegularExpression => {
647                bitwarden_api_api::models::UriMatchType::RegularExpression
648            }
649            UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
650        }
651    }
652}
653
654impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
655    fn from(cred: Fido2Credential) -> Self {
656        bitwarden_api_api::models::CipherFido2CredentialModel {
657            credential_id: Some(cred.credential_id.to_string()),
658            key_type: Some(cred.key_type.to_string()),
659            key_algorithm: Some(cred.key_algorithm.to_string()),
660            key_curve: Some(cred.key_curve.to_string()),
661            key_value: Some(cred.key_value.to_string()),
662            rp_id: Some(cred.rp_id.to_string()),
663            user_handle: cred.user_handle.map(|h| h.to_string()),
664            user_name: cred.user_name.map(|n| n.to_string()),
665            counter: Some(cred.counter.to_string()),
666            rp_name: cred.rp_name.map(|n| n.to_string()),
667            user_display_name: cred.user_display_name.map(|n| n.to_string()),
668            discoverable: Some(cred.discoverable.to_string()),
669            creation_date: cred.creation_date.to_rfc3339(),
670        }
671    }
672}
673
674impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
675    fn from(login: Login) -> Self {
676        bitwarden_api_api::models::CipherLoginModel {
677            uri: None,
678            uris: login
679                .uris
680                .map(|u| u.into_iter().map(|u| u.into()).collect()),
681            username: login.username.map(|u| u.to_string()),
682            password: login.password.map(|p| p.to_string()),
683            password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
684            totp: login.totp.map(|t| t.to_string()),
685            autofill_on_page_load: login.autofill_on_page_load,
686            fido2_credentials: login
687                .fido2_credentials
688                .map(|c| c.into_iter().map(|c| c.into()).collect()),
689        }
690    }
691}
692
693impl CipherKind for Login {
694    fn decrypt_subtitle(
695        &self,
696        ctx: &mut KeyStoreContext<KeyIds>,
697        key: SymmetricKeyId,
698    ) -> Result<String, CryptoError> {
699        let username: Option<String> = self.username.decrypt(ctx, key)?;
700
701        Ok(username.unwrap_or_default())
702    }
703
704    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
705        [
706            self.username
707                .as_ref()
708                .map(|_| CopyableCipherFields::LoginUsername),
709            self.password
710                .as_ref()
711                .map(|_| CopyableCipherFields::LoginPassword),
712            self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
713        ]
714        .into_iter()
715        .flatten()
716        .collect()
717    }
718}
719
720#[cfg(test)]
721mod tests {
722    use crate::{
723        Login,
724        cipher::cipher::{CipherKind, CopyableCipherFields},
725    };
726
727    #[test]
728    fn test_valid_checksum() {
729        let uri = super::LoginUriView {
730            uri: Some("https://example.com".to_string()),
731            r#match: Some(super::UriMatchType::Domain),
732            uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
733        };
734        assert!(uri.is_checksum_valid());
735    }
736
737    #[test]
738    fn test_invalid_checksum() {
739        let uri = super::LoginUriView {
740            uri: Some("https://example.com".to_string()),
741            r#match: Some(super::UriMatchType::Domain),
742            uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
743        };
744        assert!(!uri.is_checksum_valid());
745    }
746
747    #[test]
748    fn test_missing_checksum() {
749        let uri = super::LoginUriView {
750            uri: Some("https://example.com".to_string()),
751            r#match: Some(super::UriMatchType::Domain),
752            uri_checksum: None,
753        };
754        assert!(!uri.is_checksum_valid());
755    }
756
757    #[test]
758    fn test_generate_checksum() {
759        let mut uri = super::LoginUriView {
760            uri: Some("https://test.com".to_string()),
761            r#match: Some(super::UriMatchType::Domain),
762            uri_checksum: None,
763        };
764
765        uri.generate_checksum();
766
767        assert_eq!(
768            uri.uri_checksum.unwrap().as_str(),
769            "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
770        );
771    }
772
773    #[test]
774    fn test_get_copyable_fields_login_password() {
775        let login_with_password = Login {
776            username: None,
777            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
778            password_revision_date: None,
779            uris: None,
780            totp: None,
781            autofill_on_page_load: None,
782            fido2_credentials: None,
783        };
784
785        let copyable_fields = login_with_password.get_copyable_fields(None);
786        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
787    }
788
789    #[test]
790    fn test_get_copyable_fields_login_username() {
791        let login_with_username = Login {
792            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
793            password: None,
794            password_revision_date: None,
795            uris: None,
796            totp: None,
797            autofill_on_page_load: None,
798            fido2_credentials: None,
799        };
800
801        let copyable_fields = login_with_username.get_copyable_fields(None);
802        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
803    }
804
805    #[test]
806    fn test_get_copyable_fields_login_everything() {
807        let login = Login {
808            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
809            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
810            password_revision_date: None,
811            uris: None,
812            totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
813            autofill_on_page_load: None,
814            fido2_credentials: None,
815        };
816
817        let copyable_fields = login.get_copyable_fields(None);
818        assert_eq!(
819            copyable_fields,
820            vec![
821                CopyableCipherFields::LoginUsername,
822                CopyableCipherFields::LoginPassword,
823                CopyableCipherFields::LoginTotp
824            ]
825        );
826    }
827}