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