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