bitwarden_fido/
authenticator.rs

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        // Insert the received UV to be able to return it later in check_user
127        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        // Insert the received UV to be able to return it later in check_user
195        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    /// Returns all Fido2 credentials that can be used for autofill, in a view
271    /// tailored for integration with OS autofill systems.
272    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        // This is just a wrapper around the actual implementation to allow for ? error handling
364        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            // Remove any that don't have Fido2 credentials
379            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            // When using the credential for authentication we have to ask the user to pick one.
392            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                // Store the selected credential for later use
405                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        // This is just a wrapper around the actual implementation to allow for ? error handling
451        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            // Get the previously selected cipher and add the new credential to it
467            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            // Store the updated credential for later use
480            this.authenticator
481                .selected_cipher
482                .lock()
483                .expect("Mutex is not poisoned")
484                .replace(selected.clone());
485
486            // Encrypt the updated cipher before sending it to the clients to be stored
487            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        // This is just a wrapper around the actual implementation to allow for ? error handling
530        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            // Get the previously selected cipher and update the credential
541            let selected = this.authenticator.get_selected_credential()?;
542
543            // Check that the provided credential ID matches the selected credential
544            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            // Store the updated credential for later use
558            this.authenticator
559                .selected_cipher
560                .lock()
561                .expect("Mutex is not poisoned")
562                .replace(selected.clone());
563
564            // Encrypt the updated cipher before sending it to the clients to be stored
565            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}