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