bitwarden_fido/
authenticator.rs

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        // Insert the received UV to be able to return it later in check_user
133        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        // Insert the received UV to be able to return it later in check_user
201        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    /// Returns all Fido2 credentials that can be used for autofill, in a view
277    /// tailored for integration with OS autofill systems.
278    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        // This is just a wrapper around the actual implementation to allow for ? error handling
372        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            // Remove any that don't have Fido2 credentials
387            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            // When using the credential for authentication we have to ask the user to pick one.
400            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                // Store the selected credential for later use
413                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        // This is just a wrapper around the actual implementation to allow for ? error handling
461        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            // Get the previously selected cipher and add the new credential to it
477            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            // Store the updated credential for later use
490            this.authenticator
491                .selected_cipher
492                .lock()
493                .expect("Mutex is not poisoned")
494                .replace(selected.clone());
495
496            // Encrypt the updated cipher before sending it to the clients to be stored
497            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        // This is just a wrapper around the actual implementation to allow for ? error handling
542        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            // Get the previously selected cipher and update the credential
553            let selected = this.authenticator.get_selected_credential()?;
554
555            // Check that the provided credential ID matches the selected credential
556            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            // Store the updated credential for later use
570            this.authenticator
571                .selected_cipher
572                .lock()
573                .expect("Mutex is not poisoned")
574                .replace(selected.clone());
575
576            // Encrypt the updated cipher before sending it to the clients to be stored
577            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}