1use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel};
2use bitwarden_core::{
3 key_management::{KeySlotIds, SymmetricKeySlotId},
4 require,
5};
6use bitwarden_crypto::{
7 CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
8 PrimitiveEncryptable,
9};
10use bitwarden_encoding::B64;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14use subtle::ConstantTimeEq;
15#[cfg(feature = "wasm")]
16use tsify::Tsify;
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::wasm_bindgen;
19
20use super::cipher::{CipherKind, StrictDecrypt};
21use crate::{Cipher, PasswordHistoryView, VaultParseError, cipher::cipher::CopyableCipherFields};
22
23#[allow(missing_docs)]
24#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
25#[repr(u8)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
28#[cfg_attr(feature = "wasm", wasm_bindgen)]
29pub enum UriMatchType {
30 Domain = 0,
31 Host = 1,
32 StartsWith = 2,
33 Exact = 3,
34 RegularExpression = 4,
35 Never = 5,
36}
37
38#[derive(Serialize, Deserialize, Debug, Clone)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
41#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
42pub struct LoginUri {
43 pub uri: Option<EncString>,
44 pub r#match: Option<UriMatchType>,
45 pub uri_checksum: Option<EncString>,
46}
47
48#[allow(missing_docs)]
49#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53pub struct LoginUriView {
54 pub uri: Option<String>,
55 pub r#match: Option<UriMatchType>,
56 pub uri_checksum: Option<String>,
57}
58
59impl LoginUriView {
60 pub(crate) fn is_checksum_valid(&self) -> bool {
61 let Some(uri) = &self.uri else {
62 return false;
63 };
64 let Some(cs) = &self.uri_checksum else {
65 return false;
66 };
67 let Ok(cs) = B64::try_from(cs.as_str()) else {
68 return false;
69 };
70
71 use sha2::Digest;
72 let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
73
74 uri_hash.as_slice().ct_eq(cs.as_bytes()).into()
75 }
76
77 pub(crate) fn generate_checksum(&mut self) {
78 if let Some(uri) = &self.uri {
79 use sha2::Digest;
80 let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
81 let uri_hash = B64::from(uri_hash.as_slice()).to_string();
82 self.uri_checksum = Some(uri_hash);
83 }
84 }
85}
86
87#[allow(missing_docs)]
88#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
89#[serde(rename_all = "camelCase", deny_unknown_fields)]
90#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
91#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
92pub struct Fido2Credential {
93 pub credential_id: EncString,
94 pub key_type: EncString,
95 pub key_algorithm: EncString,
96 pub key_curve: EncString,
97 pub key_value: EncString,
98 pub rp_id: EncString,
99 pub user_handle: Option<EncString>,
100 pub user_name: Option<EncString>,
101 pub counter: EncString,
102 pub rp_name: Option<EncString>,
103 pub user_display_name: Option<EncString>,
104 pub discoverable: EncString,
105 pub creation_date: DateTime<Utc>,
106}
107
108#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
109#[serde(rename_all = "camelCase", deny_unknown_fields)]
110#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
111#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
112pub struct Fido2CredentialListView {
113 pub credential_id: String,
114 pub rp_id: String,
115 pub user_handle: Option<String>,
116 pub user_name: Option<String>,
117 pub user_display_name: Option<String>,
118 pub counter: String,
119}
120
121#[allow(missing_docs)]
122#[derive(Serialize, Deserialize, Debug, Clone)]
123#[serde(rename_all = "camelCase", deny_unknown_fields)]
124#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
125#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
126pub struct Fido2CredentialView {
127 pub credential_id: String,
128 pub key_type: String,
129 pub key_algorithm: String,
130 pub key_curve: String,
131 pub key_value: EncString,
134 pub rp_id: String,
135 pub user_handle: Option<String>,
136 pub user_name: Option<String>,
137 pub counter: String,
138 pub rp_name: Option<String>,
139 pub user_display_name: Option<String>,
140 pub discoverable: String,
141 pub creation_date: DateTime<Utc>,
142}
143
144#[allow(missing_docs)]
147#[derive(Serialize, Deserialize, Debug, Clone)]
148#[serde(rename_all = "camelCase", deny_unknown_fields)]
149#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
150pub struct Fido2CredentialFullView {
151 pub credential_id: String,
152 pub key_type: String,
153 pub key_algorithm: String,
154 pub key_curve: String,
155 pub key_value: String,
156 pub rp_id: String,
157 pub user_handle: Option<String>,
158 pub user_name: Option<String>,
159 pub counter: String,
160 pub rp_name: Option<String>,
161 pub user_display_name: Option<String>,
162 pub discoverable: String,
163 pub creation_date: DateTime<Utc>,
164}
165
166#[allow(missing_docs)]
170#[derive(Serialize, Deserialize, Debug, Clone)]
171#[serde(rename_all = "camelCase", deny_unknown_fields)]
172#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
173#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
174pub struct Fido2CredentialNewView {
175 pub credential_id: String,
176 pub key_type: String,
177 pub key_algorithm: String,
178 pub key_curve: String,
179 pub rp_id: String,
180 pub user_handle: Option<String>,
181 pub user_name: Option<String>,
182 pub counter: String,
183 pub rp_name: Option<String>,
184 pub user_display_name: Option<String>,
185 pub creation_date: DateTime<Utc>,
186}
187
188impl From<Fido2CredentialFullView> for Fido2CredentialNewView {
189 fn from(value: Fido2CredentialFullView) -> Self {
190 Fido2CredentialNewView {
191 credential_id: value.credential_id,
192 key_type: value.key_type,
193 key_algorithm: value.key_algorithm,
194 key_curve: value.key_curve,
195 rp_id: value.rp_id,
196 user_handle: value.user_handle,
197 user_name: value.user_name,
198 counter: value.counter,
199 rp_name: value.rp_name,
200 user_display_name: value.user_display_name,
201 creation_date: value.creation_date,
202 }
203 }
204}
205
206impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Fido2Credential>
207 for Fido2CredentialFullView
208{
209 fn encrypt_composite(
210 &self,
211 ctx: &mut KeyStoreContext<KeySlotIds>,
212 key: SymmetricKeySlotId,
213 ) -> Result<Fido2Credential, CryptoError> {
214 Ok(Fido2Credential {
215 credential_id: self.credential_id.encrypt(ctx, key)?,
216 key_type: self.key_type.encrypt(ctx, key)?,
217 key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
218 key_curve: self.key_curve.encrypt(ctx, key)?,
219 key_value: self.key_value.encrypt(ctx, key)?,
220 rp_id: self.rp_id.encrypt(ctx, key)?,
221 user_handle: self
222 .user_handle
223 .as_ref()
224 .map(|h| h.encrypt(ctx, key))
225 .transpose()?,
226 user_name: self.user_name.encrypt(ctx, key)?,
227 counter: self.counter.encrypt(ctx, key)?,
228 rp_name: self.rp_name.encrypt(ctx, key)?,
229 user_display_name: self.user_display_name.encrypt(ctx, key)?,
230 discoverable: self.discoverable.encrypt(ctx, key)?,
231 creation_date: self.creation_date,
232 })
233 }
234}
235
236impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialFullView> for Fido2Credential {
237 fn decrypt(
238 &self,
239 ctx: &mut KeyStoreContext<KeySlotIds>,
240 key: SymmetricKeySlotId,
241 ) -> Result<Fido2CredentialFullView, CryptoError> {
242 Ok(Fido2CredentialFullView {
243 credential_id: self.credential_id.decrypt(ctx, key)?,
244 key_type: self.key_type.decrypt(ctx, key)?,
245 key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
246 key_curve: self.key_curve.decrypt(ctx, key)?,
247 key_value: self.key_value.decrypt(ctx, key)?,
248 rp_id: self.rp_id.decrypt(ctx, key)?,
249 user_handle: self.user_handle.decrypt(ctx, key)?,
250 user_name: self.user_name.decrypt(ctx, key)?,
251 counter: self.counter.decrypt(ctx, key)?,
252 rp_name: self.rp_name.decrypt(ctx, key)?,
253 user_display_name: self.user_display_name.decrypt(ctx, key)?,
254 discoverable: self.discoverable.decrypt(ctx, key)?,
255 creation_date: self.creation_date,
256 })
257 }
258}
259
260impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialFullView> for Fido2CredentialView {
261 fn decrypt(
262 &self,
263 ctx: &mut KeyStoreContext<KeySlotIds>,
264 key: SymmetricKeySlotId,
265 ) -> Result<Fido2CredentialFullView, CryptoError> {
266 Ok(Fido2CredentialFullView {
267 credential_id: self.credential_id.clone(),
268 key_type: self.key_type.clone(),
269 key_algorithm: self.key_algorithm.clone(),
270 key_curve: self.key_curve.clone(),
271 key_value: self.key_value.decrypt(ctx, key)?,
272 rp_id: self.rp_id.clone(),
273 user_handle: self.user_handle.clone(),
274 user_name: self.user_name.clone(),
275 counter: self.counter.clone(),
276 rp_name: self.rp_name.clone(),
277 user_display_name: self.user_display_name.clone(),
278 discoverable: self.discoverable.clone(),
279 creation_date: self.creation_date,
280 })
281 }
282}
283
284#[allow(missing_docs)]
285#[derive(Serialize, Deserialize, Debug, Clone)]
286#[serde(rename_all = "camelCase")]
287#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
288#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
289pub struct Login {
290 pub username: Option<EncString>,
291 pub password: Option<EncString>,
292 pub password_revision_date: Option<DateTime<Utc>>,
293
294 pub uris: Option<Vec<LoginUri>>,
295 pub totp: Option<EncString>,
296 pub autofill_on_page_load: Option<bool>,
297
298 pub fido2_credentials: Option<Vec<Fido2Credential>>,
299}
300
301#[allow(missing_docs)]
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 LoginView {
307 pub username: Option<String>,
308 pub password: Option<String>,
309 pub password_revision_date: Option<DateTime<Utc>>,
310
311 pub uris: Option<Vec<LoginUriView>>,
312 pub totp: Option<String>,
313 pub autofill_on_page_load: Option<bool>,
314
315 pub fido2_credentials: Option<Vec<Fido2Credential>>,
317}
318
319impl LoginView {
320 pub fn generate_checksums(&mut self) {
322 if let Some(uris) = &mut self.uris {
323 for uri in uris {
324 uri.generate_checksum();
325 }
326 }
327 }
328
329 pub fn reencrypt_fido2_credentials(
331 &mut self,
332 ctx: &mut KeyStoreContext<KeySlotIds>,
333 old_key: SymmetricKeySlotId,
334 new_key: SymmetricKeySlotId,
335 ) -> Result<(), CryptoError> {
336 if let Some(creds) = &mut self.fido2_credentials {
337 let decrypted_creds: Vec<Fido2CredentialFullView> = creds.decrypt(ctx, old_key)?;
338 *creds = decrypted_creds.encrypt_composite(ctx, new_key)?;
339 }
340 Ok(())
341 }
342
343 pub(crate) fn to_list_view(
350 &self,
351 ctx: &mut KeyStoreContext<KeySlotIds>,
352 cipher_key: SymmetricKeySlotId,
353 ) -> Result<LoginListView, CryptoError> {
354 let totp = self
355 .totp
356 .as_ref()
357 .map(|t| t.encrypt(ctx, cipher_key))
358 .transpose()?;
359
360 let fido2_credentials = self
361 .fido2_credentials
362 .as_ref()
363 .map(|creds| creds.decrypt(ctx, cipher_key))
364 .transpose()?;
365
366 Ok(LoginListView {
367 has_fido2: self.fido2_credentials.is_some(),
368 fido2_credentials,
369 username: self.username.clone(),
370 totp,
371 uris: self.uris.clone(),
372 })
373 }
374
375 pub(crate) fn detect_password_change(
377 &mut self,
378 original: &Option<LoginView>,
379 ) -> Vec<PasswordHistoryView> {
380 let Some(original_login) = original else {
381 return vec![];
382 };
383
384 let original_password = original_login.password.as_deref().unwrap_or("");
385 let current_password = self.password.as_deref().unwrap_or("");
386
387 if original_password.is_empty() {
388 if !current_password.is_empty() {
390 self.password_revision_date = Some(Utc::now());
391 }
392 vec![]
393 } else if original_password == current_password {
394 self.password_revision_date = original_login.password_revision_date;
396 vec![]
397 } else {
398 self.password_revision_date = Some(Utc::now());
400 vec![PasswordHistoryView::new_password(original_password)]
401 }
402 }
403}
404
405#[allow(missing_docs)]
406#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
407#[serde(rename_all = "camelCase", deny_unknown_fields)]
408#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
409#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
410pub struct LoginListView {
411 pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
412 pub has_fido2: bool,
413 pub username: Option<String>,
414 pub totp: Option<EncString>,
416 pub uris: Option<Vec<LoginUriView>>,
417}
418
419impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, LoginUri> for LoginUriView {
420 fn encrypt_composite(
421 &self,
422 ctx: &mut KeyStoreContext<KeySlotIds>,
423 key: SymmetricKeySlotId,
424 ) -> Result<LoginUri, CryptoError> {
425 Ok(LoginUri {
426 uri: self.uri.encrypt(ctx, key)?,
427 r#match: self.r#match,
428 uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
429 })
430 }
431}
432
433impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Login> for LoginView {
434 fn encrypt_composite(
435 &self,
436 ctx: &mut KeyStoreContext<KeySlotIds>,
437 key: SymmetricKeySlotId,
438 ) -> Result<Login, CryptoError> {
439 Ok(Login {
440 username: self.username.encrypt(ctx, key)?,
441 password: self.password.encrypt(ctx, key)?,
442 password_revision_date: self.password_revision_date,
443 uris: self.uris.encrypt_composite(ctx, key)?,
444 totp: self
445 .totp
446 .clone()
447 .filter(|s| !s.is_empty())
448 .encrypt(ctx, key)?,
449 autofill_on_page_load: self.autofill_on_page_load,
450 fido2_credentials: self.fido2_credentials.clone(),
451 })
452 }
453}
454
455impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginUriView> for LoginUri {
456 fn decrypt(
457 &self,
458 ctx: &mut KeyStoreContext<KeySlotIds>,
459 key: SymmetricKeySlotId,
460 ) -> Result<LoginUriView, CryptoError> {
461 Ok(LoginUriView {
462 uri: self.uri.decrypt(ctx, key)?,
463 r#match: self.r#match,
464 uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
465 })
466 }
467}
468
469impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginView> for Login {
470 fn decrypt(
471 &self,
472 ctx: &mut KeyStoreContext<KeySlotIds>,
473 key: SymmetricKeySlotId,
474 ) -> Result<LoginView, CryptoError> {
475 Ok(LoginView {
476 username: self.username.decrypt(ctx, key).ok().flatten(),
477 password: self.password.decrypt(ctx, key).ok().flatten(),
478 password_revision_date: self.password_revision_date,
479 uris: self.uris.decrypt(ctx, key).ok().flatten(),
480 totp: self.totp.decrypt(ctx, key).ok().flatten(),
481 autofill_on_page_load: self.autofill_on_page_load,
482 fido2_credentials: self.fido2_credentials.clone(),
483 })
484 }
485}
486
487impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginListView> for Login {
488 fn decrypt(
489 &self,
490 ctx: &mut KeyStoreContext<KeySlotIds>,
491 key: SymmetricKeySlotId,
492 ) -> Result<LoginListView, CryptoError> {
493 Ok(LoginListView {
494 fido2_credentials: self
495 .fido2_credentials
496 .as_ref()
497 .and_then(|fido2_credentials| fido2_credentials.decrypt(ctx, key).ok()),
498 has_fido2: self.fido2_credentials.is_some(),
499 username: self.username.decrypt(ctx, key).ok().flatten(),
500 totp: self.totp.clone(),
501 uris: self.uris.decrypt(ctx, key).ok().flatten(),
502 })
503 }
504}
505
506impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginView> for StrictDecrypt<&Login> {
507 fn decrypt(
508 &self,
509 ctx: &mut KeyStoreContext<KeySlotIds>,
510 key: SymmetricKeySlotId,
511 ) -> Result<LoginView, CryptoError> {
512 Ok(LoginView {
513 username: self.0.username.decrypt(ctx, key)?,
514 password: self.0.password.decrypt(ctx, key)?,
515 password_revision_date: self.0.password_revision_date,
516 uris: self.0.uris.decrypt(ctx, key)?,
517 totp: self.0.totp.decrypt(ctx, key)?,
518 autofill_on_page_load: self.0.autofill_on_page_load,
519 fido2_credentials: self.0.fido2_credentials.clone(),
520 })
521 }
522}
523
524impl Decryptable<KeySlotIds, SymmetricKeySlotId, LoginListView> for StrictDecrypt<&Login> {
525 fn decrypt(
526 &self,
527 ctx: &mut KeyStoreContext<KeySlotIds>,
528 key: SymmetricKeySlotId,
529 ) -> Result<LoginListView, CryptoError> {
530 Ok(LoginListView {
531 fido2_credentials: self
532 .0
533 .fido2_credentials
534 .as_ref()
535 .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
536 .transpose()?,
537 has_fido2: self.0.fido2_credentials.is_some(),
538 username: self.0.username.decrypt(ctx, key)?,
539 totp: self.0.totp.clone(),
540 uris: self.0.uris.decrypt(ctx, key)?,
541 })
542 }
543}
544
545impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, Fido2Credential> for Fido2CredentialView {
546 fn encrypt_composite(
547 &self,
548 ctx: &mut KeyStoreContext<KeySlotIds>,
549 key: SymmetricKeySlotId,
550 ) -> Result<Fido2Credential, CryptoError> {
551 Ok(Fido2Credential {
552 credential_id: self.credential_id.encrypt(ctx, key)?,
553 key_type: self.key_type.encrypt(ctx, key)?,
554 key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
555 key_curve: self.key_curve.encrypt(ctx, key)?,
556 key_value: self.key_value.clone(),
557 rp_id: self.rp_id.encrypt(ctx, key)?,
558 user_handle: self
559 .user_handle
560 .as_ref()
561 .map(|h| h.encrypt(ctx, key))
562 .transpose()?,
563 user_name: self
564 .user_name
565 .as_ref()
566 .map(|n| n.encrypt(ctx, key))
567 .transpose()?,
568 counter: self.counter.encrypt(ctx, key)?,
569 rp_name: self.rp_name.encrypt(ctx, key)?,
570 user_display_name: self.user_display_name.encrypt(ctx, key)?,
571 discoverable: self.discoverable.encrypt(ctx, key)?,
572 creation_date: self.creation_date,
573 })
574 }
575}
576
577impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialView> for Fido2Credential {
578 fn decrypt(
579 &self,
580 ctx: &mut KeyStoreContext<KeySlotIds>,
581 key: SymmetricKeySlotId,
582 ) -> Result<Fido2CredentialView, CryptoError> {
583 Ok(Fido2CredentialView {
584 credential_id: self.credential_id.decrypt(ctx, key)?,
585 key_type: self.key_type.decrypt(ctx, key)?,
586 key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
587 key_curve: self.key_curve.decrypt(ctx, key)?,
588 key_value: self.key_value.clone(),
589 rp_id: self.rp_id.decrypt(ctx, key)?,
590 user_handle: self.user_handle.decrypt(ctx, key)?,
591 user_name: self.user_name.decrypt(ctx, key)?,
592 counter: self.counter.decrypt(ctx, key)?,
593 rp_name: self.rp_name.decrypt(ctx, key)?,
594 user_display_name: self.user_display_name.decrypt(ctx, key)?,
595 discoverable: self.discoverable.decrypt(ctx, key)?,
596 creation_date: self.creation_date,
597 })
598 }
599}
600
601impl Decryptable<KeySlotIds, SymmetricKeySlotId, Fido2CredentialListView> for Fido2Credential {
602 fn decrypt(
603 &self,
604 ctx: &mut KeyStoreContext<KeySlotIds>,
605 key: SymmetricKeySlotId,
606 ) -> Result<Fido2CredentialListView, CryptoError> {
607 Ok(Fido2CredentialListView {
608 credential_id: self.credential_id.decrypt(ctx, key)?,
609 rp_id: self.rp_id.decrypt(ctx, key)?,
610 user_handle: self.user_handle.decrypt(ctx, key)?,
611 user_name: self.user_name.decrypt(ctx, key)?,
612 user_display_name: self.user_display_name.decrypt(ctx, key)?,
613 counter: self.counter.decrypt(ctx, key)?,
614 })
615 }
616}
617
618impl TryFrom<CipherLoginModel> for Login {
619 type Error = VaultParseError;
620
621 fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
622 Ok(Self {
623 username: EncString::try_from_optional(login.username)?,
624 password: EncString::try_from_optional(login.password)?,
625 password_revision_date: login
626 .password_revision_date
627 .map(|d| d.parse())
628 .transpose()?,
629 uris: login
630 .uris
631 .map(|v| v.into_iter().map(|u| u.try_into()).collect())
632 .transpose()?,
633 totp: EncString::try_from_optional(login.totp)?,
634 autofill_on_page_load: login.autofill_on_page_load,
635 fido2_credentials: login
636 .fido2_credentials
637 .map(|v| v.into_iter().map(|c| c.try_into()).collect())
638 .transpose()?,
639 })
640 }
641}
642
643impl TryFrom<CipherLoginUriModel> for LoginUri {
644 type Error = VaultParseError;
645
646 fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
647 Ok(Self {
648 uri: EncString::try_from_optional(uri.uri)?,
649 r#match: uri.r#match.map(|m| m.try_into()).transpose()?,
650 uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
651 })
652 }
653}
654
655impl TryFrom<bitwarden_api_api::models::UriMatchType> for UriMatchType {
656 type Error = bitwarden_core::MissingFieldError;
657
658 fn try_from(value: bitwarden_api_api::models::UriMatchType) -> Result<Self, Self::Error> {
659 Ok(match value {
660 bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
661 bitwarden_api_api::models::UriMatchType::Host => Self::Host,
662 bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
663 bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
664 bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
665 bitwarden_api_api::models::UriMatchType::Never => Self::Never,
666 bitwarden_api_api::models::UriMatchType::__Unknown(_) => {
667 return Err(bitwarden_core::MissingFieldError("match"));
668 }
669 })
670 }
671}
672
673impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
674 type Error = VaultParseError;
675
676 fn try_from(
677 value: bitwarden_api_api::models::CipherFido2CredentialModel,
678 ) -> Result<Self, Self::Error> {
679 Ok(Self {
680 credential_id: require!(value.credential_id).parse()?,
681 key_type: require!(value.key_type).parse()?,
682 key_algorithm: require!(value.key_algorithm).parse()?,
683 key_curve: require!(value.key_curve).parse()?,
684 key_value: require!(value.key_value).parse()?,
685 rp_id: require!(value.rp_id).parse()?,
686 user_handle: EncString::try_from_optional(value.user_handle)
687 .ok()
688 .flatten(),
689 user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
690 counter: require!(value.counter).parse()?,
691 rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
692 user_display_name: EncString::try_from_optional(value.user_display_name)
693 .ok()
694 .flatten(),
695 discoverable: require!(value.discoverable).parse()?,
696 creation_date: value.creation_date.parse()?,
697 })
698 }
699}
700
701impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
702 fn from(uri: LoginUri) -> Self {
703 bitwarden_api_api::models::CipherLoginUriModel {
704 uri: uri.uri.map(|u| u.to_string()),
705 uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
706 r#match: uri.r#match.map(|m| m.into()),
707 }
708 }
709}
710
711impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
712 fn from(match_type: UriMatchType) -> Self {
713 match match_type {
714 UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
715 UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
716 UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
717 UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
718 UriMatchType::RegularExpression => {
719 bitwarden_api_api::models::UriMatchType::RegularExpression
720 }
721 UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
722 }
723 }
724}
725
726impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
727 fn from(cred: Fido2Credential) -> Self {
728 bitwarden_api_api::models::CipherFido2CredentialModel {
729 credential_id: Some(cred.credential_id.to_string()),
730 key_type: Some(cred.key_type.to_string()),
731 key_algorithm: Some(cred.key_algorithm.to_string()),
732 key_curve: Some(cred.key_curve.to_string()),
733 key_value: Some(cred.key_value.to_string()),
734 rp_id: Some(cred.rp_id.to_string()),
735 user_handle: cred.user_handle.map(|h| h.to_string()),
736 user_name: cred.user_name.map(|n| n.to_string()),
737 counter: Some(cred.counter.to_string()),
738 rp_name: cred.rp_name.map(|n| n.to_string()),
739 user_display_name: cred.user_display_name.map(|n| n.to_string()),
740 discoverable: Some(cred.discoverable.to_string()),
741 creation_date: cred.creation_date.to_rfc3339(),
742 }
743 }
744}
745
746impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
747 fn from(login: Login) -> Self {
748 bitwarden_api_api::models::CipherLoginModel {
749 uri: None,
750 uris: login
751 .uris
752 .map(|u| u.into_iter().map(|u| u.into()).collect()),
753 username: login.username.map(|u| u.to_string()),
754 password: login.password.map(|p| p.to_string()),
755 password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
756 totp: login.totp.map(|t| t.to_string()),
757 autofill_on_page_load: login.autofill_on_page_load,
758 fido2_credentials: login
759 .fido2_credentials
760 .map(|c| c.into_iter().map(|c| c.into()).collect()),
761 }
762 }
763}
764
765impl CipherKind for Login {
766 fn decrypt_subtitle(
767 &self,
768 ctx: &mut KeyStoreContext<KeySlotIds>,
769 key: SymmetricKeySlotId,
770 ) -> Result<String, CryptoError> {
771 let username: Option<String> = self.username.decrypt(ctx, key)?;
772
773 Ok(username.unwrap_or_default())
774 }
775
776 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
777 [
778 self.username
779 .as_ref()
780 .map(|_| CopyableCipherFields::LoginUsername),
781 self.password
782 .as_ref()
783 .map(|_| CopyableCipherFields::LoginPassword),
784 self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
785 ]
786 .into_iter()
787 .flatten()
788 .collect()
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use crate::{
795 Login,
796 cipher::cipher::{CipherKind, CopyableCipherFields},
797 };
798
799 #[test]
800 fn test_valid_checksum() {
801 let uri = super::LoginUriView {
802 uri: Some("https://example.com".to_string()),
803 r#match: Some(super::UriMatchType::Domain),
804 uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
805 };
806 assert!(uri.is_checksum_valid());
807 }
808
809 #[test]
810 fn test_invalid_checksum() {
811 let uri = super::LoginUriView {
812 uri: Some("https://example.com".to_string()),
813 r#match: Some(super::UriMatchType::Domain),
814 uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
815 };
816 assert!(!uri.is_checksum_valid());
817 }
818
819 #[test]
820 fn test_missing_checksum() {
821 let uri = super::LoginUriView {
822 uri: Some("https://example.com".to_string()),
823 r#match: Some(super::UriMatchType::Domain),
824 uri_checksum: None,
825 };
826 assert!(!uri.is_checksum_valid());
827 }
828
829 #[test]
830 fn test_generate_checksum() {
831 let mut uri = super::LoginUriView {
832 uri: Some("https://test.com".to_string()),
833 r#match: Some(super::UriMatchType::Domain),
834 uri_checksum: None,
835 };
836
837 uri.generate_checksum();
838
839 assert_eq!(
840 uri.uri_checksum.unwrap().as_str(),
841 "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
842 );
843 }
844
845 #[test]
846 fn test_get_copyable_fields_login_password() {
847 let login_with_password = Login {
848 username: None,
849 password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
850 password_revision_date: None,
851 uris: None,
852 totp: None,
853 autofill_on_page_load: None,
854 fido2_credentials: None,
855 };
856
857 let copyable_fields = login_with_password.get_copyable_fields(None);
858 assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
859 }
860
861 #[test]
862 fn test_get_copyable_fields_login_username() {
863 let login_with_username = Login {
864 username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
865 password: None,
866 password_revision_date: None,
867 uris: None,
868 totp: None,
869 autofill_on_page_load: None,
870 fido2_credentials: None,
871 };
872
873 let copyable_fields = login_with_username.get_copyable_fields(None);
874 assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
875 }
876
877 #[test]
878 fn test_get_copyable_fields_login_everything() {
879 let login = Login {
880 username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
881 password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
882 password_revision_date: None,
883 uris: None,
884 totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
885 autofill_on_page_load: None,
886 fido2_credentials: None,
887 };
888
889 let copyable_fields = login.get_copyable_fields(None);
890 assert_eq!(
891 copyable_fields,
892 vec![
893 CopyableCipherFields::LoginUsername,
894 CopyableCipherFields::LoginPassword,
895 CopyableCipherFields::LoginTotp
896 ]
897 );
898 }
899}