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.into()),
574 uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
575 })
576 }
577}
578
579impl From<bitwarden_api_api::models::UriMatchType> for UriMatchType {
580 fn from(value: bitwarden_api_api::models::UriMatchType) -> Self {
581 match value {
582 bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
583 bitwarden_api_api::models::UriMatchType::Host => Self::Host,
584 bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
585 bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
586 bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
587 bitwarden_api_api::models::UriMatchType::Never => Self::Never,
588 }
589 }
590}
591
592impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
593 type Error = VaultParseError;
594
595 fn try_from(
596 value: bitwarden_api_api::models::CipherFido2CredentialModel,
597 ) -> Result<Self, Self::Error> {
598 Ok(Self {
599 credential_id: require!(value.credential_id).parse()?,
600 key_type: require!(value.key_type).parse()?,
601 key_algorithm: require!(value.key_algorithm).parse()?,
602 key_curve: require!(value.key_curve).parse()?,
603 key_value: require!(value.key_value).parse()?,
604 rp_id: require!(value.rp_id).parse()?,
605 user_handle: EncString::try_from_optional(value.user_handle)
606 .ok()
607 .flatten(),
608 user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
609 counter: require!(value.counter).parse()?,
610 rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
611 user_display_name: EncString::try_from_optional(value.user_display_name)
612 .ok()
613 .flatten(),
614 discoverable: require!(value.discoverable).parse()?,
615 creation_date: value.creation_date.parse()?,
616 })
617 }
618}
619
620impl From<LoginUri> for bitwarden_api_api::models::CipherLoginUriModel {
621 fn from(uri: LoginUri) -> Self {
622 bitwarden_api_api::models::CipherLoginUriModel {
623 uri: uri.uri.map(|u| u.to_string()),
624 uri_checksum: uri.uri_checksum.map(|c| c.to_string()),
625 r#match: uri.r#match.map(|m| m.into()),
626 }
627 }
628}
629
630impl From<UriMatchType> for bitwarden_api_api::models::UriMatchType {
631 fn from(match_type: UriMatchType) -> Self {
632 match match_type {
633 UriMatchType::Domain => bitwarden_api_api::models::UriMatchType::Domain,
634 UriMatchType::Host => bitwarden_api_api::models::UriMatchType::Host,
635 UriMatchType::StartsWith => bitwarden_api_api::models::UriMatchType::StartsWith,
636 UriMatchType::Exact => bitwarden_api_api::models::UriMatchType::Exact,
637 UriMatchType::RegularExpression => {
638 bitwarden_api_api::models::UriMatchType::RegularExpression
639 }
640 UriMatchType::Never => bitwarden_api_api::models::UriMatchType::Never,
641 }
642 }
643}
644
645impl From<Fido2Credential> for bitwarden_api_api::models::CipherFido2CredentialModel {
646 fn from(cred: Fido2Credential) -> Self {
647 bitwarden_api_api::models::CipherFido2CredentialModel {
648 credential_id: Some(cred.credential_id.to_string()),
649 key_type: Some(cred.key_type.to_string()),
650 key_algorithm: Some(cred.key_algorithm.to_string()),
651 key_curve: Some(cred.key_curve.to_string()),
652 key_value: Some(cred.key_value.to_string()),
653 rp_id: Some(cred.rp_id.to_string()),
654 user_handle: cred.user_handle.map(|h| h.to_string()),
655 user_name: cred.user_name.map(|n| n.to_string()),
656 counter: Some(cred.counter.to_string()),
657 rp_name: cred.rp_name.map(|n| n.to_string()),
658 user_display_name: cred.user_display_name.map(|n| n.to_string()),
659 discoverable: Some(cred.discoverable.to_string()),
660 creation_date: cred.creation_date.to_rfc3339(),
661 }
662 }
663}
664
665impl From<Login> for bitwarden_api_api::models::CipherLoginModel {
666 fn from(login: Login) -> Self {
667 bitwarden_api_api::models::CipherLoginModel {
668 uri: None,
669 uris: login
670 .uris
671 .map(|u| u.into_iter().map(|u| u.into()).collect()),
672 username: login.username.map(|u| u.to_string()),
673 password: login.password.map(|p| p.to_string()),
674 password_revision_date: login.password_revision_date.map(|d| d.to_rfc3339()),
675 totp: login.totp.map(|t| t.to_string()),
676 autofill_on_page_load: login.autofill_on_page_load,
677 fido2_credentials: login
678 .fido2_credentials
679 .map(|c| c.into_iter().map(|c| c.into()).collect()),
680 }
681 }
682}
683
684impl CipherKind for Login {
685 fn decrypt_subtitle(
686 &self,
687 ctx: &mut KeyStoreContext<KeyIds>,
688 key: SymmetricKeyId,
689 ) -> Result<String, CryptoError> {
690 let username: Option<String> = self.username.decrypt(ctx, key)?;
691
692 Ok(username.unwrap_or_default())
693 }
694
695 fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
696 [
697 self.username
698 .as_ref()
699 .map(|_| CopyableCipherFields::LoginUsername),
700 self.password
701 .as_ref()
702 .map(|_| CopyableCipherFields::LoginPassword),
703 self.totp.as_ref().map(|_| CopyableCipherFields::LoginTotp),
704 ]
705 .into_iter()
706 .flatten()
707 .collect()
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use crate::{
714 Login,
715 cipher::cipher::{CipherKind, CopyableCipherFields},
716 };
717
718 #[test]
719 fn test_valid_checksum() {
720 let uri = super::LoginUriView {
721 uri: Some("https://example.com".to_string()),
722 r#match: Some(super::UriMatchType::Domain),
723 uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
724 };
725 assert!(uri.is_checksum_valid());
726 }
727
728 #[test]
729 fn test_invalid_checksum() {
730 let uri = super::LoginUriView {
731 uri: Some("https://example.com".to_string()),
732 r#match: Some(super::UriMatchType::Domain),
733 uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
734 };
735 assert!(!uri.is_checksum_valid());
736 }
737
738 #[test]
739 fn test_missing_checksum() {
740 let uri = super::LoginUriView {
741 uri: Some("https://example.com".to_string()),
742 r#match: Some(super::UriMatchType::Domain),
743 uri_checksum: None,
744 };
745 assert!(!uri.is_checksum_valid());
746 }
747
748 #[test]
749 fn test_generate_checksum() {
750 let mut uri = super::LoginUriView {
751 uri: Some("https://test.com".to_string()),
752 r#match: Some(super::UriMatchType::Domain),
753 uri_checksum: None,
754 };
755
756 uri.generate_checksum();
757
758 assert_eq!(
759 uri.uri_checksum.unwrap().as_str(),
760 "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
761 );
762 }
763
764 #[test]
765 fn test_get_copyable_fields_login_password() {
766 let login_with_password = Login {
767 username: None,
768 password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
769 password_revision_date: None,
770 uris: None,
771 totp: None,
772 autofill_on_page_load: None,
773 fido2_credentials: None,
774 };
775
776 let copyable_fields = login_with_password.get_copyable_fields(None);
777 assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginPassword]);
778 }
779
780 #[test]
781 fn test_get_copyable_fields_login_username() {
782 let login_with_username = Login {
783 username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
784 password: None,
785 password_revision_date: None,
786 uris: None,
787 totp: None,
788 autofill_on_page_load: None,
789 fido2_credentials: None,
790 };
791
792 let copyable_fields = login_with_username.get_copyable_fields(None);
793 assert_eq!(copyable_fields, vec![CopyableCipherFields::LoginUsername]);
794 }
795
796 #[test]
797 fn test_get_copyable_fields_login_everything() {
798 let login = Login {
799 username: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
800 password: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
801 password_revision_date: None,
802 uris: None,
803 totp: Some("2.38t4E88QbQEkBdK+oZNHFg==|B3BiDcG3ZfEkD2BK+FMytQ==|2Dw1/f+LCfkCmCj4gKOxOu6CRnZj93qaBYUqbzy/reU=".parse().unwrap()),
804 autofill_on_page_load: None,
805 fido2_credentials: None,
806 };
807
808 let copyable_fields = login.get_copyable_fields(None);
809 assert_eq!(
810 copyable_fields,
811 vec![
812 CopyableCipherFields::LoginUsername,
813 CopyableCipherFields::LoginPassword,
814 CopyableCipherFields::LoginTotp
815 ]
816 );
817 }
818}