hashiverse_client_wasm/
wasm_key_locker.rs1use 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 let key_public = client_id.id.to_hex_str();
132 let key_public_js = JsString::from(key_public.clone());
133
134 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 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 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 let promise = get_crypto_subtle()?
154 .import_key_with_object(
155 "jwk",
156 &jwk,
157 &JsValue::from_str("Ed25519").unchecked_ref(),
158 false, &js_sys::Array::of1(&"sign".into()),
160 )
161 .with_js_context(|| "import_key_with_object")?;
162
163 let crypto_key_handle: CryptoKey = JsFuture::from(promise).await.with_js_context(|| "import_key_with_object.await")?.into();
165
166 {
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 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}