bitwarden_shared_unlock/wasm/
drivers.rs1use bitwarden_core::UserId;
2use bitwarden_crypto::SymmetricCryptoKey;
3use bitwarden_ipc::{Endpoint, HostId};
4use bitwarden_threading::ThreadBoundRunner;
5use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
6use wasm_bindgen_futures::js_sys;
7
8use crate::{PeerLockState, SharedUnlockDriver};
9
10#[wasm_bindgen(typescript_custom_section)]
11const TS_CUSTOM_TYPES: &'static str = r#"
12export interface SharedUnlockDriver {
13 lock_user(user_id: UserId): Promise<void>;
14 unlock_user(user_id: UserId, user_key: SymmetricKey): Promise<void>;
15 list_users(): Promise<UserId[]>;
16 suppress_vault_timeout(user_id: UserId, suppression_duration: number): Promise<void>;
17 get_client_name(): Promise<string>;
18 get_vault_url(user_id: UserId): Promise<string | undefined>;
19 on_peer_state(user_id: UserId, lock_state: PeerLockState): Promise<void>;
20 set_last_manual_lock(user_id: UserId, locked_at: number): Promise<void>;
21 get_last_manual_lock(user_id: UserId): Promise<number | undefined>;
22}
23"#;
24
25#[wasm_bindgen]
26extern "C" {
27 #[wasm_bindgen(js_name = SharedUnlockDriver, typescript_type = "SharedUnlockDriver")]
29 pub type RawJsSharedUnlockDriver;
30
31 #[wasm_bindgen(method, catch)]
32 async fn lock_user(this: &RawJsSharedUnlockDriver, user_id: UserId) -> Result<(), JsValue>;
33 #[wasm_bindgen(method, catch)]
34 async fn unlock_user(
35 this: &RawJsSharedUnlockDriver,
36 user_id: UserId,
37 user_key: SymmetricCryptoKey,
38 ) -> Result<(), JsValue>;
39 #[wasm_bindgen(method, catch)]
40 async fn list_users(this: &RawJsSharedUnlockDriver) -> Result<js_sys::Array, JsValue>;
41
42 #[wasm_bindgen(method, catch)]
44 async fn suppress_vault_timeout(
45 this: &RawJsSharedUnlockDriver,
46 user_id: UserId,
47 suppression_duration: f64,
48 ) -> Result<(), JsValue>;
49
50 #[wasm_bindgen(method, catch)]
52 async fn get_client_name(this: &RawJsSharedUnlockDriver) -> Result<JsValue, JsValue>;
53
54 #[wasm_bindgen(method, catch)]
57 async fn get_vault_url(
58 this: &RawJsSharedUnlockDriver,
59 user_id: UserId,
60 ) -> Result<JsValue, JsValue>;
61
62 #[wasm_bindgen(method, catch)]
64 async fn on_peer_state(
65 this: &RawJsSharedUnlockDriver,
66 user_id: UserId,
67 lock_state: PeerLockState,
68 ) -> Result<(), JsValue>;
69
70 #[wasm_bindgen(method, catch)]
72 async fn set_last_manual_lock(
73 this: &RawJsSharedUnlockDriver,
74 user_id: UserId,
75 locked_at: f64,
76 ) -> Result<(), JsValue>;
77
78 #[wasm_bindgen(method, catch)]
80 async fn get_last_manual_lock(
81 this: &RawJsSharedUnlockDriver,
82 user_id: UserId,
83 ) -> Result<JsValue, JsValue>;
84}
85
86pub(super) struct JsSharedUnlockDriver {
87 runner: ThreadBoundRunner<RawJsSharedUnlockDriver>,
88}
89
90impl JsSharedUnlockDriver {
91 pub(super) fn new(driver: RawJsSharedUnlockDriver) -> Self {
92 Self {
93 runner: ThreadBoundRunner::new(driver),
94 }
95 }
96}
97
98async fn list_users(driver: &RawJsSharedUnlockDriver) -> Vec<UserId> {
99 match driver.list_users().await {
100 Ok(array) => array
101 .iter()
102 .filter_map(|js_value| js_value.as_string())
103 .filter_map(|s| s.parse().ok())
104 .collect(),
105 Err(error) => {
106 tracing::error!(?error, "Failed to list users");
107 vec![]
108 }
109 }
110}
111
112#[async_trait::async_trait]
113impl SharedUnlockDriver for JsSharedUnlockDriver {
114 async fn lock_user(&self, user_id: UserId) -> Result<(), ()> {
115 self.runner
116 .run_in_thread(
117 move |driver| async move { driver.lock_user(user_id).await.map_err(|_| ()) },
118 )
119 .await
120 .map_err(|_| ())?
121 }
122
123 async fn unlock_user(&self, user_id: UserId, user_key: SymmetricCryptoKey) -> Result<(), ()> {
124 self.runner
125 .run_in_thread(move |driver| async move {
126 driver.unlock_user(user_id, user_key).await.map_err(|_| ())
127 })
128 .await
129 .map_err(|_| ())?
130 }
131
132 async fn list_users(&self) -> Vec<UserId> {
133 self.runner
134 .run_in_thread(move |driver| async move { list_users(&driver).await })
135 .await
136 .unwrap_or_default()
137 }
138
139 async fn get_vault_url(&self, user_id: UserId) -> Option<String> {
140 self.runner
141 .run_in_thread(move |driver| async move {
142 driver
143 .get_vault_url(user_id)
144 .await
145 .ok()
146 .and_then(|js_value| js_value.as_string())
147 })
148 .await
149 .ok()
150 .flatten()
151 }
152
153 async fn suppress_vault_timeout(
154 &self,
155 user_id: UserId,
156 suppression_duration: std::time::Duration,
157 ) {
158 let result = self
159 .runner
160 .run_in_thread(move |driver| async move {
161 driver
162 .suppress_vault_timeout(user_id, suppression_duration.as_millis() as f64)
163 .await
164 })
165 .await;
166 match result {
167 Ok(Ok(())) => {}
168 Ok(Err(error)) => {
169 tracing::error!(
170 ?error,
171 "Failed to suppress vault timeout for user_id: {}",
172 user_id
173 )
174 }
175 Err(error) => {
176 tracing::error!(
177 ?error,
178 "Failed to suppress vault timeout for user_id: {}",
179 user_id
180 )
181 }
182 }
183 }
184
185 async fn on_peer_state(&self, user_id: UserId, lock_state: PeerLockState) {
186 let result = self
187 .runner
188 .run_in_thread(
189 move |driver| async move { driver.on_peer_state(user_id, lock_state).await },
190 )
191 .await;
192
193 match result {
194 Ok(Ok(())) => {}
195 Ok(Err(error)) => {
196 tracing::error!(?error, %user_id, "Failed to report a peer's lock state")
197 }
198 Err(error) => {
199 tracing::error!(?error, %user_id, "Failed to report a peer's lock state")
200 }
201 }
202 }
203
204 async fn set_last_manual_lock(&self, user_id: UserId, locked_at: u64) {
205 let result = self
206 .runner
207 .run_in_thread(move |driver| async move {
208 driver.set_last_manual_lock(user_id, locked_at as f64).await
209 })
210 .await;
211
212 match result {
213 Ok(Ok(())) => {}
214 Ok(Err(error)) => {
215 tracing::error!(?error, %user_id, "Failed to persist the last manual lock")
216 }
217 Err(error) => {
218 tracing::error!(?error, %user_id, "Failed to persist the last manual lock")
219 }
220 }
221 }
222
223 async fn get_last_manual_lock(&self, user_id: UserId) -> Option<u64> {
224 self.runner
225 .run_in_thread(move |driver| async move {
226 driver
227 .get_last_manual_lock(user_id)
228 .await
229 .ok()
230 .and_then(|js_value| js_value.as_f64())
231 .map(|locked_at| locked_at as u64)
232 })
233 .await
234 .ok()
235 .flatten()
236 }
237
238 async fn discover_leader(&self) -> Option<Endpoint> {
239 self.runner
240 .run_in_thread(move |driver| async move {
241 let client_name = match driver.get_client_name().await {
242 Ok(name) => name.as_string()?,
243 Err(_) => return None,
244 };
245 match client_name.as_str() {
246 "web" => Some(Endpoint::BrowserBackground { id: HostId::Own }),
247 "browser" => Some(Endpoint::DesktopRenderer),
248 "cli" => Some(Endpoint::DesktopRenderer),
249 _ => None,
250 }
251 })
252 .await
253 .ok()
254 .flatten()
255 }
256}