Skip to main content

hashiverse_client_wasm/
wasm_key_locker.rs

1use crate::with_js_context::JsResultExt;
2use anyhow::{anyhow, Context};
3use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
4use hashiverse_lib::client::key_locker::key_locker::{KeyLocker, KeyLockerManager, GUEST_CLIENT_ID};
5use hashiverse_lib::tools::client_id::ClientId;
6use hashiverse_lib::tools::keys::Keys;
7use hashiverse_lib::tools::types::{PQCommitmentBytes, Signature, VerificationKeyBytes, SIGNATURE_BYTES};
8use indexed_db_futures::database::Database;
9use indexed_db_futures::prelude::*;
10use indexed_db_futures::transaction::TransactionMode;
11use indexed_db_futures::KeyPath;
12use js_sys::{JsString, Reflect, Uint8Array};
13use log::warn;
14use std::sync::Arc;
15use wasm_bindgen::{JsCast, JsValue};
16use wasm_bindgen_futures::js_sys::Object;
17use wasm_bindgen_futures::JsFuture;
18use web_sys::{Crypto, CryptoKey, SubtleCrypto};
19
20const DATABASE_NAME: &str = "hashiverse.key_locker";
21const STORE_NAME: &str = "key";
22
23pub fn get_crypto() -> Result<Crypto, anyhow::Error> {
24    let global = js_sys::global();
25
26    if let Some(worker) = global.dyn_ref::<web_sys::WorkerGlobalScope>() {
27        return worker.crypto().map_err(|e| anyhow::anyhow!("{:?}", e));
28    }
29
30    if let Some(win) = global.dyn_ref::<web_sys::Window>() {
31        return win.crypto().map_err(|e| anyhow::anyhow!("{:?}", e));
32    }
33
34    anyhow::bail!("Could not find a global crypto object")
35}
36
37pub fn get_crypto_subtle() -> Result<SubtleCrypto, anyhow::Error> {
38    Ok(get_crypto()?.subtle())
39}
40
41async fn get_database() -> anyhow::Result<Database> {
42    let result = try {
43        let database = Database::open(DATABASE_NAME)
44            .with_version(1u8)
45            .with_on_blocked(|event| {
46                warn!("indexed_db(hashiverse.keys) upgrade blocked: {:?}", event);
47                Ok(())
48            })
49            .with_on_upgrade_needed(|event, db| {
50                let old_version = event.old_version() as u64;
51                let new_version = event.new_version().map(|v| v as u64);
52                warn!("indexed_db upgrade needed from {:?} to {:?}", old_version, new_version);
53
54                match (old_version, new_version) {
55                    (0, Some(1)) => {
56                        db.create_object_store(STORE_NAME).with_key_path(KeyPath::from("key")).build()?;
57                    }
58                    _ => {
59                        warn!("Unhandled upgrade from indexed_db(hashiverse.keys) old={:?} to new={:?}", old_version, new_version);
60                    }
61                }
62
63                Ok(())
64            })
65            .build()?
66            .await?;
67
68        database
69    };
70
71    match result {
72        Ok(x) => Ok(x),
73        Err(e) => Err(anyhow::anyhow!("{}", e)),
74    }
75}
76
77pub struct WasmKeyLocker {
78    client_id: ClientId,
79    crypto_key: CryptoKey,
80}
81
82#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
83#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
84impl KeyLocker for WasmKeyLocker {
85    fn client_id(&self) -> &ClientId {
86        &self.client_id
87    }
88
89    async fn sign(&self, data: &[u8]) -> anyhow::Result<Signature> {
90        let uint8_view = unsafe { Uint8Array::view(data) };
91
92        let promise = get_crypto_subtle()?.sign_with_str_and_js_u8_array("Ed25519", &self.crypto_key, &uint8_view).with_js_context(|| "sign_with_str_and_js_u8_array")?;
93
94        let result = JsFuture::from(promise).await.with_js_context(|| "await")?;
95        let array_buffer = result.dyn_ref::<js_sys::ArrayBuffer>().ok_or_else(|| JsValue::from_str("Not a ArrayBuffer")).with_js_context(|| "dyn_ref")?;
96        let array = js_sys::Uint8Array::new(array_buffer);
97        if array.length() != (SIGNATURE_BYTES as u32) {
98            return Err(anyhow!("sign_with_str_and_js_u8_array result length is not SIGNATURE_BYTES long"));
99        }
100
101        let mut bytes: [u8; SIGNATURE_BYTES] = [0u8; SIGNATURE_BYTES];
102        array.copy_to(&mut bytes);
103        let signature = Signature::from_bytes_exact(bytes);
104
105        Ok(signature)
106    }
107}
108
109pub struct WasmKeyLockerManager {}
110impl KeyLockerManager<WasmKeyLocker> for WasmKeyLockerManager {
111    async fn new() -> anyhow::Result<Arc<Self>> {
112        Ok(Arc::new(Self {}))
113    }
114
115    async fn list(&self) -> anyhow::Result<Vec<String>> {
116        let database = get_database().await?;
117        let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readonly).build().with_js_context(|| "transaction")?;
118        let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
119
120        let keys = object_store.get_all_keys::<String>().await.with_js_context(|| "get_all_keys")?;
121        let keys = keys.into_iter().filter_map(|v| v.ok()).filter(|k| k != GUEST_CLIENT_ID).collect();
122
123        Ok(keys)
124    }
125
126    async fn create(&self, key_phrase: String) -> anyhow::Result<Arc<WasmKeyLocker>> {
127        let keys = Keys::from_phrase(&key_phrase)?;
128        let client_id = ClientId::new(keys.verification_key_bytes, keys.pq_commitment_bytes)?;
129
130        // Prepare the Public Key as the IndexedDB key (IDB Key)
131        let key_public = client_id.id.to_hex_str();
132        let key_public_js = JsString::from(key_public.clone());
133
134        // The fields to rebuild our ClientID
135        let verification_key_js = JsString::from(keys.verification_key_bytes.to_hex());
136        let pq_commitment_js = JsString::from(keys.pq_commitment_bytes.to_hex());
137
138        // Encode the private and public keys
139        let d_encoded = URL_SAFE_NO_PAD.encode(keys.signature_key.as_ref());
140        let x_encoded = URL_SAFE_NO_PAD.encode(keys.verification_key.as_ref());
141
142        // Create the jwk object that represents our ed25519 keypair
143        let jwk = Object::new();
144        {
145            Reflect::set(&jwk, &JsValue::from_str("kty"), &JsValue::from_str("OKP")).with_js_context(|| "set")?;
146            Reflect::set(&jwk, &JsValue::from_str("crv"), &JsValue::from_str("Ed25519")).with_js_context(|| "set")?;
147            Reflect::set(&jwk, &JsValue::from_str("d"), &JsValue::from_str(&d_encoded)).with_js_context(|| "set")?;
148            Reflect::set(&jwk, &JsValue::from_str("x"), &JsValue::from_str(&x_encoded)).with_js_context(|| "set")?;
149            Reflect::set(&jwk, &JsValue::from_str("ext"), &JsValue::from_bool(true)).with_js_context(|| "set")?;
150        }
151
152        // Import the key
153        let promise = get_crypto_subtle()?
154            .import_key_with_object(
155                "jwk",
156                &jwk,
157                &JsValue::from_str("Ed25519").unchecked_ref(),
158                false, // Make the private key forever inaccessible/unextractable
159                &js_sys::Array::of1(&"sign".into()),
160            )
161            .with_js_context(|| "import_key_with_object")?;
162
163        // Wait on the promise
164        let crypto_key_handle: CryptoKey = JsFuture::from(promise).await.with_js_context(|| "import_key_with_object.await")?.into();
165
166        // Save to IndexedDB
167        {
168            let document = Object::new();
169            {
170                Reflect::set(&document, &"key".into(), &key_public_js).with_js_context(|| "set_value")?;
171                Reflect::set(&document, &"verification_key".into(), &verification_key_js).with_js_context(|| "set_value")?;
172                Reflect::set(&document, &"pq_commitment".into(), &pq_commitment_js).with_js_context(|| "set_value")?;
173                Reflect::set(&document, &"crypto_key".into(), &crypto_key_handle).with_js_context(|| "set_value")?;
174            };
175
176            let database = get_database().await?;
177            let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readwrite).build().with_js_context(|| "transaction")?;
178            let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
179
180            object_store.put(document).await.with_js_context(|| "put")?;
181            transaction.commit().await.with_js_context(|| "transaction.commit")?;
182        }
183
184        // Switch to using this key
185        let wasm_key_locker = self.switch(key_public).await?;
186        Ok(wasm_key_locker)
187    }
188
189    async fn switch(&self, key_public: String) -> anyhow::Result<Arc<WasmKeyLocker>> {
190        let key_public_js = JsString::from(key_public);
191
192        let database = get_database().await?;
193        let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readonly).build().with_js_context(|| "transaction")?;
194        let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
195
196        let js_value: Option<JsValue> = object_store.get(&key_public_js).await.with_js_context(|| "get")?;
197        if let Some(js_value) = js_value {
198            let verification_key_js = Reflect::get(&js_value, &"verification_key".into()).with_js_context(|| "get")?;
199            let pq_commitment_js = Reflect::get(&js_value, &"pq_commitment".into()).with_js_context(|| "get")?;
200            let crypto_key = Reflect::get(&js_value, &"crypto_key".into()).with_js_context(|| "get")?.unchecked_into::<CryptoKey>();
201
202            let verification_key: String = verification_key_js.as_string().context("verification_key is not a string")?;
203            let verification_key_bytes = VerificationKeyBytes::from_hex_str(&verification_key)?;
204            let pq_commitment: String = pq_commitment_js.as_string().context("verification_key is not a string")?;
205            let pq_commitment_bytes = PQCommitmentBytes::from_hex_str(&pq_commitment)?;
206
207            let client_id = ClientId::new(verification_key_bytes, pq_commitment_bytes)?;
208
209            let wasm_key_locker = Arc::new(WasmKeyLocker { client_id, crypto_key });
210            return Ok(wasm_key_locker);
211        }
212
213        Err(anyhow!("Key not found"))
214    }
215
216    async fn delete(&self, key_public: String) -> anyhow::Result<()> {
217        let key_public_js = JsString::from(key_public);
218
219        let database = get_database().await?;
220        let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readwrite).build().with_js_context(|| "transaction")?;
221        let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
222
223        object_store.delete(&key_public_js).await.with_js_context(|| "delete")?;
224        transaction.commit().await.with_js_context(|| "commit")?;
225
226        Ok(())
227    }
228
229    async fn reset(&self) -> anyhow::Result<()> {
230        let database = get_database().await?;
231        let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readwrite).build().with_js_context(|| "transaction")?;
232        let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
233
234        object_store.clear().with_js_context(|| "clear")?;
235        transaction.commit().await.with_js_context(|| "commit")?;
236
237        Ok(())
238    }
239}
240
241#[cfg(test)]
242pub mod tests {
243    extern crate wasm_bindgen_test;
244    use crate::wasm_key_locker::{WasmKeyLocker, WasmKeyLockerManager};
245    use hashiverse_lib::client::key_locker::key_locker;
246    use wasm_bindgen_test::*;
247
248    wasm_bindgen_test_configure!(run_in_browser);
249
250    #[wasm_bindgen_test]
251    async fn add_test() {
252        key_locker::tests::add_test::<WasmKeyLocker, WasmKeyLockerManager>().await;
253    }
254    #[wasm_bindgen_test]
255    async fn sign_test() {
256        key_locker::tests::sign_test::<WasmKeyLocker, WasmKeyLockerManager>().await;
257    }
258    #[wasm_bindgen_test]
259    async fn guest_client_id_excluded_from_list_test() {
260        key_locker::tests::guest_client_id_excluded_from_list_test::<WasmKeyLocker, WasmKeyLockerManager>().await;
261    }
262}