Skip to main content

bitwarden_fido/
types.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use bitwarden_core::key_management::KeyIds;
4use bitwarden_crypto::{CryptoError, KeyStoreContext};
5use bitwarden_encoding::{B64Url, NotB64UrlEncodedError};
6use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView};
7use passkey::types::webauthn::UserVerificationRequirement;
8use reqwest::Url;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use super::{
13    InvalidGuidError, SelectedCredential, UnknownEnumError, Verification,
14    get_enum_from_string_name, string_to_guid_bytes,
15};
16
17#[allow(missing_docs)]
18#[derive(Serialize, Deserialize, Debug, Clone)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
21pub struct Fido2CredentialAutofillView {
22    pub credential_id: Vec<u8>,
23    pub cipher_id: uuid::Uuid,
24    pub rp_id: String,
25    pub user_name_for_ui: Option<String>,
26    pub user_handle: Vec<u8>,
27    /// Indicates if this credential uses a signature counter (legacy passkeys).
28    /// When true, mobile clients must sync before authentication to ensure
29    /// counter values are current. Modern passkeys (counter = 0) can work offline.
30    pub has_counter: bool,
31}
32
33trait NoneWhitespace {
34    /// Convert only whitespace to None
35    fn none_whitespace(&self) -> Option<String>;
36}
37
38impl NoneWhitespace for String {
39    fn none_whitespace(&self) -> Option<String> {
40        match self.trim() {
41            "" => None,
42            s => Some(s.to_owned()),
43        }
44    }
45}
46
47impl NoneWhitespace for Option<String> {
48    fn none_whitespace(&self) -> Option<String> {
49        self.as_ref().and_then(|s| s.none_whitespace())
50    }
51}
52
53#[allow(missing_docs)]
54#[derive(Debug, Error)]
55pub enum Fido2CredentialAutofillViewError {
56    #[error("Autofill credentials can only be created from existing ciphers that have a cipher id")]
57    MissingCipherId,
58
59    #[error(transparent)]
60    InvalidGuid(#[from] InvalidGuidError),
61
62    #[error(transparent)]
63    Crypto(#[from] CryptoError),
64
65    #[error(transparent)]
66    Base64Decode(#[from] NotB64UrlEncodedError),
67}
68
69impl Fido2CredentialAutofillView {
70    #[allow(missing_docs)]
71    pub fn from_cipher_view(
72        cipher: &CipherView,
73        ctx: &mut KeyStoreContext<KeyIds>,
74    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
75        let credentials = cipher.decrypt_fido2_credentials(ctx)?;
76
77        credentials
78            .iter()
79            .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
80                c.user_handle
81                    .as_ref()
82                    .map(|u| B64Url::try_from(u.as_str()))
83                    .map(|user_handle| {
84                        Ok(Fido2CredentialAutofillView {
85                            credential_id: string_to_guid_bytes(&c.credential_id)?,
86                            cipher_id: cipher
87                                .id
88                                .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
89                                .into(),
90                            rp_id: c.rp_id.clone(),
91                            user_handle: user_handle?.into_bytes(),
92                            user_name_for_ui: c
93                                .user_name
94                                .none_whitespace()
95                                .or(c.user_display_name.none_whitespace())
96                                .or(cipher
97                                    .login
98                                    .as_ref()
99                                    .and_then(|l| l.username.none_whitespace()))
100                                .or(cipher.name.none_whitespace()),
101                            has_counter: Self::has_signature_counter(&c.counter),
102                        })
103                    })
104            })
105            .collect()
106    }
107
108    #[allow(missing_docs)]
109    pub fn from_cipher_list_view(
110        cipher: &CipherListView,
111    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
112        match &cipher.r#type {
113            CipherListViewType::Login(LoginListView {
114                fido2_credentials: Some(fido2_credentials),
115                username,
116                ..
117            }) => fido2_credentials
118                .iter()
119                .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
120                    c.user_handle
121                        .as_ref()
122                        .map(|u| B64Url::try_from(u.as_str()))
123                        .map(|user_handle| {
124                            Ok(Fido2CredentialAutofillView {
125                                credential_id: string_to_guid_bytes(&c.credential_id)?,
126                                cipher_id: cipher
127                                    .id
128                                    .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
129                                    .into(),
130                                rp_id: c.rp_id.clone(),
131                                user_handle: user_handle?.into_bytes(),
132                                user_name_for_ui: c
133                                    .user_name
134                                    .none_whitespace()
135                                    .or(c.user_display_name.none_whitespace())
136                                    .or(username.none_whitespace())
137                                    .or(cipher.name.none_whitespace()),
138                                has_counter: Self::has_signature_counter(&c.counter),
139                            })
140                        })
141                })
142                .collect(),
143            _ => Ok(vec![]),
144        }
145    }
146
147    fn has_signature_counter(str: &String) -> bool {
148        str.none_whitespace()
149            .is_some_and(|counter_str| counter_str.parse::<u64>().is_ok_and(|counter| counter > 0))
150    }
151}
152
153#[allow(missing_docs)]
154#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
155pub struct PublicKeyCredentialRpEntity {
156    pub id: String,
157    pub name: Option<String>,
158}
159
160#[allow(missing_docs)]
161#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
162pub struct PublicKeyCredentialUserEntity {
163    pub id: Vec<u8>,
164    pub display_name: String,
165    pub name: String,
166}
167
168#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
169pub struct PublicKeyCredentialParameters {
170    pub ty: String,
171    pub alg: i64,
172}
173
174#[derive(Debug, Error)]
175pub enum PublicKeyCredentialParametersError {
176    #[error("Invalid algorithm")]
177    InvalidAlgorithm,
178
179    #[error("Unknown type")]
180    UnknownEnum(#[from] UnknownEnumError),
181}
182
183impl TryFrom<PublicKeyCredentialParameters>
184    for passkey::types::webauthn::PublicKeyCredentialParameters
185{
186    type Error = PublicKeyCredentialParametersError;
187
188    fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
189        use coset::iana::EnumI64;
190        Ok(Self {
191            ty: get_enum_from_string_name(&value.ty)?,
192            alg: coset::iana::Algorithm::from_i64(value.alg)
193                .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
194        })
195    }
196}
197
198#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
199pub struct PublicKeyCredentialDescriptor {
200    pub ty: String,
201    pub id: Vec<u8>,
202    pub transports: Option<Vec<String>>,
203}
204
205impl TryFrom<PublicKeyCredentialDescriptor>
206    for passkey::types::webauthn::PublicKeyCredentialDescriptor
207{
208    type Error = UnknownEnumError;
209
210    fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
211        Ok(Self {
212            ty: get_enum_from_string_name(&value.ty)?,
213            id: value.id.into(),
214            transports: value
215                .transports
216                .map(|tt| {
217                    tt.into_iter()
218                        .map(|t| get_enum_from_string_name(&t))
219                        .collect::<Result<Vec<_>, Self::Error>>()
220                })
221                .transpose()?,
222        })
223    }
224}
225
226#[allow(missing_docs)]
227#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
228pub struct MakeCredentialRequest {
229    pub client_data_hash: Vec<u8>,
230    pub rp: PublicKeyCredentialRpEntity,
231    pub user: PublicKeyCredentialUserEntity,
232    pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
233    pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
234    pub options: Options,
235
236    /// WebAuthn client extension inputs for credential creation requests.
237    ///
238    /// Cf. <https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions>.
239    pub extensions: Option<MakeCredentialExtensionsInput>,
240}
241
242/// Fields corresponding to a WebAuthn [PublicKeyCredential][pub-key-cred]
243/// with an [AuthenticatorAttestationResponse][authenticator-attestation-response].
244///
245/// [pub-key-cred]: https://www.w3.org/TR/webauthn-3/#publickeycredential
246/// [authenticator-attestation-response]: https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse
247#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
248pub struct MakeCredentialResult {
249    /// The authenticator data extracted from within the
250    /// [`attestation_object`][Self::attestation_object].
251    pub authenticator_data: Vec<u8>,
252
253    /// [WebAuthn attestation object][webauthn-attestation-object] for the
254    /// authenticator response containing both the authenticator data and
255    /// attestation statement for the credential.
256    ///
257    /// [webauthn-attestation-object]: https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject
258    pub attestation_object: Vec<u8>,
259
260    /// ID for this credential, corresponding to [PublicKeyCredential.rawId][raw-id].
261    ///
262    /// [raw-id]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid
263    pub credential_id: Vec<u8>,
264
265    /// Mix of CTAP [unsigned extension output][unsigned-extensions] and
266    /// [WebAuthn client extensions][webauthn-client-extensions] output returned
267    /// by the authenticator.
268    ///
269    /// [unsigned-extensions]: https://www.w3.org/TR/webauthn-3/#unsigned-extension-outputs
270    /// [webauthn-client-extensions]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-clientextensionsresults-slot
271    pub extensions: MakeCredentialExtensionsOutput,
272}
273
274/// WebAuthn extension input for WebAuthn registration extensions.
275#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
276#[derive(Debug, Default)]
277pub struct MakeCredentialExtensionsInput {
278    /// PRF input for WebAuthn registration request.
279    pub prf: Option<MakeCredentialPrfInput>,
280}
281
282impl From<MakeCredentialExtensionsInput>
283    for passkey::types::ctap2::make_credential::ExtensionInputs
284{
285    fn from(value: MakeCredentialExtensionsInput) -> Self {
286        Self {
287            hmac_secret: None,
288            hmac_secret_mc: None,
289            prf: value
290                .prf
291                .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
292        }
293    }
294}
295
296/// WebAuthn extension output for registration extensions.
297#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
298#[derive(Debug)]
299pub struct MakeCredentialExtensionsOutput {
300    /// PRF output for registration extensions.
301    pub prf: Option<MakeCredentialPrfOutput>,
302}
303
304impl From<Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>>
305    for MakeCredentialExtensionsOutput
306{
307    fn from(
308        value: Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>,
309    ) -> Self {
310        if let Some(ext) = value {
311            MakeCredentialExtensionsOutput::from(ext)
312        } else {
313            MakeCredentialExtensionsOutput { prf: None }
314        }
315    }
316}
317
318impl From<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>
319    for MakeCredentialExtensionsOutput
320{
321    fn from(value: passkey::types::ctap2::make_credential::UnsignedExtensionOutputs) -> Self {
322        let prf = value.prf.map(|prf| MakeCredentialPrfOutput {
323            enabled: prf.enabled,
324            results: prf.results.map(|results| results.into()),
325        });
326        MakeCredentialExtensionsOutput { prf }
327    }
328}
329
330/// WebAuthn PRF extension input for use during registration.
331#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
332#[derive(Debug)]
333pub struct MakeCredentialPrfInput {
334    /// PRF inputs.
335    pub eval: Option<PrfInputValues>,
336}
337
338impl From<MakeCredentialPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
339    fn from(value: MakeCredentialPrfInput) -> Self {
340        Self {
341            eval: value.eval.map(|v| v.into()),
342            eval_by_credential: None,
343        }
344    }
345}
346
347/// WebAuthn PRF extension output used during registration.
348#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
349#[derive(Debug)]
350pub struct MakeCredentialPrfOutput {
351    /// Whether PRF is successfully processed for the newly created credential.
352    pub enabled: bool,
353
354    /// PRF outputs.
355    pub results: Option<PrfOutputValues>,
356}
357
358#[allow(missing_docs)]
359/// Type representing data from WebAuthn's
360/// [`PublicKeyCredentialRequestOptions`][pubkey-cred-request-options].
361///
362/// [pubkey-cred-request-options]: https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions
363#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
364pub struct GetAssertionRequest {
365    /// The RP ID for the request used to select credentials.
366    pub rp_id: String,
367
368    /// Hash of the clientDataJSON for the request.
369    pub client_data_hash: Vec<u8>,
370
371    /// Credential IDs known to the RP. If specified, it is a list of
372    /// credentials to filter by, ordered from most to least preferable. If
373    /// empty, only discoverable credentials will be returned.
374    pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
375
376    pub options: Options,
377
378    /// WebAuthn extension input for use during assertion.
379    pub extensions: Option<GetAssertionExtensionsInput>,
380}
381
382/// Fields corresponding to a WebAuthn [PublicKeyCredential][pub-key-cred]
383/// with an [AuthenticatorAssertionResponse][authenticator-assertion-response].
384///
385/// [pub-key-cred]: https://www.w3.org/TR/webauthn-3/#publickeycredential
386/// [authenticator-assertion-response]: https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
387#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
388pub struct GetAssertionResult {
389    /// ID for this credential, corresponding to [PublicKeyCredential.rawId][raw-id].
390    ///
391    /// [raw-id]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid
392    pub credential_id: Vec<u8>,
393
394    /// The authenticator data from the authenticator response.
395    pub authenticator_data: Vec<u8>,
396
397    /// Signature over the authenticator data.
398    pub signature: Vec<u8>,
399
400    /// The user handle returned from the authenticator.
401    pub user_handle: Vec<u8>,
402
403    /// A reference to the Bitwarden cipher for the selected credential.
404    pub selected_credential: SelectedCredential,
405
406    /// Mix of CTAP unsigned extension output and WebAuthn client extension output.
407    /// Signed extensions can be retrieved from authenticator data.
408    pub extensions: GetAssertionExtensionsOutput,
409}
410
411/// WebAuthn extension input for WebAuthn authentication extensions.
412#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
413#[derive(Debug)]
414pub struct GetAssertionExtensionsInput {
415    /// PRF input for the authentication ceremony.
416    pub prf: Option<GetAssertionPrfInput>,
417}
418
419impl From<GetAssertionExtensionsInput> for passkey::types::ctap2::get_assertion::ExtensionInputs {
420    fn from(value: GetAssertionExtensionsInput) -> Self {
421        Self {
422            hmac_secret: None,
423            prf: value
424                .prf
425                .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
426        }
427    }
428}
429
430/// WebAuthn extension output of an authentication ceremony.
431#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
432#[derive(Debug)]
433pub struct GetAssertionExtensionsOutput {
434    /// PRF output for an authentication ceremony.
435    pub prf: Option<GetAssertionPrfOutput>,
436}
437
438impl From<Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>>
439    for GetAssertionExtensionsOutput
440{
441    fn from(value: Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>) -> Self {
442        if let Some(value) = value {
443            value.into()
444        } else {
445            Self { prf: None }
446        }
447    }
448}
449
450impl From<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>
451    for GetAssertionExtensionsOutput
452{
453    fn from(value: passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs) -> Self {
454        let prf = value.prf.map(|prf| GetAssertionPrfOutput {
455            results: prf.results.into(),
456        });
457        GetAssertionExtensionsOutput { prf }
458    }
459}
460
461/// Input for WebAuthn PRF extension during authentication ceremonies.
462#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
463#[derive(Debug)]
464pub struct GetAssertionPrfInput {
465    /// A PRF input to use for authentication. If a map of credential IDs to PRF
466    /// inputs is specified in [`Self::eval_by_credential`] along with this
467    /// value, the extension will fallback to this
468    /// value if the returned credential ID is not contained in the map.
469    pub eval: Option<PrfInputValues>,
470
471    /// A map of credential IDs to PRF input for a set of credentials specified in the
472    /// [`GetAssertionRequest::allow_list`] field of the request. If a key of
473    /// this map does not exist in the allow list, the extension will fail.
474    pub eval_by_credential: Option<HashMap<Vec<u8>, PrfInputValues>>,
475}
476
477impl From<GetAssertionPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
478    fn from(value: GetAssertionPrfInput) -> Self {
479        let eval_by_credential = if let Some(values) = value.eval_by_credential {
480            let map: HashMap<
481                passkey::types::Bytes,
482                passkey::types::ctap2::extensions::AuthenticatorPrfValues,
483            > = values
484                .into_iter()
485                .map(|(k, v)| (k.into(), v.into()))
486                .collect();
487            Some(map)
488        } else {
489            None
490        };
491        Self {
492            eval: value.eval.map(|v| v.into()),
493            eval_by_credential,
494        }
495    }
496}
497
498/// WebAuthn PRF extension output during an authentication ceremony.
499#[allow(missing_docs)]
500#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
501#[derive(Debug)]
502pub struct GetAssertionPrfOutput {
503    /// The PRF output for the ceremony.
504    pub results: PrfOutputValues,
505}
506
507#[allow(missing_docs)]
508#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
509pub struct Options {
510    pub rk: bool,
511    pub uv: UV,
512}
513
514impl From<super::CheckUserOptions> for Options {
515    fn from(value: super::CheckUserOptions) -> Self {
516        Self {
517            rk: value.require_presence,
518            uv: value.require_verification.into(),
519        }
520    }
521}
522
523impl From<Options> for super::CheckUserOptions {
524    fn from(value: Options) -> Self {
525        Self {
526            require_presence: value.rk,
527            require_verification: value.uv.into(),
528        }
529    }
530}
531
532#[derive(Eq, PartialEq, Clone, Copy)]
533#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
534pub enum UV {
535    Discouraged,
536    Preferred,
537    Required,
538}
539
540impl From<UV> for Verification {
541    fn from(value: UV) -> Self {
542        match value {
543            UV::Discouraged => Verification::Discouraged,
544            UV::Preferred => Verification::Preferred,
545            UV::Required => Verification::Required,
546        }
547    }
548}
549
550impl From<Verification> for UV {
551    fn from(value: Verification) -> Self {
552        match value {
553            Verification::Discouraged => UV::Discouraged,
554            Verification::Preferred => UV::Preferred,
555            Verification::Required => UV::Required,
556        }
557    }
558}
559
560impl From<UserVerificationRequirement> for UV {
561    fn from(value: UserVerificationRequirement) -> Self {
562        match value {
563            UserVerificationRequirement::Discouraged => UV::Discouraged,
564            UserVerificationRequirement::Preferred => UV::Preferred,
565            UserVerificationRequirement::Required => UV::Required,
566        }
567    }
568}
569
570#[allow(missing_docs)]
571#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
572pub enum ClientData {
573    DefaultWithExtraData { android_package_name: String },
574    DefaultWithCustomHash { hash: Vec<u8> },
575}
576
577#[derive(Serialize, Clone)]
578#[serde(rename_all = "camelCase")]
579pub(super) struct AndroidClientData {
580    android_package_name: String,
581}
582
583impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
584    fn extra_client_data(&self) -> Option<AndroidClientData> {
585        match self {
586            ClientData::DefaultWithExtraData {
587                android_package_name,
588            } => Some(AndroidClientData {
589                android_package_name: android_package_name.clone(),
590            }),
591            ClientData::DefaultWithCustomHash { .. } => None,
592        }
593    }
594
595    fn client_data_hash(&self) -> Option<Vec<u8>> {
596        match self {
597            ClientData::DefaultWithExtraData { .. } => None,
598            ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
599        }
600    }
601}
602
603/// Salt inputs for WebAuthn PRF extension.
604#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
605pub struct PrfInputValues {
606    /// An input on which to evaluate PRF. Required.
607    pub first: Vec<u8>,
608
609    /// An optional secondary input on which to evaluate PRF.
610    pub second: Option<Vec<u8>>,
611}
612
613impl PrfInputValues {
614    const WEBAUTHN_PRF_CONTEXT_STRING: &[u8] = b"WebAuthn PRF\0";
615
616    fn hash_webauthn_prf_input(input: &[u8]) -> [u8; 32] {
617        passkey::types::crypto::sha256(&[Self::WEBAUTHN_PRF_CONTEXT_STRING, input].concat())
618    }
619}
620
621impl std::fmt::Debug for PrfInputValues {
622    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623        f.debug_struct("PrfInputValues")
624            .field("first", &"********")
625            .field("second", &self.second.as_ref().map(|_| "********"))
626            .finish()
627    }
628}
629
630impl From<PrfInputValues> for passkey::types::ctap2::extensions::AuthenticatorPrfValues {
631    /// This converts PRF input received from a client into the format that
632    /// passkey-rs expects. This is not valid for converting output received from passkey-rs.
633    fn from(value: PrfInputValues) -> Self {
634        // passkey-rs expects the salt input to be hashed already according to
635        // WebAuthn PRF extension client processing rules.
636        let first = PrfInputValues::hash_webauthn_prf_input(value.first.as_ref());
637        let second = value
638            .second
639            .as_deref()
640            .map(PrfInputValues::hash_webauthn_prf_input);
641        Self { first, second }
642    }
643}
644
645/// WebAuthn PRF output values.
646#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
647pub struct PrfOutputValues {
648    /// The output of the PRF evaluation of the first PRF input.
649    pub first: Vec<u8>,
650
651    /// The output of the PRF evaluation of the second PRF input, if it was specified.
652    pub second: Option<Vec<u8>>,
653}
654
655impl std::fmt::Debug for PrfOutputValues {
656    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657        f.debug_struct("PrfOutputValues")
658            .field("first", &"********")
659            .field("second", &self.second.as_ref().map(|_| "********"))
660            .finish()
661    }
662}
663
664impl From<passkey::types::ctap2::extensions::AuthenticatorPrfValues> for PrfOutputValues {
665    fn from(value: passkey::types::ctap2::extensions::AuthenticatorPrfValues) -> Self {
666        Self {
667            first: value.first.to_vec(),
668            second: value.second.map(|s| s.to_vec()),
669        }
670    }
671}
672#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
673pub struct ClientExtensionResults {
674    pub cred_props: Option<CredPropsResult>,
675}
676
677#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
678pub struct CredPropsResult {
679    pub rk: Option<bool>,
680}
681
682impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
683    fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
684        Self {
685            rk: value.discoverable,
686        }
687    }
688}
689
690#[allow(missing_docs)]
691#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
692pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
693    pub id: String,
694    pub raw_id: Vec<u8>,
695    pub ty: String,
696    pub authenticator_attachment: Option<String>,
697    pub client_extension_results: ClientExtensionResults,
698    pub response: AuthenticatorAttestationResponse,
699    pub selected_credential: SelectedCredential,
700}
701
702#[allow(missing_docs)]
703#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
704pub struct AuthenticatorAttestationResponse {
705    pub client_data_json: Vec<u8>,
706    pub authenticator_data: Vec<u8>,
707    pub public_key: Option<Vec<u8>>,
708    pub public_key_algorithm: i64,
709    pub attestation_object: Vec<u8>,
710    pub transports: Option<Vec<String>>,
711}
712
713#[allow(missing_docs)]
714#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
715pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
716    pub id: String,
717    pub raw_id: Vec<u8>,
718    pub ty: String,
719    pub authenticator_attachment: Option<String>,
720    pub client_extension_results: ClientExtensionResults,
721    pub response: AuthenticatorAssertionResponse,
722    pub selected_credential: SelectedCredential,
723}
724
725#[allow(missing_docs)]
726#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
727pub struct AuthenticatorAssertionResponse {
728    pub client_data_json: Vec<u8>,
729    pub authenticator_data: Vec<u8>,
730    pub signature: Vec<u8>,
731    pub user_handle: Vec<u8>,
732}
733
734#[derive(Debug, Error)]
735#[error("Invalid origin: {0}")]
736pub struct InvalidOriginError(String);
737
738#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
739/// An Unverified asset link.
740pub struct UnverifiedAssetLink {
741    /// Application package name.
742    package_name: String,
743    /// Fingerprint to compare.
744    sha256_cert_fingerprint: String,
745    /// Host to lookup the well known asset link.
746    host: String,
747    /// When sourced from the application statement list or parsed from host for passkeys.
748    /// Will be generated from `host` if not provided.
749    asset_link_url: Option<String>,
750}
751
752#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
753/// The origin of a WebAuthn request.
754pub enum Origin {
755    /// A Url, meant for a request in the web browser.
756    Web(String),
757    /// An android digital asset fingerprint.
758    /// Meant for a request coming from an android application.
759    Android(UnverifiedAssetLink),
760}
761
762impl TryFrom<Origin> for passkey::client::Origin<'_> {
763    type Error = InvalidOriginError;
764
765    fn try_from(value: Origin) -> Result<Self, Self::Error> {
766        Ok(match value {
767            Origin::Web(url) => {
768                let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?;
769                passkey::client::Origin::Web(Cow::Owned(url))
770            }
771            Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
772        })
773    }
774}
775
776impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
777    type Error = InvalidOriginError;
778
779    fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
780        let asset_link_url = {
781            let url = value
782                .asset_link_url
783                .unwrap_or_else(|| format!("https://{}/.well-known/assetlinks.json", value.host));
784            Url::parse(&url).map_err(|e| InvalidOriginError(e.to_string()))?
785        };
786
787        passkey::client::UnverifiedAssetLink::new(
788            Cow::from(value.package_name),
789            value.sha256_cert_fingerprint.as_str(),
790            Cow::from(value.host),
791            asset_link_url,
792        )
793        .map_err(|e| InvalidOriginError(format!("{e:?}")))
794    }
795}
796
797#[cfg(test)]
798mod tests {
799    use std::collections::HashMap;
800
801    use passkey::types::ctap2::{
802        extensions::{
803            AuthenticatorPrfGetOutputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues,
804        },
805        get_assertion, make_credential,
806    };
807    use serde::{Deserialize, Serialize};
808
809    use super::{
810        AndroidClientData, GetAssertionExtensionsInput, GetAssertionExtensionsOutput,
811        GetAssertionPrfInput, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
812        MakeCredentialPrfInput, PrfInputValues,
813    };
814
815    /// Raw PRF input for testing.
816    static TEST_SALT1_RAW_INPUT: &[u8] = b"salt1";
817
818    /// PRF input of after applying WebAuthn PRF domain separation to [TEST_SALT1_RAW_INPUT].
819    // SHA-256(UTF-8("WebAuthn PRF") || 0x00 || TEST_SALT1_RAW_INPUT)
820    static TEST_SALT1_WEBAUTHN_INPUT: [u8; 32] = [
821        0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, 0x0F,
822        0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, 0xEE, 0xCB,
823        0xDD, 0x3C,
824    ];
825
826    /// Raw PRF input for testing.
827    static TEST_SALT2_RAW_INPUT: &[u8] = b"salt2";
828
829    /// PRF input after applying WebAuthn PRF domain separation to [TEST_SALT2_RAW_INPUT].
830    ///
831    /// SHA-256(UTF-8("WebAuthn PRF") || 0x00 || TEST_SALT2_RAW_INPUT)
832    static TEST_SALT2_WEBAUTHN_INPUT: [u8; 32] = [
833        0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, 0x1D,
834        0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, 0x94, 0x9A,
835        0xDD, 0x65,
836    ];
837
838    // This is a stripped down of the passkey-rs implementation, to test the
839    // serialization of the `ClientData` enum, and to make sure that () and None
840    // are serialized the same way when going through #[serde(flatten)].
841    #[derive(Serialize, Deserialize)]
842    #[serde(rename_all = "camelCase")]
843    pub struct CollectedClientData<E = ()>
844    where
845        E: Serialize,
846    {
847        pub origin: String,
848
849        #[serde(flatten)]
850        pub extra_data: E,
851    }
852
853    #[test]
854    fn test_serialize_unit_data() {
855        let data = CollectedClientData {
856            origin: "https://example.com".to_owned(),
857            extra_data: (),
858        };
859
860        let serialized = serde_json::to_string(&data).unwrap();
861        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
862    }
863
864    #[test]
865    fn test_serialize_none_data() {
866        let data = CollectedClientData {
867            origin: "https://example.com".to_owned(),
868            extra_data: Option::<AndroidClientData>::None,
869        };
870
871        let serialized = serde_json::to_string(&data).unwrap();
872        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
873    }
874
875    #[test]
876    fn test_serialize_android_data() {
877        let data = CollectedClientData {
878            origin: "https://example.com".to_owned(),
879            extra_data: Some(AndroidClientData {
880                android_package_name: "com.example.app".to_owned(),
881            }),
882        };
883
884        let serialized = serde_json::to_string(&data).unwrap();
885        assert_eq!(
886            serialized,
887            r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
888        );
889    }
890
891    #[test]
892    fn test_transform_make_credential_extension_input() {
893        let input = MakeCredentialExtensionsInput {
894            prf: Some(MakeCredentialPrfInput {
895                eval: Some(PrfInputValues {
896                    first: TEST_SALT1_RAW_INPUT.to_vec(),
897                    second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
898                }),
899            }),
900        };
901        let transformed = make_credential::ExtensionInputs::from(input);
902        let eval = transformed.prf.unwrap().eval.unwrap();
903        assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
904        assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
905    }
906
907    #[test]
908    fn test_transform_make_credential_extension_output() {
909        let prf1: Vec<u8> = (0..32).collect();
910        let output = make_credential::UnsignedExtensionOutputs {
911            prf: Some(AuthenticatorPrfMakeOutputs {
912                enabled: true,
913                results: Some(AuthenticatorPrfValues {
914                    first: prf1.clone().try_into().unwrap(),
915                    second: None,
916                }),
917            }),
918        };
919        let transformed = MakeCredentialExtensionsOutput::from(output);
920        assert!(transformed.prf.as_ref().unwrap().enabled);
921        assert_eq!(prf1, transformed.prf.unwrap().results.unwrap().first);
922    }
923
924    #[test]
925    fn test_transform_get_assertion_extension_input() {
926        let input = GetAssertionExtensionsInput {
927            prf: Some(GetAssertionPrfInput {
928                eval: Some(PrfInputValues {
929                    first: TEST_SALT1_RAW_INPUT.to_vec(),
930                    second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
931                }),
932                eval_by_credential: None,
933            }),
934        };
935        let transformed = get_assertion::ExtensionInputs::from(input);
936        let eval = transformed.prf.unwrap().eval.unwrap();
937        assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
938        assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
939    }
940
941    #[test]
942    fn test_transform_get_assertion_extension_input_with_eval_by_credential() {
943        let cred_id = b"credential_id1".to_vec();
944        let input = GetAssertionExtensionsInput {
945            prf: Some(GetAssertionPrfInput {
946                eval: None,
947                eval_by_credential: Some(HashMap::from([(
948                    cred_id.clone(),
949                    PrfInputValues {
950                        first: TEST_SALT1_RAW_INPUT.to_vec(),
951                        second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
952                    },
953                )])),
954            }),
955        };
956        let transformed = get_assertion::ExtensionInputs::from(input);
957        let output = transformed.prf.unwrap().eval_by_credential.unwrap();
958        let results = output.get(&cred_id.into()).unwrap();
959        assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, results.first);
960        assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, results.second.unwrap());
961    }
962
963    #[test]
964    fn test_transform_get_assertion_extension_output() {
965        let prf1: Vec<u8> = (0..32).collect();
966        let output = get_assertion::UnsignedExtensionOutputs {
967            prf: Some(AuthenticatorPrfGetOutputs {
968                results: AuthenticatorPrfValues {
969                    first: prf1.clone().try_into().unwrap(),
970                    second: None,
971                },
972            }),
973        };
974        let transformed = GetAssertionExtensionsOutput::from(output);
975        assert_eq!(prf1, transformed.prf.unwrap().results.first);
976    }
977}