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        Passkey,
12        ctap2::{self, Ctap2Error, StatusCode, VendorError},
13    },
14};
15use thiserror::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).await,
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_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).await,
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    ) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
259        let key_store = self.client.internal.get_key_store();
260        let result = self.credential_store.find_credentials(None, rp_id).await?;
261
262        let mut ctx = key_store.context();
263        result
264            .into_iter()
265            .map(
266                |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
267                    Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?)
268                },
269            )
270            .flatten_ok()
271            .collect()
272    }
273
274    /// Returns all Fido2 credentials that can be used for autofill, in a view
275    /// tailored for integration with OS autofill systems.
276    pub async fn credentials_for_autofill(
277        &mut self,
278    ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
279        let all_credentials = self.credential_store.all_credentials().await?;
280
281        all_credentials
282            .into_iter()
283            .map(
284                |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
285                    Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
286                },
287            )
288            .flatten_ok()
289            .collect()
290    }
291
292    pub(super) fn get_authenticator(
293        &self,
294        create_credential: bool,
295    ) -> Authenticator<CredentialStoreImpl<'_>, UserValidationMethodImpl<'_>> {
296        Authenticator::new(
297            AAGUID,
298            CredentialStoreImpl {
299                authenticator: self,
300                create_credential,
301            },
302            UserValidationMethodImpl {
303                authenticator: self,
304            },
305        )
306    }
307
308    async fn convert_requested_uv(&self, uv: UV) -> bool {
309        let verification_enabled = self.user_interface.is_verification_enabled().await;
310        match (uv, verification_enabled) {
311            (UV::Preferred, true) => true,
312            (UV::Preferred, false) => false,
313            (UV::Required, _) => true,
314            (UV::Discouraged, _) => false,
315        }
316    }
317
318    pub(super) fn get_selected_credential(
319        &self,
320    ) -> Result<SelectedCredential, GetSelectedCredentialError> {
321        let key_store = self.client.internal.get_key_store();
322
323        let cipher = self
324            .selected_cipher
325            .lock()
326            .expect("Mutex is not poisoned")
327            .clone()
328            .ok_or(GetSelectedCredentialError::NoSelectedCredential)?;
329
330        let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?;
331
332        let credential = creds
333            .first()
334            .ok_or(GetSelectedCredentialError::NoCredentialFound)?
335            .clone();
336
337        Ok(SelectedCredential { cipher, credential })
338    }
339}
340
341pub(super) struct CredentialStoreImpl<'a> {
342    authenticator: &'a Fido2Authenticator<'a>,
343    create_credential: bool,
344}
345pub(super) struct UserValidationMethodImpl<'a> {
346    authenticator: &'a Fido2Authenticator<'a>,
347}
348
349#[async_trait::async_trait]
350impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> {
351    type PasskeyItem = CipherViewContainer;
352    async fn find_credentials(
353        &self,
354        ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
355        rp_id: &str,
356    ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
357        #[derive(Debug, Error)]
358        enum InnerError {
359            #[error(transparent)]
360            Cipher(#[from] CipherError),
361            #[error(transparent)]
362            Crypto(#[from] CryptoError),
363            #[error(transparent)]
364            Fido2Callback(#[from] Fido2CallbackError),
365        }
366
367        // This is just a wrapper around the actual implementation to allow for ? error handling
368        async fn inner(
369            this: &CredentialStoreImpl<'_>,
370            ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
371            rp_id: &str,
372        ) -> Result<Vec<CipherViewContainer>, InnerError> {
373            let ids: Option<Vec<Vec<u8>>> =
374                ids.map(|ids| ids.iter().map(|id| id.id.clone().into()).collect());
375
376            let ciphers = this
377                .authenticator
378                .credential_store
379                .find_credentials(ids, rp_id.to_string())
380                .await?;
381
382            // Remove any that don't have Fido2 credentials
383            let creds: Vec<_> = ciphers
384                .into_iter()
385                .filter(|c| {
386                    c.login
387                        .as_ref()
388                        .and_then(|l| l.fido2_credentials.as_ref())
389                        .is_some()
390                })
391                .collect();
392
393            let key_store = this.authenticator.client.internal.get_key_store();
394
395            // When using the credential for authentication we have to ask the user to pick one.
396            if this.create_credential {
397                Ok(creds
398                    .into_iter()
399                    .map(|c| CipherViewContainer::new(c, &mut key_store.context()))
400                    .collect::<Result<_, _>>()?)
401            } else {
402                let picked = this
403                    .authenticator
404                    .user_interface
405                    .pick_credential_for_authentication(creds)
406                    .await?;
407
408                // Store the selected credential for later use
409                this.authenticator
410                    .selected_cipher
411                    .lock()
412                    .expect("Mutex is not poisoned")
413                    .replace(picked.clone());
414
415                Ok(vec![CipherViewContainer::new(
416                    picked,
417                    &mut key_store.context(),
418                )?])
419            }
420        }
421
422        inner(self, ids, rp_id).await.map_err(|e| {
423            error!("Error finding credentials: {e:?}");
424            VendorError::try_from(0xF0)
425                .expect("Valid vendor error code")
426                .into()
427        })
428    }
429
430    async fn save_credential(
431        &mut self,
432        cred: Passkey,
433        user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
434        rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
435        options: passkey::types::ctap2::get_assertion::Options,
436    ) -> Result<(), StatusCode> {
437        #[derive(Debug, Error)]
438        enum InnerError {
439            #[error("Client User Id has not been set")]
440            MissingUserId,
441            #[error(transparent)]
442            FillCredential(#[from] FillCredentialError),
443            #[error(transparent)]
444            Cipher(#[from] CipherError),
445            #[error(transparent)]
446            Crypto(#[from] CryptoError),
447            #[error(transparent)]
448            Fido2Callback(#[from] Fido2CallbackError),
449
450            #[error("No selected credential available")]
451            NoSelectedCredential,
452        }
453
454        // This is just a wrapper around the actual implementation to allow for ? error handling
455        async fn inner(
456            this: &mut CredentialStoreImpl<'_>,
457            cred: Passkey,
458            user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
459            rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
460            options: passkey::types::ctap2::get_assertion::Options,
461        ) -> Result<(), InnerError> {
462            let user_id = this
463                .authenticator
464                .client
465                .internal
466                .get_user_id()
467                .ok_or(InnerError::MissingUserId)?;
468            let cred = try_from_credential_full(cred, user, rp, options)?;
469
470            // Get the previously selected cipher and add the new credential to it
471            let mut selected: CipherView = this
472                .authenticator
473                .selected_cipher
474                .lock()
475                .expect("Mutex is not poisoned")
476                .clone()
477                .ok_or(InnerError::NoSelectedCredential)?;
478
479            let key_store = this.authenticator.client.internal.get_key_store();
480
481            selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
482
483            // Store the updated credential for later use
484            this.authenticator
485                .selected_cipher
486                .lock()
487                .expect("Mutex is not poisoned")
488                .replace(selected.clone());
489
490            // Encrypt the updated cipher before sending it to the clients to be stored
491            let encrypted = key_store.encrypt(selected)?;
492
493            this.authenticator
494                .credential_store
495                .save_credential(EncryptionContext {
496                    cipher: encrypted,
497                    encrypted_for: user_id,
498                })
499                .await?;
500
501            Ok(())
502        }
503
504        inner(self, cred, user, rp, options).await.map_err(|e| {
505            error!("Error saving credential: {e:?}");
506            VendorError::try_from(0xF1)
507                .expect("Valid vendor error code")
508                .into()
509        })
510    }
511
512    async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
513        #[derive(Debug, Error)]
514        enum InnerError {
515            #[error("Client User Id has not been set")]
516            MissingUserId,
517            #[error(transparent)]
518            InvalidGuid(#[from] InvalidGuidError),
519            #[error("Credential ID does not match selected credential")]
520            CredentialIdMismatch,
521            #[error(transparent)]
522            FillCredential(#[from] FillCredentialError),
523            #[error(transparent)]
524            Cipher(#[from] CipherError),
525            #[error(transparent)]
526            Crypto(#[from] CryptoError),
527            #[error(transparent)]
528            Fido2Callback(#[from] Fido2CallbackError),
529            #[error(transparent)]
530            GetSelectedCredential(#[from] GetSelectedCredentialError),
531        }
532
533        // This is just a wrapper around the actual implementation to allow for ? error handling
534        async fn inner(
535            this: &mut CredentialStoreImpl<'_>,
536            cred: Passkey,
537        ) -> Result<(), InnerError> {
538            let user_id = this
539                .authenticator
540                .client
541                .internal
542                .get_user_id()
543                .ok_or(InnerError::MissingUserId)?;
544            // Get the previously selected cipher and update the credential
545            let selected = this.authenticator.get_selected_credential()?;
546
547            // Check that the provided credential ID matches the selected credential
548            let new_id: &Vec<u8> = &cred.credential_id;
549            let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
550            if new_id != &selected_id {
551                return Err(InnerError::CredentialIdMismatch);
552            }
553
554            let cred = fill_with_credential(&selected.credential, cred)?;
555
556            let key_store = this.authenticator.client.internal.get_key_store();
557
558            let mut selected = selected.cipher;
559            selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
560
561            // Store the updated credential for later use
562            this.authenticator
563                .selected_cipher
564                .lock()
565                .expect("Mutex is not poisoned")
566                .replace(selected.clone());
567
568            // Encrypt the updated cipher before sending it to the clients to be stored
569            let encrypted = key_store.encrypt(selected)?;
570
571            this.authenticator
572                .credential_store
573                .save_credential(EncryptionContext {
574                    cipher: encrypted,
575                    encrypted_for: user_id,
576                })
577                .await?;
578
579            Ok(())
580        }
581
582        inner(self, cred).await.map_err(|e| {
583            error!("Error updating credential: {e:?}");
584            VendorError::try_from(0xF2)
585                .expect("Valid vendor error code")
586                .into()
587        })
588    }
589
590    async fn get_info(&self) -> StoreInfo {
591        StoreInfo {
592            discoverability: DiscoverabilitySupport::Full,
593        }
594    }
595}
596
597#[async_trait::async_trait]
598impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
599    type PasskeyItem = CipherViewContainer;
600
601    async fn check_user<'a>(
602        &self,
603        hint: UIHint<'a, Self::PasskeyItem>,
604        presence: bool,
605        _verification: bool,
606    ) -> Result<UserCheck, Ctap2Error> {
607        let verification = self
608            .authenticator
609            .requested_uv
610            .lock()
611            .expect("Mutex is not poisoned")
612            .ok_or(Ctap2Error::UserVerificationInvalid)?;
613
614        let options = CheckUserOptions {
615            require_presence: presence,
616            require_verification: verification.into(),
617        };
618
619        let result = match hint {
620            UIHint::RequestNewCredential(user, rp) => {
621                let new_credential = try_from_credential_new_view(user, rp)
622                    .map_err(|_| Ctap2Error::InvalidCredential)?;
623
624                let (cipher_view, user_check) = self
625                    .authenticator
626                    .user_interface
627                    .check_user_and_pick_credential_for_creation(options, new_credential)
628                    .await
629                    .map_err(|_| Ctap2Error::OperationDenied)?;
630
631                self.authenticator
632                    .selected_cipher
633                    .lock()
634                    .expect("Mutex is not poisoned")
635                    .replace(cipher_view);
636
637                Ok(user_check)
638            }
639            _ => {
640                self.authenticator
641                    .user_interface
642                    .check_user(options, map_ui_hint(hint))
643                    .await
644            }
645        };
646
647        let result = result.map_err(|e| {
648            error!("Error checking user: {e:?}");
649            Ctap2Error::UserVerificationInvalid
650        })?;
651
652        Ok(UserCheck {
653            presence: result.user_present,
654            verification: result.user_verified,
655        })
656    }
657
658    async fn is_presence_enabled(&self) -> bool {
659        true
660    }
661
662    async fn is_verification_enabled(&self) -> Option<bool> {
663        Some(
664            self.authenticator
665                .user_interface
666                .is_verification_enabled()
667                .await,
668        )
669    }
670}
671
672fn map_ui_hint(hint: UIHint<'_, CipherViewContainer>) -> UIHint<'_, CipherView> {
673    use UIHint::*;
674    match hint {
675        InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
676        InformNoCredentialsFound => InformNoCredentialsFound,
677        RequestNewCredential(u, r) => RequestNewCredential(u, r),
678        RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
679    }
680}