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