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