Skip to main content

bitwarden_shared_unlock/wasm/
drivers.rs

1use 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    /// JavaScript implementation of shared unlock operations used by shared unlock protocol.
28    #[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    /// Supress the vault timeout for the given duration (in milliseconds).
43    #[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    /// Get the client type of the current device
51    #[wasm_bindgen(method, catch)]
52    async fn get_client_name(this: &RawJsSharedUnlockDriver) -> Result<JsValue, JsValue>;
53
54    /// Get vault URL for the user with the given ID, if available. This is used to verify IPC
55    /// message sources.
56    #[wasm_bindgen(method, catch)]
57    async fn get_vault_url(
58        this: &RawJsSharedUnlockDriver,
59        user_id: UserId,
60    ) -> Result<JsValue, JsValue>;
61
62    /// Report a peer's lock state for a user, as carried by a sync this device accepted.
63    #[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    /// Persist the date (milliseconds since the Unix epoch) of the user's last manual lock.
71    #[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    /// Get the persisted date of the user's last manual lock, if any.
79    #[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}