Skip to main content

bitwarden_vault/cipher/
login.rs

1use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel};
2use bitwarden_core::{
3    key_management::{KeySlotIds, SymmetricKeySlotId},
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, StrictDecrypt};
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<KeySlotIds, SymmetricKeySlotId, Fido2Credential>
207    for Fido2CredentialFullView
208{
209    fn encrypt_composite(
210        &self,
211        ctx: &mut KeyStoreContext<KeySlotIds>,
212        key: SymmetricKeySlotId,
213    ) -> Result<Fido2Credential, CryptoError> {
214        Ok(Fido2Credential {
215            credential_id: self.credential_id.encrypt(ctx, key)?,
216            key_type: self.key_type.encrypt(ctx, key)?,
217            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
218            key_curve: self.key_curve.encrypt(ctx, key)?,
219            key_value: self.key_value.encrypt(ctx, key)?,
220            rp_id: self.rp_id.encrypt(ctx, key)?,
221            user_handle: self
222                .user_handle
223                .as_ref()
224                .map(|h| h.encrypt(ctx, key))
225                .transpose()?,
226            user_name: self.user_name.encrypt(ctx, key)?,
227            counter: self.counter.encrypt(ctx, key)?,
228            rp_name: self.rp_name.encrypt(ctx, key)?,
229            user_display_name: self.user_display_name.encrypt(ctx, key)?,
230            discoverable: self.discoverable.encrypt(ctx, key)?,
231            creation_date: self.creation_date,
232        })
233    }
234}
235
236impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialFullView> for Fido2Credential {
237    fn decrypt(
238        &self,
239        ctx: &mut KeyStoreContext<KeySlotIds>,
240        key: SymmetricKeySlotId,
241    ) -> Result<Fido2CredentialFullView, CryptoError> {
242        Ok(Fido2CredentialFullView {
243            credential_id: self.credential_id.decrypt(ctx, key)?,
244            key_type: self.key_type.decrypt(ctx, key)?,
245            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
246            key_curve: self.key_curve.decrypt(ctx, key)?,
247            key_value: self.key_value.decrypt(ctx, key)?,
248            rp_id: self.rp_id.decrypt(ctx, key)?,
249            user_handle: self.user_handle.decrypt(ctx, key)?,
250            user_name: self.user_name.decrypt(ctx, key)?,
251            counter: self.counter.decrypt(ctx, key)?,
252            rp_name: self.rp_name.decrypt(ctx, key)?,
253            user_display_name: self.user_display_name.decrypt(ctx, key)?,
254            discoverable: self.discoverable.decrypt(ctx, key)?,
255            creation_date: self.creation_date,
256        })
257    }
258}
259
260impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialFullView> for Fido2CredentialView {
261    fn decrypt(
262        &self,
263        ctx: &mut KeyStoreContext<KeySlotIds>,
264        key: SymmetricKeySlotId,
265    ) -> Result<Fido2CredentialFullView, CryptoError> {
266        Ok(Fido2CredentialFullView {
267            credential_id: self.credential_id.clone(),
268            key_type: self.key_type.clone(),
269            key_algorithm: self.key_algorithm.clone(),
270            key_curve: self.key_curve.clone(),
271            key_value: self.key_value.decrypt(ctx, key)?,
272            rp_id: self.rp_id.clone(),
273            user_handle: self.user_handle.clone(),
274            user_name: self.user_name.clone(),
275            counter: self.counter.clone(),
276            rp_name: self.rp_name.clone(),
277            user_display_name: self.user_display_name.clone(),
278            discoverable: self.discoverable.clone(),
279            creation_date: self.creation_date,
280        })
281    }
282}
283
284#[allow(missing_docs)]
285#[derive(Serialize, Deserialize, Debug, Clone)]
286#[serde(rename_all = "camelCase")]
287#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
288#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
289pub struct Login {
290    pub username: Option<EncString>,
291    pub password: Option<EncString>,
292    pub password_revision_date: Option<DateTime<Utc>>,
293
294    pub uris: Option<Vec<LoginUri>>,
295    pub totp: Option<EncString>,
296    pub autofill_on_page_load: Option<bool>,
297
298    pub fido2_credentials: Option<Vec<Fido2Credential>>,
299}
300
301#[allow(missing_docs)]
302#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
303#[serde(rename_all = "camelCase", deny_unknown_fields)]
304#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
305#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
306pub struct LoginView {
307    pub username: Option<String>,
308    pub password: Option<String>,
309    pub password_revision_date: Option<DateTime<Utc>>,
310
311    pub uris: Option<Vec<LoginUriView>>,
312    pub totp: Option<String>,
313    pub autofill_on_page_load: Option<bool>,
314
315    // TODO: Remove this once the SDK supports state
316    pub fido2_credentials: Option<Vec<Fido2Credential>>,
317}
318
319impl LoginView {
320    /// Generate checksums for all URIs in the login view
321    pub fn generate_checksums(&mut self) {
322        if let Some(uris) = &mut self.uris {
323            for uri in uris {
324                uri.generate_checksum();
325            }
326        }
327    }
328
329    /// Re-encrypts the fido2 credentials with a new key, replacing the old encrypted values.
330    pub fn reencrypt_fido2_credentials(
331        &mut self,
332        ctx: &mut KeyStoreContext<KeySlotIds>,
333        old_key: SymmetricKeySlotId,
334        new_key: SymmetricKeySlotId,
335    ) -> Result<(), CryptoError> {
336        if let Some(creds) = &mut self.fido2_credentials {
337            let decrypted_creds: Vec<Fido2CredentialFullView> = creds.decrypt(ctx, old_key)?;
338            *creds = decrypted_creds.encrypt_composite(ctx, new_key)?;
339        }
340        Ok(())
341    }
342
343    /// Compares this LoginView to the original, and returns any new password history items.
344    pub(crate) fn detect_password_change(
345        &mut self,
346        original: &Option<LoginView>,
347    ) -> Vec<PasswordHistoryView> {
348        let Some(original_login) = original else {
349            return vec![];
350        };
351
352        let original_password = original_login.password.as_deref().unwrap_or("");
353        let current_password = self.password.as_deref().unwrap_or("");
354
355        if original_password.is_empty() {
356            // No original password - set revision date only if adding new password
357            if !current_password.is_empty() {
358                self.password_revision_date = Some(Utc::now());
359            }
360            vec![]
361        } else if original_password == current_password {
362            // Password unchanged - preserve original revision date
363            self.password_revision_date = original_login.password_revision_date;
364            vec![]
365        } else {
366            // Password changed - update revision date and track change
367            self.password_revision_date = Some(Utc::now());
368            vec![PasswordHistoryView::new_password(original_password)]
369        }
370    }
371}
372
373#[allow(missing_docs)]
374#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
375#[serde(rename_all = "camelCase", deny_unknown_fields)]
376#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
377#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
378pub struct LoginListView {
379    pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
380    pub has_fido2: bool,
381    pub username: Option<String>,
382    /// The TOTP key is not decrypted. Useable as is with [`crate::generate_totp_cipher_view`].
383    pub totp: Option<EncString>,
384    pub uris: Option<Vec<LoginUriView>>,
385}
386
387impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, LoginUri> for LoginUriView {
388    fn encrypt_composite(
389        &self,
390        ctx: &mut KeyStoreContext<KeySlotIds>,
391        key: SymmetricKeySlotId,
392    ) -> Result<LoginUri, CryptoError> {
393        Ok(LoginUri {
394            uri: self.uri.encrypt(ctx, key)?,
395            r#match: self.r#match,
396            uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
397        })
398    }
399}
400
401impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Login> for LoginView {
402    fn encrypt_composite(
403        &self,
404        ctx: &mut KeyStoreContext<KeySlotIds>,
405        key: SymmetricKeySlotId,
406    ) -> Result<Login, CryptoError> {
407        Ok(Login {
408            username: self.username.encrypt(ctx, key)?,
409            password: self.password.encrypt(ctx, key)?,
410            password_revision_date: self.password_revision_date,
411            uris: self.uris.encrypt_composite(ctx, key)?,
412            totp: self
413                .totp
414                .clone()
415                .filter(|s| !s.is_empty())
416                .encrypt(ctx, key)?,
417            autofill_on_page_load: self.autofill_on_page_load,
418            fido2_credentials: self.fido2_credentials.clone(),
419        })
420    }
421}
422
423impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginUriView> for LoginUri {
424    fn decrypt(
425        &self,
426        ctx: &mut KeyStoreContext<KeySlotIds>,
427        key: SymmetricKeySlotId,
428    ) -> Result<LoginUriView, CryptoError> {
429        Ok(LoginUriView {
430            uri: self.uri.decrypt(ctx, key)?,
431            r#match: self.r#match,
432            uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
433        })
434    }
435}
436
437impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginView> for Login {
438    fn decrypt(
439        &self,
440        ctx: &mut KeyStoreContext<KeySlotIds>,
441        key: SymmetricKeySlotId,
442    ) -> Result<LoginView, CryptoError> {
443        Ok(LoginView {
444            username: self.username.decrypt(ctx, key).ok().flatten(),
445            password: self.password.decrypt(ctx, key).ok().flatten(),
446            password_revision_date: self.password_revision_date,
447            uris: self.uris.decrypt(ctx, key).ok().flatten(),
448            totp: self.totp.decrypt(ctx, key).ok().flatten(),
449            autofill_on_page_load: self.autofill_on_page_load,
450            fido2_credentials: self.fido2_credentials.clone(),
451        })
452    }
453}
454
455impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginListView> for Login {
456    fn decrypt(
457        &self,
458        ctx: &mut KeyStoreContext<KeySlotIds>,
459        key: SymmetricKeySlotId,
460    ) -> Result<LoginListView, CryptoError> {
461        Ok(LoginListView {
462            fido2_credentials: self
463                .fido2_credentials
464                .as_ref()
465                .and_then(|fido2_credentials| fido2_credentials.decrypt(ctx, key).ok()),
466            has_fido2: self.fido2_credentials.is_some(),
467            username: self.username.decrypt(ctx, key).ok().flatten(),
468            totp: self.totp.clone(),
469            uris: self.uris.decrypt(ctx, key).ok().flatten(),
470        })
471    }
472}
473
474impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginView> for StrictDecrypt<&Login> {
475    fn decrypt(
476        &self,
477        ctx: &mut KeyStoreContext<KeySlotIds>,
478        key: SymmetricKeySlotId,
479    ) -> Result<LoginView, CryptoError> {
480        Ok(LoginView {
481            username: self.0.username.decrypt(ctx, key)?,
482            password: self.0.password.decrypt(ctx, key)?,
483            password_revision_date: self.0.password_revision_date,
484            uris: self.0.uris.decrypt(ctx, key)?,
485            totp: self.0.totp.decrypt(ctx, key)?,
486            autofill_on_page_load: self.0.autofill_on_page_load,
487            fido2_credentials: self.0.fido2_credentials.clone(),
488        })
489    }
490}
491
492impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginListView> for StrictDecrypt<&Login> {
493    fn decrypt(
494        &self,
495        ctx: &mut KeyStoreContext<KeySlotIds>,
496        key: SymmetricKeySlotId,
497    ) -> Result<LoginListView, CryptoError> {
498        Ok(LoginListView {
499            fido2_credentials: self
500                .0
501                .fido2_credentials
502                .as_ref()
503                .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
504                .transpose()?,
505            has_fido2: self.0.fido2_credentials.is_some(),
506            username: self.0.username.decrypt(ctx, key)?,
507            totp: self.0.totp.clone(),
508            uris: self.0.uris.decrypt(ctx, key)?,
509        })
510    }
511}
512
513impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Fido2Credential> for Fido2CredentialView {
514    fn encrypt_composite(
515        &self,
516        ctx: &mut KeyStoreContext<KeySlotIds>,
517        key: SymmetricKeySlotId,
518    ) -> Result<Fido2Credential, CryptoError> {
519        Ok(Fido2Credential {
520            credential_id: self.credential_id.encrypt(ctx, key)?,
521            key_type: self.key_type.encrypt(ctx, key)?,
522            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
523            key_curve: self.key_curve.encrypt(ctx, key)?,
524            key_value: self.key_value.clone(),
525            rp_id: self.rp_id.encrypt(ctx, key)?,
526            user_handle: self
527                .user_handle
528                .as_ref()
529                .map(|h| h.encrypt(ctx, key))
530                .transpose()?,
531            user_name: self
532                .user_name
533                .as_ref()
534                .map(|n| n.encrypt(ctx, key))
535                .transpose()?,
536            counter: self.counter.encrypt(ctx, key)?,
537            rp_name: self.rp_name.encrypt(ctx, key)?,
538            user_display_name: self.user_display_name.encrypt(ctx, key)?,
539            discoverable: self.discoverable.encrypt(ctx, key)?,
540            creation_date: self.creation_date,
541        })
542    }
543}
544
545impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialView> for Fido2Credential {
546    fn decrypt(
547        &self,
548        ctx: &mut KeyStoreContext<KeySlotIds>,
549        key: SymmetricKeySlotId,
550    ) -> Result<Fido2CredentialView, CryptoError> {
551        Ok(Fido2CredentialView {
552            credential_id: self.credential_id.decrypt(ctx, key)?,
553            key_type: self.key_type.decrypt(ctx, key)?,
554            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
555            key_curve: self.key_curve.decrypt(ctx, key)?,
556            key_value: self.key_value.clone(),
557            rp_id: self.rp_id.decrypt(ctx, key)?,
558            user_handle: self.user_handle.decrypt(ctx, key)?,
559            user_name: self.user_name.decrypt(ctx, key)?,
560            counter: self.counter.decrypt(ctx, key)?,
561            rp_name: self.rp_name.decrypt(ctx, key)?,
562            user_display_name: self.user_display_name.decrypt(ctx, key)?,
563            discoverable: self.discoverable.decrypt(ctx, key)?,
564            creation_date: self.creation_date,
565        })
566    }
567}
568
569impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialListView> for Fido2Credential {
570    fn decrypt(
571        &self,
572        ctx: &mut KeyStoreContext<KeySlotIds>,
573        key: SymmetricKeySlotId,
574    ) -> Result<Fido2CredentialListView, CryptoError> {
575        Ok(Fido2CredentialListView {
576            credential_id: self.credential_id.decrypt(ctx, key)?,
577            rp_id: self.rp_id.decrypt(ctx, key)?,
578            user_handle: self.user_handle.decrypt(ctx, key)?,
579            user_name: self.user_name.decrypt(ctx, key)?,
580            user_display_name: self.user_display_name.decrypt(ctx, key)?,
581            counter: self.counter.decrypt(ctx, key)?,
582        })
583    }
584}
585
586impl TryFrom<CipherLoginModel> for Login {
587    type Error = VaultParseError;
588
589    fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
590        Ok(Self {
591            username: EncString::try_from_optional(login.username)?,
592            password: EncString::try_from_optional(login.password)?,
593            password_revision_date: login
594                .password_revision_date
595                .map(|d| d.parse())
596                .transpose()?,
597            uris: login
598                .uris
599                .map(|v| v.into_iter().map(|u| u.try_into()).collect())
600                .transpose()?,
601            totp: EncString::try_from_optional(login.totp)?,
602            autofill_on_page_load: login.autofill_on_page_load,
603            fido2_credentials: login
604                .fido2_credentials
605                .map(|v| v.into_iter().map(|c| c.try_into()).collect())
606                .transpose()?,
607        })
608    }
609}
610
611impl TryFrom<CipherLoginUriModel> for LoginUri {
612    type Error = VaultParseError;
613
614    fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
615        Ok(Self {
616            uri: EncString::try_from_optional(uri.uri)?,
617            r#match: uri.r#match.map(|m| m.try_into()).transpose()?,
618            uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
619        })
620    }
621}
622
623impl TryFrom<bitwarden_api_api::models::UriMatchType> for UriMatchType {
624    type Error = bitwarden_core::MissingFieldError;
625
626    fn try_from(value: bitwarden_api_api::models::UriMatchType) -> Result<Self, Self::Error> {
627        Ok(match value {
628            bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
629            bitwarden_api_api::models::UriMatchType::Host => Self::Host,
630            bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
631            bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
632            bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
633            bitwarden_api_api::models::UriMatchType::Never => Self::Never,
634            bitwarden_api_api::models::UriMatchType::__Unknown(_) => {
635                return Err(bitwarden_core::MissingFieldError("match"));
636            }
637        })
638    }
639}
640
641impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
642    type Error = VaultParseError;
643
644    fn try_from(
645        value: bitwarden_api_api::models::CipherFido2CredentialModel,
646    ) -> Result<Self, Self::Error> {
647        Ok(Self {
648            credential_id: require!(value.credential_id).parse()?,
649            key_type: require!(value.key_type).parse()?,
650            key_algorithm: require!(value.key_algorithm).parse()?,
651            key_curve: require!(value.key_curve).parse()?,
652            key_value: require!(value.key_value).parse()?,
653            rp_id: require!(value.rp_id).parse()?,
654            user_handle: EncString::try_from_optional(value.user_handle)
655                .ok()
656                .flatten(),
657            user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
658            counter: require!(value.counter).parse()?,
659            rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
660            user_display_name: EncString::try_from_optional(value.user_display_name)
661                .ok()
662                .flatten(),
663            discoverable: require!(value.discoverable).parse()?,
664            creation_date: value.creation_date.parse()?,
665        })
666    }
667}
668
669impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
670    fn from(uri: LoginUri) -> Self {
671        bitwarden_api_api::models::CipherLoginUriModel {
672            uri: uri.uri.map(|u| u.to_string()),
673            uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
674            r#match: uri.r#match.map(|m| m.into()),
675        }
676    }
677}
678
679impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
680    fn from(match_type: UriMatchType) -> Self {
681        match match_type {
682            UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
683            UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
684            UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
685            UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
686            UriMatchType::RegularExpression => {
687                bitwarden_api_api::models::UriMatchType::RegularExpression
688            }
689            UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
690        }
691    }
692}
693
694impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
695    fn from(cred: Fido2Credential) -> Self {
696        bitwarden_api_api::models::CipherFido2CredentialModel {
697            credential_id: Some(cred.credential_id.to_string()),
698            key_type: Some(cred.key_type.to_string()),
699            key_algorithm: Some(cred.key_algorithm.to_string()),
700            key_curve: Some(cred.key_curve.to_string()),
701            key_value: Some(cred.key_value.to_string()),
702            rp_id: Some(cred.rp_id.to_string()),
703            user_handle: cred.user_handle.map(|h| h.to_string()),
704            user_name: cred.user_name.map(|n| n.to_string()),
705            counter: Some(cred.counter.to_string()),
706            rp_name: cred.rp_name.map(|n| n.to_string()),
707            user_display_name: cred.user_display_name.map(|n| n.to_string()),
708            discoverable: Some(cred.discoverable.to_string()),
709            creation_date: cred.creation_date.to_rfc3339(),
710        }
711    }
712}
713
714impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
715    fn from(login: Login) -> Self {
716        bitwarden_api_api::models::CipherLoginModel {
717            uri: None,
718            uris: login
719                .uris
720                .map(|u| u.into_iter().map(|u| u.into()).collect()),
721            username: login.username.map(|u| u.to_string()),
722            password: login.password.map(|p| p.to_string()),
723            password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
724            totp: login.totp.map(|t| t.to_string()),
725            autofill_on_page_load: login.autofill_on_page_load,
726            fido2_credentials: login
727                .fido2_credentials
728                .map(|c| c.into_iter().map(|c| c.into()).collect()),
729        }
730    }
731}
732
733impl CipherKind for Login {
734    fn decrypt_subtitle(
735        &self,
736        ctx: &mut KeyStoreContext<KeySlotIds>,
737        key: SymmetricKeySlotId,
738    ) -> Result<String, CryptoError> {
739        let username: Option<String> = self.username.decrypt(ctx, key)?;
740
741        Ok(username.unwrap_or_default())
742    }
743
744    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
745        [
746            self.username
747                .as_ref()
748                .map(|_| CopyableCipherFields::LoginUsername),
749            self.password
750                .as_ref()
751                .map(|_| CopyableCipherFields::LoginPassword),
752            self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
753        ]
754        .into_iter()
755        .flatten()
756        .collect()
757    }
758}
759
760#[cfg(test)]
761mod tests {
762    use crate::{
763        Login,
764        cipher::cipher::{CipherKind, CopyableCipherFields},
765    };
766
767    #[test]
768    fn test_valid_checksum() {
769        let uri = super::LoginUriView {
770            uri: Some("https://example.com".to_string()),
771            r#match: Some(super::UriMatchType::Domain),
772            uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
773        };
774        assert!(uri.is_checksum_valid());
775    }
776
777    #[test]
778    fn test_invalid_checksum() {
779        let uri = super::LoginUriView {
780            uri: Some("https://example.com".to_string()),
781            r#match: Some(super::UriMatchType::Domain),
782            uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
783        };
784        assert!(!uri.is_checksum_valid());
785    }
786
787    #[test]
788    fn test_missing_checksum() {
789        let uri = super::LoginUriView {
790            uri: Some("https://example.com".to_string()),
791            r#match: Some(super::UriMatchType::Domain),
792            uri_checksum: None,
793        };
794        assert!(!uri.is_checksum_valid());
795    }
796
797    #[test]
798    fn test_generate_checksum() {
799        let mut uri = super::LoginUriView {
800            uri: Some("https://test.com".to_string()),
801            r#match: Some(super::UriMatchType::Domain),
802            uri_checksum: None,
803        };
804
805        uri.generate_checksum();
806
807        assert_eq!(
808            uri.uri_checksum.unwrap().as_str(),
809            "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
810        );
811    }
812
813    #[test]
814    fn test_get_copyable_fields_login_password() {
815        let login_with_password = Login {
816            username: None,
817            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
818            password_revision_date: None,
819            uris: None,
820            totp: None,
821            autofill_on_page_load: None,
822            fido2_credentials: None,
823        };
824
825        let copyable_fields = login_with_password.get_copyable_fields(None);
826        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
827    }
828
829    #[test]
830    fn test_get_copyable_fields_login_username() {
831        let login_with_username = Login {
832            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
833            password: None,
834            password_revision_date: None,
835            uris: None,
836            totp: None,
837            autofill_on_page_load: None,
838            fido2_credentials: None,
839        };
840
841        let copyable_fields = login_with_username.get_copyable_fields(None);
842        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
843    }
844
845    #[test]
846    fn test_get_copyable_fields_login_everything() {
847        let login = Login {
848            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
849            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
850            password_revision_date: None,
851            uris: None,
852            totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
853            autofill_on_page_load: None,
854            fido2_credentials: None,
855        };
856
857        let copyable_fields = login.get_copyable_fields(None);
858        assert_eq!(
859            copyable_fields,
860            vec![
861                CopyableCipherFields::LoginUsername,
862                CopyableCipherFields::LoginPassword,
863                CopyableCipherFields::LoginTotp
864            ]
865        );
866    }
867}