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::{LockState, 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    get_user_key(user_id: UserId): Promise<SymmetricKey | undefined>;
17    suppress_vault_timeout(user_id: UserId, suppression_duration: number): Promise<void>;
18    get_client_name(): Promise<string>;
19    get_vault_url(user_id: UserId): Promise<string | undefined>;
20}
21"#;
22
23#[wasm_bindgen]
24extern "C" {
25    /// JavaScript implementation of shared unlock operations used by shared unlock protocol.
26    #[wasm_bindgen(js_name = SharedUnlockDriver, typescript_type = "SharedUnlockDriver")]
27    pub type RawJsSharedUnlockDriver;
28
29    #[wasm_bindgen(method, catch)]
30    async fn lock_user(this: &RawJsSharedUnlockDriver, user_id: UserId) -> Result<(), JsValue>;
31    #[wasm_bindgen(method, catch)]
32    async fn unlock_user(
33        this: &RawJsSharedUnlockDriver,
34        user_id: UserId,
35        user_key: SymmetricCryptoKey,
36    ) -> Result<(), JsValue>;
37    #[wasm_bindgen(method, catch)]
38    async fn list_users(this: &RawJsSharedUnlockDriver) -> Result<js_sys::Array, JsValue>;
39    #[wasm_bindgen(method, catch)]
40    async fn get_user_key(
41        this: &RawJsSharedUnlockDriver,
42        user_id: UserId,
43    ) -> Result<JsValue, JsValue>;
44
45    /// Supress the vault timeout for the given duration (in milliseconds).
46    #[wasm_bindgen(method, catch)]
47    async fn suppress_vault_timeout(
48        this: &RawJsSharedUnlockDriver,
49        user_id: UserId,
50        suppression_duration: f64,
51    ) -> Result<(), JsValue>;
52
53    /// Get the client type of the current device
54    #[wasm_bindgen(method, catch)]
55    async fn get_client_name(this: &RawJsSharedUnlockDriver) -> Result<JsValue, JsValue>;
56
57    /// Get vault URL for the user with the given ID, if available. This is used to verify IPC
58    /// message sources.
59    #[wasm_bindgen(method, catch)]
60    async fn get_vault_url(
61        this: &RawJsSharedUnlockDriver,
62        user_id: UserId,
63    ) -> Result<JsValue, JsValue>;
64}
65
66pub(super) struct JsSharedUnlockDriver {
67    runner: ThreadBoundRunner<RawJsSharedUnlockDriver>,
68}
69
70impl JsSharedUnlockDriver {
71    pub(super) fn new(driver: RawJsSharedUnlockDriver) -> Self {
72        Self {
73            runner: ThreadBoundRunner::new(driver),
74        }
75    }
76}
77
78#[async_trait::async_trait]
79impl SharedUnlockDriver for JsSharedUnlockDriver {
80    async fn lock_user(&self, user_id: UserId) -> Result<(), ()> {
81        self.runner
82            .run_in_thread(
83                move |driver| async move { driver.lock_user(user_id).await.map_err(|_| ()) },
84            )
85            .await
86            .map_err(|_| ())?
87    }
88
89    async fn unlock_user(&self, user_id: UserId, user_key: SymmetricCryptoKey) -> Result<(), ()> {
90        self.runner
91            .run_in_thread(move |driver| async move {
92                driver.unlock_user(user_id, user_key).await.map_err(|_| ())
93            })
94            .await
95            .map_err(|_| ())?
96    }
97
98    async fn list_users(&self) -> Vec<UserId> {
99        self.runner
100            .run_in_thread(move |driver| async move {
101                match driver.list_users().await {
102                    Ok(array) => array
103                        .iter()
104                        .filter_map(|js_value| js_value.as_string())
105                        .filter_map(|s| s.parse().ok())
106                        .collect(),
107                    Err(_) => vec![],
108                }
109            })
110            .await
111            .unwrap_or_default()
112    }
113
114    async fn get_user_lock_state(&self, user_id: UserId) -> LockState {
115        self.runner
116            .run_in_thread(move |driver| async move {
117                let js_value = driver.get_user_key(user_id).await.ok()?;
118                if js_value.is_null() || js_value.is_undefined() {
119                    return None;
120                }
121                let user_key = SymmetricCryptoKey::try_from(js_value).ok()?;
122                Some(LockState::Unlocked { user_key })
123            })
124            .await
125            .ok()
126            .flatten()
127            .unwrap_or(LockState::Locked)
128    }
129
130    async fn get_vault_url(&self, user_id: UserId) -> Option<String> {
131        self.runner
132            .run_in_thread(move |driver| async move {
133                driver
134                    .get_vault_url(user_id)
135                    .await
136                    .ok()
137                    .and_then(|js_value| js_value.as_string())
138            })
139            .await
140            .ok()
141            .flatten()
142    }
143
144    async fn suppress_vault_timeout(
145        &self,
146        user_id: UserId,
147        suppression_duration: std::time::Duration,
148    ) {
149        let result = self
150            .runner
151            .run_in_thread(move |driver| async move {
152                driver
153                    .suppress_vault_timeout(user_id, suppression_duration.as_millis() as f64)
154                    .await
155            })
156            .await;
157        match result {
158            Ok(Ok(())) => {}
159            Ok(Err(error)) => {
160                tracing::error!(
161                    ?error,
162                    "Failed to suppress vault timeout for user_id: {}",
163                    user_id
164                )
165            }
166            Err(error) => {
167                tracing::error!(
168                    ?error,
169                    "Failed to suppress vault timeout for user_id: {}",
170                    user_id
171                )
172            }
173        }
174    }
175
176    async fn discover_leader(&self) -> Option<Endpoint> {
177        self.runner
178            .run_in_thread(move |driver| async move {
179                let client_name = match driver.get_client_name().await {
180                    Ok(name) => name.as_string()?,
181                    Err(_) => return None,
182                };
183                match client_name.as_str() {
184                    "web" => Some(Endpoint::BrowserBackground { id: HostId::Own }),
185                    "browser" => Some(Endpoint::DesktopRenderer),
186                    "cli" => Some(Endpoint::DesktopRenderer),
187                    _ => None,
188                }
189            })
190            .await
191            .ok()
192            .flatten()
193    }
194}