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    /// Projects this [`LoginView`] into a [`LoginListView`].
344    ///
345    /// `totp` is re-encrypted under `cipher_key` because [`LoginListView`] stores the
346    /// TOTP as an [`EncString`] that [`crate::CipherListView::get_totp_key`] decrypts
347    /// on demand. `fido2_credentials` are still encrypted on [`LoginView`], so they
348    /// decrypt directly to [`Fido2CredentialListView`] via the existing impl.
349    pub(crate) fn to_list_view(
350        &self,
351        ctx: &mut KeyStoreContext<KeySlotIds>,
352        cipher_key: SymmetricKeySlotId,
353    ) -> Result<LoginListView, CryptoError> {
354        let totp = self
355            .totp
356            .as_ref()
357            .map(|t| t.encrypt(ctx, cipher_key))
358            .transpose()?;
359
360        let fido2_credentials = self
361            .fido2_credentials
362            .as_ref()
363            .map(|creds| creds.decrypt(ctx, cipher_key))
364            .transpose()?;
365
366        Ok(LoginListView {
367            has_fido2: self.fido2_credentials.is_some(),
368            fido2_credentials,
369            username: self.username.clone(),
370            totp,
371            uris: self.uris.clone(),
372        })
373    }
374
375    /// Compares this LoginView to the original, and returns any new password history items.
376    pub(crate) fn detect_password_change(
377        &mut self,
378        original: &Option<LoginView>,
379    ) -> Vec<PasswordHistoryView> {
380        let Some(original_login) = original else {
381            return vec![];
382        };
383
384        let original_password = original_login.password.as_deref().unwrap_or("");
385        let current_password = self.password.as_deref().unwrap_or("");
386
387        if original_password.is_empty() {
388            // No original password - set revision date only if adding new password
389            if !current_password.is_empty() {
390                self.password_revision_date = Some(Utc::now());
391            }
392            vec![]
393        } else if original_password == current_password {
394            // Password unchanged - preserve original revision date
395            self.password_revision_date = original_login.password_revision_date;
396            vec![]
397        } else {
398            // Password changed - update revision date and track change
399            self.password_revision_date = Some(Utc::now());
400            vec![PasswordHistoryView::new_password(original_password)]
401        }
402    }
403}
404
405#[allow(missing_docs)]
406#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
407#[serde(rename_all = "camelCase", deny_unknown_fields)]
408#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
409#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
410pub struct LoginListView {
411    pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
412    pub has_fido2: bool,
413    pub username: Option<String>,
414    /// The TOTP key is not decrypted. Useable as is with [`crate::generate_totp_cipher_view`].
415    pub totp: Option<EncString>,
416    pub uris: Option<Vec<LoginUriView>>,
417}
418
419impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, LoginUri> for LoginUriView {
420    fn encrypt_composite(
421        &self,
422        ctx: &mut KeyStoreContext<KeySlotIds>,
423        key: SymmetricKeySlotId,
424    ) -> Result<LoginUri, CryptoError> {
425        Ok(LoginUri {
426            uri: self.uri.encrypt(ctx, key)?,
427            r#match: self.r#match,
428            uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
429        })
430    }
431}
432
433// ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::CompositeEncryptable`: `LoginView` is a decrypted
434// DTO, yet it stores `fido2_credentials` as `Vec<Fido2Credential>` (already-encrypted values)
435// rather than a decrypted view type. Encryption therefore copies the ciphertext through unchanged
436// (`fido2_credentials: self.fido2_credentials.clone()` below) instead of re-encrypting it under
437// `key`. As a result decrypt(K) -> encrypt(K1) -> decrypt(K1) does NOT round-trip the credentials:
438// they remain wrapped under the original key K. Callers that rewrap the cipher key must invoke
439// `LoginView::reencrypt_fido2_credentials` explicitly to keep the credentials decryptable.
440impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Login> for LoginView {
441    fn encrypt_composite(
442        &self,
443        ctx: &mut KeyStoreContext<KeySlotIds>,
444        key: SymmetricKeySlotId,
445    ) -> Result<Login, CryptoError> {
446        Ok(Login {
447            username: self.username.encrypt(ctx, key)?,
448            password: self.password.encrypt(ctx, key)?,
449            password_revision_date: self.password_revision_date,
450            uris: self.uris.encrypt_composite(ctx, key)?,
451            totp: self
452                .totp
453                .clone()
454                .filter(|s| !s.is_empty())
455                .encrypt(ctx, key)?,
456            autofill_on_page_load: self.autofill_on_page_load,
457            // ⚠️ pass-through of already-encrypted credentials — see the contract-violation note
458            // above.
459            fido2_credentials: self.fido2_credentials.clone(),
460        })
461    }
462}
463
464impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginUriView> for LoginUri {
465    fn decrypt(
466        &self,
467        ctx: &mut KeyStoreContext<KeySlotIds>,
468        key: SymmetricKeySlotId,
469    ) -> Result<LoginUriView, CryptoError> {
470        Ok(LoginUriView {
471            uri: self.uri.decrypt(ctx, key)?,
472            r#match: self.r#match,
473            uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
474        })
475    }
476}
477
478impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginView> for Login {
479    fn decrypt(
480        &self,
481        ctx: &mut KeyStoreContext<KeySlotIds>,
482        key: SymmetricKeySlotId,
483    ) -> Result<LoginView, CryptoError> {
484        Ok(LoginView {
485            username: self.username.decrypt(ctx, key).ok().flatten(),
486            password: self.password.decrypt(ctx, key).ok().flatten(),
487            password_revision_date: self.password_revision_date,
488            uris: self.uris.decrypt(ctx, key).ok().flatten(),
489            totp: self.totp.decrypt(ctx, key).ok().flatten(),
490            autofill_on_page_load: self.autofill_on_page_load,
491            // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting `LoginView`
492            // is a decrypted DTO, but `fido2_credentials` are copied through still
493            // encrypted (`self.fido2_credentials.clone()`) rather than decrypted,
494            // because `LoginView` stores them as the encrypted `Vec<Fido2Credential>`.
495            // Consumers must decrypt each credential separately.
496            fido2_credentials: self.fido2_credentials.clone(),
497        })
498    }
499}
500
501impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginListView> for Login {
502    fn decrypt(
503        &self,
504        ctx: &mut KeyStoreContext<KeySlotIds>,
505        key: SymmetricKeySlotId,
506    ) -> Result<LoginListView, CryptoError> {
507        Ok(LoginListView {
508            fido2_credentials: self
509                .fido2_credentials
510                .as_ref()
511                .and_then(|fido2_credentials| fido2_credentials.decrypt(ctx, key).ok()),
512            has_fido2: self.fido2_credentials.is_some(),
513            username: self.username.decrypt(ctx, key).ok().flatten(),
514            totp: self.totp.clone(),
515            uris: self.uris.decrypt(ctx, key).ok().flatten(),
516        })
517    }
518}
519
520impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginView> for StrictDecrypt<&Login> {
521    fn decrypt(
522        &self,
523        ctx: &mut KeyStoreContext<KeySlotIds>,
524        key: SymmetricKeySlotId,
525    ) -> Result<LoginView, CryptoError> {
526        Ok(LoginView {
527            username: self.0.username.decrypt(ctx, key)?,
528            password: self.0.password.decrypt(ctx, key)?,
529            password_revision_date: self.0.password_revision_date,
530            uris: self.0.uris.decrypt(ctx, key)?,
531            totp: self.0.totp.decrypt(ctx, key)?,
532            autofill_on_page_load: self.0.autofill_on_page_load,
533            // ⚠️ CONTRACT VIOLATION of `bitwarden_crypto::Decryptable`: the resulting `LoginView`
534            // is a decrypted DTO, but `fido2_credentials` are copied through still
535            // encrypted (`self.0.fido2_credentials.clone()`) rather than decrypted,
536            // because `LoginView` stores them as the encrypted `Vec<Fido2Credential>`.
537            // Consumers must decrypt each credential separately.
538            fido2_credentials: self.0.fido2_credentials.clone(),
539        })
540    }
541}
542
543impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginListView> for StrictDecrypt<&Login> {
544    fn decrypt(
545        &self,
546        ctx: &mut KeyStoreContext<KeySlotIds>,
547        key: SymmetricKeySlotId,
548    ) -> Result<LoginListView, CryptoError> {
549        Ok(LoginListView {
550            fido2_credentials: self
551                .0
552                .fido2_credentials
553                .as_ref()
554                .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
555                .transpose()?,
556            has_fido2: self.0.fido2_credentials.is_some(),
557            username: self.0.username.decrypt(ctx, key)?,
558            totp: self.0.totp.clone(),
559            uris: self.0.uris.decrypt(ctx, key)?,
560        })
561    }
562}
563
564impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialView> for Fido2Credential {
565    fn decrypt(
566        &self,
567        ctx: &mut KeyStoreContext<KeySlotIds>,
568        key: SymmetricKeySlotId,
569    ) -> Result<Fido2CredentialView, CryptoError> {
570        Ok(Fido2CredentialView {
571            credential_id: self.credential_id.decrypt(ctx, key)?,
572            key_type: self.key_type.decrypt(ctx, key)?,
573            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
574            key_curve: self.key_curve.decrypt(ctx, key)?,
575            key_value: self.key_value.clone(),
576            rp_id: self.rp_id.decrypt(ctx, key)?,
577            user_handle: self.user_handle.decrypt(ctx, key)?,
578            user_name: self.user_name.decrypt(ctx, key)?,
579            counter: self.counter.decrypt(ctx, key)?,
580            rp_name: self.rp_name.decrypt(ctx, key)?,
581            user_display_name: self.user_display_name.decrypt(ctx, key)?,
582            discoverable: self.discoverable.decrypt(ctx, key)?,
583            creation_date: self.creation_date,
584        })
585    }
586}
587
588impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialListView> for Fido2Credential {
589    fn decrypt(
590        &self,
591        ctx: &mut KeyStoreContext<KeySlotIds>,
592        key: SymmetricKeySlotId,
593    ) -> Result<Fido2CredentialListView, CryptoError> {
594        Ok(Fido2CredentialListView {
595            credential_id: self.credential_id.decrypt(ctx, key)?,
596            rp_id: self.rp_id.decrypt(ctx, key)?,
597            user_handle: self.user_handle.decrypt(ctx, key)?,
598            user_name: self.user_name.decrypt(ctx, key)?,
599            user_display_name: self.user_display_name.decrypt(ctx, key)?,
600            counter: self.counter.decrypt(ctx, key)?,
601        })
602    }
603}
604
605impl TryFrom<CipherLoginModel> for Login {
606    type Error = VaultParseError;
607
608    fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
609        Ok(Self {
610            username: EncString::try_from_optional(login.username)?,
611            password: EncString::try_from_optional(login.password)?,
612            password_revision_date: login
613                .password_revision_date
614                .map(|d| d.parse())
615                .transpose()?,
616            uris: login
617                .uris
618                .map(|v| v.into_iter().map(|u| u.try_into()).collect())
619                .transpose()?,
620            totp: EncString::try_from_optional(login.totp)?,
621            autofill_on_page_load: login.autofill_on_page_load,
622            fido2_credentials: login
623                .fido2_credentials
624                .map(|v| v.into_iter().map(|c| c.try_into()).collect())
625                .transpose()?,
626        })
627    }
628}
629
630impl TryFrom<CipherLoginUriModel> for LoginUri {
631    type Error = VaultParseError;
632
633    fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
634        Ok(Self {
635            uri: EncString::try_from_optional(uri.uri)?,
636            r#match: uri.r#match.map(|m| m.try_into()).transpose()?,
637            uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
638        })
639    }
640}
641
642impl TryFrom<bitwarden_api_api::models::UriMatchType> for UriMatchType {
643    type Error = bitwarden_core::MissingFieldError;
644
645    fn try_from(value: bitwarden_api_api::models::UriMatchType) -> Result<Self, Self::Error> {
646        Ok(match value {
647            bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
648            bitwarden_api_api::models::UriMatchType::Host => Self::Host,
649            bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
650            bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
651            bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
652            bitwarden_api_api::models::UriMatchType::Never => Self::Never,
653            bitwarden_api_api::models::UriMatchType::__Unknown(_) => {
654                return Err(bitwarden_core::MissingFieldError("match"));
655            }
656        })
657    }
658}
659
660impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
661    type Error = VaultParseError;
662
663    fn try_from(
664        value: bitwarden_api_api::models::CipherFido2CredentialModel,
665    ) -> Result<Self, Self::Error> {
666        Ok(Self {
667            credential_id: require!(value.credential_id).parse()?,
668            key_type: require!(value.key_type).parse()?,
669            key_algorithm: require!(value.key_algorithm).parse()?,
670            key_curve: require!(value.key_curve).parse()?,
671            key_value: require!(value.key_value).parse()?,
672            rp_id: require!(value.rp_id).parse()?,
673            user_handle: EncString::try_from_optional(value.user_handle)
674                .ok()
675                .flatten(),
676            user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
677            counter: require!(value.counter).parse()?,
678            rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
679            user_display_name: EncString::try_from_optional(value.user_display_name)
680                .ok()
681                .flatten(),
682            discoverable: require!(value.discoverable).parse()?,
683            creation_date: value.creation_date.parse()?,
684        })
685    }
686}
687
688impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
689    fn from(uri: LoginUri) -> Self {
690        bitwarden_api_api::models::CipherLoginUriModel {
691            uri: uri.uri.map(|u| u.to_string()),
692            uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
693            r#match: uri.r#match.map(|m| m.into()),
694        }
695    }
696}
697
698impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
699    fn from(match_type: UriMatchType) -> Self {
700        match match_type {
701            UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
702            UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
703            UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
704            UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
705            UriMatchType::RegularExpression => {
706                bitwarden_api_api::models::UriMatchType::RegularExpression
707            }
708            UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
709        }
710    }
711}
712
713impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
714    fn from(cred: Fido2Credential) -> Self {
715        bitwarden_api_api::models::CipherFido2CredentialModel {
716            credential_id: Some(cred.credential_id.to_string()),
717            key_type: Some(cred.key_type.to_string()),
718            key_algorithm: Some(cred.key_algorithm.to_string()),
719            key_curve: Some(cred.key_curve.to_string()),
720            key_value: Some(cred.key_value.to_string()),
721            rp_id: Some(cred.rp_id.to_string()),
722            user_handle: cred.user_handle.map(|h| h.to_string()),
723            user_name: cred.user_name.map(|n| n.to_string()),
724            counter: Some(cred.counter.to_string()),
725            rp_name: cred.rp_name.map(|n| n.to_string()),
726            user_display_name: cred.user_display_name.map(|n| n.to_string()),
727            discoverable: Some(cred.discoverable.to_string()),
728            creation_date: cred.creation_date.to_rfc3339(),
729        }
730    }
731}
732
733impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
734    fn from(login: Login) -> Self {
735        bitwarden_api_api::models::CipherLoginModel {
736            uri: None,
737            uris: login
738                .uris
739                .map(|u| u.into_iter().map(|u| u.into()).collect()),
740            username: login.username.map(|u| u.to_string()),
741            password: login.password.map(|p| p.to_string()),
742            password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
743            totp: login.totp.map(|t| t.to_string()),
744            autofill_on_page_load: login.autofill_on_page_load,
745            fido2_credentials: login
746                .fido2_credentials
747                .map(|c| c.into_iter().map(|c| c.into()).collect()),
748        }
749    }
750}
751
752impl CipherKind for Login {
753    fn decrypt_subtitle(
754        &self,
755        ctx: &mut KeyStoreContext<KeySlotIds>,
756        key: SymmetricKeySlotId,
757    ) -> Result<String, CryptoError> {
758        let username: Option<String> = self.username.decrypt(ctx, key)?;
759
760        Ok(username.unwrap_or_default())
761    }
762
763    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
764        [
765            self.username
766                .as_ref()
767                .map(|_| CopyableCipherFields::LoginUsername),
768            self.password
769                .as_ref()
770                .map(|_| CopyableCipherFields::LoginPassword),
771            self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
772        ]
773        .into_iter()
774        .flatten()
775        .collect()
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use crate::{
782        Login,
783        cipher::cipher::{CipherKind, CopyableCipherFields},
784    };
785
786    #[test]
787    fn test_valid_checksum() {
788        let uri = super::LoginUriView {
789            uri: Some("https://example.com".to_string()),
790            r#match: Some(super::UriMatchType::Domain),
791            uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
792        };
793        assert!(uri.is_checksum_valid());
794    }
795
796    #[test]
797    fn test_invalid_checksum() {
798        let uri = super::LoginUriView {
799            uri: Some("https://example.com".to_string()),
800            r#match: Some(super::UriMatchType::Domain),
801            uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
802        };
803        assert!(!uri.is_checksum_valid());
804    }
805
806    #[test]
807    fn test_missing_checksum() {
808        let uri = super::LoginUriView {
809            uri: Some("https://example.com".to_string()),
810            r#match: Some(super::UriMatchType::Domain),
811            uri_checksum: None,
812        };
813        assert!(!uri.is_checksum_valid());
814    }
815
816    #[test]
817    fn test_generate_checksum() {
818        let mut uri = super::LoginUriView {
819            uri: Some("https://test.com".to_string()),
820            r#match: Some(super::UriMatchType::Domain),
821            uri_checksum: None,
822        };
823
824        uri.generate_checksum();
825
826        assert_eq!(
827            uri.uri_checksum.unwrap().as_str(),
828            "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
829        );
830    }
831
832    #[test]
833    fn test_get_copyable_fields_login_password() {
834        let login_with_password = Login {
835            username: None,
836            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
837            password_revision_date: None,
838            uris: None,
839            totp: None,
840            autofill_on_page_load: None,
841            fido2_credentials: None,
842        };
843
844        let copyable_fields = login_with_password.get_copyable_fields(None);
845        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
846    }
847
848    #[test]
849    fn test_get_copyable_fields_login_username() {
850        let login_with_username = Login {
851            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
852            password: None,
853            password_revision_date: None,
854            uris: None,
855            totp: None,
856            autofill_on_page_load: None,
857            fido2_credentials: None,
858        };
859
860        let copyable_fields = login_with_username.get_copyable_fields(None);
861        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
862    }
863
864    #[test]
865    fn test_get_copyable_fields_login_everything() {
866        let login = Login {
867            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
868            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
869            password_revision_date: None,
870            uris: None,
871            totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
872            autofill_on_page_load: None,
873            fido2_credentials: None,
874        };
875
876        let copyable_fields = login.get_copyable_fields(None);
877        assert_eq!(
878            copyable_fields,
879            vec![
880                CopyableCipherFields::LoginUsername,
881                CopyableCipherFields::LoginPassword,
882                CopyableCipherFields::LoginTotp
883            ]
884        );
885    }
886}