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