use crate::client::key_locker::key_locker::{KeyLocker, KeyLockerManager, GUEST_CLIENT_ID};
use crate::tools::client_id::ClientId;
use crate::tools::keys::Keys;
use crate::tools::types::{PQCommitmentBytes, Signature, VerificationKeyBytes, SIGNATURE_BYTES};
use crate::tools::with_js_context::JsResultExt;
use anyhow::{anyhow, Context};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use indexed_db_futures::database::Database;
use indexed_db_futures::prelude::*;
use indexed_db_futures::transaction::TransactionMode;
use indexed_db_futures::KeyPath;
use js_sys::{JsString, Reflect, Uint8Array};
use log::warn;
use std::sync::Arc;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::js_sys::Object;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Crypto, CryptoKey, SubtleCrypto};
const DATABASE_NAME: &str = "hashiverse.key_locker";
const STORE_NAME: &str = "key";
pub fn get_crypto() -> Result<Crypto, anyhow::Error> {
let global = js_sys::global();
if let Some(worker) = global.dyn_ref::<web_sys::WorkerGlobalScope>() {
return worker.crypto().map_err(|e| anyhow::anyhow!("{:?}", e));
}
if let Some(win) = global.dyn_ref::<web_sys::Window>() {
return win.crypto().map_err(|e| anyhow::anyhow!("{:?}", e));
}
anyhow::bail!("Could not find a global crypto object")
}
pub fn get_crypto_subtle() -> Result<SubtleCrypto, anyhow::Error> {
Ok(get_crypto()?.subtle())
}
async fn get_database() -> anyhow::Result<Database> {
let result = try {
let database = Database::open(DATABASE_NAME)
.with_version(1u8)
.with_on_blocked(|event| {
warn!("indexed_db(hashiverse.keys) upgrade blocked: {:?}", event);
Ok(())
})
.with_on_upgrade_needed(|event, db| {
let old_version = event.old_version() as u64;
let new_version = event.new_version().map(|v| v as u64);
warn!("indexed_db upgrade needed from {:?} to {:?}", old_version, new_version);
match (old_version, new_version) {
(0, Some(1)) => {
db.create_object_store(STORE_NAME).with_key_path(KeyPath::from("key")).build()?;
}
_ => {
warn!("Unhandled upgrade from indexed_db(hashiverse.keys) old={:?} to new={:?}", old_version, new_version);
}
}
Ok(())
})
.build()?
.await?;
database
};
match result {
Ok(x) => Ok(x),
Err(e) => Err(anyhow::anyhow!("{}", e)),
}
}
pub struct WasmKeyLocker {
client_id: ClientId,
crypto_key: CryptoKey,
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl KeyLocker for WasmKeyLocker {
fn client_id(&self) -> &ClientId {
&self.client_id
}
async fn sign(&self, data: &[u8]) -> anyhow::Result<Signature> {
let uint8_view = unsafe { Uint8Array::view(data) };
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")?;
let result = JsFuture::from(promise).await.with_js_context(|| "await")?;
let array_buffer = result.dyn_ref::<js_sys::ArrayBuffer>().ok_or_else(|| JsValue::from_str("Not a ArrayBuffer")).with_js_context(|| "dyn_ref")?;
let array = js_sys::Uint8Array::new(array_buffer);
if array.length() != (SIGNATURE_BYTES as u32) {
return Err(anyhow!("sign_with_str_and_js_u8_array result length is not SIGNATURE_BYTES long"));
}
let mut bytes: [u8; SIGNATURE_BYTES] = [0u8; SIGNATURE_BYTES];
array.copy_to(&mut bytes);
let signature = Signature::from_bytes_exact(bytes);
Ok(signature)
}
}
pub struct WasmKeyLockerManager {}
impl KeyLockerManager<WasmKeyLocker> for WasmKeyLockerManager {
async fn new() -> anyhow::Result<Arc<Self>> {
Ok(Arc::new(Self {}))
}
async fn list(&self) -> anyhow::Result<Vec<String>> {
let database = get_database().await?;
let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readonly).build().with_js_context(|| "transaction")?;
let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
let keys = object_store.get_all_keys::<String>().await.with_js_context(|| "get_all_keys")?;
let keys = keys.into_iter().filter_map(|v| v.ok()).filter(|k| k != GUEST_CLIENT_ID).collect();
Ok(keys)
}
async fn create(&self, key_phrase: String) -> anyhow::Result<Arc<WasmKeyLocker>> {
let keys = Keys::from_phrase(&key_phrase)?;
let client_id = ClientId::new(keys.verification_key_bytes, keys.pq_commitment_bytes)?;
let key_public = client_id.id.to_hex_str();
let key_public_js = JsString::from(key_public.clone());
let verification_key_js = JsString::from(keys.verification_key_bytes.to_hex());
let pq_commitment_js = JsString::from(keys.pq_commitment_bytes.to_hex());
let d_encoded = URL_SAFE_NO_PAD.encode(keys.signature_key.as_ref());
let x_encoded = URL_SAFE_NO_PAD.encode(keys.verification_key.as_ref());
let jwk = Object::new();
{
Reflect::set(&jwk, &JsValue::from_str("kty"), &JsValue::from_str("OKP")).with_js_context(|| "set")?;
Reflect::set(&jwk, &JsValue::from_str("crv"), &JsValue::from_str("Ed25519")).with_js_context(|| "set")?;
Reflect::set(&jwk, &JsValue::from_str("d"), &JsValue::from_str(&d_encoded)).with_js_context(|| "set")?;
Reflect::set(&jwk, &JsValue::from_str("x"), &JsValue::from_str(&x_encoded)).with_js_context(|| "set")?;
Reflect::set(&jwk, &JsValue::from_str("ext"), &JsValue::from_bool(true)).with_js_context(|| "set")?;
}
let promise = get_crypto_subtle()?
.import_key_with_object(
"jwk",
&jwk,
&JsValue::from_str("Ed25519").unchecked_ref(),
false, &js_sys::Array::of1(&"sign".into()),
)
.with_js_context(|| "import_key_with_object")?;
let crypto_key_handle: CryptoKey = JsFuture::from(promise).await.with_js_context(|| "import_key_with_object.await")?.into();
{
let document = Object::new();
{
Reflect::set(&document, &"key".into(), &key_public_js).with_js_context(|| "set_value")?;
Reflect::set(&document, &"verification_key".into(), &verification_key_js).with_js_context(|| "set_value")?;
Reflect::set(&document, &"pq_commitment".into(), &pq_commitment_js).with_js_context(|| "set_value")?;
Reflect::set(&document, &"crypto_key".into(), &crypto_key_handle).with_js_context(|| "set_value")?;
};
let database = get_database().await?;
let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readwrite).build().with_js_context(|| "transaction")?;
let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
object_store.put(document).await.with_js_context(|| "put")?;
transaction.commit().await.with_js_context(|| "transaction.commit")?;
}
let wasm_key_locker = self.switch(key_public).await?;
Ok(wasm_key_locker)
}
async fn switch(&self, client_id_hex: String) -> anyhow::Result<Arc<WasmKeyLocker>> {
let client_id_hex_js = JsString::from(client_id_hex);
let database = get_database().await?;
let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readonly).build().with_js_context(|| "transaction")?;
let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
let js_value: Option<JsValue> = object_store.get(&client_id_hex_js).await.with_js_context(|| "get")?;
if let Some(js_value) = js_value {
let verification_key_js = Reflect::get(&js_value, &"verification_key".into()).with_js_context(|| "get")?;
let pq_commitment_js = Reflect::get(&js_value, &"pq_commitment".into()).with_js_context(|| "get")?;
let crypto_key = Reflect::get(&js_value, &"crypto_key".into()).with_js_context(|| "get")?.unchecked_into::<CryptoKey>();
let verification_key: String = verification_key_js.as_string().context("verification_key is not a string")?;
let verification_key_bytes = VerificationKeyBytes::from_hex_str(&verification_key)?;
let pq_commitment: String = pq_commitment_js.as_string().context("verification_key is not a string")?;
let pq_commitment_bytes = PQCommitmentBytes::from_hex_str(&pq_commitment)?;
let client_id = ClientId::new(verification_key_bytes, pq_commitment_bytes)?;
let wasm_key_locker = Arc::new(WasmKeyLocker { client_id, crypto_key });
return Ok(wasm_key_locker);
}
Err(anyhow!("Key not found"))
}
async fn delete(&self, client_id_hex: String) -> anyhow::Result<()> {
let client_id_hex_js = JsString::from(client_id_hex);
let database = get_database().await?;
let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readwrite).build().with_js_context(|| "transaction")?;
let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
object_store.delete(&client_id_hex_js).await.with_js_context(|| "delete")?;
transaction.commit().await.with_js_context(|| "commit")?;
Ok(())
}
async fn reset(&self) -> anyhow::Result<()> {
let database = get_database().await?;
let transaction = database.transaction(STORE_NAME).with_mode(TransactionMode::Readwrite).build().with_js_context(|| "transaction")?;
let object_store = transaction.object_store(STORE_NAME).with_js_context(|| "object_store")?;
object_store.clear().with_js_context(|| "clear")?;
transaction.commit().await.with_js_context(|| "commit")?;
Ok(())
}
}
#[cfg(test)]
pub mod tests {
extern crate wasm_bindgen_test;
use super::{WasmKeyLocker, WasmKeyLockerManager};
use crate::client::key_locker::key_locker;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn add_test() {
key_locker::tests::add_test::<WasmKeyLocker, WasmKeyLockerManager>().await;
}
#[wasm_bindgen_test]
async fn sign_test() {
key_locker::tests::sign_test::<WasmKeyLocker, WasmKeyLockerManager>().await;
}
#[wasm_bindgen_test]
async fn guest_client_id_excluded_from_list_test() {
key_locker::tests::guest_client_id_excluded_from_list_test::<WasmKeyLocker, WasmKeyLockerManager>().await;
}
}