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