1use std::sync::Mutex;
2
3use bitwarden_core::{Client, VaultLockedError};
4use bitwarden_crypto::CryptoError;
5use bitwarden_vault::{CipherError, CipherView, EncryptionContext};
6use itertools::Itertools;
7use log::error;
8use passkey::{
9 authenticator::{Authenticator, DiscoverabilitySupport, StoreInfo, UIHint, UserCheck},
10 types::{
11 ctap2::{self, Ctap2Error, StatusCode, VendorError},
12 Passkey,
13 },
14};
15use thiserror::Error;
16
17use super::{
18 try_from_credential_new_view, types::*, CheckUserOptions, CipherViewContainer,
19 Fido2CredentialStore, Fido2UserInterface, SelectedCredential, UnknownEnum, AAGUID,
20};
21use crate::{
22 fill_with_credential, string_to_guid_bytes, try_from_credential_full, Fido2CallbackError,
23 FillCredentialError, InvalidGuid,
24};
25
26#[derive(Debug, Error)]
27pub enum GetSelectedCredentialError {
28 #[error("No selected credential available")]
29 NoSelectedCredential,
30 #[error("No fido2 credentials found")]
31 NoCredentialFound,
32
33 #[error(transparent)]
34 VaultLocked(#[from] VaultLockedError),
35 #[error(transparent)]
36 CryptoError(#[from] CryptoError),
37}
38
39#[derive(Debug, Error)]
40pub enum MakeCredentialError {
41 #[error(transparent)]
42 PublicKeyCredentialParametersError(#[from] PublicKeyCredentialParametersError),
43 #[error(transparent)]
44 UnknownEnum(#[from] UnknownEnum),
45 #[error(transparent)]
46 Serde(#[from] serde_json::Error),
47 #[error("Missing attested_credential_data")]
48 MissingAttestedCredentialData,
49 #[error("make_credential error: {0}")]
50 Other(String),
51}
52
53#[derive(Debug, Error)]
54pub enum GetAssertionError {
55 #[error(transparent)]
56 UnknownEnum(#[from] UnknownEnum),
57 #[error(transparent)]
58 Serde(#[from] serde_json::Error),
59 #[error(transparent)]
60 GetSelectedCredentialError(#[from] GetSelectedCredentialError),
61 #[error(transparent)]
62 InvalidGuid(#[from] InvalidGuid),
63 #[error("missing user")]
64 MissingUser,
65 #[error("get_assertion error: {0}")]
66 Other(String),
67}
68
69#[derive(Debug, Error)]
70pub enum SilentlyDiscoverCredentialsError {
71 #[error(transparent)]
72 CipherError(#[from] CipherError),
73 #[error(transparent)]
74 VaultLocked(#[from] VaultLockedError),
75 #[error(transparent)]
76 InvalidGuid(#[from] InvalidGuid),
77 #[error(transparent)]
78 Fido2CallbackError(#[from] Fido2CallbackError),
79 #[error(transparent)]
80 FromCipherViewError(#[from] Fido2CredentialAutofillViewError),
81}
82
83#[derive(Debug, Error)]
84pub enum CredentialsForAutofillError {
85 #[error(transparent)]
86 CipherError(#[from] CipherError),
87 #[error(transparent)]
88 VaultLocked(#[from] VaultLockedError),
89 #[error(transparent)]
90 InvalidGuid(#[from] InvalidGuid),
91 #[error(transparent)]
92 Fido2CallbackError(#[from] Fido2CallbackError),
93 #[error(transparent)]
94 FromCipherViewError(#[from] Fido2CredentialAutofillViewError),
95}
96
97pub struct Fido2Authenticator<'a> {
98 pub client: &'a Client,
99 pub user_interface: &'a dyn Fido2UserInterface,
100 pub credential_store: &'a dyn Fido2CredentialStore,
101
102 pub(crate) selected_cipher: Mutex<Option<CipherView>>,
103 pub(crate) requested_uv: Mutex<Option<UV>>,
104}
105
106impl<'a> Fido2Authenticator<'a> {
107 pub fn new(
108 client: &'a Client,
109 user_interface: &'a dyn Fido2UserInterface,
110 credential_store: &'a dyn Fido2CredentialStore,
111 ) -> Fido2Authenticator<'a> {
112 Fido2Authenticator {
113 client,
114 user_interface,
115 credential_store,
116 selected_cipher: Mutex::new(None),
117 requested_uv: Mutex::new(None),
118 }
119 }
120
121 pub async fn make_credential(
122 &mut self,
123 request: MakeCredentialRequest,
124 ) -> Result<MakeCredentialResult, MakeCredentialError> {
125 self.requested_uv
127 .get_mut()
128 .expect("Mutex is not poisoned")
129 .replace(request.options.uv);
130
131 let mut authenticator = self.get_authenticator(true);
132
133 let response = authenticator
134 .make_credential(ctap2::make_credential::Request {
135 client_data_hash: request.client_data_hash.into(),
136 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity {
137 id: request.rp.id,
138 name: request.rp.name,
139 },
140 user: passkey::types::webauthn::PublicKeyCredentialUserEntity {
141 id: request.user.id.into(),
142 display_name: request.user.display_name,
143 name: request.user.name,
144 },
145 pub_key_cred_params: request
146 .pub_key_cred_params
147 .into_iter()
148 .map(TryInto::try_into)
149 .collect::<Result<_, _>>()?,
150 exclude_list: request
151 .exclude_list
152 .map(|x| x.into_iter().map(TryInto::try_into).collect())
153 .transpose()?,
154 extensions: request
155 .extensions
156 .map(|e| serde_json::from_str(&e))
157 .transpose()?,
158 options: passkey::types::ctap2::make_credential::Options {
159 rk: request.options.rk,
160 up: true,
161 uv: self.convert_requested_uv(request.options.uv).await,
162 },
163 pin_auth: None,
164 pin_protocol: None,
165 })
166 .await;
167
168 let response = match response {
169 Ok(x) => x,
170 Err(e) => return Err(MakeCredentialError::Other(format!("{e:?}"))),
171 };
172
173 let attestation_object = response.as_bytes().to_vec();
174 let authenticator_data = response.auth_data.to_vec();
175 let attested_credential_data = response
176 .auth_data
177 .attested_credential_data
178 .ok_or(MakeCredentialError::MissingAttestedCredentialData)?;
179 let credential_id = attested_credential_data.credential_id().to_vec();
180
181 Ok(MakeCredentialResult {
182 authenticator_data,
183 attestation_object,
184 credential_id,
185 })
186 }
187
188 pub async fn get_assertion(
189 &mut self,
190 request: GetAssertionRequest,
191 ) -> Result<GetAssertionResult, GetAssertionError> {
192 self.requested_uv
194 .get_mut()
195 .expect("Mutex is not poisoned")
196 .replace(request.options.uv);
197
198 let mut authenticator = self.get_authenticator(false);
199
200 let response = authenticator
201 .get_assertion(ctap2::get_assertion::Request {
202 rp_id: request.rp_id,
203 client_data_hash: request.client_data_hash.into(),
204 allow_list: request
205 .allow_list
206 .map(|l| {
207 l.into_iter()
208 .map(TryInto::try_into)
209 .collect::<Result<Vec<_>, _>>()
210 })
211 .transpose()?,
212 extensions: request
213 .extensions
214 .map(|e| serde_json::from_str(&e))
215 .transpose()?,
216 options: passkey::types::ctap2::make_credential::Options {
217 rk: request.options.rk,
218 up: true,
219 uv: self.convert_requested_uv(request.options.uv).await,
220 },
221 pin_auth: None,
222 pin_protocol: None,
223 })
224 .await;
225
226 let response = match response {
227 Ok(x) => x,
228 Err(e) => return Err(GetAssertionError::Other(format!("{e:?}"))),
229 };
230
231 let selected_credential = self.get_selected_credential()?;
232 let authenticator_data = response.auth_data.to_vec();
233 let credential_id = string_to_guid_bytes(&selected_credential.credential.credential_id)?;
234
235 Ok(GetAssertionResult {
236 credential_id,
237 authenticator_data,
238 signature: response.signature.into(),
239 user_handle: response
240 .user
241 .ok_or(GetAssertionError::MissingUser)?
242 .id
243 .into(),
244 selected_credential,
245 })
246 }
247
248 pub async fn silently_discover_credentials(
249 &mut self,
250 rp_id: String,
251 ) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
252 let key_store = self.client.internal.get_key_store();
253 let result = self.credential_store.find_credentials(None, rp_id).await?;
254
255 let mut ctx = key_store.context();
256 result
257 .into_iter()
258 .map(
259 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
260 Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?)
261 },
262 )
263 .flatten_ok()
264 .collect()
265 }
266
267 pub async fn credentials_for_autofill(
270 &mut self,
271 ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
272 let all_credentials = self.credential_store.all_credentials().await?;
273
274 all_credentials
275 .into_iter()
276 .map(
277 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
278 Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
279 },
280 )
281 .flatten_ok()
282 .collect()
283 }
284
285 pub(super) fn get_authenticator(
286 &self,
287 create_credential: bool,
288 ) -> Authenticator<CredentialStoreImpl, UserValidationMethodImpl> {
289 Authenticator::new(
290 AAGUID,
291 CredentialStoreImpl {
292 authenticator: self,
293 create_credential,
294 },
295 UserValidationMethodImpl {
296 authenticator: self,
297 },
298 )
299 }
300
301 async fn convert_requested_uv(&self, uv: UV) -> bool {
302 let verification_enabled = self.user_interface.is_verification_enabled().await;
303 match (uv, verification_enabled) {
304 (UV::Preferred, true) => true,
305 (UV::Preferred, false) => false,
306 (UV::Required, _) => true,
307 (UV::Discouraged, _) => false,
308 }
309 }
310
311 pub(super) fn get_selected_credential(
312 &self,
313 ) -> Result<SelectedCredential, GetSelectedCredentialError> {
314 let key_store = self.client.internal.get_key_store();
315
316 let cipher = self
317 .selected_cipher
318 .lock()
319 .expect("Mutex is not poisoned")
320 .clone()
321 .ok_or(GetSelectedCredentialError::NoSelectedCredential)?;
322
323 let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?;
324
325 let credential = creds
326 .first()
327 .ok_or(GetSelectedCredentialError::NoCredentialFound)?
328 .clone();
329
330 Ok(SelectedCredential { cipher, credential })
331 }
332}
333
334pub(super) struct CredentialStoreImpl<'a> {
335 authenticator: &'a Fido2Authenticator<'a>,
336 create_credential: bool,
337}
338pub(super) struct UserValidationMethodImpl<'a> {
339 authenticator: &'a Fido2Authenticator<'a>,
340}
341
342#[async_trait::async_trait]
343impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> {
344 type PasskeyItem = CipherViewContainer;
345 async fn find_credentials(
346 &self,
347 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
348 rp_id: &str,
349 ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
350 #[derive(Debug, Error)]
351 enum InnerError {
352 #[error(transparent)]
353 VaultLocked(#[from] VaultLockedError),
354 #[error(transparent)]
355 CipherError(#[from] CipherError),
356 #[error(transparent)]
357 CryptoError(#[from] CryptoError),
358 #[error(transparent)]
359 Fido2CallbackError(#[from] Fido2CallbackError),
360 }
361
362 async fn inner(
364 this: &CredentialStoreImpl<'_>,
365 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
366 rp_id: &str,
367 ) -> Result<Vec<CipherViewContainer>, InnerError> {
368 let ids: Option<Vec<Vec<u8>>> =
369 ids.map(|ids| ids.iter().map(|id| id.id.clone().into()).collect());
370
371 let ciphers = this
372 .authenticator
373 .credential_store
374 .find_credentials(ids, rp_id.to_string())
375 .await?;
376
377 let creds: Vec<_> = ciphers
379 .into_iter()
380 .filter(|c| {
381 c.login
382 .as_ref()
383 .and_then(|l| l.fido2_credentials.as_ref())
384 .is_some()
385 })
386 .collect();
387
388 let key_store = this.authenticator.client.internal.get_key_store();
389
390 if this.create_credential {
392 Ok(creds
393 .into_iter()
394 .map(|c| CipherViewContainer::new(c, &mut key_store.context()))
395 .collect::<Result<_, _>>()?)
396 } else {
397 let picked = this
398 .authenticator
399 .user_interface
400 .pick_credential_for_authentication(creds)
401 .await?;
402
403 this.authenticator
405 .selected_cipher
406 .lock()
407 .expect("Mutex is not poisoned")
408 .replace(picked.clone());
409
410 Ok(vec![CipherViewContainer::new(
411 picked,
412 &mut key_store.context(),
413 )?])
414 }
415 }
416
417 inner(self, ids, rp_id).await.map_err(|e| {
418 error!("Error finding credentials: {e:?}");
419 VendorError::try_from(0xF0)
420 .expect("Valid vendor error code")
421 .into()
422 })
423 }
424
425 async fn save_credential(
426 &mut self,
427 cred: Passkey,
428 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
429 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
430 options: passkey::types::ctap2::get_assertion::Options,
431 ) -> Result<(), StatusCode> {
432 #[derive(Debug, Error)]
433 enum InnerError {
434 #[error("Client User Id has not been set")]
435 MissingUserId,
436 #[error(transparent)]
437 VaultLocked(#[from] VaultLockedError),
438 #[error(transparent)]
439 FillCredentialError(#[from] FillCredentialError),
440 #[error(transparent)]
441 CipherError(#[from] CipherError),
442 #[error(transparent)]
443 CryptoError(#[from] CryptoError),
444 #[error(transparent)]
445 Fido2CallbackError(#[from] Fido2CallbackError),
446
447 #[error("No selected credential available")]
448 NoSelectedCredential,
449 }
450
451 async fn inner(
453 this: &mut CredentialStoreImpl<'_>,
454 cred: Passkey,
455 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
456 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
457 options: passkey::types::ctap2::get_assertion::Options,
458 ) -> Result<(), InnerError> {
459 let user_id = this
460 .authenticator
461 .client
462 .internal
463 .get_user_id()
464 .ok_or(InnerError::MissingUserId)?;
465 let cred = try_from_credential_full(cred, user, rp, options)?;
466
467 let mut selected: CipherView = this
469 .authenticator
470 .selected_cipher
471 .lock()
472 .expect("Mutex is not poisoned")
473 .clone()
474 .ok_or(InnerError::NoSelectedCredential)?;
475
476 let key_store = this.authenticator.client.internal.get_key_store();
477
478 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
479
480 this.authenticator
482 .selected_cipher
483 .lock()
484 .expect("Mutex is not poisoned")
485 .replace(selected.clone());
486
487 let encrypted = key_store.encrypt(selected)?;
489
490 this.authenticator
491 .credential_store
492 .save_credential(EncryptionContext {
493 cipher: encrypted,
494 encrypted_for: user_id,
495 })
496 .await?;
497
498 Ok(())
499 }
500
501 inner(self, cred, user, rp, options).await.map_err(|e| {
502 error!("Error saving credential: {e:?}");
503 VendorError::try_from(0xF1)
504 .expect("Valid vendor error code")
505 .into()
506 })
507 }
508
509 async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
510 #[derive(Debug, Error)]
511 enum InnerError {
512 #[error("Client User Id has not been set")]
513 MissingUserId,
514 #[error(transparent)]
515 VaultLocked(#[from] VaultLockedError),
516 #[error(transparent)]
517 InvalidGuid(#[from] InvalidGuid),
518 #[error("Credential ID does not match selected credential")]
519 CredentialIdMismatch,
520 #[error(transparent)]
521 FillCredentialError(#[from] FillCredentialError),
522 #[error(transparent)]
523 CipherError(#[from] CipherError),
524 #[error(transparent)]
525 CryptoError(#[from] CryptoError),
526 #[error(transparent)]
527 Fido2CallbackError(#[from] Fido2CallbackError),
528 #[error(transparent)]
529 GetSelectedCredentialError(#[from] GetSelectedCredentialError),
530 }
531
532 async fn inner(
534 this: &mut CredentialStoreImpl<'_>,
535 cred: Passkey,
536 ) -> Result<(), InnerError> {
537 let user_id = this
538 .authenticator
539 .client
540 .internal
541 .get_user_id()
542 .ok_or(InnerError::MissingUserId)?;
543 let selected = this.authenticator.get_selected_credential()?;
545
546 let new_id: &Vec<u8> = &cred.credential_id;
548 let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
549 if new_id != &selected_id {
550 return Err(InnerError::CredentialIdMismatch);
551 }
552
553 let cred = fill_with_credential(&selected.credential, cred)?;
554
555 let key_store = this.authenticator.client.internal.get_key_store();
556
557 let mut selected = selected.cipher;
558 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
559
560 this.authenticator
562 .selected_cipher
563 .lock()
564 .expect("Mutex is not poisoned")
565 .replace(selected.clone());
566
567 let encrypted = key_store.encrypt(selected)?;
569
570 this.authenticator
571 .credential_store
572 .save_credential(EncryptionContext {
573 cipher: encrypted,
574 encrypted_for: user_id,
575 })
576 .await?;
577
578 Ok(())
579 }
580
581 inner(self, cred).await.map_err(|e| {
582 error!("Error updating credential: {e:?}");
583 VendorError::try_from(0xF2)
584 .expect("Valid vendor error code")
585 .into()
586 })
587 }
588
589 async fn get_info(&self) -> StoreInfo {
590 StoreInfo {
591 discoverability: DiscoverabilitySupport::Full,
592 }
593 }
594}
595
596#[async_trait::async_trait]
597impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
598 type PasskeyItem = CipherViewContainer;
599
600 async fn check_user<'a>(
601 &self,
602 hint: UIHint<'a, Self::PasskeyItem>,
603 presence: bool,
604 _verification: bool,
605 ) -> Result<UserCheck, Ctap2Error> {
606 let verification = self
607 .authenticator
608 .requested_uv
609 .lock()
610 .expect("Mutex is not poisoned")
611 .ok_or(Ctap2Error::UserVerificationInvalid)?;
612
613 let options = CheckUserOptions {
614 require_presence: presence,
615 require_verification: verification.into(),
616 };
617
618 let result = match hint {
619 UIHint::RequestNewCredential(user, rp) => {
620 let new_credential = try_from_credential_new_view(user, rp)
621 .map_err(|_| Ctap2Error::InvalidCredential)?;
622
623 let (cipher_view, user_check) = self
624 .authenticator
625 .user_interface
626 .check_user_and_pick_credential_for_creation(options, new_credential)
627 .await
628 .map_err(|_| Ctap2Error::OperationDenied)?;
629
630 self.authenticator
631 .selected_cipher
632 .lock()
633 .expect("Mutex is not poisoned")
634 .replace(cipher_view);
635
636 Ok(user_check)
637 }
638 _ => {
639 self.authenticator
640 .user_interface
641 .check_user(options, map_ui_hint(hint))
642 .await
643 }
644 };
645
646 let result = result.map_err(|e| {
647 error!("Error checking user: {e:?}");
648 Ctap2Error::UserVerificationInvalid
649 })?;
650
651 Ok(UserCheck {
652 presence: result.user_present,
653 verification: result.user_verified,
654 })
655 }
656
657 async fn is_presence_enabled(&self) -> bool {
658 true
659 }
660
661 async fn is_verification_enabled(&self) -> Option<bool> {
662 Some(
663 self.authenticator
664 .user_interface
665 .is_verification_enabled()
666 .await,
667 )
668 }
669}
670
671fn map_ui_hint(hint: UIHint<'_, CipherViewContainer>) -> UIHint<'_, CipherView> {
672 use UIHint::*;
673 match hint {
674 InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
675 InformNoCredentialsFound => InformNoCredentialsFound,
676 RequestNewCredential(u, r) => RequestNewCredential(u, r),
677 RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
678 }
679}