Skip to main content

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("Missing attested_credential_data")]
46    MissingAttestedCredentialData,
47    #[error("make_credential error: {0}")]
48    Other(String),
49}
50
51#[allow(missing_docs)]
52#[derive(Debug, Error)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
54pub enum GetAssertionError {
55    #[error(transparent)]
56    UnknownEnum(#[from] UnknownEnumError),
57    #[error(transparent)]
58    GetSelectedCredential(#[from] GetSelectedCredentialError),
59    #[error(transparent)]
60    InvalidGuid(#[from] InvalidGuidError),
61    #[error("missing user")]
62    MissingUser,
63    #[error("get_assertion error: {0}")]
64    Other(String),
65}
66
67#[allow(missing_docs)]
68#[derive(Debug, Error)]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
70pub enum SilentlyDiscoverCredentialsError {
71    #[error(transparent)]
72    Cipher(#[from] CipherError),
73    #[error(transparent)]
74    InvalidGuid(#[from] InvalidGuidError),
75    #[error(transparent)]
76    Fido2Callback(#[from] Fido2CallbackError),
77    #[error(transparent)]
78    FromCipherView(#[from] Fido2CredentialAutofillViewError),
79}
80
81#[allow(missing_docs)]
82#[derive(Debug, Error)]
83#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
84pub enum CredentialsForAutofillError {
85    #[error(transparent)]
86    Cipher(#[from] CipherError),
87    #[error(transparent)]
88    InvalidGuid(#[from] InvalidGuidError),
89    #[error(transparent)]
90    Fido2Callback(#[from] Fido2CallbackError),
91    #[error(transparent)]
92    FromCipherView(#[from] Fido2CredentialAutofillViewError),
93}
94
95#[allow(missing_docs)]
96pub struct Fido2Authenticator<'a> {
97    pub client: &'a Client,
98    pub user_interface: &'a dyn Fido2UserInterface,
99    pub credential_store: &'a dyn Fido2CredentialStore,
100
101    pub(crate) selected_cipher: Mutex<Option<CipherView>>,
102    pub(crate) requested_uv: Mutex<Option<UV>>,
103}
104
105impl<'a> Fido2Authenticator<'a> {
106    #[allow(missing_docs)]
107    pub fn new(
108        client: &'a Client,
109        user_interface: &'a dyn Fido2UserInterface,
110        credential_store: &'a dyn Fido2CredentialStore,
111    ) -> Fido2Authenticator<'a> {
112        Fido2Authenticator {
113            client,
114            user_interface,
115            credential_store,
116            selected_cipher: Mutex::new(None),
117            requested_uv: Mutex::new(None),
118        }
119    }
120
121    #[allow(missing_docs)]
122    pub async fn make_credential(
123        &mut self,
124        request: MakeCredentialRequest,
125    ) -> Result<MakeCredentialResult, MakeCredentialError> {
126        // Insert the received UV to be able to return it later in check_user
127        self.requested_uv
128            .get_mut()
129            .expect("Mutex is not poisoned")
130            .replace(request.options.uv);
131
132        let mut authenticator = self.get_authenticator(true);
133
134        let response = authenticator
135            .make_credential(ctap2::make_credential::Request {
136                client_data_hash: request.client_data_hash.into(),
137                rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity {
138                    id: request.rp.id,
139                    name: request.rp.name,
140                },
141                user: passkey::types::webauthn::PublicKeyCredentialUserEntity {
142                    id: request.user.id.into(),
143                    display_name: request.user.display_name,
144                    name: request.user.name,
145                },
146                pub_key_cred_params: request
147                    .pub_key_cred_params
148                    .into_iter()
149                    .map(TryInto::try_into)
150                    .collect::<Result<_, _>>()?,
151                exclude_list: request
152                    .exclude_list
153                    .map(|x| x.into_iter().map(TryInto::try_into).collect())
154                    .transpose()?,
155                // TODO(PM-30510): Even though we forward the extensions to the
156                // authenticator, they will not be processed until they are
157                // enabled in the authenticator configuration.
158                extensions: request
159                    .extensions
160                    .map(passkey::types::ctap2::make_credential::ExtensionInputs::from),
161                options: passkey::types::ctap2::make_credential::Options {
162                    rk: request.options.rk,
163                    up: true,
164                    uv: self.convert_requested_uv(request.options.uv),
165                },
166                pin_auth: None,
167                pin_protocol: None,
168            })
169            .await;
170
171        let response = match response {
172            Ok(x) => x,
173            Err(e) => return Err(MakeCredentialError::Other(format!("{e:?}"))),
174        };
175
176        let attestation_object = response.as_webauthn_bytes().to_vec();
177        let authenticator_data = response.auth_data.to_vec();
178        let attested_credential_data = response
179            .auth_data
180            .attested_credential_data
181            .ok_or(MakeCredentialError::MissingAttestedCredentialData)?;
182        let credential_id = attested_credential_data.credential_id().to_vec();
183        let extensions = response.unsigned_extension_outputs.into();
184
185        Ok(MakeCredentialResult {
186            authenticator_data,
187            attestation_object,
188            credential_id,
189            extensions,
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                // TODO(PM-30510): Even though we forward the extensions to the
219                // authenticator, they will not be processed until they are
220                // enabled in the authenticator configuration.
221                extensions: request
222                    .extensions
223                    .map(passkey::types::ctap2::get_assertion::ExtensionInputs::from),
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),
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        let extensions = response.unsigned_extension_outputs.into();
243
244        Ok(GetAssertionResult {
245            credential_id,
246            authenticator_data,
247            signature: response.signature.into(),
248            user_handle: response
249                .user
250                .ok_or(GetAssertionError::MissingUser)?
251                .id
252                .into(),
253            selected_credential,
254            extensions,
255        })
256    }
257
258    #[allow(missing_docs)]
259    pub async fn silently_discover_credentials(
260        &mut self,
261        rp_id: String,
262        user_handle: Option<Vec<u8>>,
263    ) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
264        let key_store = self.client.internal.get_key_store();
265        let result = self
266            .credential_store
267            .find_credentials(None, rp_id, user_handle)
268            .await?;
269
270        let mut ctx = key_store.context();
271        result
272            .into_iter()
273            .map(
274                |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
275                    Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?)
276                },
277            )
278            .flatten_ok()
279            .collect()
280    }
281
282    /// Returns all Fido2 credentials that can be used for autofill, in a view
283    /// tailored for integration with OS autofill systems.
284    pub async fn credentials_for_autofill(
285        &mut self,
286    ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
287        let all_credentials = self.credential_store.all_credentials().await?;
288
289        all_credentials
290            .into_iter()
291            .map(
292                |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
293                    Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
294                },
295            )
296            .flatten_ok()
297            .collect()
298    }
299
300    pub(super) fn get_authenticator(
301        &self,
302        create_credential: bool,
303    ) -> Authenticator<CredentialStoreImpl<'_>, UserValidationMethodImpl<'_>> {
304        Authenticator::new(
305            AAGUID,
306            CredentialStoreImpl {
307                authenticator: self,
308                create_credential,
309            },
310            UserValidationMethodImpl {
311                authenticator: self,
312            },
313        )
314    }
315
316    fn convert_requested_uv(&self, uv: UV) -> bool {
317        let verification_enabled = self.user_interface.is_verification_enabled();
318        match (uv, verification_enabled) {
319            (UV::Preferred, true) => true,
320            (UV::Preferred, false) => false,
321            (UV::Required, _) => true,
322            (UV::Discouraged, _) => false,
323        }
324    }
325
326    pub(super) fn get_selected_credential(
327        &self,
328    ) -> Result<SelectedCredential, GetSelectedCredentialError> {
329        let key_store = self.client.internal.get_key_store();
330
331        let cipher = self
332            .selected_cipher
333            .lock()
334            .expect("Mutex is not poisoned")
335            .clone()
336            .ok_or(GetSelectedCredentialError::NoSelectedCredential)?;
337
338        let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?;
339
340        let credential = creds
341            .first()
342            .ok_or(GetSelectedCredentialError::NoCredentialFound)?
343            .clone();
344
345        Ok(SelectedCredential { cipher, credential })
346    }
347}
348
349pub(super) struct CredentialStoreImpl<'a> {
350    authenticator: &'a Fido2Authenticator<'a>,
351    create_credential: bool,
352}
353pub(super) struct UserValidationMethodImpl<'a> {
354    authenticator: &'a Fido2Authenticator<'a>,
355}
356
357#[async_trait::async_trait]
358impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> {
359    type PasskeyItem = CipherViewContainer;
360    async fn find_credentials(
361        &self,
362        ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
363        rp_id: &str,
364        user_handle: Option<&[u8]>,
365    ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
366        #[derive(Debug, Error)]
367        enum InnerError {
368            #[error(transparent)]
369            Cipher(#[from] CipherError),
370            #[error(transparent)]
371            Crypto(#[from] CryptoError),
372            #[error(transparent)]
373            Fido2Callback(#[from] Fido2CallbackError),
374        }
375
376        // This is just a wrapper around the actual implementation to allow for ? error handling
377        async fn inner(
378            this: &CredentialStoreImpl<'_>,
379            ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
380            rp_id: &str,
381            user_handle: Option<&[u8]>,
382        ) -> Result<Vec<CipherViewContainer>, InnerError> {
383            let ids: Option<Vec<Vec<u8>>> =
384                ids.map(|ids| ids.iter().map(|id| id.id.clone().into()).collect());
385
386            let ciphers = this
387                .authenticator
388                .credential_store
389                .find_credentials(ids, rp_id.to_string(), user_handle.map(|h| h.to_vec()))
390                .await?;
391
392            // Remove any that don't have Fido2 credentials
393            let creds: Vec<_> = ciphers
394                .into_iter()
395                .filter(|c| {
396                    c.login
397                        .as_ref()
398                        .and_then(|l| l.fido2_credentials.as_ref())
399                        .is_some()
400                })
401                .collect();
402
403            let key_store = this.authenticator.client.internal.get_key_store();
404
405            // When using the credential for authentication we have to ask the user to pick one.
406            if this.create_credential {
407                Ok(creds
408                    .into_iter()
409                    .map(|c| CipherViewContainer::new(c, &mut key_store.context()))
410                    .collect::<Result<_, _>>()?)
411            } else {
412                let picked = this
413                    .authenticator
414                    .user_interface
415                    .pick_credential_for_authentication(creds)
416                    .await?;
417
418                // Store the selected credential for later use
419                this.authenticator
420                    .selected_cipher
421                    .lock()
422                    .expect("Mutex is not poisoned")
423                    .replace(picked.clone());
424
425                Ok(vec![CipherViewContainer::new(
426                    picked,
427                    &mut key_store.context(),
428                )?])
429            }
430        }
431
432        inner(self, ids, rp_id, user_handle).await.map_err(|error| {
433            error!(%error, "Error finding credentials.");
434            VendorError::try_from(0xF0)
435                .expect("Valid vendor error code")
436                .into()
437        })
438    }
439
440    async fn save_credential(
441        &mut self,
442        cred: Passkey,
443        user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
444        rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
445        options: passkey::types::ctap2::get_assertion::Options,
446    ) -> Result<(), StatusCode> {
447        #[derive(Debug, Error)]
448        enum InnerError {
449            #[error("Client User Id has not been set")]
450            MissingUserId,
451            #[error(transparent)]
452            FillCredential(#[from] FillCredentialError),
453            #[error(transparent)]
454            Cipher(#[from] CipherError),
455            #[error(transparent)]
456            Crypto(#[from] CryptoError),
457            #[error(transparent)]
458            Fido2Callback(#[from] Fido2CallbackError),
459
460            #[error("No selected credential available")]
461            NoSelectedCredential,
462        }
463
464        // This is just a wrapper around the actual implementation to allow for ? error handling
465        async fn inner(
466            this: &mut CredentialStoreImpl<'_>,
467            cred: Passkey,
468            user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
469            rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
470            options: passkey::types::ctap2::get_assertion::Options,
471        ) -> Result<(), InnerError> {
472            let user_id = this
473                .authenticator
474                .client
475                .internal
476                .get_user_id()
477                .ok_or(InnerError::MissingUserId)?;
478            let cred = try_from_credential_full(cred, user, rp, options)?;
479
480            // Get the previously selected cipher and add the new credential to it
481            let mut selected: CipherView = this
482                .authenticator
483                .selected_cipher
484                .lock()
485                .expect("Mutex is not poisoned")
486                .clone()
487                .ok_or(InnerError::NoSelectedCredential)?;
488
489            let key_store = this.authenticator.client.internal.get_key_store();
490
491            selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
492
493            // Store the updated credential for later use
494            this.authenticator
495                .selected_cipher
496                .lock()
497                .expect("Mutex is not poisoned")
498                .replace(selected.clone());
499
500            // Encrypt the updated cipher before sending it to the clients to be stored
501            let encrypted = key_store.encrypt(selected)?;
502
503            this.authenticator
504                .credential_store
505                .save_credential(EncryptionContext {
506                    cipher: encrypted,
507                    encrypted_for: user_id,
508                })
509                .await?;
510
511            Ok(())
512        }
513
514        inner(self, cred, user, rp, options).await.map_err(|error| {
515            error!(%error, "Error saving credential.");
516            VendorError::try_from(0xF1)
517                .expect("Valid vendor error code")
518                .into()
519        })
520    }
521
522    async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
523        #[derive(Debug, Error)]
524        enum InnerError {
525            #[error("Client User Id has not been set")]
526            MissingUserId,
527            #[error(transparent)]
528            InvalidGuid(#[from] InvalidGuidError),
529            #[error("Credential ID does not match selected credential")]
530            CredentialIdMismatch,
531            #[error(transparent)]
532            FillCredential(#[from] FillCredentialError),
533            #[error(transparent)]
534            Cipher(#[from] CipherError),
535            #[error(transparent)]
536            Crypto(#[from] CryptoError),
537            #[error(transparent)]
538            Fido2Callback(#[from] Fido2CallbackError),
539            #[error(transparent)]
540            GetSelectedCredential(#[from] GetSelectedCredentialError),
541        }
542
543        // This is just a wrapper around the actual implementation to allow for ? error handling
544        async fn inner(
545            this: &mut CredentialStoreImpl<'_>,
546            cred: Passkey,
547        ) -> Result<(), InnerError> {
548            let user_id = this
549                .authenticator
550                .client
551                .internal
552                .get_user_id()
553                .ok_or(InnerError::MissingUserId)?;
554            // Get the previously selected cipher and update the credential
555            let selected = this.authenticator.get_selected_credential()?;
556
557            // Check that the provided credential ID matches the selected credential
558            let new_id: &Vec<u8> = &cred.credential_id;
559            let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
560            if new_id != &selected_id {
561                return Err(InnerError::CredentialIdMismatch);
562            }
563
564            let cred = fill_with_credential(&selected.credential, cred)?;
565
566            let key_store = this.authenticator.client.internal.get_key_store();
567
568            let mut selected = selected.cipher;
569            selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
570
571            // Store the updated credential for later use
572            this.authenticator
573                .selected_cipher
574                .lock()
575                .expect("Mutex is not poisoned")
576                .replace(selected.clone());
577
578            // Encrypt the updated cipher before sending it to the clients to be stored
579            let encrypted = key_store.encrypt(selected)?;
580
581            this.authenticator
582                .credential_store
583                .save_credential(EncryptionContext {
584                    cipher: encrypted,
585                    encrypted_for: user_id,
586                })
587                .await?;
588
589            Ok(())
590        }
591
592        inner(self, cred).await.map_err(|error| {
593            error!(%error, "Error updating credential.");
594            VendorError::try_from(0xF2)
595                .expect("Valid vendor error code")
596                .into()
597        })
598    }
599
600    async fn get_info(&self) -> StoreInfo {
601        StoreInfo {
602            discoverability: DiscoverabilitySupport::Full,
603        }
604    }
605}
606
607#[async_trait::async_trait]
608impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
609    type PasskeyItem = CipherViewContainer;
610
611    async fn check_user<'a>(
612        &self,
613        hint: UiHint<'a, Self::PasskeyItem>,
614        presence: bool,
615        _verification: bool,
616    ) -> Result<UserCheck, Ctap2Error> {
617        let verification = self
618            .authenticator
619            .requested_uv
620            .lock()
621            .expect("Mutex is not poisoned")
622            .ok_or(Ctap2Error::UserVerificationInvalid)?;
623
624        let options = CheckUserOptions {
625            require_presence: presence,
626            require_verification: verification.into(),
627        };
628
629        let result = match hint {
630            UiHint::RequestNewCredential(user, rp) => {
631                let new_credential = try_from_credential_new_view(user, rp)
632                    .map_err(|_| Ctap2Error::InvalidCredential)?;
633
634                let (cipher_view, user_check) = self
635                    .authenticator
636                    .user_interface
637                    .check_user_and_pick_credential_for_creation(options, new_credential)
638                    .await
639                    .map_err(|_| Ctap2Error::OperationDenied)?;
640
641                self.authenticator
642                    .selected_cipher
643                    .lock()
644                    .expect("Mutex is not poisoned")
645                    .replace(cipher_view);
646
647                Ok(user_check)
648            }
649            _ => {
650                self.authenticator
651                    .user_interface
652                    .check_user(options, map_ui_hint(hint))
653                    .await
654            }
655        };
656
657        let result = result.map_err(|error| {
658            error!(%error, "Error checking user.");
659            Ctap2Error::UserVerificationInvalid
660        })?;
661
662        Ok(UserCheck {
663            presence: result.user_present,
664            verification: result.user_verified,
665        })
666    }
667
668    fn is_presence_enabled(&self) -> bool {
669        true
670    }
671
672    fn is_verification_enabled(&self) -> Option<bool> {
673        Some(self.authenticator.user_interface.is_verification_enabled())
674    }
675}
676
677fn map_ui_hint(hint: UiHint<'_, CipherViewContainer>) -> UiHint<'_, CipherView> {
678    use UiHint::*;
679    match hint {
680        InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
681        InformNoCredentialsFound => InformNoCredentialsFound,
682        RequestNewCredential(u, r) => RequestNewCredential(u, r),
683        RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use async_trait::async_trait;
690    use bitwarden_core::{
691        Client,
692        key_management::{KeyIds, SymmetricKeyId},
693    };
694    use bitwarden_crypto::{KeyStoreContext, PrimitiveEncryptable, SymmetricCryptoKey};
695    use bitwarden_encoding::B64Url;
696    use bitwarden_vault::{
697        CipherListView, CipherRepromptType, CipherType, CipherView, EncryptionContext,
698        Fido2Credential, Fido2CredentialNewView, LoginView,
699    };
700    use passkey::authenticator::UiHint;
701
702    use super::Fido2Authenticator;
703    use crate::{
704        CheckUserOptions, CheckUserResult, Fido2CallbackError, Fido2CredentialStore,
705        Fido2UserInterface, GetAssertionExtensionsInput, GetAssertionPrfInput, PrfInputValues,
706        guid_bytes_to_string,
707        types::{GetAssertionRequest, Options, UV},
708    };
709
710    struct MockUserInterface;
711
712    #[async_trait]
713    impl Fido2UserInterface for MockUserInterface {
714        async fn check_user<'a>(
715            &self,
716            _options: CheckUserOptions,
717            _hint: UiHint<'a, CipherView>,
718        ) -> Result<CheckUserResult, Fido2CallbackError> {
719            Ok(CheckUserResult {
720                user_present: true,
721                user_verified: true,
722            })
723        }
724
725        async fn pick_credential_for_authentication(
726            &self,
727            available_credentials: Vec<CipherView>,
728        ) -> Result<CipherView, Fido2CallbackError> {
729            available_credentials
730                .into_iter()
731                .next()
732                .ok_or(Fido2CallbackError::Unknown("no credentials".to_string()))
733        }
734
735        async fn check_user_and_pick_credential_for_creation(
736            &self,
737            _options: CheckUserOptions,
738            _new_credential: Fido2CredentialNewView,
739        ) -> Result<(CipherView, CheckUserResult), Fido2CallbackError> {
740            unimplemented!("not needed for this test")
741        }
742
743        fn is_verification_enabled(&self) -> bool {
744            true
745        }
746    }
747
748    struct MockCredentialStore {
749        cipher: CipherView,
750    }
751
752    #[async_trait]
753    impl Fido2CredentialStore for MockCredentialStore {
754        async fn find_credentials(
755            &self,
756            _ids: Option<Vec<Vec<u8>>>,
757            _rp_id: String,
758            _user_handle: Option<Vec<u8>>,
759        ) -> Result<Vec<CipherView>, Fido2CallbackError> {
760            Ok(vec![self.cipher.clone()])
761        }
762
763        async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError> {
764            Ok(vec![])
765        }
766
767        async fn save_credential(
768            &self,
769            _cred: EncryptionContext,
770        ) -> Result<(), Fido2CallbackError> {
771            Ok(())
772        }
773    }
774
775    static TEST_FIDO_CREDENTIAL_ID: &str = "a36f3d35-5dae-4d07-8b24-f89e11082090";
776    static TEST_FIDO_RP_ID: &str = "example.com";
777    static TEST_FIDO_USER_HANDLE: &str = "YWJjZA";
778    // Hardcoded P-256 private key in PKCS8 DER format for testing
779    static TEST_FIDO_P256_KEY: &[u8] = &[
780        0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d,
781        0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x6d, 0x30,
782        0x6b, 0x02, 0x01, 0x01, 0x04, 0x20, 0x06, 0x76, 0x5e, 0x85, 0xe0, 0x7f, 0xef, 0x43, 0xaa,
783        0x17, 0xe0, 0x7a, 0xd7, 0x85, 0x63, 0x01, 0x80, 0x70, 0x8c, 0x6c, 0x61, 0x43, 0x7d, 0xc3,
784        0xb1, 0xe6, 0xf9, 0x09, 0x24, 0xeb, 0x1f, 0xf5, 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0x35,
785        0x9a, 0x52, 0xf3, 0x82, 0x44, 0x66, 0x5f, 0x3f, 0xe2, 0xc4, 0x0b, 0x1c, 0x16, 0x34, 0xc5,
786        0x60, 0x07, 0x3a, 0x25, 0xfe, 0x7e, 0x7f, 0x7f, 0xda, 0xd4, 0x1c, 0x36, 0x90, 0x00, 0xee,
787        0xb1, 0x8e, 0x92, 0xb3, 0xac, 0x91, 0x7f, 0xb1, 0x8c, 0xa4, 0x85, 0xe7, 0x03, 0x07, 0xd1,
788        0xf5, 0x5b, 0xd3, 0x7b, 0xc3, 0x56, 0x11, 0xdf, 0xbc, 0x7a, 0x97, 0x70, 0x32, 0x4b, 0x3c,
789        0x84, 0x05, 0x71,
790    ];
791
792    fn create_test_cipher(ctx: &mut KeyStoreContext<KeyIds>) -> CipherView {
793        let key = SymmetricKeyId::User;
794        let key_value = B64Url::from(TEST_FIDO_P256_KEY).to_string();
795
796        let fido2_credential = Fido2Credential {
797            credential_id: TEST_FIDO_CREDENTIAL_ID.encrypt(ctx, key).unwrap(),
798            key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
799            key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
800            key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
801            key_value: key_value.encrypt(ctx, key).unwrap(),
802            rp_id: TEST_FIDO_RP_ID.encrypt(ctx, key).unwrap(),
803            user_handle: Some(TEST_FIDO_USER_HANDLE.encrypt(ctx, key).unwrap()),
804            user_name: None,
805            counter: "0".to_string().encrypt(ctx, key).unwrap(),
806            rp_name: None,
807            user_display_name: None,
808            discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
809            creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
810        };
811
812        CipherView {
813            id: Some("c2c7e624-dcfd-4f23-af41-b177014ffcb5".parse().unwrap()),
814            organization_id: None,
815            folder_id: None,
816            collection_ids: vec![],
817            key: None,
818            name: "Test Login".to_string(),
819            notes: None,
820            r#type: CipherType::Login,
821            login: Some(LoginView {
822                username: None,
823                password: None,
824                password_revision_date: None,
825                uris: None,
826                totp: None,
827                autofill_on_page_load: None,
828                fido2_credentials: Some(vec![fido2_credential]),
829            }),
830            identity: None,
831            card: None,
832            secure_note: None,
833            ssh_key: None,
834            favorite: false,
835            reprompt: CipherRepromptType::None,
836            organization_use_totp: false,
837            edit: true,
838            permissions: None,
839            view_password: true,
840            local_data: None,
841            attachments: None,
842            attachment_decryption_failures: None,
843            fields: None,
844            password_history: None,
845            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
846            deleted_date: None,
847            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
848            archived_date: None,
849        }
850    }
851
852    /// TODO(PM-30510): Even though we forward the extensions to the
853    /// authenticator, we have disabled the configuration.
854    /// When we implement PRF, this test should be updated to test that PRF _is_
855    /// evaluated when PRF extension input is received.
856    #[tokio::test]
857    async fn test_prf_is_not_evaluated() {
858        let client = Client::new(None);
859        let user_key: SymmetricCryptoKey =
860            "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q=="
861                .to_string()
862                .try_into()
863                .unwrap();
864
865        #[allow(deprecated)]
866        client
867            .internal
868            .get_key_store()
869            .context_mut()
870            .set_symmetric_key(SymmetricKeyId::User, user_key)
871            .unwrap();
872
873        let cipher = {
874            let mut ctx = client.internal.get_key_store().context();
875            create_test_cipher(&mut ctx)
876        };
877
878        let user_interface = MockUserInterface;
879        let credential_store = MockCredentialStore { cipher };
880        let mut authenticator =
881            Fido2Authenticator::new(&client, &user_interface, &credential_store);
882
883        let request = GetAssertionRequest {
884            rp_id: "example.com".to_string(),
885            client_data_hash: vec![0u8; 32],
886            allow_list: None,
887            options: Options {
888                rk: false,
889                uv: UV::Preferred,
890            },
891            extensions: Some(GetAssertionExtensionsInput {
892                prf: Some(GetAssertionPrfInput {
893                    eval: Some(PrfInputValues {
894                        first: vec![1u8; 32],
895                        second: None,
896                    }),
897                    eval_by_credential: None,
898                }),
899            }),
900        };
901
902        let result = authenticator.get_assertion(request).await.unwrap();
903        assert_eq!(
904            TEST_FIDO_CREDENTIAL_ID,
905            guid_bytes_to_string(&result.credential_id).unwrap()
906        );
907        assert!(
908            result.extensions.prf.is_none(),
909            "PRF should not be evaluated"
910        );
911    }
912}