bitwarden_fido/
types.rs

1use std::borrow::Cow;
2
3use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
4use bitwarden_core::key_management::KeyIds;
5use bitwarden_crypto::{CryptoError, KeyStoreContext};
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    get_enum_from_string_name, string_to_guid_bytes, InvalidGuid, SelectedCredential, UnknownEnum,
14    Verification,
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}
28
29trait NoneWhitespace {
30    /// Convert only whitespace to None
31    fn none_whitespace(&self) -> Option<String>;
32}
33
34impl NoneWhitespace for String {
35    fn none_whitespace(&self) -> Option<String> {
36        match self.trim() {
37            "" => None,
38            s => Some(s.to_owned()),
39        }
40    }
41}
42
43impl NoneWhitespace for Option<String> {
44    fn none_whitespace(&self) -> Option<String> {
45        self.as_ref().and_then(|s| s.none_whitespace())
46    }
47}
48
49#[allow(missing_docs)]
50#[derive(Debug, Error)]
51pub enum Fido2CredentialAutofillViewError {
52    #[error(
53        "Autofill credentials can only be created from existing ciphers that have a cipher id"
54    )]
55    MissingCipherId,
56
57    #[error(transparent)]
58    InvalidGuid(#[from] InvalidGuid),
59
60    #[error(transparent)]
61    CryptoError(#[from] CryptoError),
62
63    #[error(transparent)]
64    Base64DecodeError(#[from] base64::DecodeError),
65}
66
67impl Fido2CredentialAutofillView {
68    #[allow(missing_docs)]
69    pub fn from_cipher_view(
70        cipher: &CipherView,
71        ctx: &mut KeyStoreContext<KeyIds>,
72    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
73        let credentials = cipher.decrypt_fido2_credentials(ctx)?;
74
75        credentials
76            .iter()
77            .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
78                c.user_handle
79                    .as_ref()
80                    .map(|u| URL_SAFE_NO_PAD.decode(u))
81                    .map(|user_handle| {
82                        Ok(Fido2CredentialAutofillView {
83                            credential_id: string_to_guid_bytes(&c.credential_id)?,
84                            cipher_id: cipher
85                                .id
86                                .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
87                                .into(),
88                            rp_id: c.rp_id.clone(),
89                            user_handle: user_handle?,
90                            user_name_for_ui: c
91                                .user_name
92                                .none_whitespace()
93                                .or(c.user_display_name.none_whitespace())
94                                .or(cipher
95                                    .login
96                                    .as_ref()
97                                    .and_then(|l| l.username.none_whitespace()))
98                                .or(cipher.name.none_whitespace()),
99                        })
100                    })
101            })
102            .collect()
103    }
104
105    #[allow(missing_docs)]
106    pub fn from_cipher_list_view(
107        cipher: &CipherListView,
108    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
109        match &cipher.r#type {
110            CipherListViewType::Login(LoginListView {
111                fido2_credentials: Some(fido2_credentials),
112                username,
113                ..
114            }) => fido2_credentials
115                .iter()
116                .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
117                    c.user_handle
118                        .as_ref()
119                        .map(|u| URL_SAFE_NO_PAD.decode(u))
120                        .map(|user_handle| {
121                            Ok(Fido2CredentialAutofillView {
122                                credential_id: string_to_guid_bytes(&c.credential_id)?,
123                                cipher_id: cipher
124                                    .id
125                                    .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
126                                    .into(),
127                                rp_id: c.rp_id.clone(),
128                                user_handle: user_handle?,
129                                user_name_for_ui: c
130                                    .user_name
131                                    .none_whitespace()
132                                    .or(c.user_display_name.none_whitespace())
133                                    .or(username.none_whitespace())
134                                    .or(cipher.name.none_whitespace()),
135                            })
136                        })
137                })
138                .collect(),
139            _ => Ok(vec![]),
140        }
141    }
142}
143
144#[allow(missing_docs)]
145#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
146pub struct PublicKeyCredentialRpEntity {
147    pub id: String,
148    pub name: Option<String>,
149}
150
151#[allow(missing_docs)]
152#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
153pub struct PublicKeyCredentialUserEntity {
154    pub id: Vec<u8>,
155    pub display_name: String,
156    pub name: String,
157}
158
159#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
160pub struct PublicKeyCredentialParameters {
161    pub ty: String,
162    pub alg: i64,
163}
164
165#[derive(Debug, Error)]
166pub enum PublicKeyCredentialParametersError {
167    #[error("Invalid algorithm")]
168    InvalidAlgorithm,
169
170    #[error("Unknown type")]
171    UnknownEnum(#[from] UnknownEnum),
172}
173
174impl TryFrom<PublicKeyCredentialParameters>
175    for passkey::types::webauthn::PublicKeyCredentialParameters
176{
177    type Error = PublicKeyCredentialParametersError;
178
179    fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
180        use coset::iana::EnumI64;
181        Ok(Self {
182            ty: get_enum_from_string_name(&value.ty)?,
183            alg: coset::iana::Algorithm::from_i64(value.alg)
184                .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
185        })
186    }
187}
188
189#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
190pub struct PublicKeyCredentialDescriptor {
191    pub ty: String,
192    pub id: Vec<u8>,
193    pub transports: Option<Vec<String>>,
194}
195
196impl TryFrom<PublicKeyCredentialDescriptor>
197    for passkey::types::webauthn::PublicKeyCredentialDescriptor
198{
199    type Error = UnknownEnum;
200
201    fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
202        Ok(Self {
203            ty: get_enum_from_string_name(&value.ty)?,
204            id: value.id.into(),
205            transports: value
206                .transports
207                .map(|tt| {
208                    tt.into_iter()
209                        .map(|t| get_enum_from_string_name(&t))
210                        .collect::<Result<Vec<_>, Self::Error>>()
211                })
212                .transpose()?,
213        })
214    }
215}
216
217pub type Extensions = Option<String>;
218
219#[allow(missing_docs)]
220#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
221pub struct MakeCredentialRequest {
222    pub client_data_hash: Vec<u8>,
223    pub rp: PublicKeyCredentialRpEntity,
224    pub user: PublicKeyCredentialUserEntity,
225    pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
226    pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
227    pub options: Options,
228    pub extensions: Extensions,
229}
230
231#[allow(missing_docs)]
232#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
233pub struct MakeCredentialResult {
234    pub authenticator_data: Vec<u8>,
235    pub attestation_object: Vec<u8>,
236    pub credential_id: Vec<u8>,
237}
238
239#[allow(missing_docs)]
240#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
241pub struct GetAssertionRequest {
242    pub rp_id: String,
243    pub client_data_hash: Vec<u8>,
244    pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
245    pub options: Options,
246    pub extensions: Extensions,
247}
248
249#[allow(missing_docs)]
250#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
251pub struct Options {
252    pub rk: bool,
253    pub uv: UV,
254}
255
256impl From<super::CheckUserOptions> for Options {
257    fn from(value: super::CheckUserOptions) -> Self {
258        Self {
259            rk: value.require_presence,
260            uv: value.require_verification.into(),
261        }
262    }
263}
264
265impl From<Options> for super::CheckUserOptions {
266    fn from(value: Options) -> Self {
267        Self {
268            require_presence: value.rk,
269            require_verification: value.uv.into(),
270        }
271    }
272}
273
274#[derive(Eq, PartialEq, Clone, Copy)]
275#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
276pub enum UV {
277    Discouraged,
278    Preferred,
279    Required,
280}
281
282impl From<UV> for Verification {
283    fn from(value: UV) -> Self {
284        match value {
285            UV::Discouraged => Verification::Discouraged,
286            UV::Preferred => Verification::Preferred,
287            UV::Required => Verification::Required,
288        }
289    }
290}
291
292impl From<Verification> for UV {
293    fn from(value: Verification) -> Self {
294        match value {
295            Verification::Discouraged => UV::Discouraged,
296            Verification::Preferred => UV::Preferred,
297            Verification::Required => UV::Required,
298        }
299    }
300}
301
302impl From<UserVerificationRequirement> for UV {
303    fn from(value: UserVerificationRequirement) -> Self {
304        match value {
305            UserVerificationRequirement::Discouraged => UV::Discouraged,
306            UserVerificationRequirement::Preferred => UV::Preferred,
307            UserVerificationRequirement::Required => UV::Required,
308        }
309    }
310}
311
312#[allow(missing_docs)]
313#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
314pub struct GetAssertionResult {
315    pub credential_id: Vec<u8>,
316    pub authenticator_data: Vec<u8>,
317    pub signature: Vec<u8>,
318    pub user_handle: Vec<u8>,
319
320    pub selected_credential: SelectedCredential,
321}
322
323#[allow(missing_docs)]
324#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
325pub enum ClientData {
326    DefaultWithExtraData { android_package_name: String },
327    DefaultWithCustomHash { hash: Vec<u8> },
328}
329
330#[derive(Serialize, Clone)]
331#[serde(rename_all = "camelCase")]
332pub(super) struct AndroidClientData {
333    android_package_name: String,
334}
335
336impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
337    fn extra_client_data(&self) -> Option<AndroidClientData> {
338        match self {
339            ClientData::DefaultWithExtraData {
340                android_package_name,
341            } => Some(AndroidClientData {
342                android_package_name: android_package_name.clone(),
343            }),
344            ClientData::DefaultWithCustomHash { .. } => None,
345        }
346    }
347
348    fn client_data_hash(&self) -> Option<Vec<u8>> {
349        match self {
350            ClientData::DefaultWithExtraData { .. } => None,
351            ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
352        }
353    }
354}
355
356#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
357pub struct ClientExtensionResults {
358    pub cred_props: Option<CredPropsResult>,
359}
360
361#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
362pub struct CredPropsResult {
363    pub rk: Option<bool>,
364    pub authenticator_display_name: Option<String>,
365}
366
367impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
368    fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
369        Self {
370            rk: value.discoverable,
371            authenticator_display_name: value.authenticator_display_name,
372        }
373    }
374}
375
376#[allow(missing_docs)]
377#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
378pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
379    pub id: String,
380    pub raw_id: Vec<u8>,
381    pub ty: String,
382    pub authenticator_attachment: Option<String>,
383    pub client_extension_results: ClientExtensionResults,
384    pub response: AuthenticatorAttestationResponse,
385    pub selected_credential: SelectedCredential,
386}
387
388#[allow(missing_docs)]
389#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
390pub struct AuthenticatorAttestationResponse {
391    pub client_data_json: Vec<u8>,
392    pub authenticator_data: Vec<u8>,
393    pub public_key: Option<Vec<u8>>,
394    pub public_key_algorithm: i64,
395    pub attestation_object: Vec<u8>,
396    pub transports: Option<Vec<String>>,
397}
398
399#[allow(missing_docs)]
400#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
401pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
402    pub id: String,
403    pub raw_id: Vec<u8>,
404    pub ty: String,
405    pub authenticator_attachment: Option<String>,
406    pub client_extension_results: ClientExtensionResults,
407    pub response: AuthenticatorAssertionResponse,
408    pub selected_credential: SelectedCredential,
409}
410
411#[allow(missing_docs)]
412#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
413pub struct AuthenticatorAssertionResponse {
414    pub client_data_json: Vec<u8>,
415    pub authenticator_data: Vec<u8>,
416    pub signature: Vec<u8>,
417    pub user_handle: Vec<u8>,
418}
419
420#[derive(Debug, Error)]
421#[error("Invalid origin: {0}")]
422pub struct InvalidOriginError(String);
423
424#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
425/// An Unverified asset link.
426pub struct UnverifiedAssetLink {
427    /// Application package name.
428    package_name: String,
429    /// Fingerprint to compare.
430    sha256_cert_fingerprint: String,
431    /// Host to lookup the well known asset link.
432    host: String,
433    /// When sourced from the application statement list or parsed from host for passkeys.
434    /// Will be generated from `host` if not provided.
435    asset_link_url: Option<String>,
436}
437
438#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
439/// The origin of a WebAuthn request.
440pub enum Origin {
441    /// A Url, meant for a request in the web browser.
442    Web(String),
443    /// An android digital asset fingerprint.
444    /// Meant for a request coming from an android application.
445    Android(UnverifiedAssetLink),
446}
447
448impl TryFrom<Origin> for passkey::client::Origin<'_> {
449    type Error = InvalidOriginError;
450
451    fn try_from(value: Origin) -> Result<Self, Self::Error> {
452        Ok(match value {
453            Origin::Web(url) => {
454                let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?;
455                passkey::client::Origin::Web(Cow::Owned(url))
456            }
457            Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
458        })
459    }
460}
461
462impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
463    type Error = InvalidOriginError;
464
465    fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
466        let asset_link_url = match value.asset_link_url {
467            Some(url) => Some(Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?),
468            None => None,
469        };
470
471        passkey::client::UnverifiedAssetLink::new(
472            Cow::from(value.package_name),
473            value.sha256_cert_fingerprint.as_str(),
474            Cow::from(value.host),
475            asset_link_url,
476        )
477        .map_err(|e| InvalidOriginError(format!("{e:?}")))
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use serde::{Deserialize, Serialize};
484
485    use super::AndroidClientData;
486
487    // This is a stripped down of the passkey-rs implementation, to test the
488    // serialization of the `ClientData` enum, and to make sure that () and None
489    // are serialized the same way when going through #[serde(flatten)].
490    #[derive(Serialize, Deserialize)]
491    #[serde(rename_all = "camelCase")]
492    pub struct CollectedClientData<E = ()>
493    where
494        E: Serialize,
495    {
496        pub origin: String,
497
498        #[serde(flatten)]
499        pub extra_data: E,
500    }
501
502    #[test]
503    fn test_serialize_unit_data() {
504        let data = CollectedClientData {
505            origin: "https://example.com".to_owned(),
506            extra_data: (),
507        };
508
509        let serialized = serde_json::to_string(&data).unwrap();
510        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
511    }
512
513    #[test]
514    fn test_serialize_none_data() {
515        let data = CollectedClientData {
516            origin: "https://example.com".to_owned(),
517            extra_data: Option::<AndroidClientData>::None,
518        };
519
520        let serialized = serde_json::to_string(&data).unwrap();
521        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
522    }
523
524    #[test]
525    fn test_serialize_android_data() {
526        let data = CollectedClientData {
527            origin: "https://example.com".to_owned(),
528            extra_data: Some(AndroidClientData {
529                android_package_name: "com.example.app".to_owned(),
530            }),
531        };
532
533        let serialized = serde_json::to_string(&data).unwrap();
534        assert_eq!(
535            serialized,
536            r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
537        );
538    }
539}