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