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