bitwarden_vault/cipher/
login.rs

1use base64::{engine::general_purpose::STANDARD, Engine};
2use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel};
3use bitwarden_core::{
4    key_management::{KeyIds, SymmetricKeyId},
5    require,
6};
7use bitwarden_crypto::{
8    CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
9    PrimitiveEncryptable,
10};
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14#[cfg(feature = "wasm")]
15use tsify::Tsify;
16#[cfg(feature = "wasm")]
17use wasm_bindgen::prelude::wasm_bindgen;
18
19use super::cipher::CipherKind;
20use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
21
22#[allow(missing_docs)]
23#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
24#[repr(u8)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
27#[cfg_attr(feature = "wasm", wasm_bindgen)]
28pub enum UriMatchType {
29    Domain = 0,
30    Host = 1,
31    StartsWith = 2,
32    Exact = 3,
33    RegularExpression = 4,
34    Never = 5,
35}
36
37#[derive(Serialize, Deserialize, Debug, Clone)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41pub struct LoginUri {
42    pub uri: Option<EncString>,
43    pub r#match: Option<UriMatchType>,
44    pub uri_checksum: Option<EncString>,
45}
46
47#[allow(missing_docs)]
48#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
49#[serde(rename_all = "camelCase", deny_unknown_fields)]
50#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
51#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
52pub struct LoginUriView {
53    pub uri: Option<String>,
54    pub r#match: Option<UriMatchType>,
55    pub uri_checksum: Option<String>,
56}
57
58impl LoginUriView {
59    pub(crate) fn is_checksum_valid(&self) -> bool {
60        let Some(uri) = &self.uri else {
61            return false;
62        };
63        let Some(cs) = &self.uri_checksum else {
64            return false;
65        };
66        let Ok(cs) = STANDARD.decode(cs) else {
67            return false;
68        };
69
70        use sha2::Digest;
71        let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
72
73        uri_hash.as_slice() == cs
74    }
75
76    pub(crate) fn generate_checksum(&mut self) {
77        if let Some(uri) = &self.uri {
78            use sha2::Digest;
79            let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
80            let uri_hash = STANDARD.encode(uri_hash.as_slice());
81            self.uri_checksum = Some(uri_hash);
82        }
83    }
84}
85
86#[allow(missing_docs)]
87#[derive(Serialize, Deserialize, Debug, Clone)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
90#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
108#[serde(rename_all = "camelCase", deny_unknown_fields)]
109#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
110#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
111pub struct Fido2CredentialListView {
112    pub credential_id: String,
113    pub rp_id: String,
114    pub user_handle: Option<String>,
115    pub user_name: Option<String>,
116    pub user_display_name: Option<String>,
117    pub counter: String,
118}
119
120#[allow(missing_docs)]
121#[derive(Serialize, Deserialize, Debug, Clone)]
122#[serde(rename_all = "camelCase", deny_unknown_fields)]
123#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
124#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
125pub struct Fido2CredentialView {
126    pub credential_id: String,
127    pub key_type: String,
128    pub key_algorithm: String,
129    pub key_curve: String,
130    // This value doesn't need to be returned to the client
131    // so we keep it encrypted until we need it
132    pub key_value: EncString,
133    pub rp_id: String,
134    pub user_handle: Option<String>,
135    pub user_name: Option<String>,
136    pub counter: String,
137    pub rp_name: Option<String>,
138    pub user_display_name: Option<String>,
139    pub discoverable: String,
140    pub creation_date: DateTime<Utc>,
141}
142
143// This is mostly a copy of the Fido2CredentialView, but with the key exposed
144// Only meant to be used internally and not exposed to the outside world
145#[allow(missing_docs)]
146#[derive(Serialize, Deserialize, Debug, Clone)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
149pub struct Fido2CredentialFullView {
150    pub credential_id: String,
151    pub key_type: String,
152    pub key_algorithm: String,
153    pub key_curve: String,
154    pub key_value: String,
155    pub rp_id: String,
156    pub user_handle: Option<String>,
157    pub user_name: Option<String>,
158    pub counter: String,
159    pub rp_name: Option<String>,
160    pub user_display_name: Option<String>,
161    pub discoverable: String,
162    pub creation_date: DateTime<Utc>,
163}
164
165// This is mostly a copy of the Fido2CredentialView, meant to be exposed to the clients
166// to let them select where to store the new credential. Note that it doesn't contain
167// the encrypted key as that is only filled when the cipher is selected
168#[allow(missing_docs)]
169#[derive(Serialize, Deserialize, Debug, Clone)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
172#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
173pub struct Fido2CredentialNewView {
174    pub credential_id: String,
175    pub key_type: String,
176    pub key_algorithm: String,
177    pub key_curve: String,
178    pub rp_id: String,
179    pub user_handle: Option<String>,
180    pub user_name: Option<String>,
181    pub counter: String,
182    pub rp_name: Option<String>,
183    pub user_display_name: Option<String>,
184    pub creation_date: DateTime<Utc>,
185}
186
187impl From<Fido2CredentialFullView> for Fido2CredentialNewView {
188    fn from(value: Fido2CredentialFullView) -> Self {
189        Fido2CredentialNewView {
190            credential_id: value.credential_id,
191            key_type: value.key_type,
192            key_algorithm: value.key_algorithm,
193            key_curve: value.key_curve,
194            rp_id: value.rp_id,
195            user_handle: value.user_handle,
196            user_name: value.user_name,
197            counter: value.counter,
198            rp_name: value.rp_name,
199            user_display_name: value.user_display_name,
200            creation_date: value.creation_date,
201        }
202    }
203}
204
205impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialFullView {
206    fn encrypt_composite(
207        &self,
208        ctx: &mut KeyStoreContext<KeyIds>,
209        key: SymmetricKeyId,
210    ) -> Result<Fido2Credential, CryptoError> {
211        Ok(Fido2Credential {
212            credential_id: self.credential_id.encrypt(ctx, key)?,
213            key_type: self.key_type.encrypt(ctx, key)?,
214            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
215            key_curve: self.key_curve.encrypt(ctx, key)?,
216            key_value: self.key_value.encrypt(ctx, key)?,
217            rp_id: self.rp_id.encrypt(ctx, key)?,
218            user_handle: self
219                .user_handle
220                .as_ref()
221                .map(|h| h.encrypt(ctx, key))
222                .transpose()?,
223            user_name: self.user_name.encrypt(ctx, key)?,
224            counter: self.counter.encrypt(ctx, key)?,
225            rp_name: self.rp_name.encrypt(ctx, key)?,
226            user_display_name: self.user_display_name.encrypt(ctx, key)?,
227            discoverable: self.discoverable.encrypt(ctx, key)?,
228            creation_date: self.creation_date,
229        })
230    }
231}
232
233impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2Credential {
234    fn decrypt(
235        &self,
236        ctx: &mut KeyStoreContext<KeyIds>,
237        key: SymmetricKeyId,
238    ) -> Result<Fido2CredentialFullView, CryptoError> {
239        Ok(Fido2CredentialFullView {
240            credential_id: self.credential_id.decrypt(ctx, key)?,
241            key_type: self.key_type.decrypt(ctx, key)?,
242            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
243            key_curve: self.key_curve.decrypt(ctx, key)?,
244            key_value: self.key_value.decrypt(ctx, key)?,
245            rp_id: self.rp_id.decrypt(ctx, key)?,
246            user_handle: self.user_handle.decrypt(ctx, key)?,
247            user_name: self.user_name.decrypt(ctx, key)?,
248            counter: self.counter.decrypt(ctx, key)?,
249            rp_name: self.rp_name.decrypt(ctx, key)?,
250            user_display_name: self.user_display_name.decrypt(ctx, key)?,
251            discoverable: self.discoverable.decrypt(ctx, key)?,
252            creation_date: self.creation_date,
253        })
254    }
255}
256
257impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2CredentialView {
258    fn decrypt(
259        &self,
260        ctx: &mut KeyStoreContext<KeyIds>,
261        key: SymmetricKeyId,
262    ) -> Result<Fido2CredentialFullView, CryptoError> {
263        Ok(Fido2CredentialFullView {
264            credential_id: self.credential_id.clone(),
265            key_type: self.key_type.clone(),
266            key_algorithm: self.key_algorithm.clone(),
267            key_curve: self.key_curve.clone(),
268            key_value: self.key_value.decrypt(ctx, key)?,
269            rp_id: self.rp_id.clone(),
270            user_handle: self.user_handle.clone(),
271            user_name: self.user_name.clone(),
272            counter: self.counter.clone(),
273            rp_name: self.rp_name.clone(),
274            user_display_name: self.user_display_name.clone(),
275            discoverable: self.discoverable.clone(),
276            creation_date: self.creation_date,
277        })
278    }
279}
280
281#[allow(missing_docs)]
282#[derive(Serialize, Deserialize, Debug, Clone)]
283#[serde(rename_all = "camelCase", deny_unknown_fields)]
284#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
285#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
286pub struct Login {
287    pub username: Option<EncString>,
288    pub password: Option<EncString>,
289    pub password_revision_date: Option<DateTime<Utc>>,
290
291    pub uris: Option<Vec<LoginUri>>,
292    pub totp: Option<EncString>,
293    pub autofill_on_page_load: Option<bool>,
294
295    pub fido2_credentials: Option<Vec<Fido2Credential>>,
296}
297
298#[allow(missing_docs)]
299#[derive(Serialize, Deserialize, Debug, Clone)]
300#[serde(rename_all = "camelCase", deny_unknown_fields)]
301#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
302#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
303pub struct LoginView {
304    pub username: Option<String>,
305    pub password: Option<String>,
306    pub password_revision_date: Option<DateTime<Utc>>,
307
308    pub uris: Option<Vec<LoginUriView>>,
309    pub totp: Option<String>,
310    pub autofill_on_page_load: Option<bool>,
311
312    // TODO: Remove this once the SDK supports state
313    pub fido2_credentials: Option<Vec<Fido2Credential>>,
314}
315
316#[allow(missing_docs)]
317#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
318#[serde(rename_all = "camelCase", deny_unknown_fields)]
319#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
320#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
321pub struct LoginListView {
322    pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
323    pub has_fido2: bool,
324    pub username: Option<String>,
325    /// The TOTP key is not decrypted. Useable as is with [`crate::generate_totp_cipher_view`].
326    pub totp: Option<EncString>,
327    pub uris: Option<Vec<LoginUriView>>,
328}
329
330impl CompositeEncryptable<KeyIds, SymmetricKeyId, LoginUri> for LoginUriView {
331    fn encrypt_composite(
332        &self,
333        ctx: &mut KeyStoreContext<KeyIds>,
334        key: SymmetricKeyId,
335    ) -> Result<LoginUri, CryptoError> {
336        Ok(LoginUri {
337            uri: self.uri.encrypt(ctx, key)?,
338            r#match: self.r#match,
339            uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
340        })
341    }
342}
343
344impl CompositeEncryptable<KeyIds, SymmetricKeyId, Login> for LoginView {
345    fn encrypt_composite(
346        &self,
347        ctx: &mut KeyStoreContext<KeyIds>,
348        key: SymmetricKeyId,
349    ) -> Result<Login, CryptoError> {
350        Ok(Login {
351            username: self.username.encrypt(ctx, key)?,
352            password: self.password.encrypt(ctx, key)?,
353            password_revision_date: self.password_revision_date,
354            uris: self.uris.encrypt_composite(ctx, key)?,
355            totp: self.totp.encrypt(ctx, key)?,
356            autofill_on_page_load: self.autofill_on_page_load,
357            fido2_credentials: self.fido2_credentials.clone(),
358        })
359    }
360}
361
362impl Decryptable<KeyIds, SymmetricKeyId, LoginUriView> for LoginUri {
363    fn decrypt(
364        &self,
365        ctx: &mut KeyStoreContext<KeyIds>,
366        key: SymmetricKeyId,
367    ) -> Result<LoginUriView, CryptoError> {
368        Ok(LoginUriView {
369            uri: self.uri.decrypt(ctx, key)?,
370            r#match: self.r#match,
371            uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
372        })
373    }
374}
375
376impl Decryptable<KeyIds, SymmetricKeyId, LoginView> for Login {
377    fn decrypt(
378        &self,
379        ctx: &mut KeyStoreContext<KeyIds>,
380        key: SymmetricKeyId,
381    ) -> Result<LoginView, CryptoError> {
382        Ok(LoginView {
383            username: self.username.decrypt(ctx, key).ok().flatten(),
384            password: self.password.decrypt(ctx, key).ok().flatten(),
385            password_revision_date: self.password_revision_date,
386            uris: self.uris.decrypt(ctx, key).ok().flatten(),
387            totp: self.totp.decrypt(ctx, key).ok().flatten(),
388            autofill_on_page_load: self.autofill_on_page_load,
389            fido2_credentials: self.fido2_credentials.clone(),
390        })
391    }
392}
393
394impl Decryptable<KeyIds, SymmetricKeyId, LoginListView> for Login {
395    fn decrypt(
396        &self,
397        ctx: &mut KeyStoreContext<KeyIds>,
398        key: SymmetricKeyId,
399    ) -> Result<LoginListView, CryptoError> {
400        Ok(LoginListView {
401            fido2_credentials: self
402                .fido2_credentials
403                .as_ref()
404                .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
405                .transpose()?,
406            has_fido2: self.fido2_credentials.is_some(),
407            username: self.username.decrypt(ctx, key).ok().flatten(),
408            totp: self.totp.clone(),
409            uris: self.uris.decrypt(ctx, key).ok().flatten(),
410        })
411    }
412}
413
414impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialView {
415    fn encrypt_composite(
416        &self,
417        ctx: &mut KeyStoreContext<KeyIds>,
418        key: SymmetricKeyId,
419    ) -> Result<Fido2Credential, CryptoError> {
420        Ok(Fido2Credential {
421            credential_id: self.credential_id.encrypt(ctx, key)?,
422            key_type: self.key_type.encrypt(ctx, key)?,
423            key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
424            key_curve: self.key_curve.encrypt(ctx, key)?,
425            key_value: self.key_value.clone(),
426            rp_id: self.rp_id.encrypt(ctx, key)?,
427            user_handle: self
428                .user_handle
429                .as_ref()
430                .map(|h| h.encrypt(ctx, key))
431                .transpose()?,
432            user_name: self
433                .user_name
434                .as_ref()
435                .map(|n| n.encrypt(ctx, key))
436                .transpose()?,
437            counter: self.counter.encrypt(ctx, key)?,
438            rp_name: self.rp_name.encrypt(ctx, key)?,
439            user_display_name: self.user_display_name.encrypt(ctx, key)?,
440            discoverable: self.discoverable.encrypt(ctx, key)?,
441            creation_date: self.creation_date,
442        })
443    }
444}
445
446impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialView> for Fido2Credential {
447    fn decrypt(
448        &self,
449        ctx: &mut KeyStoreContext<KeyIds>,
450        key: SymmetricKeyId,
451    ) -> Result<Fido2CredentialView, CryptoError> {
452        Ok(Fido2CredentialView {
453            credential_id: self.credential_id.decrypt(ctx, key)?,
454            key_type: self.key_type.decrypt(ctx, key)?,
455            key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
456            key_curve: self.key_curve.decrypt(ctx, key)?,
457            key_value: self.key_value.clone(),
458            rp_id: self.rp_id.decrypt(ctx, key)?,
459            user_handle: self.user_handle.decrypt(ctx, key)?,
460            user_name: self.user_name.decrypt(ctx, key)?,
461            counter: self.counter.decrypt(ctx, key)?,
462            rp_name: self.rp_name.decrypt(ctx, key)?,
463            user_display_name: self.user_display_name.decrypt(ctx, key)?,
464            discoverable: self.discoverable.decrypt(ctx, key)?,
465            creation_date: self.creation_date,
466        })
467    }
468}
469
470impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialListView> for Fido2Credential {
471    fn decrypt(
472        &self,
473        ctx: &mut KeyStoreContext<KeyIds>,
474        key: SymmetricKeyId,
475    ) -> Result<Fido2CredentialListView, CryptoError> {
476        Ok(Fido2CredentialListView {
477            credential_id: self.credential_id.decrypt(ctx, key)?,
478            rp_id: self.rp_id.decrypt(ctx, key)?,
479            user_handle: self.user_handle.decrypt(ctx, key)?,
480            user_name: self.user_name.decrypt(ctx, key)?,
481            user_display_name: self.user_display_name.decrypt(ctx, key)?,
482            counter: self.counter.decrypt(ctx, key)?,
483        })
484    }
485}
486
487impl TryFrom<CipherLoginModel> for Login {
488    type Error = VaultParseError;
489
490    fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
491        Ok(Self {
492            username: EncString::try_from_optional(login.username)?,
493            password: EncString::try_from_optional(login.password)?,
494            password_revision_date: login
495                .password_revision_date
496                .map(|d| d.parse())
497                .transpose()?,
498            uris: login
499                .uris
500                .map(|v| v.into_iter().map(|u| u.try_into()).collect())
501                .transpose()?,
502            totp: EncString::try_from_optional(login.totp)?,
503            autofill_on_page_load: login.autofill_on_page_load,
504            fido2_credentials: login
505                .fido2_credentials
506                .map(|v| v.into_iter().map(|c| c.try_into()).collect())
507                .transpose()?,
508        })
509    }
510}
511
512impl TryFrom<CipherLoginUriModel> for LoginUri {
513    type Error = VaultParseError;
514
515    fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
516        Ok(Self {
517            uri: EncString::try_from_optional(uri.uri)?,
518            r#match: uri.r#match.map(|m| m.into()),
519            uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
520        })
521    }
522}
523
524impl From<bitwarden_api_api::models::UriMatchType> for UriMatchType {
525    fn from(value: bitwarden_api_api::models::UriMatchType) -> Self {
526        match value {
527            bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
528            bitwarden_api_api::models::UriMatchType::Host => Self::Host,
529            bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
530            bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
531            bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
532            bitwarden_api_api::models::UriMatchType::Never => Self::Never,
533        }
534    }
535}
536
537impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
538    type Error = VaultParseError;
539
540    fn try_from(
541        value: bitwarden_api_api::models::CipherFido2CredentialModel,
542    ) -> Result<Self, Self::Error> {
543        Ok(Self {
544            credential_id: require!(value.credential_id).parse()?,
545            key_type: require!(value.key_type).parse()?,
546            key_algorithm: require!(value.key_algorithm).parse()?,
547            key_curve: require!(value.key_curve).parse()?,
548            key_value: require!(value.key_value).parse()?,
549            rp_id: require!(value.rp_id).parse()?,
550            user_handle: EncString::try_from_optional(value.user_handle)
551                .ok()
552                .flatten(),
553            user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
554            counter: require!(value.counter).parse()?,
555            rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
556            user_display_name: EncString::try_from_optional(value.user_display_name)
557                .ok()
558                .flatten(),
559            discoverable: require!(value.discoverable).parse()?,
560            creation_date: value.creation_date.parse()?,
561        })
562    }
563}
564
565impl CipherKind for Login {
566    fn decrypt_subtitle(
567        &self,
568        ctx: &mut KeyStoreContext<KeyIds>,
569        key: SymmetricKeyId,
570    ) -> Result<String, CryptoError> {
571        let username: Option<String> = self.username.decrypt(ctx, key)?;
572
573        Ok(username.unwrap_or_default())
574    }
575
576    fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
577        [
578            self.username
579                .as_ref()
580                .map(|_| CopyableCipherFields::LoginUsername),
581            self.password
582                .as_ref()
583                .map(|_| CopyableCipherFields::LoginPassword),
584            self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
585        ]
586        .into_iter()
587        .flatten()
588        .collect()
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use crate::{
595        cipher::cipher::{CipherKind, CopyableCipherFields},
596        Login,
597    };
598
599    #[test]
600    fn test_valid_checksum() {
601        let uri = super::LoginUriView {
602            uri: Some("https://example.com".to_string()),
603            r#match: Some(super::UriMatchType::Domain),
604            uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
605        };
606        assert!(uri.is_checksum_valid());
607    }
608
609    #[test]
610    fn test_invalid_checksum() {
611        let uri = super::LoginUriView {
612            uri: Some("https://example.com".to_string()),
613            r#match: Some(super::UriMatchType::Domain),
614            uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
615        };
616        assert!(!uri.is_checksum_valid());
617    }
618
619    #[test]
620    fn test_missing_checksum() {
621        let uri = super::LoginUriView {
622            uri: Some("https://example.com".to_string()),
623            r#match: Some(super::UriMatchType::Domain),
624            uri_checksum: None,
625        };
626        assert!(!uri.is_checksum_valid());
627    }
628
629    #[test]
630    fn test_generate_checksum() {
631        let mut uri = super::LoginUriView {
632            uri: Some("https://test.com".to_string()),
633            r#match: Some(super::UriMatchType::Domain),
634            uri_checksum: None,
635        };
636
637        uri.generate_checksum();
638
639        assert_eq!(
640            uri.uri_checksum.unwrap().as_str(),
641            "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
642        );
643    }
644
645    #[test]
646    fn test_get_copyable_fields_login_password() {
647        let login_with_password = Login {
648            username: None,
649            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
650            password_revision_date: None,
651            uris: None,
652            totp: None,
653            autofill_on_page_load: None,
654            fido2_credentials: None,
655        };
656
657        let copyable_fields = login_with_password.get_copyable_fields(None);
658        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
659    }
660
661    #[test]
662    fn test_get_copyable_fields_login_username() {
663        let login_with_username = Login {
664            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
665            password: None,
666            password_revision_date: None,
667            uris: None,
668            totp: None,
669            autofill_on_page_load: None,
670            fido2_credentials: None,
671        };
672
673        let copyable_fields = login_with_username.get_copyable_fields(None);
674        assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
675    }
676
677    #[test]
678    fn test_get_copyable_fields_login_everything() {
679        let login = Login {
680            username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
681            password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
682            password_revision_date: None,
683            uris: None,
684            totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
685            autofill_on_page_load: None,
686            fido2_credentials: None,
687        };
688
689        let copyable_fields = login.get_copyable_fields(None);
690        assert_eq!(
691            copyable_fields,
692            vec![
693                CopyableCipherFields::LoginUsername,
694                CopyableCipherFields::LoginPassword,
695                CopyableCipherFields::LoginTotp
696            ]
697        );
698    }
699}