Skip to main content

bitwarden_uniffi/platform/fido2/
mod.rs

1mod device_auth_key;
2
3use std::sync::Arc;
4
5use bitwarden_fido::{
6    CheckUserOptions, ClientData, Fido2CallbackError as BitFido2CallbackError,
7    Fido2CredentialAutofillView, GetAssertionRequest, GetAssertionResult, MakeCredentialRequest,
8    MakeCredentialResult, Origin, PublicKeyCredentialAuthenticatorAssertionResponse,
9    PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity,
10    PublicKeyCredentialUserEntity,
11};
12use bitwarden_vault::{CipherListView, CipherView, EncryptionContext, Fido2CredentialNewView};
13use device_auth_key::{ClientDeviceAuthKeyAuthenticator, DeviceAuthKeyStore};
14
15use crate::error::{Error, Result};
16
17#[derive(uniffi::Object)]
18pub struct ClientFido2(pub(crate) bitwarden_fido::ClientFido2);
19
20#[uniffi::export]
21impl ClientFido2 {
22    pub fn authenticator(
23        &self,
24        user_interface: Arc<dyn Fido2UserInterface>,
25        credential_store: Arc<dyn Fido2CredentialStore>,
26    ) -> Arc<ClientFido2Authenticator> {
27        Arc::new(ClientFido2Authenticator(
28            self.0.clone(),
29            user_interface,
30            credential_store,
31        ))
32    }
33
34    pub fn device_auth_key_authenticator(
35        &self,
36        credential_store: Arc<dyn DeviceAuthKeyStore>,
37    ) -> Arc<ClientDeviceAuthKeyAuthenticator> {
38        Arc::new(ClientDeviceAuthKeyAuthenticator {
39            client: self.0.clone(),
40            store: credential_store,
41        })
42    }
43
44    pub fn client(
45        &self,
46        user_interface: Arc<dyn Fido2UserInterface>,
47        credential_store: Arc<dyn Fido2CredentialStore>,
48    ) -> Arc<ClientFido2Client> {
49        Arc::new(ClientFido2Client(ClientFido2Authenticator(
50            self.0.clone(),
51            user_interface,
52            credential_store,
53        )))
54    }
55
56    pub fn decrypt_fido2_autofill_credentials(
57        &self,
58        cipher_view: CipherView,
59    ) -> Result<Vec<Fido2CredentialAutofillView>> {
60        let result = self
61            .0
62            .decrypt_fido2_autofill_credentials(cipher_view)
63            .map_err(Error::DecryptFido2AutofillCredentials)?;
64
65        Ok(result)
66    }
67}
68
69#[derive(uniffi::Object)]
70pub struct ClientFido2Authenticator(
71    pub(crate) bitwarden_fido::ClientFido2,
72    pub(crate) Arc<dyn Fido2UserInterface>,
73    pub(crate) Arc<dyn Fido2CredentialStore>,
74);
75
76#[uniffi::export(async_runtime = "tokio")]
77impl ClientFido2Authenticator {
78    pub async fn make_credential(
79        &self,
80        request: MakeCredentialRequest,
81    ) -> Result<MakeCredentialResult> {
82        let ui = UniffiTraitBridge(self.1.as_ref());
83        let cs = UniffiTraitBridge(self.2.as_ref());
84        let mut auth = self.0.create_authenticator(&ui, &cs);
85
86        let result = auth
87            .make_credential(request)
88            .await
89            .map_err(Error::MakeCredential)?;
90        Ok(result)
91    }
92
93    pub async fn get_assertion(&self, request: GetAssertionRequest) -> Result<GetAssertionResult> {
94        let ui = UniffiTraitBridge(self.1.as_ref());
95        let cs = UniffiTraitBridge(self.2.as_ref());
96        let mut auth = self.0.create_authenticator(&ui, &cs);
97
98        let result = auth
99            .get_assertion(request)
100            .await
101            .map_err(Error::GetAssertion)?;
102        Ok(result)
103    }
104
105    pub async fn silently_discover_credentials(
106        &self,
107        rp_id: String,
108        user_handle: Option<Vec<u8>>,
109    ) -> Result<Vec<Fido2CredentialAutofillView>> {
110        let ui = UniffiTraitBridge(self.1.as_ref());
111        let cs = UniffiTraitBridge(self.2.as_ref());
112        let mut auth = self.0.create_authenticator(&ui, &cs);
113
114        let result = auth
115            .silently_discover_credentials(rp_id, user_handle)
116            .await
117            .map_err(Error::SilentlyDiscoverCredentials)?;
118        Ok(result)
119    }
120
121    pub async fn credentials_for_autofill(&self) -> Result<Vec<Fido2CredentialAutofillView>> {
122        let ui = UniffiTraitBridge(self.1.as_ref());
123        let cs = UniffiTraitBridge(self.2.as_ref());
124        let mut auth = self.0.create_authenticator(&ui, &cs);
125
126        let result = auth
127            .credentials_for_autofill()
128            .await
129            .map_err(Error::CredentialsForAutofill)?;
130        Ok(result)
131    }
132}
133
134#[derive(uniffi::Object)]
135pub struct ClientFido2Client(pub(crate) ClientFido2Authenticator);
136
137#[uniffi::export(async_runtime = "tokio")]
138impl ClientFido2Client {
139    pub async fn register(
140        &self,
141        origin: Origin,
142        request: String,
143        client_data: ClientData,
144    ) -> Result<PublicKeyCredentialAuthenticatorAttestationResponse> {
145        let ui = UniffiTraitBridge(self.0.1.as_ref());
146        let cs = UniffiTraitBridge(self.0.2.as_ref());
147        let mut client = self.0.0.create_client(&ui, &cs);
148
149        let result = client
150            .register(origin, request, client_data)
151            .await
152            .map_err(Error::Fido2Client)?;
153        Ok(result)
154    }
155
156    pub async fn authenticate(
157        &self,
158        origin: Origin,
159        request: String,
160        client_data: ClientData,
161    ) -> Result<PublicKeyCredentialAuthenticatorAssertionResponse> {
162        let ui = UniffiTraitBridge(self.0.1.as_ref());
163        let cs = UniffiTraitBridge(self.0.2.as_ref());
164        let mut client = self.0.0.create_client(&ui, &cs);
165
166        let result = client
167            .authenticate(origin, request, client_data)
168            .await
169            .map_err(Error::Fido2Client)?;
170        Ok(result)
171    }
172}
173
174// Note that uniffi doesn't support external traits for now it seems, so we have to duplicate them
175// here.
176
177#[allow(dead_code)]
178#[derive(uniffi::Record)]
179pub struct CheckUserResult {
180    user_present: bool,
181    user_verified: bool,
182}
183
184impl From<CheckUserResult> for bitwarden_fido::CheckUserResult {
185    fn from(val: CheckUserResult) -> Self {
186        Self {
187            user_present: val.user_present,
188            user_verified: val.user_verified,
189        }
190    }
191}
192
193#[allow(dead_code)]
194#[derive(uniffi::Record)]
195pub struct CheckUserAndPickCredentialForCreationResult {
196    cipher: CipherViewWrapper,
197    check_user_result: CheckUserResult,
198}
199
200#[derive(Debug, thiserror::Error, uniffi::Error)]
201pub enum Fido2CallbackError {
202    #[error("The operation requires user interaction")]
203    UserInterfaceRequired,
204
205    #[error("The operation was cancelled by the user")]
206    OperationCancelled,
207
208    #[error("Unknown error: {reason}")]
209    Unknown { reason: String },
210}
211
212// Need to implement this From<> impl in order to handle unexpected callback errors.  See the
213// following page in the Uniffi user guide:
214// <https://mozilla.github.io/uniffi-rs/foreign_traits.html#error-handling>
215impl From<uniffi::UnexpectedUniFFICallbackError> for Fido2CallbackError {
216    fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
217        Self::Unknown { reason: e.reason }
218    }
219}
220
221impl From<Fido2CallbackError> for BitFido2CallbackError {
222    fn from(val: Fido2CallbackError) -> Self {
223        match val {
224            Fido2CallbackError::UserInterfaceRequired => Self::UserInterfaceRequired,
225            Fido2CallbackError::OperationCancelled => Self::OperationCancelled,
226            Fido2CallbackError::Unknown { reason } => Self::Unknown(reason),
227        }
228    }
229}
230
231#[uniffi::export(with_foreign)]
232#[async_trait::async_trait]
233pub trait Fido2UserInterface: Send + Sync {
234    async fn check_user(
235        &self,
236        options: CheckUserOptions,
237        hint: UIHint,
238    ) -> Result<CheckUserResult, Fido2CallbackError>;
239    async fn pick_credential_for_authentication(
240        &self,
241        available_credentials: Vec<CipherView>,
242    ) -> Result<CipherViewWrapper, Fido2CallbackError>;
243    async fn check_user_and_pick_credential_for_creation(
244        &self,
245        options: CheckUserOptions,
246        new_credential: Fido2CredentialNewView,
247    ) -> Result<CheckUserAndPickCredentialForCreationResult, Fido2CallbackError>;
248    fn is_verification_enabled(&self) -> bool;
249}
250
251#[uniffi::export(with_foreign)]
252#[async_trait::async_trait]
253pub trait Fido2CredentialStore: Send + Sync {
254    async fn find_credentials(
255        &self,
256        ids: Option<Vec<Vec<u8>>>,
257        rip_id: String,
258        user_handle: Option<Vec<u8>>,
259    ) -> Result<Vec<CipherView>, Fido2CallbackError>;
260
261    async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError>;
262
263    async fn save_credential(&self, cred: EncryptionContext) -> Result<(), Fido2CallbackError>;
264}
265
266// Because uniffi doesn't support external traits, we have to make a copy of the trait here.
267// Ideally we'd want to implement the original trait for every item that implements our local copy,
268// but the orphan rules don't allow us to blanket implement an external trait. So we have to wrap
269// the trait in a newtype and implement the trait for the newtype.
270struct UniffiTraitBridge<T>(T);
271
272#[async_trait::async_trait]
273impl bitwarden_fido::Fido2CredentialStore for UniffiTraitBridge<&dyn Fido2CredentialStore> {
274    async fn find_credentials(
275        &self,
276        ids: Option<Vec<Vec<u8>>>,
277        rip_id: String,
278        user_handle: Option<Vec<u8>>,
279    ) -> Result<Vec<CipherView>, BitFido2CallbackError> {
280        self.0
281            .find_credentials(ids, rip_id, user_handle)
282            .await
283            .map_err(Into::into)
284    }
285
286    async fn all_credentials(&self) -> Result<Vec<CipherListView>, BitFido2CallbackError> {
287        self.0.all_credentials().await.map_err(Into::into)
288    }
289
290    async fn save_credential(&self, cred: EncryptionContext) -> Result<(), BitFido2CallbackError> {
291        self.0.save_credential(cred).await.map_err(Into::into)
292    }
293}
294
295// Uniffi seems to have trouble generating code for Android when a local trait returns a type from
296// an external crate. If the type is small we can just copy it over and convert back and forth, but
297// Cipher is too big for that to be practical. So we wrap it in a newtype, which is local to the
298// trait and so we can sidestep the Uniffi issue
299#[derive(uniffi::Record)]
300pub struct CipherViewWrapper {
301    cipher: CipherView,
302}
303
304#[derive(uniffi::Enum)]
305pub enum UIHint {
306    InformExcludedCredentialFound(CipherView),
307    InformNoCredentialsFound,
308    RequestNewCredential(PublicKeyCredentialUserEntity, PublicKeyCredentialRpEntity),
309    RequestExistingCredential(CipherView),
310}
311
312impl From<bitwarden_fido::UiHint<'_, CipherView>> for UIHint {
313    fn from(hint: bitwarden_fido::UiHint<'_, CipherView>) -> Self {
314        use bitwarden_fido::UiHint as BWUIHint;
315        match hint {
316            BWUIHint::InformExcludedCredentialFound(cipher) => {
317                UIHint::InformExcludedCredentialFound(cipher.clone())
318            }
319            BWUIHint::InformNoCredentialsFound => UIHint::InformNoCredentialsFound,
320            BWUIHint::RequestNewCredential(user, rp) => UIHint::RequestNewCredential(
321                PublicKeyCredentialUserEntity {
322                    id: user.id.clone().into(),
323                    name: user.name.clone().unwrap_or_default(),
324                    display_name: user.display_name.clone().unwrap_or_default(),
325                },
326                PublicKeyCredentialRpEntity {
327                    id: rp.id.clone(),
328                    name: rp.name.clone(),
329                },
330            ),
331            BWUIHint::RequestExistingCredential(cipher) => {
332                UIHint::RequestExistingCredential(cipher.clone())
333            }
334        }
335    }
336}
337
338#[async_trait::async_trait]
339impl bitwarden_fido::Fido2UserInterface for UniffiTraitBridge<&dyn Fido2UserInterface> {
340    async fn check_user<'a>(
341        &self,
342        options: CheckUserOptions,
343        hint: bitwarden_fido::UiHint<'a, CipherView>,
344    ) -> Result<bitwarden_fido::CheckUserResult, BitFido2CallbackError> {
345        self.0
346            .check_user(options.clone(), hint.into())
347            .await
348            .map(Into::into)
349            .map_err(Into::into)
350    }
351    async fn pick_credential_for_authentication(
352        &self,
353        available_credentials: Vec<CipherView>,
354    ) -> Result<CipherView, BitFido2CallbackError> {
355        self.0
356            .pick_credential_for_authentication(available_credentials)
357            .await
358            .map(|v| v.cipher)
359            .map_err(Into::into)
360    }
361    async fn check_user_and_pick_credential_for_creation(
362        &self,
363        options: CheckUserOptions,
364        new_credential: Fido2CredentialNewView,
365    ) -> Result<(CipherView, bitwarden_fido::CheckUserResult), BitFido2CallbackError> {
366        self.0
367            .check_user_and_pick_credential_for_creation(options, new_credential)
368            .await
369            .map(|v| (v.cipher.cipher, v.check_user_result.into()))
370            .map_err(Into::into)
371    }
372    fn is_verification_enabled(&self) -> bool {
373        self.0.is_verification_enabled()
374    }
375}