Skip to main content

bitwarden_fido/
device_auth_key.rs

1use bitwarden_api_api::models::{
2    AttestationResponse, AuthenticationExtensionsClientOutputs,
3    AuthenticatorAttestationRawResponse, AuthenticatorTransport, CredentialCreateOptions,
4    PublicKeyCredentialType, SecretVerificationRequestModel, UserVerificationRequirement,
5    WebAuthnCredentialCreateOptionsResponseModel, WebAuthnLoginCredentialCreateRequestModel,
6};
7use bitwarden_core::{
8    Client, key_management::SymmetricKeySlotId, mobile::KdfClient,
9    platform::SecretVerificationRequest,
10};
11use bitwarden_crypto::{HashPurpose, Kdf, RotateableKeySet};
12use chrono::{DateTime, Utc};
13use coset::{CborSerializable, CoseKey};
14use passkey::{
15    authenticator::{
16        DiscoverabilitySupport, StoreInfo, UiHint, UserCheck, extensions::HmacSecretConfig,
17    },
18    types::{
19        CredentialExtensions, Passkey, StoredHmacSecret,
20        crypto::sha256,
21        ctap2::{
22            self, Ctap2Code, Ctap2Error, StatusCode, VendorError,
23            extensions::{AuthenticatorPrfInputs, AuthenticatorPrfValues},
24            make_credential::Options,
25        },
26    },
27};
28use reqwest::Url;
29
30use crate::{
31    GetAssertionRequest, MakeCredentialResult, PublicKeyCredentialRpEntity,
32    PublicKeyCredentialUserEntity,
33    types::{
34        GetAssertionExtensionsOutput, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
35        UV, WebAuthnEntityError,
36    },
37};
38
39/// A FIDO authenticator that uses the device auth key for its key material.
40pub struct DeviceAuthKeyAuthenticator<'a> {
41    /// The SDK client.
42    pub client: &'a Client,
43
44    /// Callbacks for storing and retrieving the device auth key during FIDO operations.
45    pub store: &'a mut dyn DeviceAuthKeyStore,
46}
47
48impl DeviceAuthKeyAuthenticator<'_> {
49    /// Create a device auth key by registering an unlock passkey and PRF keyset with the server.
50    /// The passkey private key and metadata will be stored on the device using the provided trait
51    /// implementation.
52    pub async fn create_device_auth_key(
53        &mut self,
54        client_name: String,
55        web_vault_url: String,
56        // TODO(PM-22681): We should define an enum to accept all the different
57        // SecretVerificationRequest input methods that the server can accept,
58        // and have a centralized place where the secret verification can be
59        // derived from the input.
60        //
61        // For now, we are hard-coding master password hash and OTP.
62        // When PM-22681 is complete, we can use that implementation here as a
63        // breaking change.
64        email: String,
65        secret_verification_request: SecretVerificationRequest,
66        kdf_params: Kdf,
67    ) -> Result<(), DeviceAuthKeyError> {
68        // Derive secret verification request
69        let config = self.client.internal.get_api_configurations();
70        let api_client = &config.api_client;
71
72        // Request WebAuthn credential creation options
73        let secret_verification_request_model = build_secret_verification_request(
74            &secret_verification_request,
75            email,
76            kdf_params,
77            &self.client.kdf(),
78        )
79        .await?;
80        let options_response = api_client
81            .web_authn_api()
82            .attestation_options(Some(secret_verification_request_model))
83            .await
84            .map_err(|err| {
85                tracing::error!(%err, "Failed to retrieve attestation options");
86                DeviceAuthKeyError::RetrieveRegistrationOptionsFailure
87            })?;
88        let WebAuthnCredentialCreateOptionsResponseModel { options, token, .. } = options_response;
89
90        // Convert creation options
91        let (default_rp_id, origin) = {
92            let url =
93                Url::parse(&web_vault_url).map_err(|_| DeviceAuthKeyError::InvalidWebVaultUrl)?;
94            let Some(default_rp_id) = url.host().map(|host| host.to_string()) else {
95                return Err(DeviceAuthKeyError::InvalidWebVaultUrl);
96            };
97            let origin = url.origin().ascii_serialization();
98            (default_rp_id, origin)
99        };
100        let (request, client_data_json) = convert_creation_options(options.as_ref(), default_rp_id, origin).map_err(|err| {
101            tracing::error!(%err, ?options, "Received invalid WebAuthn attestation options from server");
102            DeviceAuthKeyError::RetrieveRegistrationOptionsFailure
103        })?;
104
105        // Extract user/RP data from request before make_credential consumes it.
106        let rp_id = request.rp.id.clone();
107        let user_handle = request.user.id.to_vec();
108        let user_name = request.user.name.clone();
109        let user_display_name = request.user.display_name.clone();
110
111        // Create credential with passkey-rs, store record on device with given trait implementation
112        let store = DeviceAuthKeyStoreInternal { store: self.store };
113        let ui = DeviceAuthKeyUiInternal {};
114        let mut authenticator =
115            passkey::authenticator::Authenticator::new(super::AAGUID, store, ui)
116                .hmac_secret(HmacSecretConfig::new_with_uv_only().enable_on_make_credential());
117        let response = authenticator
118            .make_credential(request)
119            .await
120            .map_err(|status_code| {
121                tracing::error!(?status_code, "Failed to make FIDO credential");
122                if let StatusCode::Ctap2(Ctap2Code::Known(Ctap2Error::CredentialExcluded)) =
123                    status_code
124                {
125                    DeviceAuthKeyError::CredentialExcluded
126                } else {
127                    DeviceAuthKeyError::AuthenticatorFailure
128                }
129            })?;
130
131        // Convert response
132        let result: MakeCredentialResult = response
133            .try_into()
134            .map_err(|_| DeviceAuthKeyError::AuthenticatorFailure)?;
135
136        // Make PRF key set
137        let prf_result = result
138            .extensions
139            .prf
140            .and_then(|prf| prf.results)
141            .ok_or_else(|| {
142                tracing::error!("No PRF output received from authenticator response");
143                DeviceAuthKeyError::PrfFailure
144            })?
145            .first;
146        let prf_key =
147            bitwarden_crypto::derive_symmetric_key_from_prf(&prf_result).map_err(|err| {
148                tracing::error!(?err, "Failed to derive symmetric key from PRF output");
149                DeviceAuthKeyError::PrfFailure
150            })?;
151        let key_set = {
152            let ctx = self.client.internal.get_key_store().context();
153            RotateableKeySet::new(&ctx, &prf_key, SymmetricKeySlotId::User).map_err(|err| {
154                tracing::error!(%err, "Failed to gen/Conerate rotateable key set from PRF output");
155                DeviceAuthKeyError::PrfFailure
156            })?
157        };
158
159        // Send registration request to server
160        let credential_id = result.credential_id.clone();
161        let create_request = WebAuthnLoginCredentialCreateRequestModel {
162            device_response: Box::new(AuthenticatorAttestationRawResponse {
163                // The WebAuthn `id` is the base64url encoding of the raw credential ID.
164                id: bitwarden_encoding::B64Url::from(result.credential_id.as_slice()).to_string(),
165                raw_id: result.credential_id,
166                r#type: PublicKeyCredentialType::PublicKey,
167                response: Box::new(AttestationResponse {
168                    attestation_object: Some(result.attestation_object),
169                    client_data_json: Some(client_data_json.into_bytes()),
170                    // The device auth key lives in device-bound storage, so the credential is
171                    // only reachable through the platform authenticator.
172                    transports: vec![AuthenticatorTransport::Internal],
173                }),
174                // Deprecated alias for client_extension_results. Both deserialize into the same
175                // value with no precedence, so only client_extension_results is populated.
176                extensions: None,
177                // The authenticator's PRF output is consumed locally for the rotateable key set,
178                // and PRF support is reported through supports_prf, so the server needs no client
179                // extension outputs here.
180                client_extension_results: Box::new(AuthenticationExtensionsClientOutputs::new()),
181            }),
182            name: client_name,
183            token,
184            supports_prf: true,
185            encrypted_user_key: Some(key_set.encapsulated_downstream_key.to_string()),
186            encrypted_public_key: Some(key_set.encrypted_encapsulation_key.to_string()),
187            encrypted_private_key: Some(key_set.encrypted_decapsulation_key.to_string()),
188        };
189        let server_response = api_client
190            .web_authn_api()
191            .post(Some(create_request))
192            .await
193            .map_err(|err| {
194                tracing::error!(%err, "Failed to submit passkey and PRF key set to server");
195                DeviceAuthKeyError::SubmitRegistrationFailure
196            })?;
197        let record_identifier = server_response
198            .id
199            .ok_or(DeviceAuthKeyError::SubmitRegistrationFailure)?;
200
201        // Save metadata now that we have the server-assigned record identifier
202        let metadata = DeviceAuthKeyMetadata {
203            record_identifier,
204            creation_date: chrono::offset::Utc::now(),
205            credential_id,
206            rp_id,
207            user_handle,
208            user_name,
209            user_display_name,
210        };
211        self.store.create_metadata(metadata).await.map_err(|err| {
212            tracing::error!(%err, "Failed to save device auth key metadata");
213            err
214        })?;
215        Ok(())
216    }
217
218    /// Satisfy the given FIDO assertion `request` using the device auth key.
219    /// The device auth key will be looked up from the
220    /// [DeviceAuthKeyAuthenticator::store] provided in the initializer.
221    pub async fn assert_device_auth_key(
222        &mut self,
223        request: GetAssertionRequest,
224    ) -> Result<DeviceAuthKeyGetAssertionResult, DeviceAuthKeyError> {
225        // Convert request
226        let request = ctap2::get_assertion::Request {
227            rp_id: request.rp_id,
228            client_data_hash: request.client_data_hash.into(),
229            allow_list: request
230                .allow_list
231                .map(|l| {
232                    l.into_iter()
233                        .map(TryInto::try_into)
234                        .collect::<Result<Vec<_>, _>>()
235                        .map_err(|_| DeviceAuthKeyError::InvalidPublicKeyCredentialDescriptor)
236                })
237                .transpose()?,
238            extensions: request
239                .extensions
240                .map(passkey::types::ctap2::get_assertion::ExtensionInputs::from),
241            options: passkey::types::ctap2::make_credential::Options {
242                rk: request.options.rk,
243                up: true,
244                uv: match request.options.uv {
245                    UV::Discouraged => false,
246                    UV::Preferred => true,
247                    UV::Required => true,
248                },
249            },
250            pin_auth: None,
251            pin_protocol: None,
252        };
253
254        // Only use the requested credential ID if exactly one is specified.
255        let requested_cred_id = if let Some([cred]) = request.allow_list.as_deref() {
256            Some(cred.id.to_vec())
257        } else {
258            None
259        };
260
261        // Get signature
262        let store = DeviceAuthKeyStoreInternal { store: self.store };
263        let ui = DeviceAuthKeyUiInternal {};
264        let mut authenticator =
265            passkey::authenticator::Authenticator::new(super::AAGUID, store, ui)
266                .hmac_secret(HmacSecretConfig::new_with_uv_only().enable_on_make_credential());
267        let response = authenticator
268            .get_assertion(request)
269            .await
270            .map_err(|status_code| {
271                tracing::error!(?status_code, "Authenticator failed to assert credential");
272                DeviceAuthKeyError::AuthenticatorFailure
273            })?;
274
275        // Convert response
276        let authenticator_data = response.auth_data.to_vec();
277        // Credential ID may be omitted if there is only one credential ID
278        // specified in the allow list. We currently use device auth keys exclusively as a
279        // discoverable credentials, which means the allow list will always be
280        // empty and the credential ID should always be returned, but if that
281        // changes, we should attempt to read it from the allow list, just in case.
282        let credential_id = response
283            .credential
284            .map(|cred| cred.id.to_vec())
285            .or(requested_cred_id)
286            .ok_or(DeviceAuthKeyError::MissingCredentialId)?;
287        let extensions: GetAssertionExtensionsOutput = response.unsigned_extension_outputs.into();
288        let user_handle = response
289            .user
290            .map(|u| u.id.to_vec())
291            .ok_or(DeviceAuthKeyError::MissingUserHandle)?;
292        Ok(DeviceAuthKeyGetAssertionResult {
293            credential_id,
294            authenticator_data,
295            signature: response.signature.to_vec(),
296            user_handle,
297            extensions,
298        })
299    }
300
301    /// Delete the device auth key from the device and unregister it from the server.
302    pub async fn unregister_device_auth_key(
303        &mut self,
304        email: String,
305        secret_verification_request: SecretVerificationRequest,
306        kdf_params: Kdf,
307    ) -> Result<(), DeviceAuthKeyError> {
308        // Retrieve metadata before we delete it
309        let metadata = self
310            .store
311            .get_metadata()
312            .await?
313            .ok_or(DeviceAuthKeyError::MissingDeviceAuthKey)?;
314
315        self.store.delete_record_and_metadata().await?;
316
317        let record_id = metadata
318            .record_identifier
319            .parse::<uuid::Uuid>()
320            .map_err(|err| {
321                tracing::error!(%err, "Failed to parse record identifier as UUID");
322                DeviceAuthKeyError::InvalidRecordIdentifier
323            })?;
324
325        // Attempt to unregister the device auth key from the server.
326        let config = self.client.internal.get_api_configurations();
327        let api_client = &config.api_client;
328        let secret_verification_request_model = build_secret_verification_request(
329            &secret_verification_request,
330            email,
331            kdf_params,
332            &self.client.kdf(),
333        )
334        .await?;
335        api_client
336            .web_authn_api()
337            .delete(record_id, Some(secret_verification_request_model))
338            .await
339            .map_err(|err| {
340                tracing::error!(%err, "Failed to unregister device auth key from server");
341                DeviceAuthKeyError::UnregisterFailure
342            })?;
343
344        Ok(())
345    }
346}
347
348async fn build_secret_verification_request(
349    input: &SecretVerificationRequest,
350    email: String,
351    kdf_params: Kdf,
352    kdf_client: &KdfClient,
353) -> Result<SecretVerificationRequestModel, DeviceAuthKeyError> {
354    let master_password_hash = if let Some(master_password) = &input.master_password {
355        Some(
356            kdf_client
357                .hash_password(
358                    email,
359                    master_password.to_string(),
360                    kdf_params,
361                    HashPurpose::ServerAuthorization,
362                )
363                .await
364                .map_err(|_| DeviceAuthKeyError::MasterPasswordHash)?
365                .to_string(),
366        )
367    } else {
368        None
369    };
370
371    Ok(SecretVerificationRequestModel {
372        master_password_hash,
373        otp: input.otp.clone(),
374        auth_request_access_code: None,
375        secret: None,
376    })
377}
378
379/// Create a CTAP2 makeCredential request and clientDataJSON from the WebAuthn credential
380/// attestations options received from the server. Generates clientDataJSON from given origin and
381/// challenge, and injects the default RP ID if it's missing.
382fn convert_creation_options(
383    options: &CredentialCreateOptions,
384    default_rp_id: String,
385    origin: String,
386) -> Result<(passkey::types::ctap2::make_credential::Request, String), WebAuthnEntityError> {
387    let mut missing_fields = Vec::with_capacity(0);
388    if options.challenge.is_none() {
389        missing_fields.push("challenge".to_string());
390    }
391    if options.pub_key_cred_params.is_none() {
392        missing_fields.push("pubKeyCredParams".to_string());
393    }
394    if !missing_fields.is_empty() {
395        return Err(WebAuthnEntityError::MissingRequiredFields(missing_fields));
396    }
397
398    let CredentialCreateOptions {
399        rp,
400        user,
401        challenge: Some(challenge),
402        pub_key_cred_params: Some(pub_key_cred_params),
403        authenticator_selection,
404        exclude_credentials,
405        extensions,
406        ..
407    } = options
408    else {
409        // these required fields should be manually checked above, so this shouldn't be reached.
410        unreachable!("Missing required fields on options");
411    };
412
413    let challenge_b64 = bitwarden_encoding::B64Url::from(challenge.as_ref()).to_string();
414    let client_data_json = format!(
415        r#"{{"type":"webauthn.create","challenge":"{}","origin":"{}","crossOrigin":false}}"#,
416        challenge_b64, origin
417    );
418    let client_data_hash = passkey::types::crypto::sha256(client_data_json.as_bytes()).to_vec();
419
420    // Inject default RP ID
421    let mut rp = rp.clone();
422    rp.id.get_or_insert(default_rp_id);
423    let rp = TryInto::<PublicKeyCredentialRpEntity>::try_into(rp.as_ref())?.into();
424
425    let user_entity = TryInto::<PublicKeyCredentialUserEntity>::try_into(user.as_ref())?.into();
426    let pub_key_cred_params = pub_key_cred_params
427        .iter()
428        .map(|p| {
429            PublicKeyCredentialParameters::try_from(p).and_then(|ours| {
430                passkey::types::webauthn::PublicKeyCredentialParameters::try_from(ours)
431            })
432        })
433        .collect::<Result<Vec<passkey::types::webauthn::PublicKeyCredentialParameters>, _>>()?;
434    let exclude_list = exclude_credentials
435        .as_ref()
436        .map(|l| {
437            l.iter()
438                .map(|c| {
439                    let descriptor = PublicKeyCredentialDescriptor::try_from(c);
440
441                    descriptor.and_then(|c| c.try_into().map_err(WebAuthnEntityError::from))
442                })
443                .collect()
444        })
445        .transpose()?;
446    let authenticator_options = authenticator_selection
447        .as_ref()
448        .map(|o| Options {
449            // TODO: Consider deriving `rk` from `resident_key` instead. Fido2NetLib marks
450            // `RequireResidentKey` obsolete in favor of `ResidentKey`. The device auth key must
451            // be discoverable, so options that carry only `residentKey` would leave `rk` false.
452            rk: o.require_resident_key.unwrap_or_default(),
453            uv: !matches!(
454                o.user_verification,
455                Some(UserVerificationRequirement::Discouraged)
456            ),
457            up: true,
458        })
459        .unwrap_or_else(|| Options {
460            rk: false,
461            uv: true,
462            up: true,
463        });
464
465    // Note, we currently hard-code this value instead of getting it from the server.
466    let prf_input = AuthenticatorPrfInputs {
467        eval: Some(AuthenticatorPrfValues {
468            first: sha256("passwordless-login".as_bytes()),
469            second: None,
470        }),
471        eval_by_credential: None,
472    };
473
474    let request = passkey::types::ctap2::make_credential::Request {
475        client_data_hash: client_data_hash.into(),
476        rp,
477        user: user_entity,
478        pub_key_cred_params,
479        exclude_list,
480        options: authenticator_options,
481        extensions: extensions
482            .as_ref()
483            .map(|_| ctap2::make_credential::ExtensionInputs {
484                hmac_secret: None,
485                hmac_secret_mc: None,
486                prf: Some(prf_input),
487            }),
488        pin_auth: None,
489        pin_protocol: None,
490    };
491    Ok((request, client_data_json))
492}
493
494/// Fields corresponding to a WebAuthn [PublicKeyCredential][pub-key-cred]
495/// with an [AuthenticatorAssertionResponse][authenticator-assertion-response].
496///
497/// Similar to [GetAssertionResult][crate::GetAssertionResult], but without the reference to the
498/// vault cipher.
499///
500/// [pub-key-cred]: https://www.w3.org/TR/webauthn-3/#publickeycredential
501/// [authenticator-assertion-response]: https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
502#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
503pub struct DeviceAuthKeyGetAssertionResult {
504    /// ID for this credential, corresponding to [`PublicKeyCredential.rawId`][raw-id].
505    ///
506    /// [raw-id]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid
507    pub credential_id: Vec<u8>,
508
509    /// The authenticator data from the authenticator response.
510    pub authenticator_data: Vec<u8>,
511
512    /// Signature over the authenticator data.
513    pub signature: Vec<u8>,
514
515    /// The user handle returned from the authenticator.
516    pub user_handle: Vec<u8>,
517
518    /// Mix of CTAP unsigned extension output and WebAuthn client extension output.
519    /// Signed extensions can be retrieved from authenticator data.
520    pub extensions: GetAssertionExtensionsOutput,
521}
522
523/// The private key material for the device auth key.
524/// This should be stored separately from the metadata and gated behind
525/// user-verifying access control.
526#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
527pub struct DeviceAuthKeyRecord {
528    /// Credential ID for the WebAuthn credential.
529    pub credential_id: Vec<u8>,
530
531    /// Private key material, formatted as a COSE key.
532    pub key: Vec<u8>,
533
534    /// RP ID of the WebAuthn credential.
535    pub rp_id: String,
536
537    /// User ID for the WebAuthn credential.
538    pub user_id: Vec<u8>,
539
540    /// WebAuthn counter for the credential.
541    pub counter: Option<u32>,
542
543    /// HMAC Secret seed, which can also be used in WebAuthn PRF extension.
544    pub hmac_secret: Vec<u8>,
545}
546
547impl TryFrom<Passkey> for DeviceAuthKeyRecord {
548    type Error = DeviceAuthKeyError;
549    fn try_from(value: Passkey) -> Result<Self, Self::Error> {
550        let credential_id = value.credential_id.to_vec();
551        let key = value.key.to_vec().map_err(|err| {
552            tracing::error!(%err, "Failed to serialize COSE key to bytes.");
553            DeviceAuthKeyError::InvalidCoseKey
554        })?;
555        let user_id = value
556            .user_handle
557            .ok_or(DeviceAuthKeyError::MissingUserHandle)?
558            .to_vec();
559        let hmac_secret = value
560            .extensions
561            .hmac_secret
562            .as_ref()
563            .ok_or(DeviceAuthKeyError::MissingHmacSecret)?
564            .cred_with_uv
565            .clone();
566        Ok(DeviceAuthKeyRecord {
567            credential_id,
568            key,
569            rp_id: value.rp_id,
570            user_id,
571            counter: value.counter,
572            hmac_secret,
573        })
574    }
575}
576
577impl TryFrom<DeviceAuthKeyRecord> for Passkey {
578    type Error = DeviceAuthKeyError;
579    fn try_from(value: DeviceAuthKeyRecord) -> Result<Self, Self::Error> {
580        Ok(Passkey {
581            credential_id: value.credential_id.into(),
582            key: CoseKey::from_slice(&value.key).map_err(|err| {
583                tracing::error!(%err, "Failed to deserialize COSE key from bytes");
584                DeviceAuthKeyError::InvalidCoseKey
585            })?,
586            rp_id: value.rp_id,
587            user_handle: Some(value.user_id.into()),
588            counter: value.counter,
589            extensions: CredentialExtensions {
590                hmac_secret: Some(StoredHmacSecret {
591                    cred_with_uv: value.hmac_secret,
592                    cred_without_uv: None,
593                }),
594            },
595        })
596    }
597}
598
599/// The metadata for the device auth key useful for looking up whether the
600/// authenticator can satisfy a given request before invoking user-verifying
601/// access control.
602#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
603pub struct DeviceAuthKeyMetadata {
604    /// A unique identifier for the device auth key passkey.
605    /// This can be used as a unique identifier in OS autofill stores.
606    pub record_identifier: String,
607
608    /// Date the device auth key was created.
609    pub creation_date: DateTime<Utc>,
610
611    /// FIDO credential ID for the device auth key.
612    pub credential_id: Vec<u8>,
613
614    /// WebAuthn RP ID for the device auth key.
615    pub rp_id: String,
616
617    /// The login or username for user.
618    ///
619    /// Corresponds to the [user.name] in the original WebAuthn request that created the
620    /// credential.
621    pub user_name: String,
622
623    /// The ID for the user.
624    ///
625    /// Corresponds to the [user.id] in the original WebAuthn request that created the credential.
626    pub user_handle: Vec<u8>,
627
628    /// The display name for the user
629    ///
630    /// Corresponds to the [user.displayName] in the original WebAuthn request that created the
631    /// credential.
632    pub user_display_name: String,
633}
634
635/// Errors related to processing the device auth key.
636#[derive(Debug, thiserror::Error)]
637#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
638pub enum DeviceAuthKeyError {
639    /// Authenticator failed to produce a valid response.
640    #[error("The authenticator failed to produce a valid response")]
641    AuthenticatorFailure,
642
643    /// Failed to convert between Rust types.
644    #[error("Failed to convert between Rust types")]
645    Conversion,
646
647    /// Credential excluded.
648    #[error("The existing device auth key is already registered on the server.")]
649    CredentialExcluded,
650
651    /// The record identifier stored in metadata is not a valid UUID.
652    #[error("The record identifier is not a valid UUID")]
653    InvalidRecordIdentifier,
654
655    /// Invalid Web Vault URL specified.
656    #[error("Invalid Web Vault URL specified")]
657    InvalidWebVaultUrl,
658
659    /// No device auth key exists on this device.
660    #[error("No device auth key exists on this device")]
661    MissingDeviceAuthKey,
662
663    /// Failed to unregister device auth key from server.
664    #[error("Failed to unregister device auth key from server")]
665    UnregisterFailure,
666
667    /// Failed to de-/serialize COSE key data.
668    #[error("Failed to de-/serialize COSE key data")]
669    InvalidCoseKey,
670
671    /// An invalid public key credential descriptor was passed in the allow list.
672    #[error("An invalid public key credential descriptor was passed in the allow list")]
673    InvalidPublicKeyCredentialDescriptor,
674
675    /// A master password hash could not be generated for the given master password.
676    #[error("A master password hash could not be generated for the given master password")]
677    MasterPasswordHash,
678
679    /// Credential ID was not returned in the response and was not passed in the request.
680    #[error(
681        "No credential ID was returned in the response nor was a single credential ID passed in the request"
682    )]
683    MissingCredentialId,
684
685    /// No HMAC secret was returned with the credential.
686    #[error("No HMAC secret was returned with the credential")]
687    MissingHmacSecret,
688
689    /// User handle was not returned in the response.
690    #[error("User handle was not returned in the response")]
691    MissingUserHandle,
692
693    /// Feature is not yet implemented.
694    #[error("Feature is not yet implemented")]
695    NotImplemented,
696
697    /// Failed to retrieve the registration options from the server.
698    #[error("Failed to retrieve the registration options from the server")]
699    RetrieveRegistrationOptionsFailure,
700
701    /// Failed to generate rotateable key set from PRF output.
702    #[error("Failed to generate rotateable key set from PRF output")]
703    PrfFailure,
704
705    /// Failed to submit registration request to the server.
706    #[error("Failed to submit registration request to the server")]
707    SubmitRegistrationFailure,
708
709    /// User cancelled the operation.
710    #[error("User cancelled the operation")]
711    UserCancelled,
712
713    /// An unknown error occurred.
714    #[error("An unknown error occurred")]
715    Unknown {
716        /// Reason for the error.
717        reason: String,
718    },
719}
720
721// Need to implement this From<> impl in order to handle unexpected callback errors.  See the
722// following page in the Uniffi user guide:
723// <https://mozilla.github.io/uniffi-rs/foreign_traits.html#error-handling>
724#[cfg(feature = "uniffi")]
725impl From<uniffi::UnexpectedUniFFICallbackError> for DeviceAuthKeyError {
726    fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
727        Self::Unknown { reason: e.reason }
728    }
729}
730
731/// A trait used to interact with the device auth key data on the device.
732#[async_trait::async_trait]
733pub trait DeviceAuthKeyStore: Send + Sync {
734    /// Create a record (private key material).
735    ///
736    /// The record should be stored in device-bound storage and protected with user-verifying access
737    /// controls.
738    async fn create_record(
739        &mut self,
740        record: DeviceAuthKeyRecord,
741    ) -> Result<(), DeviceAuthKeyError>;
742
743    /// Create metadata for the device auth key.
744    ///
745    /// The metadata should be stored separately without access controls that require UI.
746    async fn create_metadata(
747        &mut self,
748        metadata: DeviceAuthKeyMetadata,
749    ) -> Result<(), DeviceAuthKeyError>;
750
751    /// Retrieve the device auth key metadata.
752    async fn get_metadata(&self) -> Result<Option<DeviceAuthKeyMetadata>, DeviceAuthKeyError>;
753
754    /// Retrieve the device auth key private key material.
755    async fn get_record(&self) -> Result<Option<DeviceAuthKeyRecord>, DeviceAuthKeyError>;
756
757    /// Delete the device auth key (both the record and metadata) from the device.
758    async fn delete_record_and_metadata(&mut self) -> Result<(), DeviceAuthKeyError>;
759}
760
761struct DeviceAuthKeyStoreInternal<'a> {
762    store: &'a mut dyn DeviceAuthKeyStore,
763}
764
765#[async_trait::async_trait]
766impl passkey::authenticator::CredentialStore for DeviceAuthKeyStoreInternal<'_> {
767    type PasskeyItem = DeviceAuthKeyRecord;
768
769    async fn find_credentials(
770        &self,
771        _ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
772        _rp_id: &str,
773        _user_handle: Option<&[u8]>,
774    ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
775        match self.store.get_record().await {
776            Ok(Some(key)) => Ok(vec![key]),
777            Ok(None) => return Ok(vec![]),
778            Err(_) => Err(VendorError::try_from(0xf0)
779                .expect("valid vendor error")
780                .into()),
781        }
782    }
783
784    async fn save_credential(
785        &mut self,
786        cred: Passkey,
787        _user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
788        _rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
789        _options: passkey::types::ctap2::get_assertion::Options,
790    ) -> Result<(), StatusCode> {
791        let record = cred
792            .try_into()
793            .map_err(|_| VendorError::try_from(0xf0).expect("valid vendor error"))?;
794
795        self.store.create_record(record).await.map_err(|_| {
796            StatusCode::from(VendorError::try_from(0xf0).expect("valid vendor error"))
797        })?;
798        Ok(())
799    }
800
801    async fn update_credential(&mut self, _cred: Passkey) -> Result<(), StatusCode> {
802        // This is only used to update the counter, which we're not currently using.
803        tracing::warn!("called update_credential() on device auth key, which is not supported");
804        Err(StatusCode::Ctap2(
805            VendorError::try_from(0xF3)
806                .expect("valid vendor error")
807                .into(),
808        ))
809    }
810
811    async fn get_info(&self) -> StoreInfo {
812        StoreInfo {
813            discoverability: DiscoverabilitySupport::Full,
814        }
815    }
816}
817
818struct DeviceAuthKeyUiInternal {}
819
820#[async_trait::async_trait]
821impl passkey::authenticator::UserValidationMethod for DeviceAuthKeyUiInternal {
822    type PasskeyItem = DeviceAuthKeyRecord;
823
824    async fn check_user<'a>(
825        &self,
826        _hint: UiHint<'a, Self::PasskeyItem>,
827        _presence: bool,
828        _verification: bool,
829    ) -> Result<UserCheck, Ctap2Error> {
830        // The DeviceAuthKeyStore trait should store with user-verifying access
831        // control, so we assume that user presence and verification has been
832        // achieved.
833        Ok(UserCheck {
834            presence: true,
835            verification: true,
836        })
837    }
838
839    fn is_presence_enabled(&self) -> bool {
840        true
841    }
842
843    fn is_verification_enabled(&self) -> Option<bool> {
844        Some(true)
845    }
846}