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