1use base64::{engine::general_purpose::STANDARD, Engine};
2use bitwarden_api_api::models::{CipherLoginModel, CipherLoginUriModel};
3use bitwarden_core::{
4 key_management::{KeyIds, SymmetricKeyId},
5 require,
6};
7use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use serde_repr::{Deserialize_repr, Serialize_repr};
11#[cfg(feature = "wasm")]
12use tsify_next::Tsify;
13#[cfg(feature = "wasm")]
14use wasm_bindgen::prelude::wasm_bindgen;
15
16use crate::VaultParseError;
17
18#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
19#[repr(u8)]
20#[serde(rename_all = "camelCase", deny_unknown_fields)]
21#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
22#[cfg_attr(feature = "wasm", wasm_bindgen)]
23pub enum UriMatchType {
24 Domain = 0,
25 Host = 1,
26 StartsWith = 2,
27 Exact = 3,
28 RegularExpression = 4,
29 Never = 5,
30}
31
32#[derive(Serialize, Deserialize, Debug, Clone)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
35#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
36pub struct LoginUri {
37 pub uri: Option<EncString>,
38 pub r#match: Option<UriMatchType>,
39 pub uri_checksum: Option<EncString>,
40}
41
42#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
43#[serde(rename_all = "camelCase", deny_unknown_fields)]
44#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
45#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
46pub struct LoginUriView {
47 pub uri: Option<String>,
48 pub r#match: Option<UriMatchType>,
49 pub uri_checksum: Option<String>,
50}
51
52impl LoginUriView {
53 pub(crate) fn is_checksum_valid(&self) -> bool {
54 let Some(uri) = &self.uri else {
55 return false;
56 };
57 let Some(cs) = &self.uri_checksum else {
58 return false;
59 };
60 let Ok(cs) = STANDARD.decode(cs) else {
61 return false;
62 };
63
64 use sha2::Digest;
65 let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
66
67 uri_hash.as_slice() == cs
68 }
69
70 pub(crate) fn generate_checksum(&mut self) {
71 if let Some(uri) = &self.uri {
72 use sha2::Digest;
73 let uri_hash = sha2::Sha256::new().chain_update(uri.as_bytes()).finalize();
74 let uri_hash = STANDARD.encode(uri_hash.as_slice());
75 self.uri_checksum = Some(uri_hash);
76 }
77 }
78}
79
80#[derive(Serialize, Deserialize, Debug, Clone)]
81#[serde(rename_all = "camelCase", deny_unknown_fields)]
82#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
83#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
84pub struct Fido2Credential {
85 pub credential_id: EncString,
86 pub key_type: EncString,
87 pub key_algorithm: EncString,
88 pub key_curve: EncString,
89 pub key_value: EncString,
90 pub rp_id: EncString,
91 pub user_handle: Option<EncString>,
92 pub user_name: Option<EncString>,
93 pub counter: EncString,
94 pub rp_name: Option<EncString>,
95 pub user_display_name: Option<EncString>,
96 pub discoverable: EncString,
97 pub creation_date: DateTime<Utc>,
98}
99
100#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
101#[serde(rename_all = "camelCase", deny_unknown_fields)]
102#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
103#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
104pub struct Fido2CredentialListView {
105 pub credential_id: String,
106 pub rp_id: String,
107 pub user_handle: Option<String>,
108 pub user_name: Option<String>,
109 pub user_display_name: Option<String>,
110}
111
112#[derive(Serialize, Deserialize, Debug, Clone)]
113#[serde(rename_all = "camelCase", deny_unknown_fields)]
114#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
115#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
116pub struct Fido2CredentialView {
117 pub credential_id: String,
118 pub key_type: String,
119 pub key_algorithm: String,
120 pub key_curve: String,
121 pub key_value: EncString,
124 pub rp_id: String,
125 pub user_handle: Option<String>,
126 pub user_name: Option<String>,
127 pub counter: String,
128 pub rp_name: Option<String>,
129 pub user_display_name: Option<String>,
130 pub discoverable: String,
131 pub creation_date: DateTime<Utc>,
132}
133
134#[derive(Serialize, Deserialize, Debug, Clone)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct Fido2CredentialFullView {
139 pub credential_id: String,
140 pub key_type: String,
141 pub key_algorithm: String,
142 pub key_curve: String,
143 pub key_value: String,
144 pub rp_id: String,
145 pub user_handle: Option<String>,
146 pub user_name: Option<String>,
147 pub counter: String,
148 pub rp_name: Option<String>,
149 pub user_display_name: Option<String>,
150 pub discoverable: String,
151 pub creation_date: DateTime<Utc>,
152}
153
154#[derive(Serialize, Deserialize, Debug, Clone)]
158#[serde(rename_all = "camelCase", deny_unknown_fields)]
159#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
160#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
161pub struct Fido2CredentialNewView {
162 pub credential_id: String,
163 pub key_type: String,
164 pub key_algorithm: String,
165 pub key_curve: String,
166 pub rp_id: String,
167 pub user_handle: Option<String>,
168 pub user_name: Option<String>,
169 pub counter: String,
170 pub rp_name: Option<String>,
171 pub user_display_name: Option<String>,
172 pub creation_date: DateTime<Utc>,
173}
174
175impl From<Fido2CredentialFullView> for Fido2CredentialNewView {
176 fn from(value: Fido2CredentialFullView) -> Self {
177 Fido2CredentialNewView {
178 credential_id: value.credential_id,
179 key_type: value.key_type,
180 key_algorithm: value.key_algorithm,
181 key_curve: value.key_curve,
182 rp_id: value.rp_id,
183 user_handle: value.user_handle,
184 user_name: value.user_name,
185 counter: value.counter,
186 rp_name: value.rp_name,
187 user_display_name: value.user_display_name,
188 creation_date: value.creation_date,
189 }
190 }
191}
192
193impl Encryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialFullView {
194 fn encrypt(
195 &self,
196 ctx: &mut KeyStoreContext<KeyIds>,
197 key: SymmetricKeyId,
198 ) -> Result<Fido2Credential, CryptoError> {
199 Ok(Fido2Credential {
200 credential_id: self.credential_id.encrypt(ctx, key)?,
201 key_type: self.key_type.encrypt(ctx, key)?,
202 key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
203 key_curve: self.key_curve.encrypt(ctx, key)?,
204 key_value: self.key_value.encrypt(ctx, key)?,
205 rp_id: self.rp_id.encrypt(ctx, key)?,
206 user_handle: self
207 .user_handle
208 .as_ref()
209 .map(|h| h.encrypt(ctx, key))
210 .transpose()?,
211 user_name: self.user_name.encrypt(ctx, key)?,
212 counter: self.counter.encrypt(ctx, key)?,
213 rp_name: self.rp_name.encrypt(ctx, key)?,
214 user_display_name: self.user_display_name.encrypt(ctx, key)?,
215 discoverable: self.discoverable.encrypt(ctx, key)?,
216 creation_date: self.creation_date,
217 })
218 }
219}
220
221impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2Credential {
222 fn decrypt(
223 &self,
224 ctx: &mut KeyStoreContext<KeyIds>,
225 key: SymmetricKeyId,
226 ) -> Result<Fido2CredentialFullView, CryptoError> {
227 Ok(Fido2CredentialFullView {
228 credential_id: self.credential_id.decrypt(ctx, key)?,
229 key_type: self.key_type.decrypt(ctx, key)?,
230 key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
231 key_curve: self.key_curve.decrypt(ctx, key)?,
232 key_value: self.key_value.decrypt(ctx, key)?,
233 rp_id: self.rp_id.decrypt(ctx, key)?,
234 user_handle: self.user_handle.decrypt(ctx, key)?,
235 user_name: self.user_name.decrypt(ctx, key)?,
236 counter: self.counter.decrypt(ctx, key)?,
237 rp_name: self.rp_name.decrypt(ctx, key)?,
238 user_display_name: self.user_display_name.decrypt(ctx, key)?,
239 discoverable: self.discoverable.decrypt(ctx, key)?,
240 creation_date: self.creation_date,
241 })
242 }
243}
244
245impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialFullView> for Fido2CredentialView {
246 fn decrypt(
247 &self,
248 ctx: &mut KeyStoreContext<KeyIds>,
249 key: SymmetricKeyId,
250 ) -> Result<Fido2CredentialFullView, CryptoError> {
251 Ok(Fido2CredentialFullView {
252 credential_id: self.credential_id.clone(),
253 key_type: self.key_type.clone(),
254 key_algorithm: self.key_algorithm.clone(),
255 key_curve: self.key_curve.clone(),
256 key_value: self.key_value.decrypt(ctx, key)?,
257 rp_id: self.rp_id.clone(),
258 user_handle: self.user_handle.clone(),
259 user_name: self.user_name.clone(),
260 counter: self.counter.clone(),
261 rp_name: self.rp_name.clone(),
262 user_display_name: self.user_display_name.clone(),
263 discoverable: self.discoverable.clone(),
264 creation_date: self.creation_date,
265 })
266 }
267}
268
269#[derive(Serialize, Deserialize, Debug, Clone)]
270#[serde(rename_all = "camelCase", deny_unknown_fields)]
271#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
272#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
273pub struct Login {
274 pub username: Option<EncString>,
275 pub password: Option<EncString>,
276 pub password_revision_date: Option<DateTime<Utc>>,
277
278 pub uris: Option<Vec<LoginUri>>,
279 pub totp: Option<EncString>,
280 pub autofill_on_page_load: Option<bool>,
281
282 pub fido2_credentials: Option<Vec<Fido2Credential>>,
283}
284
285#[derive(Serialize, Deserialize, Debug, Clone)]
286#[serde(rename_all = "camelCase", deny_unknown_fields)]
287#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
288#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
289pub struct LoginView {
290 pub username: Option<String>,
291 pub password: Option<String>,
292 pub password_revision_date: Option<DateTime<Utc>>,
293
294 pub uris: Option<Vec<LoginUriView>>,
295 pub totp: Option<String>,
296 pub autofill_on_page_load: Option<bool>,
297
298 pub fido2_credentials: Option<Vec<Fido2Credential>>,
300}
301
302#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
303#[serde(rename_all = "camelCase", deny_unknown_fields)]
304#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
305#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
306pub struct LoginListView {
307 pub fido2_credentials: Option<Vec<Fido2CredentialListView>>,
308 pub has_fido2: bool,
309 pub username: Option<String>,
310 pub totp: Option<EncString>,
312 pub uris: Option<Vec<LoginUriView>>,
313}
314
315impl Encryptable<KeyIds, SymmetricKeyId, LoginUri> for LoginUriView {
316 fn encrypt(
317 &self,
318 ctx: &mut KeyStoreContext<KeyIds>,
319 key: SymmetricKeyId,
320 ) -> Result<LoginUri, CryptoError> {
321 Ok(LoginUri {
322 uri: self.uri.encrypt(ctx, key)?,
323 r#match: self.r#match,
324 uri_checksum: self.uri_checksum.encrypt(ctx, key)?,
325 })
326 }
327}
328
329impl Encryptable<KeyIds, SymmetricKeyId, Login> for LoginView {
330 fn encrypt(
331 &self,
332 ctx: &mut KeyStoreContext<KeyIds>,
333 key: SymmetricKeyId,
334 ) -> Result<Login, CryptoError> {
335 Ok(Login {
336 username: self.username.encrypt(ctx, key)?,
337 password: self.password.encrypt(ctx, key)?,
338 password_revision_date: self.password_revision_date,
339 uris: self.uris.encrypt(ctx, key)?,
340 totp: self.totp.encrypt(ctx, key)?,
341 autofill_on_page_load: self.autofill_on_page_load,
342 fido2_credentials: self.fido2_credentials.clone(),
343 })
344 }
345}
346
347impl Decryptable<KeyIds, SymmetricKeyId, LoginUriView> for LoginUri {
348 fn decrypt(
349 &self,
350 ctx: &mut KeyStoreContext<KeyIds>,
351 key: SymmetricKeyId,
352 ) -> Result<LoginUriView, CryptoError> {
353 Ok(LoginUriView {
354 uri: self.uri.decrypt(ctx, key)?,
355 r#match: self.r#match,
356 uri_checksum: self.uri_checksum.decrypt(ctx, key)?,
357 })
358 }
359}
360
361impl Decryptable<KeyIds, SymmetricKeyId, LoginView> for Login {
362 fn decrypt(
363 &self,
364 ctx: &mut KeyStoreContext<KeyIds>,
365 key: SymmetricKeyId,
366 ) -> Result<LoginView, CryptoError> {
367 Ok(LoginView {
368 username: self.username.decrypt(ctx, key).ok().flatten(),
369 password: self.password.decrypt(ctx, key).ok().flatten(),
370 password_revision_date: self.password_revision_date,
371 uris: self.uris.decrypt(ctx, key).ok().flatten(),
372 totp: self.totp.decrypt(ctx, key).ok().flatten(),
373 autofill_on_page_load: self.autofill_on_page_load,
374 fido2_credentials: self.fido2_credentials.clone(),
375 })
376 }
377}
378
379impl Decryptable<KeyIds, SymmetricKeyId, LoginListView> for Login {
380 fn decrypt(
381 &self,
382 ctx: &mut KeyStoreContext<KeyIds>,
383 key: SymmetricKeyId,
384 ) -> Result<LoginListView, CryptoError> {
385 Ok(LoginListView {
386 fido2_credentials: self
387 .fido2_credentials
388 .as_ref()
389 .map(|fido2_credentials| fido2_credentials.decrypt(ctx, key))
390 .transpose()?,
391 has_fido2: self.fido2_credentials.is_some(),
392 username: self.username.decrypt(ctx, key).ok().flatten(),
393 totp: self.totp.clone(),
394 uris: self.uris.decrypt(ctx, key).ok().flatten(),
395 })
396 }
397}
398
399impl Encryptable<KeyIds, SymmetricKeyId, Fido2Credential> for Fido2CredentialView {
400 fn encrypt(
401 &self,
402 ctx: &mut KeyStoreContext<KeyIds>,
403 key: SymmetricKeyId,
404 ) -> Result<Fido2Credential, CryptoError> {
405 Ok(Fido2Credential {
406 credential_id: self.credential_id.encrypt(ctx, key)?,
407 key_type: self.key_type.encrypt(ctx, key)?,
408 key_algorithm: self.key_algorithm.encrypt(ctx, key)?,
409 key_curve: self.key_curve.encrypt(ctx, key)?,
410 key_value: self.key_value.clone(),
411 rp_id: self.rp_id.encrypt(ctx, key)?,
412 user_handle: self
413 .user_handle
414 .as_ref()
415 .map(|h| h.encrypt(ctx, key))
416 .transpose()?,
417 user_name: self
418 .user_name
419 .as_ref()
420 .map(|n| n.encrypt(ctx, key))
421 .transpose()?,
422 counter: self.counter.encrypt(ctx, key)?,
423 rp_name: self.rp_name.encrypt(ctx, key)?,
424 user_display_name: self.user_display_name.encrypt(ctx, key)?,
425 discoverable: self.discoverable.encrypt(ctx, key)?,
426 creation_date: self.creation_date,
427 })
428 }
429}
430
431impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialView> for Fido2Credential {
432 fn decrypt(
433 &self,
434 ctx: &mut KeyStoreContext<KeyIds>,
435 key: SymmetricKeyId,
436 ) -> Result<Fido2CredentialView, CryptoError> {
437 Ok(Fido2CredentialView {
438 credential_id: self.credential_id.decrypt(ctx, key)?,
439 key_type: self.key_type.decrypt(ctx, key)?,
440 key_algorithm: self.key_algorithm.decrypt(ctx, key)?,
441 key_curve: self.key_curve.decrypt(ctx, key)?,
442 key_value: self.key_value.clone(),
443 rp_id: self.rp_id.decrypt(ctx, key)?,
444 user_handle: self.user_handle.decrypt(ctx, key)?,
445 user_name: self.user_name.decrypt(ctx, key)?,
446 counter: self.counter.decrypt(ctx, key)?,
447 rp_name: self.rp_name.decrypt(ctx, key)?,
448 user_display_name: self.user_display_name.decrypt(ctx, key)?,
449 discoverable: self.discoverable.decrypt(ctx, key)?,
450 creation_date: self.creation_date,
451 })
452 }
453}
454
455impl Decryptable<KeyIds, SymmetricKeyId, Fido2CredentialListView> for Fido2Credential {
456 fn decrypt(
457 &self,
458 ctx: &mut KeyStoreContext<KeyIds>,
459 key: SymmetricKeyId,
460 ) -> Result<Fido2CredentialListView, CryptoError> {
461 Ok(Fido2CredentialListView {
462 credential_id: self.credential_id.decrypt(ctx, key)?,
463 rp_id: self.rp_id.decrypt(ctx, key)?,
464 user_handle: self.user_handle.decrypt(ctx, key)?,
465 user_name: self.user_name.decrypt(ctx, key)?,
466 user_display_name: self.user_display_name.decrypt(ctx, key)?,
467 })
468 }
469}
470
471impl TryFrom<CipherLoginModel> for Login {
472 type Error = VaultParseError;
473
474 fn try_from(login: CipherLoginModel) -> Result<Self, Self::Error> {
475 Ok(Self {
476 username: EncString::try_from_optional(login.username)?,
477 password: EncString::try_from_optional(login.password)?,
478 password_revision_date: login
479 .password_revision_date
480 .map(|d| d.parse())
481 .transpose()?,
482 uris: login
483 .uris
484 .map(|v| v.into_iter().map(|u| u.try_into()).collect())
485 .transpose()?,
486 totp: EncString::try_from_optional(login.totp)?,
487 autofill_on_page_load: login.autofill_on_page_load,
488 fido2_credentials: login
489 .fido2_credentials
490 .map(|v| v.into_iter().map(|c| c.try_into()).collect())
491 .transpose()?,
492 })
493 }
494}
495
496impl TryFrom<CipherLoginUriModel> for LoginUri {
497 type Error = VaultParseError;
498
499 fn try_from(uri: CipherLoginUriModel) -> Result<Self, Self::Error> {
500 Ok(Self {
501 uri: EncString::try_from_optional(uri.uri)?,
502 r#match: uri.r#match.map(|m| m.into()),
503 uri_checksum: EncString::try_from_optional(uri.uri_checksum)?,
504 })
505 }
506}
507
508impl From<bitwarden_api_api::models::UriMatchType> for UriMatchType {
509 fn from(value: bitwarden_api_api::models::UriMatchType) -> Self {
510 match value {
511 bitwarden_api_api::models::UriMatchType::Domain => Self::Domain,
512 bitwarden_api_api::models::UriMatchType::Host => Self::Host,
513 bitwarden_api_api::models::UriMatchType::StartsWith => Self::StartsWith,
514 bitwarden_api_api::models::UriMatchType::Exact => Self::Exact,
515 bitwarden_api_api::models::UriMatchType::RegularExpression => Self::RegularExpression,
516 bitwarden_api_api::models::UriMatchType::Never => Self::Never,
517 }
518 }
519}
520
521impl TryFrom<bitwarden_api_api::models::CipherFido2CredentialModel> for Fido2Credential {
522 type Error = VaultParseError;
523
524 fn try_from(
525 value: bitwarden_api_api::models::CipherFido2CredentialModel,
526 ) -> Result<Self, Self::Error> {
527 Ok(Self {
528 credential_id: require!(value.credential_id).parse()?,
529 key_type: require!(value.key_type).parse()?,
530 key_algorithm: require!(value.key_algorithm).parse()?,
531 key_curve: require!(value.key_curve).parse()?,
532 key_value: require!(value.key_value).parse()?,
533 rp_id: require!(value.rp_id).parse()?,
534 user_handle: EncString::try_from_optional(value.user_handle)
535 .ok()
536 .flatten(),
537 user_name: EncString::try_from_optional(value.user_name).ok().flatten(),
538 counter: require!(value.counter).parse()?,
539 rp_name: EncString::try_from_optional(value.rp_name).ok().flatten(),
540 user_display_name: EncString::try_from_optional(value.user_display_name)
541 .ok()
542 .flatten(),
543 discoverable: require!(value.discoverable).parse()?,
544 creation_date: value.creation_date.parse()?,
545 })
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 #[test]
552 fn test_valid_checksum() {
553 let uri = super::LoginUriView {
554 uri: Some("https://example.com".to_string()),
555 r#match: Some(super::UriMatchType::Domain),
556 uri_checksum: Some("EAaArVRs5qV39C9S3zO0z9ynVoWeZkuNfeMpsVDQnOk=".to_string()),
557 };
558 assert!(uri.is_checksum_valid());
559 }
560
561 #[test]
562 fn test_invalid_checksum() {
563 let uri = super::LoginUriView {
564 uri: Some("https://example.com".to_string()),
565 r#match: Some(super::UriMatchType::Domain),
566 uri_checksum: Some("UtSgIv8LYfEdOu7yqjF7qXWhmouYGYC8RSr7/ryZg5Q=".to_string()),
567 };
568 assert!(!uri.is_checksum_valid());
569 }
570
571 #[test]
572 fn test_missing_checksum() {
573 let uri = super::LoginUriView {
574 uri: Some("https://example.com".to_string()),
575 r#match: Some(super::UriMatchType::Domain),
576 uri_checksum: None,
577 };
578 assert!(!uri.is_checksum_valid());
579 }
580
581 #[test]
582 fn test_generate_checksum() {
583 let mut uri = super::LoginUriView {
584 uri: Some("https://test.com".to_string()),
585 r#match: Some(super::UriMatchType::Domain),
586 uri_checksum: None,
587 };
588
589 uri.generate_checksum();
590
591 assert_eq!(
592 uri.uri_checksum.unwrap().as_str(),
593 "OWk2vQvwYD1nhLZdA+ltrpBWbDa2JmHyjUEWxRZSS8w="
594 );
595 }
596}