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