Skip to main content

bitwarden_fido/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use bitwarden_core::key_management::KeySlotIds;
4use bitwarden_crypto::KeyStoreContext;
5use bitwarden_encoding::{B64Url, NotB64UrlEncodedError};
6use bitwarden_vault::{
7    CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView,
8};
9use crypto::{CoseKeyToPkcs8Error, PrivateKeyFromSecretKeyError};
10use passkey::types::{CredentialExtensions, Passkey, ctap2::Aaguid};
11
12#[cfg(feature = "uniffi")]
13uniffi::setup_scaffolding!();
14#[cfg(feature = "uniffi")]
15mod uniffi_support;
16
17mod authenticator;
18mod client;
19mod client_fido;
20mod crypto;
21mod device_auth_key;
22mod traits;
23mod types;
24pub use authenticator::{
25    CredentialsForAutofillError, Fido2Authenticator, GetAssertionError, MakeCredentialError,
26    SilentlyDiscoverCredentialsError,
27};
28pub use client::{Fido2Client, Fido2ClientError};
29pub use client_fido::{ClientFido2, ClientFido2Ext, DecryptFido2AutofillCredentialsError};
30pub use device_auth_key::{
31    DeviceAuthKeyAuthenticator, DeviceAuthKeyError, DeviceAuthKeyGetAssertionResult,
32    DeviceAuthKeyMetadata, DeviceAuthKeyRecord, DeviceAuthKeyStore,
33};
34pub use passkey::authenticator::UiHint;
35use thiserror::Error;
36pub use traits::{
37    CheckUserOptions, CheckUserResult, Fido2CallbackError, Fido2CredentialStore,
38    Fido2UserInterface, Verification,
39};
40pub use types::{
41    AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, ClientData,
42    Fido2CredentialAutofillView, Fido2CredentialAutofillViewError, GetAssertionExtensionsInput,
43    GetAssertionExtensionsOutput, GetAssertionPrfInput, GetAssertionPrfOutput, GetAssertionRequest,
44    GetAssertionResult, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
45    MakeCredentialPrfInput, MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResult,
46    Options, Origin, PrfInputValues, PrfOutputValues,
47    PublicKeyCredentialAuthenticatorAssertionResponse,
48    PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity,
49    PublicKeyCredentialUserEntity, UnverifiedAssetLink,
50};
51
52use self::crypto::{cose_key_to_pkcs8, pkcs8_to_cose_key};
53
54// This is the AAGUID for the Bitwarden Passkey provider (d548826e-79b4-db40-a3d8-11116f7e8349)
55// It is used for the Relaying Parties to identify the authenticator during registration
56const AAGUID: Aaguid = Aaguid([
57    0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49,
58]);
59
60#[allow(dead_code, missing_docs)]
61#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
62pub struct SelectedCredential {
63    cipher: CipherView,
64    credential: Fido2CredentialView,
65}
66
67// This container is needed so we can properly implement the TryFrom trait for Passkey
68// Otherwise we need to decrypt the Fido2 credentials every time we create a CipherView
69#[derive(Clone)]
70pub(crate) struct CipherViewContainer {
71    cipher: CipherView,
72    fido2_credentials: Vec<Fido2CredentialFullView>,
73}
74
75impl CipherViewContainer {
76    fn new(cipher: CipherView, ctx: &mut KeyStoreContext<KeySlotIds>) -> Result<Self, CipherError> {
77        let fido2_credentials = cipher.get_fido2_credentials(ctx)?;
78        Ok(Self {
79            cipher,
80            fido2_credentials,
81        })
82    }
83}
84
85#[allow(missing_docs)]
86#[derive(Debug, Error)]
87pub enum Fido2Error {
88    #[error(transparent)]
89    Decode(#[from] NotB64UrlEncodedError),
90
91    #[error(transparent)]
92    UnknownEnum(#[from] UnknownEnumError),
93
94    #[error(transparent)]
95    InvalidGuid(#[from] InvalidGuidError),
96
97    #[error(transparent)]
98    PrivateKeyFromSecretKey(#[from] PrivateKeyFromSecretKeyError),
99
100    #[error("No Fido2 credentials found")]
101    NoFido2CredentialsFound,
102
103    #[error("Invalid counter")]
104    InvalidCounter,
105}
106
107impl TryFrom<CipherViewContainer> for Passkey {
108    type Error = Fido2Error;
109
110    fn try_from(value: CipherViewContainer) -> Result<Self, Self::Error> {
111        let cred = value
112            .fido2_credentials
113            .first()
114            .ok_or(Fido2Error::NoFido2CredentialsFound)?;
115
116        try_from_credential_full_view(cred.clone())
117    }
118}
119
120fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result<Passkey, Fido2Error> {
121    let counter: u32 = value
122        .counter
123        .parse()
124        .map_err(|_| Fido2Error::InvalidCounter)?;
125    let counter = (counter != 0).then_some(counter);
126    let key_value = B64Url::try_from(value.key_value)?;
127    let user_handle = value.user_handle.map(B64Url::try_from).transpose()?;
128
129    let key = pkcs8_to_cose_key(key_value.as_bytes())?;
130
131    Ok(Passkey {
132        key,
133        credential_id: string_to_guid_bytes(&value.credential_id)?.into(),
134        rp_id: value.rp_id.clone(),
135        user_handle: user_handle.map(|u| u.into_bytes().into()),
136        counter,
137        extensions: CredentialExtensions { hmac_secret: None },
138    })
139}
140
141#[allow(missing_docs)]
142#[derive(Debug, Error)]
143pub enum FillCredentialError {
144    #[error(transparent)]
145    InvalidInputLength(#[from] InvalidInputLengthError),
146    #[error(transparent)]
147    CoseKeyToPkcs8(#[from] CoseKeyToPkcs8Error),
148}
149
150#[allow(missing_docs)]
151pub fn fill_with_credential(
152    view: &Fido2CredentialView,
153    value: Passkey,
154) -> Result<Fido2CredentialFullView, FillCredentialError> {
155    let cred_id: Vec<u8> = value.credential_id.into();
156    let user_handle = value
157        .user_handle
158        .map(|u| B64Url::from(u.to_vec()).to_string());
159    let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string();
160
161    Ok(Fido2CredentialFullView {
162        credential_id: guid_bytes_to_string(&cred_id)?,
163        key_type: "public-key".to_owned(),
164        key_algorithm: "ECDSA".to_owned(),
165        key_curve: "P-256".to_owned(),
166        key_value,
167        rp_id: value.rp_id,
168        rp_name: view.rp_name.clone(),
169        user_handle,
170
171        counter: value.counter.unwrap_or(0).to_string(),
172        user_name: view.user_name.clone(),
173        user_display_name: view.user_display_name.clone(),
174        discoverable: "true".to_owned(),
175        creation_date: chrono::offset::Utc::now(),
176    })
177}
178
179pub(crate) fn try_from_credential_new_view(
180    user: &passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
181    rp: &passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
182) -> Result<Fido2CredentialNewView, InvalidInputLengthError> {
183    let cred_id: Vec<u8> = vec![0; 16];
184    let user_handle = B64Url::from(user.id.to_vec()).to_string();
185
186    Ok(Fido2CredentialNewView {
187        // TODO: Why do we have a credential id here?
188        credential_id: guid_bytes_to_string(&cred_id)?,
189        key_type: "public-key".to_owned(),
190        key_algorithm: "ECDSA".to_owned(),
191        key_curve: "P-256".to_owned(),
192        rp_id: rp.id.clone(),
193        rp_name: rp.name.clone(),
194        user_handle: Some(user_handle),
195
196        counter: 0.to_string(),
197        user_name: user.name.clone(),
198        user_display_name: user.display_name.clone(),
199        creation_date: chrono::offset::Utc::now(),
200    })
201}
202
203pub(crate) fn try_from_credential_full(
204    value: Passkey,
205    user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
206    rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
207    options: passkey::types::ctap2::get_assertion::Options,
208) -> Result<Fido2CredentialFullView, FillCredentialError> {
209    let cred_id: Vec<u8> = value.credential_id.into();
210    let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string();
211    let user_handle = B64Url::from(user.id.to_vec()).to_string();
212
213    Ok(Fido2CredentialFullView {
214        credential_id: guid_bytes_to_string(&cred_id)?,
215        key_type: "public-key".to_owned(),
216        key_algorithm: "ECDSA".to_owned(),
217        key_curve: "P-256".to_owned(),
218        key_value,
219        rp_id: value.rp_id,
220        rp_name: rp.name,
221        user_handle: Some(user_handle),
222
223        counter: value.counter.unwrap_or(0).to_string(),
224        user_name: user.name,
225        user_display_name: user.display_name,
226        discoverable: options.rk.to_string(),
227        creation_date: chrono::offset::Utc::now(),
228    })
229}
230
231#[allow(missing_docs)]
232#[derive(Debug, Error)]
233#[error("Input should be a 16 byte array")]
234pub struct InvalidInputLengthError;
235
236#[allow(missing_docs)]
237pub fn guid_bytes_to_string(source: &[u8]) -> Result<String, InvalidInputLengthError> {
238    if source.len() != 16 {
239        return Err(InvalidInputLengthError);
240    }
241    Ok(uuid::Uuid::from_bytes(source.try_into().expect("Invalid length")).to_string())
242}
243
244#[allow(missing_docs)]
245#[derive(Debug, Error)]
246#[error("Invalid GUID")]
247pub struct InvalidGuidError;
248
249#[allow(missing_docs)]
250pub fn string_to_guid_bytes(source: &str) -> Result<Vec<u8>, InvalidGuidError> {
251    if source.starts_with("b64.") {
252        let bytes =
253            B64Url::try_from(source.trim_start_matches("b64.")).map_err(|_| InvalidGuidError)?;
254        Ok(bytes.as_bytes().to_vec())
255    } else {
256        let Ok(uuid) = uuid::Uuid::try_parse(source) else {
257            return Err(InvalidGuidError);
258        };
259        Ok(uuid.as_bytes().to_vec())
260    }
261}
262
263#[allow(missing_docs)]
264#[derive(Debug, Error)]
265#[error("Unknown enum value")]
266pub struct UnknownEnumError;
267
268// Some utilities to convert back and forth between enums and strings
269fn get_enum_from_string_name<T: serde::de::DeserializeOwned>(
270    s: &str,
271) -> Result<T, UnknownEnumError> {
272    let serialized = format!(r#""{s}""#);
273    let deserialized: T = serde_json::from_str(&serialized).map_err(|_| UnknownEnumError)?;
274    Ok(deserialized)
275}
276
277fn get_string_name_from_enum(s: impl serde::Serialize) -> Result<String, serde_json::Error> {
278    let serialized = serde_json::to_string(&s)?;
279    let deserialized: String = serde_json::from_str(&serialized)?;
280    Ok(deserialized)
281}
282
283#[cfg(test)]
284mod tests {
285    use passkey::types::webauthn::AuthenticatorAttachment;
286
287    use super::{get_enum_from_string_name, get_string_name_from_enum};
288
289    #[test]
290    fn test_enum_string_conversion_works_as_expected() {
291        assert_eq!(
292            get_string_name_from_enum(AuthenticatorAttachment::CrossPlatform).unwrap(),
293            "cross-platform"
294        );
295
296        assert_eq!(
297            get_enum_from_string_name::<AuthenticatorAttachment>("cross-platform").unwrap(),
298            AuthenticatorAttachment::CrossPlatform
299        );
300    }
301
302    #[test]
303    fn string_to_guid_with_uuid_works() {
304        let uuid = "d548826e-79b4-db40-a3d8-11116f7e8349";
305        let bytes = super::string_to_guid_bytes(uuid).unwrap();
306        assert_eq!(
307            bytes,
308            vec![
309                213, 72, 130, 110, 121, 180, 219, 64, 163, 216, 17, 17, 111, 126, 131, 73
310            ]
311        );
312    }
313
314    #[test]
315    fn string_to_guid_with_b64_works() {
316        let b64 = "b64.1UiCbnm020Cj2BERb36DSQ";
317        let bytes = super::string_to_guid_bytes(b64).unwrap();
318        assert_eq!(
319            bytes,
320            vec![
321                213, 72, 130, 110, 121, 180, 219, 64, 163, 216, 17, 17, 111, 126, 131, 73
322            ]
323        );
324    }
325}