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