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.totp.encrypt(ctx, key)?,
411            autofill_on_page_load: self.autofill_on_page_load,
412            fido2_credentials: self.fido2_credentials.clone(),
413        })
414    }
415}
416
417impl Decryptable<KeyIds, SymmetricKeyId, LoginUriView> for LoginUri {
418    fn decrypt(
419        &self,
420        ctx: &mut KeyStoreContext<KeyIds>,
421        key: SymmetricKeyId,
422    ) -> Result<LoginUriView, CryptoError> {
423        Ok(LoginUriView {
424            uri: self.uri.decrypt(ctx, key)?,
425            r#match: self.r#match,
426            uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
427        })
428    }
429}
430
431impl Decryptable<KeyIds, SymmetricKeyId, LoginView> for Login {
432    fn decrypt(
433        &self,
434        ctx: &mut KeyStoreContext<KeyIds>,
435        key: SymmetricKeyId,
436    ) -> Result<LoginView, CryptoError> {
437        Ok(LoginView {
438            username: self.username.decrypt(ctx, key).ok().flatten(),
439            password: self.password.decrypt(ctx, key).ok().flatten(),
440            password_revision_date: self.password_revision_date,
441            uris: self.uris.decrypt(ctx, key).ok().flatten(),
442            totp: self.totp.decrypt(ctx, key).ok().flatten(),
443            autofill_on_page_load: self.autofill_on_page_load,
444            fido2_credentials: self.fido2_credentials.clone(),
445        })
446    }
447}
448
449impl Decryptable<KeyIds, SymmetricKeyId, LoginListView> for Login {
450    fn decrypt(
451        &self,
452        ctx: &mut KeyStoreContext<KeyIds>,
453        key: SymmetricKeyId,
454    ) -> Result<LoginListView, CryptoError> {
455        Ok(LoginListView {
456            fido2_credentials: self
457                .fido2_credentials
458                .as_ref()
459                .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
460                .transpose()?,
461            has_fido2: self.fido2_credentials.is_some(),
462            username: self.username.decrypt(ctx, key).ok().flatten(),
463            totp: self.totp.clone(),
464            uris: self.uris.decrypt(ctx, key).ok().flatten(),
465        })
466    }
467}
468
469impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialView {
470    fn encrypt_composite(
471        &self,
472        ctx: &mut KeyStoreContext<KeyIds>,
473        key: SymmetricKeyId,
474    ) -> Result<Fido2Credential, CryptoError> {
475        Ok(Fido2Credential {
476            credential_id: self.credential_id.encrypt(ctx, key)?,
477            key_type: self.key_type.encrypt(ctx, key)?,
478            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
479            key_curve: self.key_curve.encrypt(ctx, key)?,
480            key_value: self.key_value.clone(),
481            rp_id: self.rp_id.encrypt(ctx, key)?,
482            user_handle: self
483                .user_handle
484                .as_ref()
485                .map(|h| h.encrypt(ctx, key))
486                .transpose()?,
487            user_name: self
488                .user_name
489                .as_ref()
490                .map(|n| n.encrypt(ctx, key))
491                .transpose()?,
492            counter: self.counter.encrypt(ctx, key)?,
493            rp_name: self.rp_name.encrypt(ctx, key)?,
494            user_display_name: self.user_display_name.encrypt(ctx, key)?,
495            discoverable: self.discoverable.encrypt(ctx, key)?,
496            creation_date: self.creation_date,
497        })
498    }
499}
500
501impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialView> for Fido2Credential {
502    fn decrypt(
503        &self,
504        ctx: &mut KeyStoreContext<KeyIds>,
505        key: SymmetricKeyId,
506    ) -> Result<Fido2CredentialView, CryptoError> {
507        Ok(Fido2CredentialView {
508            credential_id: self.credential_id.decrypt(ctx, key)?,
509            key_type: self.key_type.decrypt(ctx, key)?,
510            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
511            key_curve: self.key_curve.decrypt(ctx, key)?,
512            key_value: self.key_value.clone(),
513            rp_id: self.rp_id.decrypt(ctx, key)?,
514            user_handle: self.user_handle.decrypt(ctx, key)?,
515            user_name: self.user_name.decrypt(ctx, key)?,
516            counter: self.counter.decrypt(ctx, key)?,
517            rp_name: self.rp_name.decrypt(ctx, key)?,
518            user_display_name: self.user_display_name.decrypt(ctx, key)?,
519            discoverable: self.discoverable.decrypt(ctx, key)?,
520            creation_date: self.creation_date,
521        })
522    }
523}
524
525impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialListView> for Fido2Credential {
526    fn decrypt(
527        &self,
528        ctx: &mut KeyStoreContext<KeyIds>,
529        key: SymmetricKeyId,
530    ) -> Result<Fido2CredentialListView, CryptoError> {
531        Ok(Fido2CredentialListView {
532            credential_id: self.credential_id.decrypt(ctx, key)?,
533            rp_id: self.rp_id.decrypt(ctx, key)?,
534            user_handle: self.user_handle.decrypt(ctx, key)?,
535            user_name: self.user_name.decrypt(ctx, key)?,
536            user_display_name: self.user_display_name.decrypt(ctx, key)?,
537            counter: self.counter.decrypt(ctx, key)?,
538        })
539    }
540}
541
542impl TryFrom<CipherLoginModel> for Login {
543    type Error = VaultParseError;
544
545    fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
546        Ok(Self {
547            username: EncString::try_from_optional(login.username)?,
548            password: EncString::try_from_optional(login.password)?,
549            password_revision_date: login
550                .password_revision_date
551                .map(|d| d.parse())
552                .transpose()?,
553            uris: login
554                .uris
555                .map(|v| v.into_iter().map(|u| u.try_into()).collect())
556                .transpose()?,
557            totp: EncString::try_from_optional(login.totp)?,
558            autofill_on_page_load: login.autofill_on_page_load,
559            fido2_credentials: login
560                .fido2_credentials
561                .map(|v| v.into_iter().map(|c| c.try_into()).collect())
562                .transpose()?,
563        })
564    }
565}
566
567impl TryFrom<CipherLoginUriModel> for LoginUri {
568    type Error = VaultParseError;
569
570    fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
571        Ok(Self {
572            uri: EncString::try_from_optional(uri.uri)?,
573            r#match: uri.r#match.map(|m| m.into()),
574            uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
575        })
576    }
577}
578
579impl From<bitwarden_api_api::models::UriMatchType> for UriMatchType {
580    fn from(value: bitwarden_api_api::models::UriMatchType) -> Self {
581        match value {
582            bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
583            bitwarden_api_api::models::UriMatchType::Host => Self::Host,
584            bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
585            bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
586            bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
587            bitwarden_api_api::models::UriMatchType::Never => Self::Never,
588        }
589    }
590}
591
592impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
593    type Error = VaultParseError;
594
595    fn try_from(
596        value: bitwarden_api_api::models::CipherFido2CredentialModel,
597    ) -> Result<Self, Self::Error> {
598        Ok(Self {
599            credential_id: require!(value.credential_id).parse()?,
600            key_type: require!(value.key_type).parse()?,
601            key_algorithm: require!(value.key_algorithm).parse()?,
602            key_curve: require!(value.key_curve).parse()?,
603            key_value: require!(value.key_value).parse()?,
604            rp_id: require!(value.rp_id).parse()?,
605            user_handle: EncString::try_from_optional(value.user_handle)
606                .ok()
607                .flatten(),
608            user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
609            counter: require!(value.counter).parse()?,
610            rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
611            user_display_name: EncString::try_from_optional(value.user_display_name)
612                .ok()
613                .flatten(),
614            discoverable: require!(value.discoverable).parse()?,
615            creation_date: value.creation_date.parse()?,
616        })
617    }
618}
619
620impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
621    fn from(uri: LoginUri) -> Self {
622        bitwarden_api_api::models::CipherLoginUriModel {
623            uri: uri.uri.map(|u| u.to_string()),
624            uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
625            r#match: uri.r#match.map(|m| m.into()),
626        }
627    }
628}
629
630impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
631    fn from(match_type: UriMatchType) -> Self {
632        match match_type {
633            UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
634            UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
635            UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
636            UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
637            UriMatchType::RegularExpression => {
638                bitwarden_api_api::models::UriMatchType::RegularExpression
639            }
640            UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
641        }
642    }
643}
644
645impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
646    fn from(cred: Fido2Credential) -> Self {
647        bitwarden_api_api::models::CipherFido2CredentialModel {
648            credential_id: Some(cred.credential_id.to_string()),
649            key_type: Some(cred.key_type.to_string()),
650            key_algorithm: Some(cred.key_algorithm.to_string()),
651            key_curve: Some(cred.key_curve.to_string()),
652            key_value: Some(cred.key_value.to_string()),
653            rp_id: Some(cred.rp_id.to_string()),
654            user_handle: cred.user_handle.map(|h| h.to_string()),
655            user_name: cred.user_name.map(|n| n.to_string()),
656            counter: Some(cred.counter.to_string()),
657            rp_name: cred.rp_name.map(|n| n.to_string()),
658            user_display_name: cred.user_display_name.map(|n| n.to_string()),
659            discoverable: Some(cred.discoverable.to_string()),
660            creation_date: cred.creation_date.to_rfc3339(),
661        }
662    }
663}
664
665impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
666    fn from(login: Login) -> Self {
667        bitwarden_api_api::models::CipherLoginModel {
668            uri: None,
669            uris: login
670                .uris
671                .map(|u| u.into_iter().map(|u| u.into()).collect()),
672            username: login.username.map(|u| u.to_string()),
673            password: login.password.map(|p| p.to_string()),
674            password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
675            totp: login.totp.map(|t| t.to_string()),
676            autofill_on_page_load: login.autofill_on_page_load,
677            fido2_credentials: login
678                .fido2_credentials
679                .map(|c| c.into_iter().map(|c| c.into()).collect()),
680        }
681    }
682}
683
684impl CipherKind for Login {
685    fn decrypt_subtitle(
686        &self,
687        ctx: &mut KeyStoreContext<KeyIds>,
688        key: SymmetricKeyId,
689    ) -> Result<String, CryptoError> {
690        let username: Option<String> = self.username.decrypt(ctx, key)?;
691
692        Ok(username.unwrap_or_default())
693    }
694
695    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
696        [
697            self.username
698                .as_ref()
699                .map(|_| CopyableCipherFields::LoginUsername),
700            self.password
701                .as_ref()
702                .map(|_| CopyableCipherFields::LoginPassword),
703            self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
704        ]
705        .into_iter()
706        .flatten()
707        .collect()
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use crate::{
714        Login,
715        cipher::cipher::{CipherKind, CopyableCipherFields},
716    };
717
718    #[test]
719    fn test_valid_checksum() {
720        let uri = super::LoginUriView {
721            uri: Some("https://example.com".to_string()),
722            r#match: Some(super::UriMatchType::Domain),
723            uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
724        };
725        assert!(uri.is_checksum_valid());
726    }
727
728    #[test]
729    fn test_invalid_checksum() {
730        let uri = super::LoginUriView {
731            uri: Some("https://example.com".to_string()),
732            r#match: Some(super::UriMatchType::Domain),
733            uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
734        };
735        assert!(!uri.is_checksum_valid());
736    }
737
738    #[test]
739    fn test_missing_checksum() {
740        let uri = super::LoginUriView {
741            uri: Some("https://example.com".to_string()),
742            r#match: Some(super::UriMatchType::Domain),
743            uri_checksum: None,
744        };
745        assert!(!uri.is_checksum_valid());
746    }
747
748    #[test]
749    fn test_generate_checksum() {
750        let mut uri = super::LoginUriView {
751            uri: Some("https://test.com".to_string()),
752            r#match: Some(super::UriMatchType::Domain),
753            uri_checksum: None,
754        };
755
756        uri.generate_checksum();
757
758        assert_eq!(
759            uri.uri_checksum.unwrap().as_str(),
760            "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
761        );
762    }
763
764    #[test]
765    fn test_get_copyable_fields_login_password() {
766        let login_with_password = Login {
767            username: None,
768            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
769            password_revision_date: None,
770            uris: None,
771            totp: None,
772            autofill_on_page_load: None,
773            fido2_credentials: None,
774        };
775
776        let copyable_fields = login_with_password.get_copyable_fields(None);
777        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
778    }
779
780    #[test]
781    fn test_get_copyable_fields_login_username() {
782        let login_with_username = Login {
783            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
784            password: None,
785            password_revision_date: None,
786            uris: None,
787            totp: None,
788            autofill_on_page_load: None,
789            fido2_credentials: None,
790        };
791
792        let copyable_fields = login_with_username.get_copyable_fields(None);
793        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
794    }
795
796    #[test]
797    fn test_get_copyable_fields_login_everything() {
798        let login = Login {
799            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
800            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
801            password_revision_date: None,
802            uris: None,
803            totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
804            autofill_on_page_load: None,
805            fido2_credentials: None,
806        };
807
808        let copyable_fields = login.get_copyable_fields(None);
809        assert_eq!(
810            copyable_fields,
811            vec![
812                CopyableCipherFields::LoginUsername,
813                CopyableCipherFields::LoginPassword,
814                CopyableCipherFields::LoginTotp
815            ]
816        );
817    }
818}