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 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#[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#[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 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 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}