Skip to main content

bitwarden_shared_unlock/wasm/
biometrics.rs

1//! WASM-specific biometrics-over-IPC. This allows clients, such as browser, CLI and web to
2//! interact with the platform biometrics system of the desktop app.
3//!
4//! Note: This should eventually be moved to a bitwarden-biometrics crate that also contains
5//! implementations for platform biometrics on all platforms. However, it is too early for that
6//! currently, and it is not sufficiently clear what is required / how it should be structured, as
7//! it should capture the needs of all platforms (mobile and web).
8
9use bitwarden_core::UserId;
10use bitwarden_ipc::{Endpoint, IpcClientExt, RequestError, RpcHandler, RpcRequest};
11use bitwarden_threading::{
12    ThreadBoundRunner,
13    cancellation_token::wasm::{AbortSignal, AbortSignalExt},
14};
15use serde::{Deserialize, Serialize};
16use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
17
18#[wasm_bindgen(typescript_custom_section)]
19const TS_BIOMETRICS_TYPES: &'static str = r#"
20export interface BiometricsUnlock {
21    /**
22     * Returns the status of biometrics unlock for the given user.
23     */
24    get_biometrics_status(user_id: UserId): Promise<BiometricsStatus>;
25    /**
26     * Triggers a biometric unlock flow for the given user.
27     */
28    unlock_biometrics(user_id: UserId): Promise<void>;
29    /**
30      * Triggers a biometrics UV check. Retruns true if the check succeeded.
31      */
32    authenticate_biometrics(): Promise<boolean>;
33}
34"#;
35
36#[wasm_bindgen]
37extern "C" {
38    /// JavaScript implementation of user lock-management operations used by shared unlock.
39    #[wasm_bindgen(js_name = BiometricsUnlock, typescript_type = "BiometricsUnlock")]
40    pub type RawJsBiometricsDriver;
41
42    /// Returns the status of biometrics unlock for the given user.
43    #[wasm_bindgen(method, catch)]
44    async fn get_biometrics_status(
45        this: &RawJsBiometricsDriver,
46        user_id: UserId,
47    ) -> Result<JsValue, JsValue>;
48
49    /// Triggers a biometric unlock flow for the given user.
50    #[wasm_bindgen(method, catch)]
51    async fn unlock_biometrics(
52        this: &RawJsBiometricsDriver,
53        user_id: UserId,
54    ) -> Result<(), JsValue>;
55
56    /// Triggers a biometrics UV check. Returns true if the check succeeded.
57    #[wasm_bindgen(method, catch)]
58    async fn authenticate_biometrics(this: &RawJsBiometricsDriver) -> Result<bool, JsValue>;
59}
60
61pub(super) struct JsBiometricsUnlock {
62    runner: ThreadBoundRunner<RawJsBiometricsDriver>,
63}
64
65impl JsBiometricsUnlock {
66    pub(super) fn new(runner: ThreadBoundRunner<RawJsBiometricsDriver>) -> Self {
67        Self { runner }
68    }
69
70    pub(super) async fn get_biometrics_status(&self, user_id: UserId) -> BiometricsStatus {
71        self.runner
72            .run_in_thread(move |driver| async move {
73                let status = driver
74                    .get_biometrics_status(user_id)
75                    .await
76                    .unwrap_or(BiometricsStatus::NotEnabled.into());
77                status.try_into().unwrap_or(BiometricsStatus::NotEnabled)
78            })
79            .await
80            .unwrap_or(BiometricsStatus::NotEnabled)
81    }
82
83    pub(super) async fn unlock_biometrics(&self, user_id: UserId) {
84        self.runner
85            .run_in_thread(move |driver| async move {
86                driver.unlock_biometrics(user_id).await.unwrap_or(())
87            })
88            .await
89            .unwrap_or(())
90    }
91
92    pub(super) async fn authenticate_biometrics(&self) -> bool {
93        self.runner
94            .run_in_thread(move |driver| async move {
95                driver.authenticate_biometrics().await.unwrap_or(false)
96            })
97            .await
98            .unwrap_or(false)
99    }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[wasm_bindgen]
104/// The current biometric capability state for a specific user on this client.
105pub enum BiometricsStatus {
106    /// Biometrics is available and can be used immediately.
107    Available,
108    /// Biometrics is supported, but user interaction is required before unlock can proceed.
109    UnlockNeeded,
110    /// Biometrics hardware or platform support is unavailable.
111    HardwareUnavailable,
112    /// Biometrics is supported but not enabled for this user.
113    NotEnabled,
114}
115
116impl TryFrom<JsValue> for BiometricsStatus {
117    type Error = ();
118
119    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
120        let status = value.as_f64().ok_or(())? as u8;
121        match status {
122            0 => Ok(BiometricsStatus::Available),
123            1 => Ok(BiometricsStatus::UnlockNeeded),
124            2 => Ok(BiometricsStatus::HardwareUnavailable),
125            3 => Ok(BiometricsStatus::NotEnabled),
126            _ => Err(()),
127        }
128    }
129}
130
131/// RPC request to check whether biometric unlock is available for a user.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct GetBiometricsStatusRequest {
134    /// The user to check biometrics status for.
135    pub user_id: UserId,
136}
137
138impl RpcRequest for GetBiometricsStatusRequest {
139    type Response = BiometricsStatus;
140
141    const NAME: &str = "GetBiometricsStatus";
142}
143
144/// RPC request to trigger biometric unlock for a user.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct UnlockBiometricsRequest {
147    /// The user to unlock with biometrics.
148    pub user_id: UserId,
149}
150
151impl RpcRequest for UnlockBiometricsRequest {
152    type Response = ();
153
154    const NAME: &str = "UnlockBiometrics";
155}
156
157/// RPC request to trigger a biometrics UV check.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct AuthenticateBiometricsRequest;
160
161impl RpcRequest for AuthenticateBiometricsRequest {
162    type Response = bool;
163
164    const NAME: &str = "AuthenticateBiometrics";
165}
166
167/// RPC handler for [`GetBiometricsStatusRequest`].
168pub struct GetBiometricsStatusHandler {
169    biometrics_unlock: JsBiometricsUnlock,
170}
171
172impl GetBiometricsStatusHandler {
173    /// Creates a new handler backed by the provided biometrics driver.
174    fn new(biometrics_unlock: JsBiometricsUnlock) -> Self {
175        Self { biometrics_unlock }
176    }
177}
178
179impl RpcHandler for GetBiometricsStatusHandler {
180    type Request = GetBiometricsStatusRequest;
181
182    async fn handle(&self, request: Self::Request) -> BiometricsStatus {
183        self.biometrics_unlock
184            .get_biometrics_status(request.user_id)
185            .await
186    }
187}
188
189/// RPC handler for [`UnlockBiometricsRequest`].
190pub struct UnlockBiometricsHandler {
191    biometrics_unlock: JsBiometricsUnlock,
192}
193
194impl UnlockBiometricsHandler {
195    /// Creates a new handler backed by the provided biometrics driver.
196    fn new(biometrics_unlock: JsBiometricsUnlock) -> Self {
197        Self { biometrics_unlock }
198    }
199}
200
201impl RpcHandler for UnlockBiometricsHandler {
202    type Request = UnlockBiometricsRequest;
203
204    async fn handle(&self, request: Self::Request) {
205        self.biometrics_unlock
206            .unlock_biometrics(request.user_id)
207            .await
208    }
209}
210
211/// RPC handler for [`AuthenticateBiometricsRequest`].
212pub struct AuthenticateBiometricsHandler {
213    biometrics_unlock: JsBiometricsUnlock,
214}
215
216impl AuthenticateBiometricsHandler {
217    /// Creates a new handler backed by the provided biometrics driver.
218    fn new(biometrics_unlock: JsBiometricsUnlock) -> Self {
219        Self { biometrics_unlock }
220    }
221}
222
223impl RpcHandler for AuthenticateBiometricsHandler {
224    type Request = AuthenticateBiometricsRequest;
225
226    async fn handle(&self, _: Self::Request) -> bool {
227        self.biometrics_unlock.authenticate_biometrics().await
228    }
229}
230
231/// Registers shared-unlock biometrics RPC handlers on the IPC client.
232#[wasm_bindgen(js_name = ipcRegisterBiometricsHandlers)]
233pub async fn ipc_register_biometrics_handlers(
234    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
235    biometrics_unlock: RawJsBiometricsDriver,
236) {
237    let runner = ThreadBoundRunner::new(biometrics_unlock);
238    let handler_driver = JsBiometricsUnlock::new(runner.clone());
239
240    ipc_client
241        .client
242        .register_rpc_handler(GetBiometricsStatusHandler::new(handler_driver))
243        .await;
244    ipc_client
245        .client
246        .register_rpc_handler(UnlockBiometricsHandler::new(JsBiometricsUnlock::new(
247            runner.clone(),
248        )))
249        .await;
250    ipc_client
251        .client
252        .register_rpc_handler(AuthenticateBiometricsHandler::new(JsBiometricsUnlock::new(
253            runner,
254        )))
255        .await;
256}
257
258/// Sends a `GetBiometricsStatus` RPC request to the desktop renderer
259#[wasm_bindgen(js_name = ipcRequestGetBiometricsStatus)]
260pub async fn ipc_request_get_biometrics_status(
261    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
262    user_id: UserId,
263    abort_signal: Option<AbortSignal>,
264) -> Result<BiometricsStatus, RequestError> {
265    ipc_client
266        .client
267        .request(
268            GetBiometricsStatusRequest { user_id },
269            Endpoint::DesktopRenderer,
270            abort_signal.map(|signal| signal.to_cancellation_token()),
271        )
272        .await
273}
274
275/// Sends an `UnlockBiometrics` RPC request to the desktop renderer
276#[wasm_bindgen(js_name = ipcRequestUnlockBiometrics)]
277pub async fn ipc_request_unlock_biometrics(
278    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
279    user_id: UserId,
280    abort_signal: Option<AbortSignal>,
281) -> Result<(), RequestError> {
282    ipc_client
283        .client
284        .request(
285            UnlockBiometricsRequest { user_id },
286            Endpoint::DesktopRenderer,
287            abort_signal.map(|signal| signal.to_cancellation_token()),
288        )
289        .await
290}
291
292/// Sends an `AuthenticateBiometrics` RPC request to the desktop renderer.
293#[wasm_bindgen(js_name = ipcRequestAuthenticateBiometrics)]
294pub async fn ipc_request_authenticate_biometrics(
295    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
296    abort_signal: Option<AbortSignal>,
297) -> Result<bool, RequestError> {
298    ipc_client
299        .client
300        .request(
301            AuthenticateBiometricsRequest,
302            Endpoint::DesktopRenderer,
303            abort_signal.map(|signal| signal.to_cancellation_token()),
304        )
305        .await
306}