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 std::str::FromStr;
10
11use bitwarden_core::UserId;
12use bitwarden_crypto::SymmetricCryptoKey;
13use bitwarden_ipc::{Endpoint, IpcClientExt, RequestError, RpcHandler, RpcRequest};
14use bitwarden_threading::{
15    ThreadBoundRunner,
16    cancellation_token::wasm::{AbortSignal, AbortSignalExt},
17};
18use serde::{Deserialize, Serialize};
19use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
20
21#[wasm_bindgen(typescript_custom_section)]
22const TS_BIOMETRICS_TYPES: &'static str = r#"
23export interface BiometricsUnlock {
24    /**
25     * Returns the status of biometrics unlock for the given user.
26     */
27    get_biometrics_status(user_id: UserId): Promise<BiometricsStatus>;
28    /**
29     * Triggers a biometric unlock flow for the given user. Resolves with the
30     * user's symmetric key on success, or `undefined` if the unlock was
31     * canceled or otherwise failed.
32     * 
33     * Please note, the user-key is only returned temporarily until
34     * shared unlock is rolled out and will be removed afterwards.
35     */
36    unlock_biometrics(user_id: UserId): Promise<SymmetricKey | undefined>;
37    /**
38      * Triggers a biometrics UV check. Retruns true if the check succeeded.
39      */
40    authenticate_biometrics(): Promise<boolean>;
41}
42"#;
43
44#[wasm_bindgen]
45extern "C" {
46    /// JavaScript implementation of user lock-management operations used by shared unlock.
47    #[wasm_bindgen(js_name = BiometricsUnlock, typescript_type = "BiometricsUnlock")]
48    pub type RawJsBiometricsDriver;
49
50    /// Returns the status of biometrics unlock for the given user.
51    #[wasm_bindgen(method, catch)]
52    async fn get_biometrics_status(
53        this: &RawJsBiometricsDriver,
54        user_id: UserId,
55    ) -> Result<JsValue, JsValue>;
56
57    /// Triggers a biometric unlock flow for the given user. Returns the user's
58    /// symmetric key on success, or `None` if the unlock was canceled/failed.
59    #[wasm_bindgen(method, catch)]
60    async fn unlock_biometrics(
61        this: &RawJsBiometricsDriver,
62        user_id: UserId,
63    ) -> Result<JsValue, JsValue>;
64
65    /// Triggers a biometrics UV check. Returns true if the check succeeded.
66    #[wasm_bindgen(method, catch)]
67    async fn authenticate_biometrics(this: &RawJsBiometricsDriver) -> Result<JsValue, JsValue>;
68}
69
70pub(super) struct JsBiometricsUnlock {
71    runner: ThreadBoundRunner<RawJsBiometricsDriver>,
72}
73
74impl JsBiometricsUnlock {
75    pub(super) fn new(runner: ThreadBoundRunner<RawJsBiometricsDriver>) -> Self {
76        Self { runner }
77    }
78
79    pub(super) async fn get_biometrics_status(&self, user_id: UserId) -> BiometricsStatus {
80        self.runner
81            .run_in_thread(move |driver| async move {
82                let status = driver
83                    .get_biometrics_status(user_id)
84                    .await
85                    .unwrap_or(BiometricsStatus::NotEnabled.into());
86                status.try_into().unwrap_or(BiometricsStatus::NotEnabled)
87            })
88            .await
89            .unwrap_or(BiometricsStatus::NotEnabled)
90    }
91
92    pub(super) async fn unlock_biometrics(&self, user_id: UserId) -> Option<SymmetricCryptoKey> {
93        self.runner
94            .run_in_thread(move |driver| async move {
95                driver
96                    .unlock_biometrics(user_id)
97                    .await
98                    .map(|js_value| {
99                        SymmetricCryptoKey::from_str(js_value.as_string()?.as_str()).ok()
100                    })
101                    .ok()
102                    .flatten()
103            })
104            .await
105            .ok()
106            .flatten()
107    }
108
109    pub(super) async fn authenticate_biometrics(&self) -> bool {
110        self.runner
111            .run_in_thread(move |driver| async move {
112                driver
113                    .authenticate_biometrics()
114                    .await
115                    .ok()
116                    .and_then(|v| v.as_bool())
117                    .unwrap_or(false)
118            })
119            .await
120            .unwrap_or(false)
121    }
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[wasm_bindgen]
126/// The current biometric capability state for a specific user on this client.
127pub enum BiometricsStatus {
128    /// Biometrics is available and can be used immediately.
129    Available,
130    /// Biometrics is supported, but user interaction is required before unlock can proceed.
131    UnlockNeeded,
132    /// Biometrics hardware or platform support is unavailable.
133    HardwareUnavailable,
134    /// Biometrics is supported but not enabled for this user.
135    NotEnabled,
136}
137
138impl TryFrom<JsValue> for BiometricsStatus {
139    type Error = ();
140
141    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
142        let status = value.as_f64().ok_or(())? as u8;
143        match status {
144            0 => Ok(BiometricsStatus::Available),
145            1 => Ok(BiometricsStatus::UnlockNeeded),
146            2 => Ok(BiometricsStatus::HardwareUnavailable),
147            3 => Ok(BiometricsStatus::NotEnabled),
148            _ => Err(()),
149        }
150    }
151}
152
153/// RPC request to check whether biometric unlock is available for a user.
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct GetBiometricsStatusRequest {
156    /// The user to check biometrics status for.
157    pub user_id: UserId,
158}
159
160impl RpcRequest for GetBiometricsStatusRequest {
161    type Response = BiometricsStatus;
162
163    const NAME: &str = "GetBiometricsStatus";
164}
165
166/// RPC request to trigger biometric unlock for a user.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct UnlockBiometricsRequest {
169    /// The user to unlock with biometrics.
170    pub user_id: UserId,
171}
172
173/// RPC response for [`UnlockBiometricsRequest`]. `user_key` is `None` if the
174/// biometric prompt was canceled or otherwise failed on the responding device.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176#[cfg_attr(
177    feature = "wasm",
178    derive(tsify::Tsify),
179    tsify(into_wasm_abi, from_wasm_abi)
180)]
181pub struct UnlockBiometricsResponse {
182    /// The unlocked user's symmetric key, if the unlock succeeded.
183    #[tsify(optional, type = "SymmetricKey")]
184    pub user_key: Option<SymmetricCryptoKey>,
185}
186
187impl RpcRequest for UnlockBiometricsRequest {
188    type Response = UnlockBiometricsResponse;
189
190    const NAME: &str = "UnlockBiometrics";
191}
192
193/// RPC request to trigger a biometrics UV check.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct AuthenticateBiometricsRequest;
196
197impl RpcRequest for AuthenticateBiometricsRequest {
198    type Response = bool;
199
200    const NAME: &str = "AuthenticateBiometrics";
201}
202
203/// RPC handler for [`GetBiometricsStatusRequest`].
204pub struct GetBiometricsStatusHandler {
205    biometrics_unlock: JsBiometricsUnlock,
206}
207
208impl GetBiometricsStatusHandler {
209    /// Creates a new handler backed by the provided biometrics driver.
210    fn new(biometrics_unlock: JsBiometricsUnlock) -> Self {
211        Self { biometrics_unlock }
212    }
213}
214
215impl RpcHandler for GetBiometricsStatusHandler {
216    type Request = GetBiometricsStatusRequest;
217
218    async fn handle(&self, request: Self::Request) -> BiometricsStatus {
219        self.biometrics_unlock
220            .get_biometrics_status(request.user_id)
221            .await
222    }
223}
224
225/// RPC handler for [`UnlockBiometricsRequest`].
226pub struct UnlockBiometricsHandler {
227    biometrics_unlock: JsBiometricsUnlock,
228}
229
230impl UnlockBiometricsHandler {
231    /// Creates a new handler backed by the provided biometrics driver.
232    fn new(biometrics_unlock: JsBiometricsUnlock) -> Self {
233        Self { biometrics_unlock }
234    }
235}
236
237impl RpcHandler for UnlockBiometricsHandler {
238    type Request = UnlockBiometricsRequest;
239
240    async fn handle(&self, request: Self::Request) -> UnlockBiometricsResponse {
241        let user_key = self
242            .biometrics_unlock
243            .unlock_biometrics(request.user_id)
244            .await;
245        UnlockBiometricsResponse { user_key }
246    }
247}
248
249/// RPC handler for [`AuthenticateBiometricsRequest`].
250pub struct AuthenticateBiometricsHandler {
251    biometrics_unlock: JsBiometricsUnlock,
252}
253
254impl AuthenticateBiometricsHandler {
255    /// Creates a new handler backed by the provided biometrics driver.
256    fn new(biometrics_unlock: JsBiometricsUnlock) -> Self {
257        Self { biometrics_unlock }
258    }
259}
260
261impl RpcHandler for AuthenticateBiometricsHandler {
262    type Request = AuthenticateBiometricsRequest;
263
264    async fn handle(&self, _: Self::Request) -> bool {
265        self.biometrics_unlock.authenticate_biometrics().await
266    }
267}
268
269/// Registers shared-unlock biometrics RPC handlers on the IPC client.
270#[wasm_bindgen(js_name = ipcRegisterBiometricsHandlers)]
271pub async fn ipc_register_biometrics_handlers(
272    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
273    biometrics_unlock: RawJsBiometricsDriver,
274) {
275    let runner = ThreadBoundRunner::new(biometrics_unlock);
276    let handler_driver = JsBiometricsUnlock::new(runner.clone());
277
278    ipc_client
279        .client
280        .register_rpc_handler(GetBiometricsStatusHandler::new(handler_driver))
281        .await;
282    ipc_client
283        .client
284        .register_rpc_handler(UnlockBiometricsHandler::new(JsBiometricsUnlock::new(
285            runner.clone(),
286        )))
287        .await;
288    ipc_client
289        .client
290        .register_rpc_handler(AuthenticateBiometricsHandler::new(JsBiometricsUnlock::new(
291            runner,
292        )))
293        .await;
294}
295
296/// Sends a `GetBiometricsStatus` RPC request to the desktop renderer
297#[wasm_bindgen(js_name = ipcRequestGetBiometricsStatus)]
298pub async fn ipc_request_get_biometrics_status(
299    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
300    user_id: UserId,
301    abort_signal: Option<AbortSignal>,
302) -> Result<BiometricsStatus, RequestError> {
303    ipc_client
304        .client
305        .request(
306            GetBiometricsStatusRequest { user_id },
307            Endpoint::DesktopRenderer,
308            abort_signal.map(|signal| signal.to_cancellation_token()),
309        )
310        .await
311}
312
313/// Sends an `UnlockBiometrics` RPC request to the desktop renderer.
314#[wasm_bindgen(js_name = ipcRequestUnlockBiometrics)]
315pub async fn ipc_request_unlock_biometrics(
316    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
317    user_id: UserId,
318    abort_signal: Option<AbortSignal>,
319) -> Result<UnlockBiometricsResponse, RequestError> {
320    ipc_client
321        .client
322        .request(
323            UnlockBiometricsRequest { user_id },
324            Endpoint::DesktopRenderer,
325            abort_signal.map(|signal| signal.to_cancellation_token()),
326        )
327        .await
328}
329
330/// Sends an `AuthenticateBiometrics` RPC request to the desktop renderer.
331#[wasm_bindgen(js_name = ipcRequestAuthenticateBiometrics)]
332pub async fn ipc_request_authenticate_biometrics(
333    ipc_client: &bitwarden_ipc::wasm::JsIpcClient,
334    abort_signal: Option<AbortSignal>,
335) -> Result<bool, RequestError> {
336    ipc_client
337        .client
338        .request(
339            AuthenticateBiometricsRequest,
340            Endpoint::DesktopRenderer,
341            abort_signal.map(|signal| signal.to_cancellation_token()),
342        )
343        .await
344}