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