bitwarden_shared_unlock/wasm/
biometrics.rs1use 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 #[wasm_bindgen(js_name = BiometricsUnlock, typescript_type = "BiometricsUnlock")]
48 pub type RawJsBiometricsDriver;
49
50 #[wasm_bindgen(method, catch)]
52 async fn get_biometrics_status(
53 this: &RawJsBiometricsDriver,
54 user_id: UserId,
55 ) -> Result<JsValue, JsValue>;
56
57 #[wasm_bindgen(method, catch)]
60 async fn unlock_biometrics(
61 this: &RawJsBiometricsDriver,
62 user_id: UserId,
63 ) -> Result<JsValue, JsValue>;
64
65 #[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]
126pub enum BiometricsStatus {
128 Available,
130 UnlockNeeded,
132 HardwareUnavailable,
134 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#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct GetBiometricsStatusRequest {
156 pub user_id: UserId,
158}
159
160impl RpcRequest for GetBiometricsStatusRequest {
161 type Response = BiometricsStatus;
162
163 const NAME: &str = "GetBiometricsStatus";
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct UnlockBiometricsRequest {
169 pub user_id: UserId,
171}
172
173#[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 #[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#[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
203pub struct GetBiometricsStatusHandler {
205 biometrics_unlock: JsBiometricsUnlock,
206}
207
208impl GetBiometricsStatusHandler {
209 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
225pub struct UnlockBiometricsHandler {
227 biometrics_unlock: JsBiometricsUnlock,
228}
229
230impl UnlockBiometricsHandler {
231 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
249pub struct AuthenticateBiometricsHandler {
251 biometrics_unlock: JsBiometricsUnlock,
252}
253
254impl AuthenticateBiometricsHandler {
255 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#[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#[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#[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#[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}