bitwarden_uniffi/platform/
fido2.rs

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