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.totp.encrypt(ctx, key)?,
411 autofill_on_page_load: self.autofill_on_page_load,
412 fido2_credentials: self.fido2_credentials.clone(),
413 })
414 }
415}
416
417impl Decryptable<KeyIds, SymmetricKeyId, LoginUriView> for LoginUri {
418 fn decrypt(
419 &self,
420 ctx: &mut KeyStoreContext<KeyIds>,
421 key: SymmetricKeyId,
422 ) -> Result<LoginUriView, CryptoError> {
423 Ok(LoginUriView {
424 uri: self.uri.decrypt(ctx, key)?,
425 r#match: self.r#match,
426 uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
427 })
428 }
429}
430
431impl Decryptable<KeyIds, SymmetricKeyId, LoginView> for Login {
432 fn decrypt(
433 &self,
434 ctx: &mut KeyStoreContext<KeyIds>,
435 key: SymmetricKeyId,
436 ) -> Result<LoginView, CryptoError> {
437 Ok(LoginView {
438 username: self.username.decrypt(ctx, key).ok().flatten(),
439 password: self.password.decrypt(ctx, key).ok().flatten(),
440 password_revision_date: self.password_revision_date,
441 uris: self.uris.decrypt(ctx, key).ok().flatten(),
442 totp: self.totp.decrypt(ctx, key).ok().flatten(),
443 autofill_on_page_load: self.autofill_on_page_load,
444 fido2_credentials: self.fido2_credentials.clone(),
445 })
446 }
447}
448
449impl Decryptable<KeyIds, SymmetricKeyId, LoginListView> for Login {
450 fn decrypt(
451 &self,
452 ctx: &mut KeyStoreContext<KeyIds>,
453 key: SymmetricKeyId,
454 ) -> Result<LoginListView, CryptoError> {
455 Ok(LoginListView {
456 fido2_credentials: self
457 .fido2_credentials
458 .as_ref()
459 .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
460 .transpose()?,
461 has_fido2: self.fido2_credentials.is_some(),
462 username: self.username.decrypt(ctx, key).ok().flatten(),
463 totp: self.totp.clone(),
464 uris: self.uris.decrypt(ctx, key).ok().flatten(),
465 })
466 }
467}
468
469impl CompositeEncryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialView {
470 fn encrypt_composite(
471 &self,
472 ctx: &mut KeyStoreContext<KeyIds>,
473 key: SymmetricKeyId,
474 ) -> Result<Fido2Credential, CryptoError> {
475 Ok(Fido2Credential {
476 credential_id: self.credential_id.encrypt(ctx, key)?,
477 key_type: self.key_type.encrypt(ctx, key)?,
478 key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
479 key_curve: self.key_curve.encrypt(ctx, key)?,
480 key_value: self.key_value.clone(),
481 rp_id: self.rp_id.encrypt(ctx, key)?,
482 user_handle: self
483 .user_handle
484 .as_ref()
485 .map(|h| h.encrypt(ctx, key))
486 .transpose()?,
487 user_name: self
488 .user_name
489 .as_ref()
490 .map(|n| n.encrypt(ctx, key))
491 .transpose()?,
492 counter: self.counter.encrypt(ctx, key)?,
493 rp_name: self.rp_name.encrypt(ctx, key)?,
494 user_display_name: self.user_display_name.encrypt(ctx, key)?,
495 discoverable: self.discoverable.encrypt(ctx, key)?,
496 creation_date: self.creation_date,
497 })
498 }
499}
500
501impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialView> for Fido2Credential {
502 fn decrypt(
503 &self,
504 ctx: &mut KeyStoreContext<KeyIds>,
505 key: SymmetricKeyId,
506 ) -> Result<Fido2CredentialView, CryptoError> {
507 Ok(Fido2CredentialView {
508 credential_id: self.credential_id.decrypt(ctx, key)?,
509 key_type: self.key_type.decrypt(ctx, key)?,
510 key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
511 key_curve: self.key_curve.decrypt(ctx, key)?,
512 key_value: self.key_value.clone(),
513 rp_id: self.rp_id.decrypt(ctx, key)?,
514 user_handle: self.user_handle.decrypt(ctx, key)?,
515 user_name: self.user_name.decrypt(ctx, key)?,
516 counter: self.counter.decrypt(ctx, key)?,
517 rp_name: self.rp_name.decrypt(ctx, key)?,
518 user_display_name: self.user_display_name.decrypt(ctx, key)?,
519 discoverable: self.discoverable.decrypt(ctx, key)?,
520 creation_date: self.creation_date,
521 })
522 }
523}
524
525impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialListView> for Fido2Credential {
526 fn decrypt(
527 &self,
528 ctx: &mut KeyStoreContext<KeyIds>,
529 key: SymmetricKeyId,
530 ) -> Result<Fido2CredentialListView, CryptoError> {
531 Ok(Fido2CredentialListView {
532 credential_id: self.credential_id.decrypt(ctx, key)?,
533 rp_id: self.rp_id.decrypt(ctx, key)?,
534 user_handle: self.user_handle.decrypt(ctx, key)?,
535 user_name: self.user_name.decrypt(ctx, key)?,
536 user_display_name: self.user_display_name.decrypt(ctx, key)?,
537 counter: self.counter.decrypt(ctx, key)?,
538 })
539 }
540}
541
542impl TryFrom<CipherLoginModel> for Login {
543 type Error = VaultParseError;
544
545 fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
546 Ok(Self {
547 username: EncString::try_from_optional(login.username)?,
548 password: EncString::try_from_optional(login.password)?,
549 password_revision_date: login
550 .password_revision_date
551 .map(|d| d.parse())
552 .transpose()?,
553 uris: login
554 .uris
555 .map(|v| v.into_iter().map(|u| u.try_into()).collect())
556 .transpose()?,
557 totp: EncString::try_from_optional(login.totp)?,
558 autofill_on_page_load: login.autofill_on_page_load,
559 fido2_credentials: login
560 .fido2_credentials
561 .map(|v| v.into_iter().map(|c| c.try_into()).collect())
562 .transpose()?,
563 })
564 }
565}
566
567impl TryFrom<CipherLoginUriModel> for LoginUri {
568 type Error = VaultParseError;
569
570 fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
571 Ok(Self {
572 uri: EncString::try_from_optional(uri.uri)?,
573 r#match: uri.r#match.map(|m| m.try_into()).transpose()?,
574 uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
575 })
576 }
577}
578
579impl TryFrom<bitwarden_api_api::models::UriMatchType> for UriMatchType {
580 type Error = bitwarden_core::MissingFieldError;
581
582 fn try_from(value: bitwarden_api_api::models::UriMatchType) -> Result<Self, Self::Error> {
583 Ok(match value {
584 bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
585 bitwarden_api_api::models::UriMatchType::Host => Self::Host,
586 bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
587 bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
588 bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
589 bitwarden_api_api::models::UriMatchType::Never => Self::Never,
590 bitwarden_api_api::models::UriMatchType::__Unknown(_) => {
591 return Err(bitwarden_core::MissingFieldError("match"));
592 }
593 })
594 }
595}
596
597impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
598 type Error = VaultParseError;
599
600 fn try_from(
601 value: bitwarden_api_api::models::CipherFido2CredentialModel,
602 ) -> Result<Self, Self::Error> {
603 Ok(Self {
604 credential_id: require!(value.credential_id).parse()?,
605 key_type: require!(value.key_type).parse()?,
606 key_algorithm: require!(value.key_algorithm).parse()?,
607 key_curve: require!(value.key_curve).parse()?,
608 key_value: require!(value.key_value).parse()?,
609 rp_id: require!(value.rp_id).parse()?,
610 user_handle: EncString::try_from_optional(value.user_handle)
611 .ok()
612 .flatten(),
613 user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
614 counter: require!(value.counter).parse()?,
615 rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
616 user_display_name: EncString::try_from_optional(value.user_display_name)
617 .ok()
618 .flatten(),
619 discoverable: require!(value.discoverable).parse()?,
620 creation_date: value.creation_date.parse()?,
621 })
622 }
623}
624
625impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
626 fn from(uri: LoginUri) -> Self {
627 bitwarden_api_api::models::CipherLoginUriModel {
628 uri: uri.uri.map(|u| u.to_string()),
629 uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
630 r#match: uri.r#match.map(|m| m.into()),
631 }
632 }
633}
634
635impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
636 fn from(match_type: UriMatchType) -> Self {
637 match match_type {
638 UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
639 UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
640 UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
641 UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
642 UriMatchType::RegularExpression => {
643 bitwarden_api_api::models::UriMatchType::RegularExpression
644 }
645 UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
646 }
647 }
648}
649
650impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
651 fn from(cred: Fido2Credential) -> Self {
652 bitwarden_api_api::models::CipherFido2CredentialModel {
653 credential_id: Some(cred.credential_id.to_string()),
654 key_type: Some(cred.key_type.to_string()),
655 key_algorithm: Some(cred.key_algorithm.to_string()),
656 key_curve: Some(cred.key_curve.to_string()),
657 key_value: Some(cred.key_value.to_string()),
658 rp_id: Some(cred.rp_id.to_string()),
659 user_handle: cred.user_handle.map(|h| h.to_string()),
660 user_name: cred.user_name.map(|n| n.to_string()),
661 counter: Some(cred.counter.to_string()),
662 rp_name: cred.rp_name.map(|n| n.to_string()),
663 user_display_name: cred.user_display_name.map(|n| n.to_string()),
664 discoverable: Some(cred.discoverable.to_string()),
665 creation_date: cred.creation_date.to_rfc3339(),
666 }
667 }
668}
669
670impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
671 fn from(login: Login) -> Self {
672 bitwarden_api_api::models::CipherLoginModel {
673 uri: None,
674 uris: login
675 .uris
676 .map(|u| u.into_iter().map(|u| u.into()).collect()),
677 username: login.username.map(|u| u.to_string()),
678 password: login.password.map(|p| p.to_string()),
679 password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
680 totp: login.totp.map(|t| t.to_string()),
681 autofill_on_page_load: login.autofill_on_page_load,
682 fido2_credentials: login
683 .fido2_credentials
684 .map(|c| c.into_iter().map(|c| c.into()).collect()),
685 }
686 }
687}
688
689impl CipherKind for Login {
690 fn decrypt_subtitle(
691 &self,
692 ctx: &mut KeyStoreContext<KeyIds>,
693 key: SymmetricKeyId,
694 ) -> Result<String, CryptoError> {
695 let username: Option<String> = self.username.decrypt(ctx, key)?;
696
697 Ok(username.unwrap_or_default())
698 }
699
700 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
701 [
702 self.username
703 .as_ref()
704 .map(|_| CopyableCipherFields::LoginUsername),
705 self.password
706 .as_ref()
707 .map(|_| CopyableCipherFields::LoginPassword),
708 self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
709 ]
710 .into_iter()
711 .flatten()
712 .collect()
713 }
714}
715
716#[cfg(test)]
717mod tests {
718 use crate::{
719 Login,
720 cipher::cipher::{CipherKind, CopyableCipherFields},
721 };
722
723 #[test]
724 fn test_valid_checksum() {
725 let uri = super::LoginUriView {
726 uri: Some("https://example.com".to_string()),
727 r#match: Some(super::UriMatchType::Domain),
728 uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
729 };
730 assert!(uri.is_checksum_valid());
731 }
732
733 #[test]
734 fn test_invalid_checksum() {
735 let uri = super::LoginUriView {
736 uri: Some("https://example.com".to_string()),
737 r#match: Some(super::UriMatchType::Domain),
738 uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
739 };
740 assert!(!uri.is_checksum_valid());
741 }
742
743 #[test]
744 fn test_missing_checksum() {
745 let uri = super::LoginUriView {
746 uri: Some("https://example.com".to_string()),
747 r#match: Some(super::UriMatchType::Domain),
748 uri_checksum: None,
749 };
750 assert!(!uri.is_checksum_valid());
751 }
752
753 #[test]
754 fn test_generate_checksum() {
755 let mut uri = super::LoginUriView {
756 uri: Some("https://test.com".to_string()),
757 r#match: Some(super::UriMatchType::Domain),
758 uri_checksum: None,
759 };
760
761 uri.generate_checksum();
762
763 assert_eq!(
764 uri.uri_checksum.unwrap().as_str(),
765 "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
766 );
767 }
768
769 #[test]
770 fn test_get_copyable_fields_login_password() {
771 let login_with_password = Login {
772 username: None,
773 password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
774 password_revision_date: None,
775 uris: None,
776 totp: None,
777 autofill_on_page_load: None,
778 fido2_credentials: None,
779 };
780
781 let copyable_fields = login_with_password.get_copyable_fields(None);
782 assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
783 }
784
785 #[test]
786 fn test_get_copyable_fields_login_username() {
787 let login_with_username = Login {
788 username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
789 password: None,
790 password_revision_date: None,
791 uris: None,
792 totp: None,
793 autofill_on_page_load: None,
794 fido2_credentials: None,
795 };
796
797 let copyable_fields = login_with_username.get_copyable_fields(None);
798 assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
799 }
800
801 #[test]
802 fn test_get_copyable_fields_login_everything() {
803 let login = Login {
804 username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
805 password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
806 password_revision_date: None,
807 uris: None,
808 totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
809 autofill_on_page_load: None,
810 fido2_credentials: None,
811 };
812
813 let copyable_fields = login.get_copyable_fields(None);
814 assert_eq!(
815 copyable_fields,
816 vec![
817 CopyableCipherFields::LoginUsername,
818 CopyableCipherFields::LoginPassword,
819 CopyableCipherFields::LoginTotp
820 ]
821 );
822 }
823}