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