feat: implement browser extension UI with WebAssembly integration
1
.gitignore
vendored
@ -8,3 +8,4 @@
|
|||||||
|
|
||||||
# Ignore test databases
|
# Ignore test databases
|
||||||
/vault/vault_native_test/
|
/vault/vault_native_test/
|
||||||
|
node_modules/
|
@ -4,6 +4,6 @@ members = [
|
|||||||
"kvstore",
|
"kvstore",
|
||||||
"vault",
|
"vault",
|
||||||
"evm_client",
|
"evm_client",
|
||||||
"wasm",
|
"wasm_app",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
12
Makefile
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
BROWSER ?= firefox
|
BROWSER ?= firefox
|
||||||
|
|
||||||
.PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client
|
.PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client build-wasm-app
|
||||||
|
|
||||||
test-browser-all: test-browser-kvstore test-browser-vault test-browser-evm-client
|
test-browser-all: test-browser-kvstore test-browser-vault test-browser-evm-client
|
||||||
|
|
||||||
@ -21,3 +21,13 @@ test-browser-vault:
|
|||||||
test-browser-evm-client:
|
test-browser-evm-client:
|
||||||
cd evm_client && wasm-pack test --headless --$(BROWSER)
|
cd evm_client && wasm-pack test --headless --$(BROWSER)
|
||||||
|
|
||||||
|
# Build wasm_app as a WASM library
|
||||||
|
build-wasm-app:
|
||||||
|
cd wasm_app && wasm-pack build --target web
|
||||||
|
|
||||||
|
# Build everything: wasm, copy, then extension
|
||||||
|
build-extension-all: build-wasm-app
|
||||||
|
cp wasm_app/pkg/wasm_app.js extension/public/wasm/wasm_app.js
|
||||||
|
cp wasm_app/pkg/wasm_app_bg.wasm extension/public/wasm/wasm_app_bg.wasm
|
||||||
|
cd extension && npm run build
|
||||||
|
|
||||||
|
@ -29,8 +29,67 @@ The browser extension is the main user interface for interacting with the modula
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Script Permissions & Security
|
## Security Considerations
|
||||||
- **Session Password Handling**: The extension stores the keyspace password (or a derived key) securely in memory only for the duration of an unlocked session. The password is never persisted or written to disk/storage, and is zeroized from memory immediately upon session lock/logout, following cryptographic best practices (see also Developer Notes below).
|
|
||||||
|
### Restricting WASM and Session API Access to the Extension
|
||||||
|
|
||||||
|
To ensure that sensitive APIs (such as session state, cryptographic operations, and key management) are accessible **only** from the browser extension and not from arbitrary web pages, follow these best practices:
|
||||||
|
|
||||||
|
1. **Export Only Safe, High-Level APIs**
|
||||||
|
- Use `#[wasm_bindgen]` only on functions you explicitly want to expose to the extension.
|
||||||
|
- Do **not** export internal helpers, state singletons, or low-level APIs.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Safe to export
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOT exported: internal state
|
||||||
|
// pub static SESSION_MANAGER: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Do Not Attach WASM Exports to `window` or `globalThis`**
|
||||||
|
- When loading the WASM module in your extension, do not attach its exports to any global object accessible by web pages.
|
||||||
|
- Keep all WASM interaction within the extension’s background/content scripts.
|
||||||
|
|
||||||
|
3. **Validate All Inputs**
|
||||||
|
- Even though only your extension should call WASM APIs, always validate inputs to exported functions to prevent injection or misuse.
|
||||||
|
|
||||||
|
4. **Use Message Passing Carefully**
|
||||||
|
- If you use `postMessage` or similar mechanisms, always check the message origin and type before processing.
|
||||||
|
- Only process messages from trusted origins (e.g., your extension’s own scripts).
|
||||||
|
|
||||||
|
5. **Load WASM in Extension-Only Context**
|
||||||
|
- Load and instantiate the WASM module in a context (such as a background script or content script) that is not accessible to arbitrary websites.
|
||||||
|
- Never inject your WASM module directly into web page scopes.
|
||||||
|
|
||||||
|
#### Example: Secure WASM Export
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Only export high-level, safe APIs
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
// Do NOT export SESSION_MANAGER or internal helpers
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example: Secure JS Loading (Extension Only)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// In your extension's background or content script:
|
||||||
|
import init, { run_rhai } from "./your_wasm_module.js";
|
||||||
|
|
||||||
|
// Only your extension's JS can call run_rhai
|
||||||
|
// Do NOT attach run_rhai to window/globalThis
|
||||||
|
```
|
||||||
|
|
||||||
|
By following these guidelines, your WASM session state and sensitive APIs will only be accessible to your browser extension, not to untrusted web pages.
|
||||||
|
|
||||||
|
### Session Password Handling
|
||||||
|
- The extension stores the keyspace password (or a derived key) securely in memory only for the duration of an unlocked session. The password is never persisted or written to disk/storage, and is zeroized from memory immediately upon session lock/logout, following cryptographic best practices (see also Developer Notes below).
|
||||||
- **Signer Access**: Scripts can access the session's signer only after explicit user approval per execution.
|
- **Signer Access**: Scripts can access the session's signer only after explicit user approval per execution.
|
||||||
- **Approval Model**: Every script execution (local or remote) requires user approval.
|
- **Approval Model**: Every script execution (local or remote) requires user approval.
|
||||||
- **No global permissions**: Permissions are not granted globally or permanently.
|
- **No global permissions**: Permissions are not granted globally or permanently.
|
||||||
|
@ -54,7 +54,7 @@ async fn main() {
|
|||||||
use kvstore::{KVStore, WasmStore};
|
use kvstore::{KVStore, WasmStore};
|
||||||
|
|
||||||
// Must be called from an async context (e.g., JS Promise)
|
// Must be called from an async context (e.g., JS Promise)
|
||||||
let store = WasmStore::open("mydb").await.unwrap();
|
let store = WasmStore::open("vault").await.unwrap();
|
||||||
store.set("foo", b"bar").await.unwrap();
|
store.set("foo", b"bar").await.unwrap();
|
||||||
let val = store.get("foo").await.unwrap();
|
let val = store.get("foo").await.unwrap();
|
||||||
// Use the value as needed
|
// Use the value as needed
|
||||||
|
@ -39,7 +39,7 @@ sal/
|
|||||||
|
|
||||||
- **Each core component (`kvstore`, `vault`, `evm_client`, `rhai`) is a separate crate at the repo root.**
|
- **Each core component (`kvstore`, `vault`, `evm_client`, `rhai`) is a separate crate at the repo root.**
|
||||||
- **CLI binary** is in `cli_app` and depends on the core crates.
|
- **CLI binary** is in `cli_app` and depends on the core crates.
|
||||||
- **WebAssembly target** is in `web_app`.
|
- **WebAssembly target** is in `wasm_app`.
|
||||||
- **Rhai bindings** live in their own crate (`rhai/`), so both CLI and WASM can depend on them.
|
- **Rhai bindings** live in their own crate (`rhai/`), so both CLI and WASM can depend on them.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -7,20 +7,16 @@ edition = "2021"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
kvstore = { path = "../kvstore" }
|
# Only universal/core dependencies here
|
||||||
|
|
||||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||||
rhai = "1.16"
|
rhai = "1.16"
|
||||||
ethers-core = "2.0"
|
ethers-core = "2.0"
|
||||||
gloo-net = { version = "0.5", features = ["http"] }
|
|
||||||
rlp = "0.5"
|
rlp = "0.5"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
vault = { path = "../vault" }
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
alloy-rlp = { version = "0.3.11", features = ["derive"] }
|
|
||||||
alloy-primitives = "1.1.0"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
k256 = { version = "0.13", features = ["ecdsa"] }
|
k256 = { version = "0.13", features = ["ecdsa"] }
|
||||||
@ -32,10 +28,17 @@ web-sys = { version = "0.3", features = ["console"] }
|
|||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
console_error_panic_hook = "0.1"
|
# console_error_panic_hook = "0.1"
|
||||||
|
gloo-net = { version = "0.5", features = ["http"] }
|
||||||
|
console_log = "1"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
9
evm_client/src/error.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum EvmError {
|
||||||
|
#[error("RPC error: {0}")]
|
||||||
|
Rpc(String),
|
||||||
|
#[error("Signing error: {0}")]
|
||||||
|
Signing(String),
|
||||||
|
#[error("Other error: {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
@ -14,22 +14,151 @@
|
|||||||
|
|
||||||
pub use ethers_core::types::*;
|
pub use ethers_core::types::*;
|
||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
pub mod signer;
|
||||||
pub mod rhai_bindings;
|
pub mod rhai_bindings;
|
||||||
pub mod rhai_sync_helpers;
|
pub mod rhai_sync_helpers;
|
||||||
|
pub mod error;
|
||||||
pub use provider::send_rpc;
|
pub use provider::send_rpc;
|
||||||
|
pub use error::EvmError;
|
||||||
|
|
||||||
/// Public EVM client struct for use in bindings and sync helpers
|
/// Public EVM client struct for use in bindings and sync helpers
|
||||||
|
pub struct Provider {
|
||||||
|
pub rpc_url: String,
|
||||||
|
pub chain_id: u64,
|
||||||
|
pub explorer_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct EvmClient {
|
pub struct EvmClient {
|
||||||
// Add fields as needed for your implementation
|
pub provider: Provider,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EvmClient {
|
impl EvmClient {
|
||||||
pub async fn get_balance(&self, provider_url: &str, public_key: &[u8]) -> Result<u64, String> {
|
pub fn new(provider: Provider) -> Self {
|
||||||
// TODO: Implement actual logic
|
Self { provider }
|
||||||
Ok(0)
|
|
||||||
}
|
}
|
||||||
pub async fn send_transaction(&self, provider_url: &str, key_id: &str, password: &[u8], tx_data: rhai::Map) -> Result<String, String> {
|
|
||||||
// TODO: Implement actual logic
|
/// Initialize logging for the current target (native: env_logger, WASM: console_log)
|
||||||
Ok("tx_hash_placeholder".to_string())
|
/// Call this before using any log macros.
|
||||||
|
pub fn init_logging() {
|
||||||
|
use std::sync::Once;
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
INIT.call_once(|| {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
use env_logger;
|
||||||
|
let _ = env_logger::builder().is_test(false).try_init();
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
use console_log;
|
||||||
|
let _ = console_log::init_with_level(log::Level::Debug);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_balance(&self, address: Address) -> Result<U256, EvmError> {
|
||||||
|
// TODO: Use provider info
|
||||||
|
provider::get_balance(&self.provider.rpc_url, address)
|
||||||
|
.await
|
||||||
|
.map_err(|e| EvmError::Rpc(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_transaction(
|
||||||
|
&self,
|
||||||
|
mut tx: provider::Transaction,
|
||||||
|
signer: &dyn crate::signer::Signer,
|
||||||
|
) -> Result<ethers_core::types::H256, EvmError> {
|
||||||
|
use ethers_core::types::{U256, H256, Bytes, Address};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use serde_json::json;
|
||||||
|
use crate::provider::{send_rpc, parse_signature_rs_v};
|
||||||
|
|
||||||
|
// 1. Fill in missing fields via JSON-RPC if needed
|
||||||
|
// Parse signer address as H160
|
||||||
|
let signer_addr = ethers_core::types::Address::from_str(&signer.address())
|
||||||
|
.map_err(|e| EvmError::Rpc(format!("Invalid signer address: {}", e)))?;
|
||||||
|
// Nonce
|
||||||
|
if tx.nonce.is_none() {
|
||||||
|
let body = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "eth_getTransactionCount",
|
||||||
|
"params": [format!("0x{:x}", signer_addr), "pending"],
|
||||||
|
"id": 1
|
||||||
|
}).to_string();
|
||||||
|
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_getTransactionCount".to_string()))?;
|
||||||
|
tx.nonce = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?);
|
||||||
|
}
|
||||||
|
// Gas Price
|
||||||
|
if tx.gas_price.is_none() {
|
||||||
|
let body = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "eth_gasPrice",
|
||||||
|
"params": [],
|
||||||
|
"id": 1
|
||||||
|
}).to_string();
|
||||||
|
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_gasPrice".to_string()))?;
|
||||||
|
tx.gas_price = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?);
|
||||||
|
}
|
||||||
|
// Chain ID
|
||||||
|
if tx.chain_id.is_none() {
|
||||||
|
tx.chain_id = Some(self.provider.chain_id);
|
||||||
|
}
|
||||||
|
// Gas (optional: estimate if missing)
|
||||||
|
if tx.gas.is_none() {
|
||||||
|
let body = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "eth_estimateGas",
|
||||||
|
"params": [{
|
||||||
|
"to": format!("0x{:x}", tx.to),
|
||||||
|
"from": format!("0x{:x}", signer_addr),
|
||||||
|
"value": format!("0x{:x}", tx.value),
|
||||||
|
"data": format!("0x{}", hex::encode(&tx.data)),
|
||||||
|
}],
|
||||||
|
"id": 1
|
||||||
|
}).to_string();
|
||||||
|
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_estimateGas".to_string()))?;
|
||||||
|
tx.gas = Some(U256::from_str_radix(hex.trim_start_matches("0x"), 16).map_err(|e| EvmError::Rpc(e.to_string()))?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. RLP encode unsigned transaction
|
||||||
|
let rlp_unsigned = tx.rlp_encode_unsigned();
|
||||||
|
|
||||||
|
// 3. Sign the RLP-encoded unsigned transaction
|
||||||
|
let sig = signer.sign(&rlp_unsigned).await?;
|
||||||
|
let (r, s, v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?;
|
||||||
|
|
||||||
|
// 4. RLP encode signed transaction (EIP-155)
|
||||||
|
use rlp::RlpStream;
|
||||||
|
let mut rlp_stream = RlpStream::new_list(9);
|
||||||
|
rlp_stream.append(&tx.nonce.unwrap());
|
||||||
|
rlp_stream.append(&tx.gas_price.unwrap());
|
||||||
|
rlp_stream.append(&tx.gas.unwrap());
|
||||||
|
rlp_stream.append(&tx.to);
|
||||||
|
rlp_stream.append(&tx.value);
|
||||||
|
rlp_stream.append(&tx.data.to_vec());
|
||||||
|
rlp_stream.append(&tx.chain_id.unwrap());
|
||||||
|
rlp_stream.append(&r);
|
||||||
|
rlp_stream.append(&s);
|
||||||
|
let raw_tx = rlp_stream.out().to_vec();
|
||||||
|
|
||||||
|
// 5. Broadcast the raw transaction
|
||||||
|
let raw_hex = format!("0x{}", hex::encode(&raw_tx));
|
||||||
|
let body = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "eth_sendRawTransaction",
|
||||||
|
"params": [raw_hex],
|
||||||
|
"id": 1
|
||||||
|
}).to_string();
|
||||||
|
let resp = send_rpc(&self.provider.rpc_url, &body).await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&resp).map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let tx_hash_hex = v["result"].as_str().ok_or_else(|| EvmError::Rpc("No result field in eth_sendRawTransaction".to_string()))?;
|
||||||
|
let tx_hash = H256::from_slice(&hex::decode(tx_hash_hex.trim_start_matches("0x")).map_err(|e| EvmError::Rpc(e.to_string()))?);
|
||||||
|
Ok(tx_hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,13 +30,13 @@ pub async fn send_rpc(url: &str, body: &str) -> Result<String, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub struct Transaction {
|
pub struct Transaction {
|
||||||
pub nonce: U256,
|
|
||||||
pub to: Address,
|
pub to: Address,
|
||||||
pub value: U256,
|
pub value: U256,
|
||||||
pub gas: U256,
|
|
||||||
pub gas_price: U256,
|
|
||||||
pub data: Bytes,
|
pub data: Bytes,
|
||||||
pub chain_id: u64,
|
pub gas: Option<U256>,
|
||||||
|
pub gas_price: Option<U256>,
|
||||||
|
pub nonce: Option<U256>,
|
||||||
|
pub chain_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transaction {
|
impl Transaction {
|
||||||
@ -94,5 +94,4 @@ pub async fn get_balance(url: &str, address: Address) -> Result<U256, Box<dyn st
|
|||||||
Ok(balance)
|
Ok(balance)
|
||||||
}
|
}
|
||||||
|
|
||||||
// (Remove old sign_and_serialize placeholder)
|
|
||||||
|
|
||||||
|
@ -14,20 +14,16 @@ pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClie
|
|||||||
}
|
}
|
||||||
impl RhaiEvmClient {
|
impl RhaiEvmClient {
|
||||||
/// Get balance using the EVM client.
|
/// Get balance using the EVM client.
|
||||||
pub fn get_balance(&self, provider_url: String, public_key: rhai::Blob) -> Result<String, String> {
|
pub fn get_balance(&self, address_hex: String) -> Result<String, String> {
|
||||||
// Use the sync helper from crate::rhai_sync_helpers
|
use ethers_core::types::Address;
|
||||||
crate::rhai_sync_helpers::get_balance_sync(&self.inner, &provider_url, &public_key)
|
let address = Address::from_slice(&hex::decode(address_hex.trim_start_matches("0x")).map_err(|e| format!("hex decode error: {e}"))?);
|
||||||
|
crate::rhai_sync_helpers::get_balance_sync(&self.inner, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send transaction using the EVM client.
|
|
||||||
pub fn send_transaction(&self, provider_url: String, key_id: String, password: rhai::Blob, tx_data: rhai::Map) -> Result<String, String> {
|
|
||||||
// Use the sync helper from crate::rhai_sync_helpers
|
|
||||||
crate::rhai_sync_helpers::send_transaction_sync(&self.inner, &provider_url, &key_id, &password, tx_data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
engine.register_type::<RhaiEvmClient>();
|
engine.register_type::<RhaiEvmClient>();
|
||||||
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
||||||
engine.register_fn("send_transaction", RhaiEvmClient::send_transaction);
|
|
||||||
// Register instance for scripts
|
// Register instance for scripts
|
||||||
let rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
|
let rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
|
||||||
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
|
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
|
||||||
|
@ -11,11 +11,10 @@ use tokio::runtime::Handle;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn get_balance_sync(
|
pub fn get_balance_sync(
|
||||||
evm_client: &EvmClient,
|
evm_client: &EvmClient,
|
||||||
provider_url: &str,
|
address: ethers_core::types::Address,
|
||||||
public_key: &[u8],
|
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
Handle::current().block_on(async {
|
Handle::current().block_on(async {
|
||||||
evm_client.get_balance(provider_url, public_key)
|
evm_client.get_balance(address)
|
||||||
.await
|
.await
|
||||||
.map(|b| b.to_string())
|
.map(|b| b.to_string())
|
||||||
.map_err(|e| format!("get_balance error: {e}"))
|
.map_err(|e| format!("get_balance error: {e}"))
|
||||||
@ -26,15 +25,13 @@ pub fn get_balance_sync(
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn send_transaction_sync(
|
pub fn send_transaction_sync(
|
||||||
evm_client: &EvmClient,
|
evm_client: &EvmClient,
|
||||||
provider_url: &str,
|
tx: crate::provider::Transaction,
|
||||||
key_id: &str,
|
signer: &dyn crate::signer::Signer,
|
||||||
password: &[u8],
|
|
||||||
tx_data: Map,
|
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
Handle::current().block_on(async {
|
Handle::current().block_on(async {
|
||||||
evm_client.send_transaction(provider_url, key_id, password, tx_data)
|
evm_client.send_transaction(tx, signer)
|
||||||
.await
|
.await
|
||||||
.map(|tx| tx.to_string())
|
.map(|tx| format!("0x{:x}", tx))
|
||||||
.map_err(|e| format!("send_transaction error: {e}"))
|
.map_err(|e| format!("send_transaction error: {e}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,80 +1,8 @@
|
|||||||
// Signing should be done using ethers-core utilities directly. This file is now empty.
|
use super::error::EvmError;
|
||||||
|
|
||||||
// Native: Only compile for non-WASM
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||||
#[async_trait]
|
|
||||||
pub trait Signer: Send + Sync {
|
pub trait Signer: Send + Sync {
|
||||||
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
|
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
|
||||||
fn address(&self) -> String;
|
fn address(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM: Only compile for WASM
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
pub trait Signer {
|
|
||||||
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
|
|
||||||
fn address(&self) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Implementation for vault::SessionManager ---
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl<S: vault::KVStore> Signer for vault::SessionManager<S> {
|
|
||||||
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError> {
|
|
||||||
log::debug!("SessionManager::sign called");
|
|
||||||
self.sign(message)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("Vault signing error: {}", e);
|
|
||||||
EvmError::Vault(e.to_string())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn address(&self) -> String {
|
|
||||||
log::debug!("SessionManager::address called");
|
|
||||||
self.current_keypair()
|
|
||||||
.map(|k| {
|
|
||||||
if k.key_type == vault::KeyType::Secp256k1 {
|
|
||||||
let pubkey = &k.public_key;
|
|
||||||
use alloy_primitives::keccak256;
|
|
||||||
let hash = keccak256(&pubkey[1..]);
|
|
||||||
format!("0x{}", hex::encode(&hash[12..]))
|
|
||||||
} else {
|
|
||||||
format!("0x{}", hex::encode(&k.public_key))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
|
|
||||||
impl<S: vault::KVStore + Send + Sync> Signer for vault::SessionManager<S> {
|
|
||||||
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError> {
|
|
||||||
log::debug!("SessionManager::sign called");
|
|
||||||
self.sign(message)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
log::error!("Vault signing error: {}", e);
|
|
||||||
EvmError::Vault(e.to_string())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fn address(&self) -> String {
|
|
||||||
log::debug!("SessionManager::address called");
|
|
||||||
self.current_keypair()
|
|
||||||
.map(|k| {
|
|
||||||
if k.key_type == vault::KeyType::Secp256k1 {
|
|
||||||
let pubkey = &k.public_key;
|
|
||||||
use alloy_primitives::keccak256;
|
|
||||||
let hash = keccak256(&pubkey[1..]);
|
|
||||||
format!("0x{}", hex::encode(&hash[12..]))
|
|
||||||
} else {
|
|
||||||
format!("0x{}", hex::encode(&k.public_key))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,63 +1,21 @@
|
|||||||
// This file contains native-only integration tests for EVM client balance and signing logic.
|
// This file contains native-only integration tests for EVM client balance and signing logic.
|
||||||
// All code is strictly separated from WASM code using cfg attributes.
|
// All code is strictly separated from WASM code using cfg attributes.
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
mod native_tests {
|
|
||||||
use vault::{SessionManager, KeyType};
|
|
||||||
use tempfile::TempDir;
|
|
||||||
use kvstore::native::NativeStore;
|
|
||||||
use alloy_primitives::keccak256;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_vault_sessionmanager_balance_for_new_keypair() {
|
|
||||||
use ethers_core::types::{Address, U256};
|
|
||||||
use evm_client::provider::get_balance;
|
|
||||||
|
|
||||||
let tmp_dir = TempDir::new().expect("create temp dir");
|
|
||||||
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("Failed to open native store");
|
|
||||||
let mut vault = vault::Vault::new(store.clone());
|
|
||||||
let keyspace = "testspace";
|
|
||||||
let password = b"testpass";
|
|
||||||
// 1. Create keyspace
|
|
||||||
vault.create_keyspace(keyspace, password, None).await.expect("create keyspace");
|
|
||||||
// 2. Add secp256k1 keypair
|
|
||||||
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), None).await.expect("add keypair");
|
|
||||||
// 3. Create SessionManager and unlock keyspace
|
|
||||||
let mut session = SessionManager::new(vault);
|
|
||||||
session.unlock_keyspace(keyspace, password).await.expect("unlock keyspace");
|
|
||||||
session.select_keyspace(keyspace).expect("select keyspace");
|
|
||||||
session.select_keypair(&key_id).expect("select keypair");
|
|
||||||
let kp = session.current_keypair().expect("current keypair");
|
|
||||||
// 4. Derive Ethereum address from public key (same as in evm_client)
|
|
||||||
let pubkey = &kp.public_key;
|
|
||||||
// Remove leading 0x04 if present (uncompressed SEC1)
|
|
||||||
let pubkey = if pubkey.len() == 65 && pubkey[0] == 0x04 { &pubkey[1..] } else { pubkey.as_slice() };
|
|
||||||
let hash = keccak256(pubkey);
|
|
||||||
let address = Address::from_slice(&hash[12..]);
|
|
||||||
// 5. Query balance
|
|
||||||
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
|
||||||
let balance = get_balance(url, address).await.expect("Failed to get balance");
|
|
||||||
assert_eq!(balance, ethers_core::types::U256::zero(), "New keypair should have zero balance");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use ethers_core::types::Bytes;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rlp_encode_unsigned() {
|
fn test_rlp_encode_unsigned() {
|
||||||
use ethers_core::types::{Address, U256, Bytes};
|
use ethers_core::types::{Address, U256, Bytes};
|
||||||
use evm_client::provider::Transaction;
|
use evm_client::provider::Transaction;
|
||||||
|
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
nonce: U256::from(1),
|
|
||||||
to: Address::zero(),
|
to: Address::zero(),
|
||||||
value: U256::from(100),
|
value: U256::from(100),
|
||||||
gas: U256::from(21000),
|
|
||||||
gas_price: U256::from(1),
|
|
||||||
data: Bytes::new(),
|
data: Bytes::new(),
|
||||||
chain_id: 1u64,
|
gas: Some(U256::from(21000)),
|
||||||
|
gas_price: Some(U256::from(1)),
|
||||||
|
nonce: Some(U256::from(1)),
|
||||||
|
chain_id: Some(1u64),
|
||||||
};
|
};
|
||||||
let rlp = tx.rlp_encode_unsigned();
|
let rlp = tx.rlp_encode_unsigned();
|
||||||
assert!(!rlp.is_empty());
|
assert!(!rlp.is_empty());
|
||||||
@ -86,6 +44,6 @@ mod native_tests {
|
|||||||
let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
||||||
let address = ethers_core::types::Address::from_slice(&hex::decode(address).unwrap());
|
let address = ethers_core::types::Address::from_slice(&hex::decode(address).unwrap());
|
||||||
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
||||||
let balance = get_balance(url, address).await.expect("Failed to get balance");
|
let balance = get_balance(url, address).await.expect("Failed to get balance"); // TODO: Update to use new EvmClient API
|
||||||
assert!(balance > ethers_core::types::U256::zero(), "Vitalik's balance should be greater than zero");
|
assert!(balance > ethers_core::types::U256::zero(), "Vitalik's balance should be greater than zero");
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,13 @@ use hex;
|
|||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn test_rlp_encode_unsigned() {
|
fn test_rlp_encode_unsigned() {
|
||||||
let tx = Transaction {
|
let tx = Transaction {
|
||||||
nonce: U256::from(1),
|
|
||||||
to: Address::zero(),
|
to: Address::zero(),
|
||||||
value: U256::from(100),
|
value: U256::from(100),
|
||||||
gas: U256::from(21000),
|
|
||||||
gas_price: U256::from(1),
|
|
||||||
data: Bytes::new(),
|
data: Bytes::new(),
|
||||||
chain_id: 1,
|
gas: Some(U256::from(21000)),
|
||||||
|
gas_price: Some(U256::from(1)),
|
||||||
|
nonce: Some(U256::from(1)),
|
||||||
|
chain_id: Some(1),
|
||||||
};
|
};
|
||||||
let rlp = tx.rlp_encode_unsigned();
|
let rlp = tx.rlp_encode_unsigned();
|
||||||
assert!(!rlp.is_empty());
|
assert!(!rlp.is_empty());
|
||||||
@ -31,44 +31,11 @@ pub async fn test_get_balance_real_address_wasm_unique() {
|
|||||||
let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
||||||
let address = Address::from_slice(&hex::decode(address).unwrap());
|
let address = Address::from_slice(&hex::decode(address).unwrap());
|
||||||
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
||||||
let balance = get_balance(url, address).await.expect("Failed to get balance");
|
let balance = get_balance(url, address).await.expect("Failed to get balance"); // TODO: Update to use new EvmClient API
|
||||||
web_sys::console::log_1(&format!("Balance: {balance:?}").into());
|
web_sys::console::log_1(&format!("Balance: {balance:?}").into());
|
||||||
assert!(balance > U256::zero(), "Vitalik's balance should be greater than zero");
|
assert!(balance > U256::zero(), "Vitalik's balance should be greater than zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen_test(async)]
|
|
||||||
pub async fn test_vault_sessionmanager_balance_for_new_keypair_wasm() {
|
|
||||||
use vault::{SessionManager, KeyType};
|
|
||||||
use ethers_core::types::Address;
|
|
||||||
use evm_client::provider::get_balance;
|
|
||||||
use alloy_primitives::keccak256;
|
|
||||||
web_sys::console::log_1(&"WASM vault-session balance test running!".into());
|
|
||||||
let store = kvstore::wasm::WasmStore::open("test-db").await.expect("open");
|
|
||||||
let mut vault = vault::Vault::new(store);
|
|
||||||
let keyspace = "testspace-wasm";
|
|
||||||
let password = b"testpass";
|
|
||||||
// 1. Create keyspace
|
|
||||||
vault.create_keyspace(keyspace, password, None).await.expect("create keyspace");
|
|
||||||
// 2. Add secp256k1 keypair
|
|
||||||
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), None).await.expect("add keypair");
|
|
||||||
// 3. Create SessionManager and unlock keyspace
|
|
||||||
let mut session = SessionManager::new(vault);
|
|
||||||
session.unlock_keyspace(keyspace, password).await.expect("unlock keyspace");
|
|
||||||
session.select_keyspace(keyspace).expect("select keyspace");
|
|
||||||
session.select_keypair(&key_id).expect("select keypair");
|
|
||||||
let kp = session.current_keypair().expect("current keypair");
|
|
||||||
// 4. Derive Ethereum address from public key
|
|
||||||
let pubkey = &kp.public_key;
|
|
||||||
let pubkey = if pubkey.len() == 65 && pubkey[0] == 0x04 { &pubkey[1..] } else { pubkey.as_slice() };
|
|
||||||
let hash = keccak256(pubkey);
|
|
||||||
let address = Address::from_slice(&hash[12..]);
|
|
||||||
// 5. Query balance
|
|
||||||
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
|
||||||
let balance = get_balance(url, address).await.expect("Failed to get balance");
|
|
||||||
web_sys::console::log_1(&format!("Balance: {balance:?}").into());
|
|
||||||
assert_eq!(balance, ethers_core::types::U256::zero(), "New keypair should have zero balance");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
fn test_parse_signature_rs_v() {
|
fn test_parse_signature_rs_v() {
|
||||||
let mut sig = [0u8; 65];
|
let mut sig = [0u8; 65];
|
||||||
|
35
extension/README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Modular Vault Browser Extension
|
||||||
|
|
||||||
|
A cross-browser (Manifest V3) extension for secure cryptographic operations and Rhai scripting, powered by Rust/WASM.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Session/keypair management
|
||||||
|
- Cryptographic signing, encryption, and EVM actions
|
||||||
|
- Secure WASM integration (signing only accessible from extension scripts)
|
||||||
|
- React-based popup UI with dark mode
|
||||||
|
- Future: WebSocket integration for remote scripting
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
- `manifest.json`: Extension manifest (MV3, Chrome/Firefox)
|
||||||
|
- `popup/`: React UI for user interaction
|
||||||
|
- `background/`: Service worker for session, keypair, and WASM logic
|
||||||
|
- `assets/`: Icons and static assets
|
||||||
|
|
||||||
|
## Dev Workflow
|
||||||
|
1. Build Rust WASM: `wasm-pack build --target web --out-dir ../extension/wasm`
|
||||||
|
2. Install JS deps: `npm install` (from `extension/`)
|
||||||
|
3. Build popup: `npm run build`
|
||||||
|
4. Load `/extension` as an unpacked extension in your browser
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
- WASM cryptographic APIs are only accessible from extension scripts (not content scripts or web pages).
|
||||||
|
- All sensitive actions require explicit user approval.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
- Implement background logic for session/keypair
|
||||||
|
- Integrate popup UI with WASM APIs
|
||||||
|
- Add WebSocket support (Phase 2)
|
BIN
extension/assets/icon-128.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
extension/assets/icon-16.png
Normal file
After Width: | Height: | Size: 454 B |
BIN
extension/assets/icon-32.png
Normal file
After Width: | Height: | Size: 712 B |
BIN
extension/assets/icon-48.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
81
extension/background/index.js
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Background service worker for Modular Vault Extension
|
||||||
|
// Handles state persistence between popup sessions
|
||||||
|
|
||||||
|
console.log('Background service worker started');
|
||||||
|
|
||||||
|
// Store session state locally for quicker access
|
||||||
|
let sessionState = {
|
||||||
|
currentKeyspace: null,
|
||||||
|
keypairs: [],
|
||||||
|
selectedKeypair: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize state from storage
|
||||||
|
chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair'])
|
||||||
|
.then(state => {
|
||||||
|
sessionState = {
|
||||||
|
currentKeyspace: state.currentKeyspace || null,
|
||||||
|
keypairs: state.keypairs || [],
|
||||||
|
selectedKeypair: state.selectedKeypair || null
|
||||||
|
};
|
||||||
|
console.log('Session state loaded from storage:', sessionState);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load session state:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages from the popup
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
console.log('Background received message:', message.action, message.type || '');
|
||||||
|
|
||||||
|
// Update session state
|
||||||
|
if (message.action === 'update_session') {
|
||||||
|
try {
|
||||||
|
const { type, data } = message;
|
||||||
|
|
||||||
|
// Update our local state
|
||||||
|
if (type === 'keyspace') {
|
||||||
|
sessionState.currentKeyspace = data;
|
||||||
|
} else if (type === 'keypair_selected') {
|
||||||
|
sessionState.selectedKeypair = data;
|
||||||
|
} else if (type === 'keypair_added') {
|
||||||
|
sessionState.keypairs = [...sessionState.keypairs, data];
|
||||||
|
} else if (type === 'keypairs_loaded') {
|
||||||
|
// Replace the entire keypair list with what came from the vault
|
||||||
|
console.log('Updating keypairs from vault:', data);
|
||||||
|
sessionState.keypairs = data;
|
||||||
|
} else if (type === 'session_locked') {
|
||||||
|
// When locking, we don't need to maintain keypairs in memory anymore
|
||||||
|
// since they'll be reloaded from the vault when unlocking
|
||||||
|
sessionState = {
|
||||||
|
currentKeyspace: null,
|
||||||
|
keypairs: [], // Clear keypairs from memory since they're in the vault
|
||||||
|
selectedKeypair: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist to storage
|
||||||
|
chrome.storage.local.set(sessionState)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Updated session state in storage:', sessionState);
|
||||||
|
sendResponse({ success: true });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to persist session state:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
return true; // Keep connection open for async response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in update_session message handler:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session state
|
||||||
|
if (message.action === 'get_session') {
|
||||||
|
sendResponse(sessionState);
|
||||||
|
return false; // No async response needed
|
||||||
|
}
|
||||||
|
});
|
84
extension/build.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
// Simple build script for browser extension
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Paths
|
||||||
|
const sourceDir = __dirname;
|
||||||
|
const distDir = path.join(sourceDir, 'dist');
|
||||||
|
|
||||||
|
// Make sure the dist directory exists
|
||||||
|
if (!fs.existsSync(distDir)) {
|
||||||
|
fs.mkdirSync(distDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to copy a file
|
||||||
|
function copyFile(src, dest) {
|
||||||
|
// Create destination directory if it doesn't exist
|
||||||
|
const destDir = path.dirname(dest);
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the file
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
console.log(`Copied: ${path.relative(sourceDir, src)} -> ${path.relative(sourceDir, dest)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to copy an entire directory
|
||||||
|
function copyDir(src, dest) {
|
||||||
|
// Create destination directory
|
||||||
|
if (!fs.existsSync(dest)) {
|
||||||
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get list of files
|
||||||
|
const files = fs.readdirSync(src);
|
||||||
|
|
||||||
|
// Copy each file
|
||||||
|
for (const file of files) {
|
||||||
|
const srcPath = path.join(src, file);
|
||||||
|
const destPath = path.join(dest, file);
|
||||||
|
|
||||||
|
const stat = fs.statSync(srcPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
// Recursively copy directories
|
||||||
|
copyDir(srcPath, destPath);
|
||||||
|
} else {
|
||||||
|
// Copy file
|
||||||
|
copyFile(srcPath, destPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy manifest
|
||||||
|
copyFile(
|
||||||
|
path.join(sourceDir, 'manifest.json'),
|
||||||
|
path.join(distDir, 'manifest.json')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy assets
|
||||||
|
copyDir(
|
||||||
|
path.join(sourceDir, 'assets'),
|
||||||
|
path.join(distDir, 'assets')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy popup files
|
||||||
|
copyDir(
|
||||||
|
path.join(sourceDir, 'popup'),
|
||||||
|
path.join(distDir, 'popup')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy background script
|
||||||
|
copyDir(
|
||||||
|
path.join(sourceDir, 'background'),
|
||||||
|
path.join(distDir, 'background')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy WebAssembly files
|
||||||
|
copyDir(
|
||||||
|
path.join(sourceDir, 'wasm'),
|
||||||
|
path.join(distDir, 'wasm')
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Build complete! Extension files copied to dist directory.');
|
BIN
extension/dist/assets/icon-128.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
extension/dist/assets/icon-16.png
vendored
Normal file
After Width: | Height: | Size: 454 B |
BIN
extension/dist/assets/icon-32.png
vendored
Normal file
After Width: | Height: | Size: 712 B |
BIN
extension/dist/assets/icon-48.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
2
extension/dist/assets/popup.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
(function(){"use strict";var o=document.createElement("style");o.textContent=`body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;margin:0;padding:0;background-color:#202124;color:#e8eaed}.container{width:350px;padding:15px}h1{font-size:18px;margin:0 0 15px;border-bottom:1px solid #3c4043;padding-bottom:10px}h2{font-size:16px;margin:10px 0}.form-section{margin-bottom:20px;background-color:#292a2d;border-radius:8px;padding:15px}.form-group{margin-bottom:10px}label{display:block;margin-bottom:5px;font-size:13px;color:#9aa0a6}input,textarea{width:100%;padding:8px;border:1px solid #3c4043;border-radius:4px;background-color:#202124;color:#e8eaed;box-sizing:border-box}textarea{min-height:60px;resize:vertical}button{background-color:#8ab4f8;color:#202124;border:none;border-radius:4px;padding:8px 16px;font-weight:500;cursor:pointer;transition:background-color .3s}button:hover{background-color:#669df6}button.small{padding:4px 8px;font-size:12px}.button-group{display:flex;gap:10px}.status{margin:10px 0;padding:8px;background-color:#292a2d;border-radius:4px;font-size:13px}.list{margin-top:10px;max-height:150px;overflow-y:auto}.list-item{display:flex;justify-content:space-between;align-items:center;padding:8px;border-bottom:1px solid #3c4043}.list-item.selected{background-color:#8ab4f81a}.hidden{display:none}.session-info{margin-top:15px}
|
||||||
|
`,document.head.appendChild(o);const e=""})();
|
81
extension/dist/background/index.js
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Background service worker for Modular Vault Extension
|
||||||
|
// Handles state persistence between popup sessions
|
||||||
|
|
||||||
|
console.log('Background service worker started');
|
||||||
|
|
||||||
|
// Store session state locally for quicker access
|
||||||
|
let sessionState = {
|
||||||
|
currentKeyspace: null,
|
||||||
|
keypairs: [],
|
||||||
|
selectedKeypair: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize state from storage
|
||||||
|
chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair'])
|
||||||
|
.then(state => {
|
||||||
|
sessionState = {
|
||||||
|
currentKeyspace: state.currentKeyspace || null,
|
||||||
|
keypairs: state.keypairs || [],
|
||||||
|
selectedKeypair: state.selectedKeypair || null
|
||||||
|
};
|
||||||
|
console.log('Session state loaded from storage:', sessionState);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to load session state:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages from the popup
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
console.log('Background received message:', message.action, message.type || '');
|
||||||
|
|
||||||
|
// Update session state
|
||||||
|
if (message.action === 'update_session') {
|
||||||
|
try {
|
||||||
|
const { type, data } = message;
|
||||||
|
|
||||||
|
// Update our local state
|
||||||
|
if (type === 'keyspace') {
|
||||||
|
sessionState.currentKeyspace = data;
|
||||||
|
} else if (type === 'keypair_selected') {
|
||||||
|
sessionState.selectedKeypair = data;
|
||||||
|
} else if (type === 'keypair_added') {
|
||||||
|
sessionState.keypairs = [...sessionState.keypairs, data];
|
||||||
|
} else if (type === 'keypairs_loaded') {
|
||||||
|
// Replace the entire keypair list with what came from the vault
|
||||||
|
console.log('Updating keypairs from vault:', data);
|
||||||
|
sessionState.keypairs = data;
|
||||||
|
} else if (type === 'session_locked') {
|
||||||
|
// When locking, we don't need to maintain keypairs in memory anymore
|
||||||
|
// since they'll be reloaded from the vault when unlocking
|
||||||
|
sessionState = {
|
||||||
|
currentKeyspace: null,
|
||||||
|
keypairs: [], // Clear keypairs from memory since they're in the vault
|
||||||
|
selectedKeypair: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist to storage
|
||||||
|
chrome.storage.local.set(sessionState)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Updated session state in storage:', sessionState);
|
||||||
|
sendResponse({ success: true });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to persist session state:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
return true; // Keep connection open for async response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in update_session message handler:', error);
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session state
|
||||||
|
if (message.action === 'get_session') {
|
||||||
|
sendResponse(sessionState);
|
||||||
|
return false; // No async response needed
|
||||||
|
}
|
||||||
|
});
|
36
extension/dist/manifest.json
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Modular Vault Extension",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Cross-browser modular vault for cryptographic operations and scripting.",
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup/index.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "assets/icon-16.png",
|
||||||
|
"32": "assets/icon-32.png",
|
||||||
|
"48": "assets/icon-48.png",
|
||||||
|
"128": "assets/icon-128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background/index.js",
|
||||||
|
"type": "module"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"scripting"
|
||||||
|
],
|
||||||
|
"host_permissions": [],
|
||||||
|
"icons": {
|
||||||
|
"16": "assets/icon-16.png",
|
||||||
|
"32": "assets/icon-32.png",
|
||||||
|
"48": "assets/icon-48.png",
|
||||||
|
"128": "assets/icon-128.png"
|
||||||
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["wasm/*.wasm", "wasm/*.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
13
extension/dist/popup/index.html
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Modular Vault Extension</title>
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
117
extension/dist/popup/popup.css
vendored
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/* Basic styles for the extension popup */
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #202124;
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 350px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
border-bottom: 1px solid #3c4043;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: #292a2d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9aa0a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #3c4043;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #202124;
|
||||||
|
color: #e8eaed;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 60px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #8ab4f8;
|
||||||
|
color: #202124;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #669df6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.small {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #292a2d;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item.selected {
|
||||||
|
background-color: rgba(138, 180, 248, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
306
extension/dist/popup/popup.js
vendored
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
// Simple non-module JavaScript for browser extension popup
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="container">
|
||||||
|
<h1>Modular Vault Extension</h1>
|
||||||
|
<div id="status" class="status">Loading WASM module...</div>
|
||||||
|
|
||||||
|
<div id="session-controls">
|
||||||
|
<div id="keyspace-form" class="form-section">
|
||||||
|
<h2>Session</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="keyspace">Keyspace:</label>
|
||||||
|
<input type="text" id="keyspace" placeholder="Enter keyspace name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" placeholder="Enter password">
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="unlock-btn">Unlock</button>
|
||||||
|
<button id="create-btn">Create New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="session-info" class="session-info hidden">
|
||||||
|
<h2>Active Session</h2>
|
||||||
|
<p>Current keyspace: <span id="current-keyspace"></span></p>
|
||||||
|
<button id="lock-btn">Lock Session</button>
|
||||||
|
|
||||||
|
<div id="keypair-section" class="form-section">
|
||||||
|
<h2>Keypairs</h2>
|
||||||
|
<button id="create-keypair-btn">Create New Keypair</button>
|
||||||
|
<div id="keypair-list" class="list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sign-section" class="form-section hidden">
|
||||||
|
<h2>Sign Message</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message">Message:</label>
|
||||||
|
<textarea id="message" placeholder="Enter message to sign"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="sign-btn">Sign</button>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="signature">Signature:</label>
|
||||||
|
<textarea id="signature" readonly></textarea>
|
||||||
|
<button id="copy-btn" class="small">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const keyspaceFormEl = document.getElementById('keyspace-form');
|
||||||
|
const sessionInfoEl = document.getElementById('session-info');
|
||||||
|
const currentKeyspaceEl = document.getElementById('current-keyspace');
|
||||||
|
const keyspaceInput = document.getElementById('keyspace');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const unlockBtn = document.getElementById('unlock-btn');
|
||||||
|
const createBtn = document.getElementById('create-btn');
|
||||||
|
const lockBtn = document.getElementById('lock-btn');
|
||||||
|
const createKeypairBtn = document.getElementById('create-keypair-btn');
|
||||||
|
const keypairListEl = document.getElementById('keypair-list');
|
||||||
|
const signSectionEl = document.getElementById('sign-section');
|
||||||
|
const messageInput = document.getElementById('message');
|
||||||
|
const signBtn = document.getElementById('sign-btn');
|
||||||
|
const signatureOutput = document.getElementById('signature');
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
|
||||||
|
// State
|
||||||
|
let wasmModule = null;
|
||||||
|
let currentKeyspace = null;
|
||||||
|
let keypairs = [];
|
||||||
|
let selectedKeypairId = null;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
init();
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
// Get session state from background
|
||||||
|
const sessionState = await getSessionState();
|
||||||
|
|
||||||
|
if (sessionState.currentKeyspace) {
|
||||||
|
// We have an active session
|
||||||
|
currentKeyspace = sessionState.currentKeyspace;
|
||||||
|
keypairs = sessionState.keypairs || [];
|
||||||
|
selectedKeypairId = sessionState.selectedKeypair;
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Ready';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (currentKeyspace) {
|
||||||
|
// Show session info
|
||||||
|
keyspaceFormEl.classList.add('hidden');
|
||||||
|
sessionInfoEl.classList.remove('hidden');
|
||||||
|
currentKeyspaceEl.textContent = currentKeyspace;
|
||||||
|
|
||||||
|
// Update keypair list
|
||||||
|
updateKeypairList();
|
||||||
|
|
||||||
|
// Show/hide sign section based on selected keypair
|
||||||
|
if (selectedKeypairId) {
|
||||||
|
signSectionEl.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
signSectionEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show keyspace form
|
||||||
|
keyspaceFormEl.classList.remove('hidden');
|
||||||
|
sessionInfoEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKeypairList() {
|
||||||
|
// Clear list
|
||||||
|
keypairListEl.innerHTML = '';
|
||||||
|
|
||||||
|
// Add each keypair
|
||||||
|
keypairs.forEach(keypair => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'list-item' + (selectedKeypairId === keypair.id ? ' selected' : '');
|
||||||
|
item.innerHTML = `
|
||||||
|
<span>${keypair.label || keypair.id}</span>
|
||||||
|
<button class="select-btn" data-id="${keypair.id}">Select</button>
|
||||||
|
`;
|
||||||
|
keypairListEl.appendChild(item);
|
||||||
|
|
||||||
|
// Add select handler
|
||||||
|
item.querySelector('.select-btn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
statusEl.textContent = 'Selecting keypair...';
|
||||||
|
// Use background service to select keypair for now
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_selected',
|
||||||
|
data: keypair.id
|
||||||
|
});
|
||||||
|
selectedKeypairId = keypair.id;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Keypair selected: ' + keypair.id;
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error selecting keypair: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session state from background
|
||||||
|
async function getSessionState() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ action: 'get_session' }, (response) => {
|
||||||
|
resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
unlockBtn.addEventListener('click', async () => {
|
||||||
|
const keyspace = keyspaceInput.value.trim();
|
||||||
|
const password = passwordInput.value;
|
||||||
|
|
||||||
|
if (!keyspace || !password) {
|
||||||
|
statusEl.textContent = 'Please enter keyspace and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Unlocking session...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, use the background service worker mock
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keyspace',
|
||||||
|
data: keyspace
|
||||||
|
});
|
||||||
|
|
||||||
|
currentKeyspace = keyspace;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Session unlocked!';
|
||||||
|
|
||||||
|
// Refresh state
|
||||||
|
const state = await getSessionState();
|
||||||
|
keypairs = state.keypairs || [];
|
||||||
|
selectedKeypairId = state.selectedKeypair;
|
||||||
|
updateUI();
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error unlocking session: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createBtn.addEventListener('click', async () => {
|
||||||
|
const keyspace = keyspaceInput.value.trim();
|
||||||
|
const password = passwordInput.value;
|
||||||
|
|
||||||
|
if (!keyspace || !password) {
|
||||||
|
statusEl.textContent = 'Please enter keyspace and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Creating keyspace...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, use the background service worker mock
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keyspace',
|
||||||
|
data: keyspace
|
||||||
|
});
|
||||||
|
|
||||||
|
currentKeyspace = keyspace;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Keyspace created and unlocked!';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error creating keyspace: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lockBtn.addEventListener('click', async () => {
|
||||||
|
statusEl.textContent = 'Locking session...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'session_locked'
|
||||||
|
});
|
||||||
|
|
||||||
|
currentKeyspace = null;
|
||||||
|
keypairs = [];
|
||||||
|
selectedKeypairId = null;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Session locked';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error locking session: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createKeypairBtn.addEventListener('click', async () => {
|
||||||
|
statusEl.textContent = 'Creating keypair...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate a mock keypair ID
|
||||||
|
const keyId = 'key-' + Date.now().toString(16);
|
||||||
|
const newKeypair = {
|
||||||
|
id: keyId,
|
||||||
|
label: `Secp256k1-Key-${keypairs.length + 1}`
|
||||||
|
};
|
||||||
|
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_added',
|
||||||
|
data: newKeypair
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh state
|
||||||
|
const state = await getSessionState();
|
||||||
|
keypairs = state.keypairs || [];
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
statusEl.textContent = 'Keypair created: ' + keyId;
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error creating keypair: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signBtn.addEventListener('click', async () => {
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
statusEl.textContent = 'Please enter a message to sign';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedKeypairId) {
|
||||||
|
statusEl.textContent = 'Please select a keypair first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Signing message...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, generate a mock signature
|
||||||
|
const mockSignature = Array.from({length: 64}, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||||
|
signatureOutput.value = mockSignature;
|
||||||
|
statusEl.textContent = 'Message signed!';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error signing message: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
signatureOutput.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
statusEl.textContent = 'Signature copied to clipboard!';
|
||||||
|
});
|
||||||
|
});
|
765
extension/dist/wasm/wasm_app.js
vendored
Normal file
@ -0,0 +1,765 @@
|
|||||||
|
import * as __wbg_star0 from 'env';
|
||||||
|
|
||||||
|
let wasm;
|
||||||
|
|
||||||
|
function addToExternrefTable0(obj) {
|
||||||
|
const idx = wasm.__externref_table_alloc();
|
||||||
|
wasm.__wbindgen_export_2.set(idx, obj);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(f, args) {
|
||||||
|
try {
|
||||||
|
return f.apply(this, args);
|
||||||
|
} catch (e) {
|
||||||
|
const idx = addToExternrefTable0(e);
|
||||||
|
wasm.__wbindgen_exn_store(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||||
|
|
||||||
|
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||||
|
|
||||||
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
|
function getUint8ArrayMemory0() {
|
||||||
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||||
|
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedUint8ArrayMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikeNone(x) {
|
||||||
|
return x === undefined || x === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayU8FromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||||
|
|
||||||
|
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||||
|
? function (arg, view) {
|
||||||
|
return cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
}
|
||||||
|
: function (arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len, 1) >>> 0;
|
||||||
|
|
||||||
|
const mem = getUint8ArrayMemory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||||
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = encodeString(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedDataViewMemory0 = null;
|
||||||
|
|
||||||
|
function getDataViewMemory0() {
|
||||||
|
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||||
|
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedDataViewMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(state => {
|
||||||
|
wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b)
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeMutClosure(arg0, arg1, dtor, f) {
|
||||||
|
const state = { a: arg0, b: arg1, cnt: 1, dtor };
|
||||||
|
const real = (...args) => {
|
||||||
|
// First up with a closure we increment the internal reference
|
||||||
|
// count. This ensures that the Rust closure environment won't
|
||||||
|
// be deallocated while we're invoking it.
|
||||||
|
state.cnt++;
|
||||||
|
const a = state.a;
|
||||||
|
state.a = 0;
|
||||||
|
try {
|
||||||
|
return f(a, state.b, ...args);
|
||||||
|
} finally {
|
||||||
|
if (--state.cnt === 0) {
|
||||||
|
wasm.__wbindgen_export_5.get(state.dtor)(a, state.b);
|
||||||
|
CLOSURE_DTORS.unregister(state);
|
||||||
|
} else {
|
||||||
|
state.a = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
real.original = state;
|
||||||
|
CLOSURE_DTORS.register(real, state, state);
|
||||||
|
return real;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugString(val) {
|
||||||
|
// primitive types
|
||||||
|
const type = typeof val;
|
||||||
|
if (type == 'number' || type == 'boolean' || val == null) {
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
if (type == 'string') {
|
||||||
|
return `"${val}"`;
|
||||||
|
}
|
||||||
|
if (type == 'symbol') {
|
||||||
|
const description = val.description;
|
||||||
|
if (description == null) {
|
||||||
|
return 'Symbol';
|
||||||
|
} else {
|
||||||
|
return `Symbol(${description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 'function') {
|
||||||
|
const name = val.name;
|
||||||
|
if (typeof name == 'string' && name.length > 0) {
|
||||||
|
return `Function(${name})`;
|
||||||
|
} else {
|
||||||
|
return 'Function';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// objects
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const length = val.length;
|
||||||
|
let debug = '[';
|
||||||
|
if (length > 0) {
|
||||||
|
debug += debugString(val[0]);
|
||||||
|
}
|
||||||
|
for(let i = 1; i < length; i++) {
|
||||||
|
debug += ', ' + debugString(val[i]);
|
||||||
|
}
|
||||||
|
debug += ']';
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
// Test for built-in
|
||||||
|
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||||
|
let className;
|
||||||
|
if (builtInMatches && builtInMatches.length > 1) {
|
||||||
|
className = builtInMatches[1];
|
||||||
|
} else {
|
||||||
|
// Failed to match the standard '[object ClassName]'
|
||||||
|
return toString.call(val);
|
||||||
|
}
|
||||||
|
if (className == 'Object') {
|
||||||
|
// we're a user defined class or Object
|
||||||
|
// JSON.stringify avoids problems with cycles, and is generally much
|
||||||
|
// easier than looping through ownProperties of `val`.
|
||||||
|
try {
|
||||||
|
return 'Object(' + JSON.stringify(val) + ')';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// errors
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||||
|
}
|
||||||
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initialize the scripting environment (must be called before run_rhai)
|
||||||
|
*/
|
||||||
|
export function init_rhai_env() {
|
||||||
|
wasm.init_rhai_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeFromExternrefTable0(idx) {
|
||||||
|
const value = wasm.__wbindgen_export_2.get(idx);
|
||||||
|
wasm.__externref_table_dealloc(idx);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||||
|
* @param {string} script
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export function run_rhai(script) {
|
||||||
|
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.run_rhai(ptr0, len0);
|
||||||
|
if (ret[2]) {
|
||||||
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
|
}
|
||||||
|
return takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize session with keyspace and password
|
||||||
|
* @param {string} keyspace
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export function init_session(keyspace, password) {
|
||||||
|
const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.init_session(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the session (zeroize password and session)
|
||||||
|
*/
|
||||||
|
export function lock_session() {
|
||||||
|
wasm.lock_session();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all keypairs from the current session
|
||||||
|
* Returns an array of keypair objects with id, type, and metadata
|
||||||
|
* Select keypair for the session
|
||||||
|
* @param {string} key_id
|
||||||
|
*/
|
||||||
|
export function select_keypair(key_id) {
|
||||||
|
const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.select_keypair(ptr0, len0);
|
||||||
|
if (ret[1]) {
|
||||||
|
throw takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List keypairs in the current session's keyspace
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function list_keypairs() {
|
||||||
|
const ret = wasm.list_keypairs();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a keypair to the current keyspace
|
||||||
|
* @param {string | null} [key_type]
|
||||||
|
* @param {string | null} [metadata]
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function add_keypair(key_type, metadata) {
|
||||||
|
var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.add_keypair(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passArray8ToWasm0(arg, malloc) {
|
||||||
|
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||||
|
WASM_VECTOR_LEN = arg.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sign message with current session
|
||||||
|
* @param {Uint8Array} message
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function sign(message) {
|
||||||
|
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sign(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||||
|
wasm.closure77_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||||
|
wasm.closure126_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||||
|
wasm.closure188_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_123(arg0, arg1, arg2, arg3) {
|
||||||
|
wasm.closure213_externref_shim(arg0, arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||||
|
|
||||||
|
async function __wbg_load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(module, imports);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||||
|
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return { instance, module };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_get_imports() {
|
||||||
|
const imports = {};
|
||||||
|
imports.wbg = {};
|
||||||
|
imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) {
|
||||||
|
const ret = arg0.buffer;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.call(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.call(arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
|
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) {
|
||||||
|
const ret = arg0.crypto;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||||
|
console.error(arg0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) {
|
||||||
|
const ret = arg0.error;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) {
|
||||||
|
arg0.getRandomValues(arg1);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) {
|
||||||
|
const ret = arg1[arg2 >>> 0];
|
||||||
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = Reflect.get(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.get(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBDatabase;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBFactory;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBOpenDBRequest;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBRequest;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||||
|
const ret = arg0.length;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||||
|
const ret = arg0.msCrypto;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
|
||||||
|
try {
|
||||||
|
var state0 = {a: arg0, b: arg1};
|
||||||
|
var cb0 = (arg0, arg1) => {
|
||||||
|
const a = state0.a;
|
||||||
|
state0.a = 0;
|
||||||
|
try {
|
||||||
|
return __wbg_adapter_123(a, state0.b, arg0, arg1);
|
||||||
|
} finally {
|
||||||
|
state0.a = a;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ret = new Promise(cb0);
|
||||||
|
return ret;
|
||||||
|
} finally {
|
||||||
|
state0.a = state0.b = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_405e22f390576ce2 = function() {
|
||||||
|
const ret = new Object();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_78feb108b6472713 = function() {
|
||||||
|
const ret = new Array();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||||
|
const ret = new Uint8Array(arg0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
|
||||||
|
const ret = new Function(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) {
|
||||||
|
const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) {
|
||||||
|
const ret = new Uint8Array(arg0 >>> 0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) {
|
||||||
|
const ret = arg0.node;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) {
|
||||||
|
const ret = arg0.objectStoreNames;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
|
const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) {
|
||||||
|
const ret = arg0.process;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) {
|
||||||
|
const ret = arg0.push(arg1);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.put(arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.put(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
|
||||||
|
queueMicrotask(arg0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
|
||||||
|
const ret = arg0.queueMicrotask;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
arg0.randomFillSync(arg1);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||||
|
const ret = module.require;
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
|
||||||
|
const ret = Promise.resolve(arg0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) {
|
||||||
|
const ret = arg0.result;
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||||
|
arg0.set(arg1, arg2 >>> 0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||||
|
arg0.onerror = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||||
|
arg0.onsuccess = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) {
|
||||||
|
arg0.onupgradeneeded = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
|
||||||
|
const ret = typeof global === 'undefined' ? null : global;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
|
||||||
|
const ret = typeof globalThis === 'undefined' ? null : globalThis;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
|
||||||
|
const ret = typeof self === 'undefined' ? null : self;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
|
||||||
|
const ret = typeof window === 'undefined' ? null : window;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) {
|
||||||
|
const ret = arg0.target;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
|
||||||
|
const ret = arg0.then(arg1);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) {
|
||||||
|
const ret = arg0.versions;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||||
|
const obj = arg0.original;
|
||||||
|
if (obj.cnt-- == 1) {
|
||||||
|
obj.a = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ret = false;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||||
|
const ret = debugString(arg1);
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||||
|
const table = wasm.__wbindgen_export_2;
|
||||||
|
const offset = table.grow(4);
|
||||||
|
table.set(0, undefined);
|
||||||
|
table.set(offset + 0, undefined);
|
||||||
|
table.set(offset + 1, null);
|
||||||
|
table.set(offset + 2, true);
|
||||||
|
table.set(offset + 3, false);
|
||||||
|
;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_function = function(arg0) {
|
||||||
|
const ret = typeof(arg0) === 'function';
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_null = function(arg0) {
|
||||||
|
const ret = arg0 === null;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_object = function(arg0) {
|
||||||
|
const val = arg0;
|
||||||
|
const ret = typeof(val) === 'object' && val !== null;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_string = function(arg0) {
|
||||||
|
const ret = typeof(arg0) === 'string';
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_undefined = function(arg0) {
|
||||||
|
const ret = arg0 === undefined;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_json_parse = function(arg0, arg1) {
|
||||||
|
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
|
||||||
|
const obj = arg1;
|
||||||
|
const ret = JSON.stringify(obj === undefined ? null : obj);
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_memory = function() {
|
||||||
|
const ret = wasm.memory;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||||
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
};
|
||||||
|
imports['env'] = __wbg_star0;
|
||||||
|
|
||||||
|
return imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_init_memory(imports, memory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_finalize_init(instance, module) {
|
||||||
|
wasm = instance.exports;
|
||||||
|
__wbg_init.__wbindgen_wasm_module = module;
|
||||||
|
cachedDataViewMemory0 = null;
|
||||||
|
cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
|
|
||||||
|
wasm.__wbindgen_start();
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSync(module) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||||
|
({module} = module)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
__wbg_init_memory(imports);
|
||||||
|
|
||||||
|
if (!(module instanceof WebAssembly.Module)) {
|
||||||
|
module = new WebAssembly.Module(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new WebAssembly.Instance(module, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_init(module_or_path) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module_or_path !== 'undefined') {
|
||||||
|
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||||
|
({module_or_path} = module_or_path)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'undefined') {
|
||||||
|
module_or_path = new URL('wasm_app_bg.wasm', import.meta.url);
|
||||||
|
}
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||||
|
module_or_path = fetch(module_or_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
__wbg_init_memory(imports);
|
||||||
|
|
||||||
|
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initSync };
|
||||||
|
export default __wbg_init;
|
BIN
extension/dist/wasm/wasm_app_bg.wasm
vendored
Normal file
36
extension/manifest.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Modular Vault Extension",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Cross-browser modular vault for cryptographic operations and scripting.",
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup/index.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "assets/icon-16.png",
|
||||||
|
"32": "assets/icon-32.png",
|
||||||
|
"48": "assets/icon-48.png",
|
||||||
|
"128": "assets/icon-128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background/index.js",
|
||||||
|
"type": "module"
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"scripting"
|
||||||
|
],
|
||||||
|
"host_permissions": [],
|
||||||
|
"icons": {
|
||||||
|
"16": "assets/icon-16.png",
|
||||||
|
"32": "assets/icon-32.png",
|
||||||
|
"48": "assets/icon-48.png",
|
||||||
|
"128": "assets/icon-128.png"
|
||||||
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["wasm/*.wasm", "wasm/*.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1474
extension/package-lock.json
generated
Normal file
21
extension/package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "modular-vault-extension",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Cross-browser modular vault extension with secure WASM integration and React UI.",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --mode development",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:ext": "node build.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^4.5.0",
|
||||||
|
"vite-plugin-top-level-await": "^1.4.0",
|
||||||
|
"vite-plugin-wasm": "^3.4.1"
|
||||||
|
}
|
||||||
|
}
|
219
extension/popup/App.jsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import KeyspaceManager from './KeyspaceManager';
|
||||||
|
import KeypairManager from './KeypairManager';
|
||||||
|
import SignMessage from './SignMessage';
|
||||||
|
import * as wasmHelper from './WasmHelper';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [wasmState, setWasmState] = useState({
|
||||||
|
loading: false,
|
||||||
|
initialized: false,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
const [locked, setLocked] = useState(true);
|
||||||
|
const [keyspaces, setKeyspaces] = useState([]);
|
||||||
|
const [currentKeyspace, setCurrentKeyspace] = useState('');
|
||||||
|
const [keypairs, setKeypairs] = useState([]); // [{id, label, publicKey}]
|
||||||
|
const [selectedKeypair, setSelectedKeypair] = useState('');
|
||||||
|
const [signature, setSignature] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
|
||||||
|
// Load WebAssembly on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
async function initWasm() {
|
||||||
|
try {
|
||||||
|
setStatus('Loading WebAssembly module...');
|
||||||
|
await wasmHelper.loadWasmModule();
|
||||||
|
setWasmState(wasmHelper.getWasmState());
|
||||||
|
setStatus('WebAssembly module loaded');
|
||||||
|
// Load session state
|
||||||
|
await refreshStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load WebAssembly:', error);
|
||||||
|
setStatus('Error loading WebAssembly: ' + (error.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initWasm();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch status from background on mount
|
||||||
|
async function refreshStatus() {
|
||||||
|
const state = await wasmHelper.getSessionState();
|
||||||
|
setCurrentKeyspace(state.currentKeyspace || '');
|
||||||
|
setKeypairs(state.keypairs || []);
|
||||||
|
setSelectedKeypair(state.selectedKeypair || '');
|
||||||
|
setLocked(!state.currentKeyspace);
|
||||||
|
|
||||||
|
// For demo: collect all keyspaces from storage
|
||||||
|
if (state.keypairs && state.keypairs.length > 0) {
|
||||||
|
setKeyspaces([state.currentKeyspace]);
|
||||||
|
} else {
|
||||||
|
setKeyspaces([state.currentKeyspace].filter(Boolean));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session unlock/create
|
||||||
|
const handleUnlock = async (keyspace, password) => {
|
||||||
|
if (!wasmState.initialized) {
|
||||||
|
setStatus('WebAssembly module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setStatus('Unlocking...');
|
||||||
|
try {
|
||||||
|
await wasmHelper.initSession(keyspace, password);
|
||||||
|
setCurrentKeyspace(keyspace);
|
||||||
|
setLocked(false);
|
||||||
|
setStatus('Session unlocked!');
|
||||||
|
await refreshStatus();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Unlock failed: ' + e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateKeyspace = async (keyspace, password) => {
|
||||||
|
if (!wasmState.initialized) {
|
||||||
|
setStatus('WebAssembly module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setStatus('Creating keyspace...');
|
||||||
|
try {
|
||||||
|
await wasmHelper.initSession(keyspace, password);
|
||||||
|
setCurrentKeyspace(keyspace);
|
||||||
|
setLocked(false);
|
||||||
|
setStatus('Keyspace created and unlocked!');
|
||||||
|
await refreshStatus();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Create failed: ' + e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLock = async () => {
|
||||||
|
if (!wasmState.initialized) {
|
||||||
|
setStatus('WebAssembly module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setStatus('Locking...');
|
||||||
|
try {
|
||||||
|
await wasmHelper.lockSession();
|
||||||
|
setLocked(true);
|
||||||
|
setCurrentKeyspace('');
|
||||||
|
setKeypairs([]);
|
||||||
|
setSelectedKeypair('');
|
||||||
|
setStatus('Session locked.');
|
||||||
|
await refreshStatus();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Lock failed: ' + e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectKeypair = async (id) => {
|
||||||
|
if (!wasmState.initialized) {
|
||||||
|
setStatus('WebAssembly module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setStatus('Selecting keypair...');
|
||||||
|
try {
|
||||||
|
await wasmHelper.selectKeypair(id);
|
||||||
|
setSelectedKeypair(id);
|
||||||
|
setStatus('Keypair selected.');
|
||||||
|
await refreshStatus();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Select failed: ' + e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateKeypair = async () => {
|
||||||
|
if (!wasmState.initialized) {
|
||||||
|
setStatus('WebAssembly module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setStatus('Creating keypair...');
|
||||||
|
try {
|
||||||
|
const keyId = await wasmHelper.addKeypair();
|
||||||
|
setStatus('Keypair created. ID: ' + keyId);
|
||||||
|
await refreshStatus();
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Create failed: ' + e);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSign = async (message) => {
|
||||||
|
if (!wasmState.initialized) {
|
||||||
|
setStatus('WebAssembly module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setStatus('Signing message...');
|
||||||
|
try {
|
||||||
|
if (!selectedKeypair) {
|
||||||
|
throw new Error('No keypair selected');
|
||||||
|
}
|
||||||
|
const sig = await wasmHelper.sign(message);
|
||||||
|
setSignature(sig);
|
||||||
|
setStatus('Message signed!');
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('Signing failed: ' + e);
|
||||||
|
setSignature('');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<h1>Modular Vault Extension</h1>
|
||||||
|
{wasmState.error && (
|
||||||
|
<div className="error">
|
||||||
|
WebAssembly Error: {wasmState.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<KeyspaceManager
|
||||||
|
keyspaces={keyspaces}
|
||||||
|
onUnlock={handleUnlock}
|
||||||
|
onCreate={handleCreateKeyspace}
|
||||||
|
locked={locked}
|
||||||
|
onLock={handleLock}
|
||||||
|
currentKeyspace={currentKeyspace}
|
||||||
|
/>
|
||||||
|
{!locked && (
|
||||||
|
<>
|
||||||
|
<KeypairManager
|
||||||
|
keypairs={keypairs}
|
||||||
|
onSelect={handleSelectKeypair}
|
||||||
|
onCreate={handleCreateKeypair}
|
||||||
|
selectedKeypair={selectedKeypair}
|
||||||
|
/>
|
||||||
|
{selectedKeypair && (
|
||||||
|
<SignMessage
|
||||||
|
onSign={handleSign}
|
||||||
|
signature={signature}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="status" style={{marginTop: '1rem', minHeight: 24}}>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
30
extension/popup/KeypairManager.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
export default function KeypairManager({ keypairs, onSelect, onCreate, selectedKeypair }) {
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="keypair-manager">
|
||||||
|
<label>Keypair:</label>
|
||||||
|
<select value={selectedKeypair || ''} onChange={e => onSelect(e.target.value)}>
|
||||||
|
<option value="" disabled>Select keypair</option>
|
||||||
|
{keypairs.map(kp => (
|
||||||
|
<option key={kp.id} value={kp.id}>{kp.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button onClick={() => setCreating(true)} style={{marginLeft: 8}}>Create New</button>
|
||||||
|
{creating && (
|
||||||
|
<div style={{marginTop: '0.5rem'}}>
|
||||||
|
<button onClick={() => { onCreate(); setCreating(false); }}>Create Secp256k1 Keypair</button>
|
||||||
|
<button onClick={() => setCreating(false)} style={{marginLeft: 8}}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedKeypair && (
|
||||||
|
<div style={{marginTop: '0.5rem'}}>
|
||||||
|
<span>Public Key: <code>{keypairs.find(kp => kp.id === selectedKeypair)?.publicKey}</code></span>
|
||||||
|
<button onClick={() => navigator.clipboard.writeText(keypairs.find(kp => kp.id === selectedKeypair)?.publicKey)} style={{marginLeft: 8}}>Copy</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
30
extension/popup/KeyspaceManager.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
export default function KeyspaceManager({ keyspaces, onUnlock, onCreate, locked, onLock, currentKeyspace }) {
|
||||||
|
const [selected, setSelected] = useState(keyspaces[0] || '');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [newKeyspace, setNewKeyspace] = useState('');
|
||||||
|
|
||||||
|
if (locked) {
|
||||||
|
return (
|
||||||
|
<div className="keyspace-manager">
|
||||||
|
<label>Keyspace:</label>
|
||||||
|
<select value={selected} onChange={e => setSelected(e.target.value)}>
|
||||||
|
{keyspaces.map(k => <option key={k} value={k}>{k}</option>)}
|
||||||
|
</select>
|
||||||
|
<button onClick={() => onUnlock(selected, password)} disabled={!selected || !password}>Unlock</button>
|
||||||
|
<div style={{marginTop: '0.5rem'}}>
|
||||||
|
<input placeholder="New keyspace name" value={newKeyspace} onChange={e => setNewKeyspace(e.target.value)} />
|
||||||
|
<input placeholder="Password" type="password" value={password} onChange={e => setPassword(e.target.value)} />
|
||||||
|
<button onClick={() => onCreate(newKeyspace, password)} disabled={!newKeyspace || !password}>Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="keyspace-manager">
|
||||||
|
<span>Keyspace: <b>{currentKeyspace}</b></span>
|
||||||
|
<button onClick={onLock} style={{marginLeft: 8}}>Lock Session</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
27
extension/popup/SignMessage.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
export default function SignMessage({ onSign, signature, loading }) {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sign-message">
|
||||||
|
<label>Message to sign:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter plaintext message"
|
||||||
|
value={message}
|
||||||
|
onChange={e => setMessage(e.target.value)}
|
||||||
|
style={{width: '100%', marginBottom: 8}}
|
||||||
|
/>
|
||||||
|
<button onClick={() => onSign(message)} disabled={!message || loading}>
|
||||||
|
{loading ? 'Signing...' : 'Sign'}
|
||||||
|
</button>
|
||||||
|
{signature && (
|
||||||
|
<div style={{marginTop: '0.5rem'}}>
|
||||||
|
<span>Signature: <code>{signature}</code></span>
|
||||||
|
<button onClick={() => navigator.clipboard.writeText(signature)} style={{marginLeft: 8}}>Copy</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
667
extension/popup/WasmHelper.js
Normal file
@ -0,0 +1,667 @@
|
|||||||
|
/**
|
||||||
|
* Browser extension-friendly WebAssembly loader and helper functions
|
||||||
|
* This handles loading the WebAssembly module without relying on ES modules
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Global reference to the loaded WebAssembly module
|
||||||
|
let wasmModule = null;
|
||||||
|
|
||||||
|
// Initialization state
|
||||||
|
const state = {
|
||||||
|
loading: false,
|
||||||
|
initialized: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the WebAssembly module
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function loadWasmModule() {
|
||||||
|
if (state.initialized || state.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get paths to WebAssembly files
|
||||||
|
const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js');
|
||||||
|
const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
||||||
|
|
||||||
|
console.log('Loading WASM JS from:', wasmJsPath);
|
||||||
|
console.log('Loading WASM binary from:', wasmBinaryPath);
|
||||||
|
|
||||||
|
// Create a container for our temporary WebAssembly globals
|
||||||
|
window.__wasmApp = {};
|
||||||
|
|
||||||
|
// Create a script element to load the JS file
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = wasmJsPath;
|
||||||
|
|
||||||
|
// Wait for the script to load
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = () => reject(new Error('Failed to load WASM JavaScript file'));
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the wasm_app global was created
|
||||||
|
if (!window.wasm_app && !window.__wbg_init) {
|
||||||
|
throw new Error('WASM module did not export expected functions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the initialization function
|
||||||
|
const init = window.__wbg_init || (window.wasm_app && window.wasm_app.default);
|
||||||
|
|
||||||
|
if (!init || typeof init !== 'function') {
|
||||||
|
throw new Error('WASM init function not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the WASM binary file
|
||||||
|
const response = await fetch(wasmBinaryPath);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch WASM binary: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the binary data
|
||||||
|
const wasmBinary = await response.arrayBuffer();
|
||||||
|
|
||||||
|
// Initialize the WASM module
|
||||||
|
await init(wasmBinary);
|
||||||
|
|
||||||
|
// Debug logging for available functions in the WebAssembly module
|
||||||
|
console.log('Available WebAssembly functions:');
|
||||||
|
console.log('init_rhai_env:', typeof window.init_rhai_env, typeof (window.wasm_app && window.wasm_app.init_rhai_env));
|
||||||
|
console.log('init_session:', typeof window.init_session, typeof (window.wasm_app && window.wasm_app.init_session));
|
||||||
|
console.log('lock_session:', typeof window.lock_session, typeof (window.wasm_app && window.wasm_app.lock_session));
|
||||||
|
console.log('add_keypair:', typeof window.add_keypair, typeof (window.wasm_app && window.wasm_app.add_keypair));
|
||||||
|
console.log('select_keypair:', typeof window.select_keypair, typeof (window.wasm_app && window.wasm_app.select_keypair));
|
||||||
|
console.log('sign:', typeof window.sign, typeof (window.wasm_app && window.wasm_app.sign));
|
||||||
|
console.log('run_rhai:', typeof window.run_rhai, typeof (window.wasm_app && window.wasm_app.run_rhai));
|
||||||
|
console.log('list_keypairs:', typeof window.list_keypairs, typeof (window.wasm_app && window.wasm_app.list_keypairs));
|
||||||
|
|
||||||
|
// Store reference to all the exported functions
|
||||||
|
wasmModule = {
|
||||||
|
init_rhai_env: window.init_rhai_env || (window.wasm_app && window.wasm_app.init_rhai_env),
|
||||||
|
init_session: window.init_session || (window.wasm_app && window.wasm_app.init_session),
|
||||||
|
lock_session: window.lock_session || (window.wasm_app && window.wasm_app.lock_session),
|
||||||
|
add_keypair: window.add_keypair || (window.wasm_app && window.wasm_app.add_keypair),
|
||||||
|
select_keypair: window.select_keypair || (window.wasm_app && window.wasm_app.select_keypair),
|
||||||
|
sign: window.sign || (window.wasm_app && window.wasm_app.sign),
|
||||||
|
run_rhai: window.run_rhai || (window.wasm_app && window.wasm_app.run_rhai),
|
||||||
|
list_keypairs: window.list_keypairs || (window.wasm_app && window.wasm_app.list_keypairs),
|
||||||
|
list_keypairs_debug: window.list_keypairs_debug || (window.wasm_app && window.wasm_app.list_keypairs_debug),
|
||||||
|
check_indexeddb: window.check_indexeddb || (window.wasm_app && window.wasm_app.check_indexeddb)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log what was actually registered
|
||||||
|
console.log('Registered WebAssembly module functions:');
|
||||||
|
for (const [key, value] of Object.entries(wasmModule)) {
|
||||||
|
console.log(`${key}: ${typeof value}`, value ? 'Available' : 'Missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the WASM environment
|
||||||
|
if (typeof wasmModule.init_rhai_env === 'function') {
|
||||||
|
wasmModule.init_rhai_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.initialized = true;
|
||||||
|
console.log('WASM module loaded and initialized successfully');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load WASM module:', error);
|
||||||
|
state.error = error.message || 'Unknown error loading WebAssembly module';
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of the WebAssembly module
|
||||||
|
* @returns {{loading: boolean, initialized: boolean, error: string|null}}
|
||||||
|
*/
|
||||||
|
export function getWasmState() {
|
||||||
|
return { ...state };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the WebAssembly module
|
||||||
|
* @returns {object|null} The WebAssembly module or null if not loaded
|
||||||
|
*/
|
||||||
|
export function getWasmModule() {
|
||||||
|
return wasmModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug function to check the vault state
|
||||||
|
* @returns {Promise<object>} State information
|
||||||
|
*/
|
||||||
|
export async function debugVaultState() {
|
||||||
|
const module = getWasmModule();
|
||||||
|
if (!module) {
|
||||||
|
throw new Error('WebAssembly module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Debugging vault state...');
|
||||||
|
|
||||||
|
// Check if we have a valid session using Rhai script
|
||||||
|
const sessionCheck = `
|
||||||
|
let has_session = vault::has_active_session();
|
||||||
|
let keyspace = "";
|
||||||
|
if has_session {
|
||||||
|
keyspace = vault::get_current_keyspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return info about the session
|
||||||
|
{
|
||||||
|
"has_session": has_session,
|
||||||
|
"keyspace": keyspace
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('Checking session status...');
|
||||||
|
const sessionStatus = await module.run_rhai(sessionCheck);
|
||||||
|
console.log('Session status:', sessionStatus);
|
||||||
|
|
||||||
|
// Get keypair info if we have a session
|
||||||
|
if (sessionStatus && sessionStatus.has_session) {
|
||||||
|
const keypairsScript = `
|
||||||
|
// Get all keypairs for the current keyspace
|
||||||
|
let keypairs = vault::list_keypairs();
|
||||||
|
|
||||||
|
// Add diagnostic information
|
||||||
|
let diagnostic = {
|
||||||
|
"keypair_count": keypairs.len(),
|
||||||
|
"keyspace": vault::get_current_keyspace(),
|
||||||
|
"keypairs": keypairs
|
||||||
|
};
|
||||||
|
|
||||||
|
diagnostic
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('Fetching keypair details...');
|
||||||
|
const keypairDiagnostic = await module.run_rhai(keypairsScript);
|
||||||
|
console.log('Keypair diagnostic:', keypairDiagnostic);
|
||||||
|
|
||||||
|
return keypairDiagnostic;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionStatus;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in debug function:', error);
|
||||||
|
return { error: error.toString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get keypairs from the vault
|
||||||
|
* @returns {Promise<Array>} List of keypairs
|
||||||
|
*/
|
||||||
|
export async function getKeypairsFromVault() {
|
||||||
|
console.log('===============================================');
|
||||||
|
console.log('Starting getKeypairsFromVault...');
|
||||||
|
const module = getWasmModule();
|
||||||
|
if (!module) {
|
||||||
|
console.error('WebAssembly module not loaded!');
|
||||||
|
throw new Error('WebAssembly module not loaded');
|
||||||
|
}
|
||||||
|
console.log('WebAssembly module:', module);
|
||||||
|
console.log('Module functions available:', Object.keys(module));
|
||||||
|
|
||||||
|
// Check if IndexedDB is available and working
|
||||||
|
const isIndexedDBAvailable = await checkIndexedDBAvailability();
|
||||||
|
if (!isIndexedDBAvailable) {
|
||||||
|
console.warn('IndexedDB is not available or not working properly');
|
||||||
|
// We'll continue, but this is likely why keypairs aren't persisting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force re-initialization of the current session if needed
|
||||||
|
try {
|
||||||
|
// This checks if we have the debug function available
|
||||||
|
if (typeof module.list_keypairs_debug === 'function') {
|
||||||
|
console.log('Using debug function to diagnose keypair loading issues...');
|
||||||
|
const debugResult = await module.list_keypairs_debug();
|
||||||
|
console.log('Debug keypair listing result:', debugResult);
|
||||||
|
if (Array.isArray(debugResult) && debugResult.length > 0) {
|
||||||
|
console.log('Debug function returned keypairs:', debugResult);
|
||||||
|
// If debug function worked but regular function doesn't, use its result
|
||||||
|
return debugResult;
|
||||||
|
} else {
|
||||||
|
console.log('Debug function did not return keypairs, continuing with normal flow...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in debug function:', err);
|
||||||
|
// Continue with normal flow even if the debug function fails
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('-----------------------------------------------');
|
||||||
|
console.log('Running diagnostics to check vault state...');
|
||||||
|
// Run diagnostic first to log vault state
|
||||||
|
await debugVaultState();
|
||||||
|
console.log('Diagnostics complete');
|
||||||
|
console.log('-----------------------------------------------');
|
||||||
|
|
||||||
|
console.log('Checking if list_keypairs function is available:', typeof module.list_keypairs);
|
||||||
|
for (const key in module) {
|
||||||
|
console.log(`Module function: ${key} = ${typeof module[key]}`);
|
||||||
|
}
|
||||||
|
if (typeof module.list_keypairs !== 'function') {
|
||||||
|
console.error('list_keypairs function is not available in the WebAssembly module!');
|
||||||
|
console.log('Available functions:', Object.keys(module));
|
||||||
|
// Fall back to Rhai script
|
||||||
|
console.log('Falling back to using Rhai script for listing keypairs...');
|
||||||
|
const script = `
|
||||||
|
// Get all keypairs from the current keyspace
|
||||||
|
let keypairs = vault::list_keypairs();
|
||||||
|
keypairs
|
||||||
|
`;
|
||||||
|
const keypairList = await module.run_rhai(script);
|
||||||
|
console.log('Retrieved keypairs from vault using Rhai:', keypairList);
|
||||||
|
return keypairList;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Calling WebAssembly list_keypairs function...');
|
||||||
|
// Use the direct list_keypairs function from WebAssembly instead of Rhai script
|
||||||
|
const keypairList = await module.list_keypairs();
|
||||||
|
console.log('Retrieved keypairs from vault:', keypairList);
|
||||||
|
|
||||||
|
console.log('Raw keypair list type:', typeof keypairList);
|
||||||
|
console.log('Is array?', Array.isArray(keypairList));
|
||||||
|
console.log('Raw keypair list:', keypairList);
|
||||||
|
|
||||||
|
// Format keypairs for UI
|
||||||
|
const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => {
|
||||||
|
// Parse metadata if available
|
||||||
|
let metadata = {};
|
||||||
|
if (kp.metadata) {
|
||||||
|
try {
|
||||||
|
if (typeof kp.metadata === 'string') {
|
||||||
|
metadata = JSON.parse(kp.metadata);
|
||||||
|
} else {
|
||||||
|
metadata = kp.metadata;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse keypair metadata:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: kp.id,
|
||||||
|
label: metadata.label || `Key-${kp.id.substring(0, 4)}`
|
||||||
|
};
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
console.log('Formatted keypairs for UI:', formattedKeypairs);
|
||||||
|
|
||||||
|
// Update background service worker
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypairs_loaded',
|
||||||
|
data: formattedKeypairs
|
||||||
|
}, (response) => {
|
||||||
|
console.log('Background response to keypairs update:', response);
|
||||||
|
resolve(formattedKeypairs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching keypairs from vault:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if IndexedDB is available and working
|
||||||
|
* @returns {Promise<boolean>} True if IndexedDB is working
|
||||||
|
*/
|
||||||
|
export async function checkIndexedDBAvailability() {
|
||||||
|
console.log('Checking IndexedDB availability...');
|
||||||
|
|
||||||
|
// First check if IndexedDB is available in the browser
|
||||||
|
if (!window.indexedDB) {
|
||||||
|
console.error('IndexedDB is not available in this browser');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = getWasmModule();
|
||||||
|
if (!module || typeof module.check_indexeddb !== 'function') {
|
||||||
|
console.error('WebAssembly module or check_indexeddb function not available');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await module.check_indexeddb();
|
||||||
|
console.log('IndexedDB check result:', result);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('IndexedDB check failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a session with the given keyspace and password
|
||||||
|
* @param {string} keyspace
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {Promise<Array>} List of keypairs after initialization
|
||||||
|
*/
|
||||||
|
export async function initSession(keyspace, password) {
|
||||||
|
const module = getWasmModule();
|
||||||
|
if (!module) {
|
||||||
|
throw new Error('WebAssembly module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Initializing session for keyspace: ${keyspace}`);
|
||||||
|
|
||||||
|
// Check if IndexedDB is working
|
||||||
|
const isIndexedDBAvailable = await checkIndexedDBAvailability();
|
||||||
|
if (!isIndexedDBAvailable) {
|
||||||
|
console.warn('IndexedDB is not available or not working properly. Keypairs might not persist.');
|
||||||
|
// Continue anyway as we might fall back to memory storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the session using the WASM module
|
||||||
|
await module.init_session(keyspace, password);
|
||||||
|
console.log('Session initialized successfully');
|
||||||
|
|
||||||
|
// Check if we have stored keypairs for this keyspace in Chrome storage
|
||||||
|
const storedKeypairs = await new Promise(resolve => {
|
||||||
|
chrome.storage.local.get([`keypairs:${keyspace}`], result => {
|
||||||
|
resolve(result[`keypairs:${keyspace}`] || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${storedKeypairs.length} stored keypairs for keyspace ${keyspace}`);
|
||||||
|
|
||||||
|
// Import stored keypairs into the WebAssembly session if they don't exist already
|
||||||
|
if (storedKeypairs.length > 0) {
|
||||||
|
console.log('Importing stored keypairs into WebAssembly session...');
|
||||||
|
|
||||||
|
// First get current keypairs from the vault directly
|
||||||
|
const wasmKeypairs = await module.list_keypairs();
|
||||||
|
console.log('Current keypairs in WebAssembly vault:', wasmKeypairs);
|
||||||
|
|
||||||
|
// Get the IDs of existing keypairs in the vault
|
||||||
|
const existingIds = new Set(wasmKeypairs.map(kp => kp.id));
|
||||||
|
|
||||||
|
// Import keypairs that don't already exist in the vault
|
||||||
|
for (const keypair of storedKeypairs) {
|
||||||
|
if (!existingIds.has(keypair.id)) {
|
||||||
|
console.log(`Importing keypair ${keypair.id} into WebAssembly vault...`);
|
||||||
|
|
||||||
|
// Create metadata for the keypair
|
||||||
|
const metadata = JSON.stringify({
|
||||||
|
label: keypair.label || `Key-${keypair.id.substring(0, 8)}`,
|
||||||
|
imported: true,
|
||||||
|
importDate: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// For adding existing keypairs, we'd normally need the private key
|
||||||
|
// Since we can't retrieve it, we'll create a new one with the same label
|
||||||
|
// This is a placeholder - in a real implementation, you'd need to use the actual keys
|
||||||
|
try {
|
||||||
|
const keyType = keypair.type || 'Secp256k1';
|
||||||
|
await module.add_keypair(keyType, metadata);
|
||||||
|
console.log(`Created keypair of type ${keyType} with label ${keypair.label}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to import keypair ${keypair.id}:`, err);
|
||||||
|
// Continue with other keypairs even if one fails
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Keypair ${keypair.id} already exists in vault, skipping import`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize session using WASM (await the async function)
|
||||||
|
await module.init_session(keyspace, password);
|
||||||
|
|
||||||
|
// Get keypairs from the vault after session is ready
|
||||||
|
const currentKeypairs = await getKeypairsFromVault();
|
||||||
|
|
||||||
|
// Update keypairs in background service worker
|
||||||
|
await new Promise(resolve => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypairs_loaded',
|
||||||
|
data: currentKeypairs
|
||||||
|
}, response => {
|
||||||
|
console.log('Updated keypairs in background service worker');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentKeypairs;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the current session
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function lockSession() {
|
||||||
|
const module = getWasmModule();
|
||||||
|
if (!module) {
|
||||||
|
throw new Error('WebAssembly module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Locking session...');
|
||||||
|
|
||||||
|
// First run diagnostics to see what we have before locking
|
||||||
|
await debugVaultState();
|
||||||
|
|
||||||
|
// Call the WASM lock_session function
|
||||||
|
module.lock_session();
|
||||||
|
console.log('Session locked in WebAssembly module');
|
||||||
|
|
||||||
|
// Update session state in background
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'session_locked'
|
||||||
|
}, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
console.log('Background service worker updated for locked session');
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to update session state in background:', response?.error);
|
||||||
|
reject(new Error(response?.error || 'Failed to update session state'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify session is locked properly
|
||||||
|
const sessionStatus = await debugVaultState();
|
||||||
|
console.log('Session status after locking:', sessionStatus);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error locking session:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new keypair
|
||||||
|
* @param {string} keyType The type of key to create (default: 'Secp256k1')
|
||||||
|
* @param {string} label Optional custom label for the keypair
|
||||||
|
* @returns {Promise<{id: string, label: string}>} The created keypair info
|
||||||
|
*/
|
||||||
|
export async function addKeypair(keyType = 'Secp256k1', label = null) {
|
||||||
|
const module = getWasmModule();
|
||||||
|
if (!module) {
|
||||||
|
throw new Error('WebAssembly module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current keyspace
|
||||||
|
const sessionState = await getSessionState();
|
||||||
|
const keyspace = sessionState.currentKeyspace;
|
||||||
|
if (!keyspace) {
|
||||||
|
throw new Error('No active keyspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate default label if not provided
|
||||||
|
const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`;
|
||||||
|
|
||||||
|
// Create metadata JSON
|
||||||
|
const metadata = JSON.stringify({
|
||||||
|
label: keyLabel,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
type: keyType
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`);
|
||||||
|
console.log('Keypair metadata:', metadata);
|
||||||
|
|
||||||
|
// Call the WASM add_keypair function with metadata
|
||||||
|
// This will add the keypair to the WebAssembly vault
|
||||||
|
const keyId = await module.add_keypair(keyType, metadata);
|
||||||
|
console.log(`Keypair created with ID: ${keyId} in WebAssembly vault`);
|
||||||
|
|
||||||
|
// Create keypair object for UI and storage
|
||||||
|
const newKeypair = {
|
||||||
|
id: keyId,
|
||||||
|
label: keyLabel,
|
||||||
|
type: keyType,
|
||||||
|
created: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the latest keypairs from the WebAssembly vault to ensure consistency
|
||||||
|
const vaultKeypairs = await module.list_keypairs();
|
||||||
|
console.log('Current keypairs in vault after addition:', vaultKeypairs);
|
||||||
|
|
||||||
|
// Format the vault keypairs for storage
|
||||||
|
const formattedVaultKeypairs = vaultKeypairs.map(kp => {
|
||||||
|
// Parse metadata if available
|
||||||
|
let metadata = {};
|
||||||
|
if (kp.metadata) {
|
||||||
|
try {
|
||||||
|
if (typeof kp.metadata === 'string') {
|
||||||
|
metadata = JSON.parse(kp.metadata);
|
||||||
|
} else {
|
||||||
|
metadata = kp.metadata;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse keypair metadata:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: kp.id,
|
||||||
|
label: metadata.label || `Key-${kp.id.substring(0, 8)}`,
|
||||||
|
type: kp.type || 'Secp256k1',
|
||||||
|
created: metadata.created || new Date().toISOString()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the formatted keypairs to Chrome storage
|
||||||
|
await new Promise(resolve => {
|
||||||
|
chrome.storage.local.set({ [`keypairs:${keyspace}`]: formattedVaultKeypairs }, () => {
|
||||||
|
console.log(`Saved ${formattedVaultKeypairs.length} keypairs to Chrome storage for keyspace ${keyspace}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update session state in background with the new keypair information
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_added',
|
||||||
|
data: newKeypair
|
||||||
|
}, async (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
console.log('Background service worker updated with new keypair');
|
||||||
|
resolve(newKeypair);
|
||||||
|
} else {
|
||||||
|
const error = response?.error || 'Failed to update session state';
|
||||||
|
console.error('Error updating background state:', error);
|
||||||
|
reject(new Error(error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update the complete keypair list in background with the current vault state
|
||||||
|
await new Promise(resolve => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypairs_loaded',
|
||||||
|
data: formattedVaultKeypairs
|
||||||
|
}, () => {
|
||||||
|
console.log('Updated complete keypair list in background with vault state');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return newKeypair;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding keypair:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a keypair
|
||||||
|
* @param {string} keyId The ID of the keypair to select
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function selectKeypair(keyId) {
|
||||||
|
if (!wasmModule || !wasmModule.select_keypair) {
|
||||||
|
throw new Error('WASM module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the WASM select_keypair function
|
||||||
|
await wasmModule.select_keypair(keyId);
|
||||||
|
|
||||||
|
// Update session state in background
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_selected',
|
||||||
|
data: keyId
|
||||||
|
}, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(response && response.error ? response.error : 'Failed to update session state');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a message with the selected keypair
|
||||||
|
* @param {string} message The message to sign
|
||||||
|
* @returns {Promise<string>} The signature as a hex string
|
||||||
|
*/
|
||||||
|
export async function sign(message) {
|
||||||
|
if (!wasmModule || !wasmModule.sign) {
|
||||||
|
throw new Error('WASM module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert message to Uint8Array
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const messageBytes = encoder.encode(message);
|
||||||
|
|
||||||
|
// Call the WASM sign function
|
||||||
|
return await wasmModule.sign(messageBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current session state
|
||||||
|
* @returns {Promise<{currentKeyspace: string|null, keypairs: Array, selectedKeypair: string|null}>}
|
||||||
|
*/
|
||||||
|
export async function getSessionState() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ action: 'get_session' }, (response) => {
|
||||||
|
resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
88
extension/popup/WasmLoader.jsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React, { useState, useEffect, createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
// Create a context to share the WASM module across components
|
||||||
|
export const WasmContext = createContext(null);
|
||||||
|
|
||||||
|
// Hook to access WASM module
|
||||||
|
export function useWasm() {
|
||||||
|
return useContext(WasmContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component that loads and initializes the WASM module
|
||||||
|
export function WasmProvider({ children }) {
|
||||||
|
const [wasmModule, setWasmModule] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadWasm() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Instead of using dynamic imports which require correct MIME types,
|
||||||
|
// we'll use fetch to load the JavaScript file as text and eval it
|
||||||
|
const wasmJsPath = chrome.runtime.getURL('wasm/wasm_app.js');
|
||||||
|
console.log('Loading WASM JS from:', wasmJsPath);
|
||||||
|
|
||||||
|
// Load the JavaScript file
|
||||||
|
const jsResponse = await fetch(wasmJsPath);
|
||||||
|
if (!jsResponse.ok) {
|
||||||
|
throw new Error(`Failed to load WASM JS: ${jsResponse.status} ${jsResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the JavaScript code as text
|
||||||
|
const jsCode = await jsResponse.text();
|
||||||
|
|
||||||
|
// Create a function to execute the code in an isolated scope
|
||||||
|
let wasmModuleExports = {};
|
||||||
|
const moduleFunction = new Function('exports', jsCode + '\nreturn { initSync, default: __wbg_init, init_rhai_env, init_session, lock_session, add_keypair, select_keypair, sign, run_rhai };');
|
||||||
|
|
||||||
|
// Execute the function to get the exports
|
||||||
|
const wasmModule = moduleFunction(wasmModuleExports);
|
||||||
|
|
||||||
|
// Initialize WASM with the binary
|
||||||
|
const wasmBinaryPath = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
||||||
|
console.log('Initializing WASM with binary:', wasmBinaryPath);
|
||||||
|
|
||||||
|
const binaryResponse = await fetch(wasmBinaryPath);
|
||||||
|
if (!binaryResponse.ok) {
|
||||||
|
throw new Error(`Failed to load WASM binary: ${binaryResponse.status} ${binaryResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasmBinary = await binaryResponse.arrayBuffer();
|
||||||
|
|
||||||
|
// Initialize the WASM module
|
||||||
|
await wasmModule.default(wasmBinary);
|
||||||
|
|
||||||
|
// Initialize the WASM environment
|
||||||
|
if (typeof wasmModule.init_rhai_env === 'function') {
|
||||||
|
wasmModule.init_rhai_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('WASM module loaded successfully');
|
||||||
|
setWasmModule(wasmModule);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load WASM module:', error);
|
||||||
|
setError(error.message || 'Failed to load WebAssembly module');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWasm();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="wasm-loading">Loading WebAssembly module...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="wasm-error">Error: {error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WasmContext.Provider value={wasmModule}>
|
||||||
|
{children}
|
||||||
|
</WasmContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
88
extension/popup/debug_rhai.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Debug helper for WebAssembly Vault with Rhai scripts
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Helper to try various Rhai scripts for debugging
|
||||||
|
export const RHAI_SCRIPTS = {
|
||||||
|
// Check if there's an active session
|
||||||
|
CHECK_SESSION: `
|
||||||
|
let has_session = false;
|
||||||
|
let current_keyspace = "";
|
||||||
|
|
||||||
|
// Try to access functions expected to exist in the vault namespace
|
||||||
|
if (isdef(vault) && isdef(vault::has_active_session)) {
|
||||||
|
has_session = vault::has_active_session();
|
||||||
|
if (has_session && isdef(vault::get_current_keyspace)) {
|
||||||
|
current_keyspace = vault::get_current_keyspace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"has_session": has_session,
|
||||||
|
"keyspace": current_keyspace,
|
||||||
|
"available_functions": [
|
||||||
|
isdef(vault::list_keypairs) ? "list_keypairs" : null,
|
||||||
|
isdef(vault::add_keypair) ? "add_keypair" : null,
|
||||||
|
isdef(vault::has_active_session) ? "has_active_session" : null,
|
||||||
|
isdef(vault::get_current_keyspace) ? "get_current_keyspace" : null
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Explicitly get keypairs for the current keyspace using session data
|
||||||
|
LIST_KEYPAIRS: `
|
||||||
|
let result = {"error": "Not initialized"};
|
||||||
|
|
||||||
|
if (isdef(vault) && isdef(vault::has_active_session) && vault::has_active_session()) {
|
||||||
|
let keyspace = vault::get_current_keyspace();
|
||||||
|
|
||||||
|
// Try to list the keypairs from the current session
|
||||||
|
if (isdef(vault::get_keypairs_from_session)) {
|
||||||
|
result = {
|
||||||
|
"keyspace": keyspace,
|
||||||
|
"keypairs": vault::get_keypairs_from_session()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
result = {
|
||||||
|
"error": "vault::get_keypairs_from_session is not defined",
|
||||||
|
"keyspace": keyspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
`,
|
||||||
|
|
||||||
|
// Use Rhai to inspect the Vault storage directly (for advanced debugging)
|
||||||
|
INSPECT_VAULT_STORAGE: `
|
||||||
|
let result = {"error": "Not accessible"};
|
||||||
|
|
||||||
|
if (isdef(vault) && isdef(vault::inspect_storage)) {
|
||||||
|
result = vault::inspect_storage();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run all debug scripts and collect results
|
||||||
|
export async function runDiagnostics(wasmModule) {
|
||||||
|
if (!wasmModule || !wasmModule.run_rhai) {
|
||||||
|
throw new Error('WebAssembly module not loaded or run_rhai not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
for (const [name, script] of Object.entries(RHAI_SCRIPTS)) {
|
||||||
|
try {
|
||||||
|
console.log(`Running Rhai diagnostic script: ${name}`);
|
||||||
|
results[name] = await wasmModule.run_rhai(script);
|
||||||
|
console.log(`Result from ${name}:`, results[name]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error running script ${name}:`, error);
|
||||||
|
results[name] = { error: error.toString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
13
extension/popup/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Modular Vault Extension</title>
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
8
extension/popup/index.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
|
// Render the React app
|
||||||
|
const root = createRoot(document.getElementById('root'));
|
||||||
|
root.render(<App />);
|
117
extension/popup/popup.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
/* Basic styles for the extension popup */
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #202124;
|
||||||
|
color: #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 350px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
border-bottom: 1px solid #3c4043;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color: #292a2d;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #9aa0a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #3c4043;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #202124;
|
||||||
|
color: #e8eaed;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 60px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #8ab4f8;
|
||||||
|
color: #202124;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #669df6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.small {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #292a2d;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #3c4043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item.selected {
|
||||||
|
background-color: rgba(138, 180, 248, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
306
extension/popup/popup.js
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
// Simple non-module JavaScript for browser extension popup
|
||||||
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
root.innerHTML = `
|
||||||
|
<div class="container">
|
||||||
|
<h1>Modular Vault Extension</h1>
|
||||||
|
<div id="status" class="status">Loading WASM module...</div>
|
||||||
|
|
||||||
|
<div id="session-controls">
|
||||||
|
<div id="keyspace-form" class="form-section">
|
||||||
|
<h2>Session</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="keyspace">Keyspace:</label>
|
||||||
|
<input type="text" id="keyspace" placeholder="Enter keyspace name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" placeholder="Enter password">
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="unlock-btn">Unlock</button>
|
||||||
|
<button id="create-btn">Create New</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="session-info" class="session-info hidden">
|
||||||
|
<h2>Active Session</h2>
|
||||||
|
<p>Current keyspace: <span id="current-keyspace"></span></p>
|
||||||
|
<button id="lock-btn">Lock Session</button>
|
||||||
|
|
||||||
|
<div id="keypair-section" class="form-section">
|
||||||
|
<h2>Keypairs</h2>
|
||||||
|
<button id="create-keypair-btn">Create New Keypair</button>
|
||||||
|
<div id="keypair-list" class="list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="sign-section" class="form-section hidden">
|
||||||
|
<h2>Sign Message</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message">Message:</label>
|
||||||
|
<textarea id="message" placeholder="Enter message to sign"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="sign-btn">Sign</button>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="signature">Signature:</label>
|
||||||
|
<textarea id="signature" readonly></textarea>
|
||||||
|
<button id="copy-btn" class="small">Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const keyspaceFormEl = document.getElementById('keyspace-form');
|
||||||
|
const sessionInfoEl = document.getElementById('session-info');
|
||||||
|
const currentKeyspaceEl = document.getElementById('current-keyspace');
|
||||||
|
const keyspaceInput = document.getElementById('keyspace');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const unlockBtn = document.getElementById('unlock-btn');
|
||||||
|
const createBtn = document.getElementById('create-btn');
|
||||||
|
const lockBtn = document.getElementById('lock-btn');
|
||||||
|
const createKeypairBtn = document.getElementById('create-keypair-btn');
|
||||||
|
const keypairListEl = document.getElementById('keypair-list');
|
||||||
|
const signSectionEl = document.getElementById('sign-section');
|
||||||
|
const messageInput = document.getElementById('message');
|
||||||
|
const signBtn = document.getElementById('sign-btn');
|
||||||
|
const signatureOutput = document.getElementById('signature');
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
|
||||||
|
// State
|
||||||
|
let wasmModule = null;
|
||||||
|
let currentKeyspace = null;
|
||||||
|
let keypairs = [];
|
||||||
|
let selectedKeypairId = null;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
init();
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
try {
|
||||||
|
// Get session state from background
|
||||||
|
const sessionState = await getSessionState();
|
||||||
|
|
||||||
|
if (sessionState.currentKeyspace) {
|
||||||
|
// We have an active session
|
||||||
|
currentKeyspace = sessionState.currentKeyspace;
|
||||||
|
keypairs = sessionState.keypairs || [];
|
||||||
|
selectedKeypairId = sessionState.selectedKeypair;
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Ready';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (currentKeyspace) {
|
||||||
|
// Show session info
|
||||||
|
keyspaceFormEl.classList.add('hidden');
|
||||||
|
sessionInfoEl.classList.remove('hidden');
|
||||||
|
currentKeyspaceEl.textContent = currentKeyspace;
|
||||||
|
|
||||||
|
// Update keypair list
|
||||||
|
updateKeypairList();
|
||||||
|
|
||||||
|
// Show/hide sign section based on selected keypair
|
||||||
|
if (selectedKeypairId) {
|
||||||
|
signSectionEl.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
signSectionEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show keyspace form
|
||||||
|
keyspaceFormEl.classList.remove('hidden');
|
||||||
|
sessionInfoEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateKeypairList() {
|
||||||
|
// Clear list
|
||||||
|
keypairListEl.innerHTML = '';
|
||||||
|
|
||||||
|
// Add each keypair
|
||||||
|
keypairs.forEach(keypair => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'list-item' + (selectedKeypairId === keypair.id ? ' selected' : '');
|
||||||
|
item.innerHTML = `
|
||||||
|
<span>${keypair.label || keypair.id}</span>
|
||||||
|
<button class="select-btn" data-id="${keypair.id}">Select</button>
|
||||||
|
`;
|
||||||
|
keypairListEl.appendChild(item);
|
||||||
|
|
||||||
|
// Add select handler
|
||||||
|
item.querySelector('.select-btn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
statusEl.textContent = 'Selecting keypair...';
|
||||||
|
// Use background service to select keypair for now
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_selected',
|
||||||
|
data: keypair.id
|
||||||
|
});
|
||||||
|
selectedKeypairId = keypair.id;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Keypair selected: ' + keypair.id;
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error selecting keypair: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get session state from background
|
||||||
|
async function getSessionState() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ action: 'get_session' }, (response) => {
|
||||||
|
resolve(response || { currentKeyspace: null, keypairs: [], selectedKeypair: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
unlockBtn.addEventListener('click', async () => {
|
||||||
|
const keyspace = keyspaceInput.value.trim();
|
||||||
|
const password = passwordInput.value;
|
||||||
|
|
||||||
|
if (!keyspace || !password) {
|
||||||
|
statusEl.textContent = 'Please enter keyspace and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Unlocking session...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, use the background service worker mock
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keyspace',
|
||||||
|
data: keyspace
|
||||||
|
});
|
||||||
|
|
||||||
|
currentKeyspace = keyspace;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Session unlocked!';
|
||||||
|
|
||||||
|
// Refresh state
|
||||||
|
const state = await getSessionState();
|
||||||
|
keypairs = state.keypairs || [];
|
||||||
|
selectedKeypairId = state.selectedKeypair;
|
||||||
|
updateUI();
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error unlocking session: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createBtn.addEventListener('click', async () => {
|
||||||
|
const keyspace = keyspaceInput.value.trim();
|
||||||
|
const password = passwordInput.value;
|
||||||
|
|
||||||
|
if (!keyspace || !password) {
|
||||||
|
statusEl.textContent = 'Please enter keyspace and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Creating keyspace...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, use the background service worker mock
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keyspace',
|
||||||
|
data: keyspace
|
||||||
|
});
|
||||||
|
|
||||||
|
currentKeyspace = keyspace;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Keyspace created and unlocked!';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error creating keyspace: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lockBtn.addEventListener('click', async () => {
|
||||||
|
statusEl.textContent = 'Locking session...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'session_locked'
|
||||||
|
});
|
||||||
|
|
||||||
|
currentKeyspace = null;
|
||||||
|
keypairs = [];
|
||||||
|
selectedKeypairId = null;
|
||||||
|
updateUI();
|
||||||
|
statusEl.textContent = 'Session locked';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error locking session: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createKeypairBtn.addEventListener('click', async () => {
|
||||||
|
statusEl.textContent = 'Creating keypair...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate a mock keypair ID
|
||||||
|
const keyId = 'key-' + Date.now().toString(16);
|
||||||
|
const newKeypair = {
|
||||||
|
id: keyId,
|
||||||
|
label: `Secp256k1-Key-${keypairs.length + 1}`
|
||||||
|
};
|
||||||
|
|
||||||
|
await chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_added',
|
||||||
|
data: newKeypair
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh state
|
||||||
|
const state = await getSessionState();
|
||||||
|
keypairs = state.keypairs || [];
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
statusEl.textContent = 'Keypair created: ' + keyId;
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error creating keypair: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signBtn.addEventListener('click', async () => {
|
||||||
|
const message = messageInput.value.trim();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
statusEl.textContent = 'Please enter a message to sign';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedKeypairId) {
|
||||||
|
statusEl.textContent = 'Please select a keypair first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusEl.textContent = 'Signing message...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For now, generate a mock signature
|
||||||
|
const mockSignature = Array.from({length: 64}, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||||
|
signatureOutput.value = mockSignature;
|
||||||
|
statusEl.textContent = 'Message signed!';
|
||||||
|
} catch (error) {
|
||||||
|
statusEl.textContent = 'Error signing message: ' + (error.message || 'Unknown error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
signatureOutput.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
statusEl.textContent = 'Signature copied to clipboard!';
|
||||||
|
});
|
||||||
|
});
|
26
extension/popup/style.css
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', Arial, sans-serif;
|
||||||
|
background: #181c20;
|
||||||
|
color: #f3f6fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
padding: 1.5rem;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 400px;
|
||||||
|
background: #23272e;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: #b0bac9;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
317
extension/popup/wasm.js
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
// WebAssembly API functions for accessing WASM operations directly
|
||||||
|
// and synchronizing state with background service worker
|
||||||
|
|
||||||
|
// Get session state from the background service worker
|
||||||
|
export function getStatus() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ action: 'get_session' }, (response) => {
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug function to examine vault state using Rhai scripts
|
||||||
|
export async function debugVaultState(wasmModule) {
|
||||||
|
if (!wasmModule) {
|
||||||
|
throw new Error('WASM module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('🔍 Debugging vault state...');
|
||||||
|
|
||||||
|
// First check if we have a valid session
|
||||||
|
const sessionCheck = `
|
||||||
|
let has_session = vault::has_active_session();
|
||||||
|
let keyspace = "";
|
||||||
|
if has_session {
|
||||||
|
keyspace = vault::get_current_keyspace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return info about the session
|
||||||
|
{
|
||||||
|
"has_session": has_session,
|
||||||
|
"keyspace": keyspace
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('Checking session status...');
|
||||||
|
const sessionStatus = await wasmModule.run_rhai(sessionCheck);
|
||||||
|
console.log('Session status:', sessionStatus);
|
||||||
|
|
||||||
|
// Only try to get keypairs if we have an active session
|
||||||
|
if (sessionStatus && sessionStatus.has_session) {
|
||||||
|
// Get information about all keypairs
|
||||||
|
const keypairsScript = `
|
||||||
|
// Get all keypairs for the current keyspace
|
||||||
|
let keypairs = vault::list_keypairs();
|
||||||
|
|
||||||
|
// Add more diagnostic information
|
||||||
|
let diagnostic = {
|
||||||
|
"keypair_count": keypairs.len(),
|
||||||
|
"keyspace": vault::get_current_keyspace(),
|
||||||
|
"keypairs": keypairs
|
||||||
|
};
|
||||||
|
|
||||||
|
diagnostic
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log('Fetching keypair details...');
|
||||||
|
const keypairDiagnostic = await wasmModule.run_rhai(keypairsScript);
|
||||||
|
console.log('Keypair diagnostic:', keypairDiagnostic);
|
||||||
|
|
||||||
|
return keypairDiagnostic;
|
||||||
|
} else {
|
||||||
|
console.log('No active session, cannot fetch keypairs');
|
||||||
|
return { error: 'No active session' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in debug function:', error);
|
||||||
|
return { error: error.toString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all keypairs from the WebAssembly vault
|
||||||
|
export async function getKeypairsFromVault(wasmModule) {
|
||||||
|
if (!wasmModule) {
|
||||||
|
throw new Error('WASM module not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First run diagnostics for debugging
|
||||||
|
await debugVaultState(wasmModule);
|
||||||
|
|
||||||
|
console.log('Calling list_keypairs WebAssembly binding...');
|
||||||
|
|
||||||
|
// Use our new direct WebAssembly binding instead of Rhai script
|
||||||
|
const keypairList = await wasmModule.list_keypairs();
|
||||||
|
console.log('Retrieved keypairs from vault:', keypairList);
|
||||||
|
|
||||||
|
// Transform the keypairs into the expected format
|
||||||
|
// The WebAssembly binding returns an array of objects with id, type, and metadata
|
||||||
|
const formattedKeypairs = Array.isArray(keypairList) ? keypairList.map(kp => {
|
||||||
|
// Parse metadata if it's a string
|
||||||
|
let metadata = {};
|
||||||
|
if (kp.metadata) {
|
||||||
|
try {
|
||||||
|
if (typeof kp.metadata === 'string') {
|
||||||
|
metadata = JSON.parse(kp.metadata);
|
||||||
|
} else {
|
||||||
|
metadata = kp.metadata;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse keypair metadata:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: kp.id,
|
||||||
|
label: metadata.label || `${kp.type}-Key-${kp.id.substring(0, 4)}`
|
||||||
|
};
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
console.log('Formatted keypairs:', formattedKeypairs);
|
||||||
|
|
||||||
|
// Update the keypairs in the background service worker
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypairs_loaded',
|
||||||
|
data: formattedKeypairs
|
||||||
|
}, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
console.log('Successfully updated keypairs in background');
|
||||||
|
resolve(formattedKeypairs);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to update keypairs in background:', response?.error);
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching keypairs from vault:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize session with the WASM module
|
||||||
|
export function initSession(wasmModule, keyspace, password) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!wasmModule) {
|
||||||
|
reject('WASM module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the WASM init_session function
|
||||||
|
console.log(`Initializing session for keyspace: ${keyspace}`);
|
||||||
|
await wasmModule.init_session(keyspace, password);
|
||||||
|
|
||||||
|
// Update the session state in the background service worker
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keyspace',
|
||||||
|
data: keyspace
|
||||||
|
}, async (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
try {
|
||||||
|
// After successful session initialization, fetch keypairs from the vault
|
||||||
|
console.log('Session initialized, fetching keypairs from vault...');
|
||||||
|
const keypairs = await getKeypairsFromVault(wasmModule);
|
||||||
|
console.log('Keypairs loaded:', keypairs);
|
||||||
|
resolve(keypairs);
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error('Error fetching keypairs:', fetchError);
|
||||||
|
// Even if fetching keypairs fails, the session is initialized
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(response && response.error ? response.error : 'Failed to update session state');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Session initialization error:', error);
|
||||||
|
reject(error.message || 'Failed to initialize session');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the session using the WASM module
|
||||||
|
export function lockSession(wasmModule) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!wasmModule) {
|
||||||
|
reject('WASM module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the WASM lock_session function
|
||||||
|
wasmModule.lock_session();
|
||||||
|
|
||||||
|
// Update the session state in the background service worker
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'session_locked'
|
||||||
|
}, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(response && response.error ? response.error : 'Failed to update session state');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error.message || 'Failed to lock session');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a keypair using the WASM module
|
||||||
|
export function addKeypair(wasmModule, keyType = 'Secp256k1', label = null) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!wasmModule) {
|
||||||
|
reject('WASM module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a default label if none provided
|
||||||
|
const keyLabel = label || `${keyType}-Key-${Date.now().toString(16).slice(-4)}`;
|
||||||
|
|
||||||
|
// Create metadata JSON for the keypair
|
||||||
|
const metadata = JSON.stringify({
|
||||||
|
label: keyLabel,
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
type: keyType
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Adding new keypair of type ${keyType} with label ${keyLabel}`);
|
||||||
|
|
||||||
|
// Call the WASM add_keypair function with metadata
|
||||||
|
const keyId = await wasmModule.add_keypair(keyType, metadata);
|
||||||
|
console.log(`Keypair created with ID: ${keyId}`);
|
||||||
|
|
||||||
|
// Create keypair object with ID and label
|
||||||
|
const newKeypair = {
|
||||||
|
id: keyId,
|
||||||
|
label: keyLabel
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the session state in the background service worker
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_added',
|
||||||
|
data: newKeypair
|
||||||
|
}, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
// After adding a keypair, refresh the whole list from the vault
|
||||||
|
getKeypairsFromVault(wasmModule)
|
||||||
|
.then(() => {
|
||||||
|
console.log('Keypair list refreshed from vault');
|
||||||
|
resolve(keyId);
|
||||||
|
})
|
||||||
|
.catch(refreshError => {
|
||||||
|
console.warn('Error refreshing keypair list:', refreshError);
|
||||||
|
// Still resolve with the key ID since the key was created
|
||||||
|
resolve(keyId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(response && response.error ? response.error : 'Failed to update session state');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding keypair:', error);
|
||||||
|
reject(error.message || 'Failed to add keypair');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select a keypair using the WASM module
|
||||||
|
export function selectKeypair(wasmModule, keyId) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!wasmModule) {
|
||||||
|
reject('WASM module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call the WASM select_keypair function
|
||||||
|
await wasmModule.select_keypair(keyId);
|
||||||
|
|
||||||
|
// Update the session state in the background service worker
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'update_session',
|
||||||
|
type: 'keypair_selected',
|
||||||
|
data: keyId
|
||||||
|
}, (response) => {
|
||||||
|
if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(response && response.error ? response.error : 'Failed to update session state');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
reject(error.message || 'Failed to select keypair');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign a message using the WASM module
|
||||||
|
export function sign(wasmModule, message) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
if (!wasmModule) {
|
||||||
|
reject('WASM module not loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert message to Uint8Array for WASM
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const messageBytes = encoder.encode(message);
|
||||||
|
|
||||||
|
// Call the WASM sign function
|
||||||
|
const signature = await wasmModule.sign(messageBytes);
|
||||||
|
resolve(signature);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error.message || 'Failed to sign message');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
102
extension/public/background/index.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// Background service worker for Modular Vault Extension
|
||||||
|
// Handles session, keypair, and WASM logic
|
||||||
|
|
||||||
|
// We need to use dynamic imports for service workers in MV3
|
||||||
|
let wasmModule;
|
||||||
|
let init;
|
||||||
|
let wasm;
|
||||||
|
let wasmReady = false;
|
||||||
|
|
||||||
|
// Initialize WASM on startup with dynamic import
|
||||||
|
async function loadWasm() {
|
||||||
|
try {
|
||||||
|
// Using importScripts for service worker
|
||||||
|
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app.js');
|
||||||
|
wasmModule = await import(wasmUrl);
|
||||||
|
init = wasmModule.default;
|
||||||
|
wasm = wasmModule;
|
||||||
|
|
||||||
|
// Initialize WASM with explicit WASM file path
|
||||||
|
await init(chrome.runtime.getURL('wasm/wasm_app_bg.wasm'));
|
||||||
|
wasmReady = true;
|
||||||
|
console.log('WASM initialized in background');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize WASM:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start loading WASM
|
||||||
|
loadWasm();
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
||||||
|
if (!wasmReady) {
|
||||||
|
sendResponse({ error: 'WASM not ready' });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Session unlock/create
|
||||||
|
if (request.action === 'init_session') {
|
||||||
|
try {
|
||||||
|
const result = await wasm.init_session(request.keyspace, request.password);
|
||||||
|
// Persist current session info
|
||||||
|
await chrome.storage.local.set({ currentKeyspace: request.keyspace });
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse({ error: e.message });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Lock session
|
||||||
|
if (request.action === 'lock_session') {
|
||||||
|
try {
|
||||||
|
wasm.lock_session();
|
||||||
|
await chrome.storage.local.set({ currentKeyspace: null });
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse({ error: e.message });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Add keypair
|
||||||
|
if (request.action === 'add_keypair') {
|
||||||
|
try {
|
||||||
|
const keyId = await wasm.add_keypair('Secp256k1', null);
|
||||||
|
let keypairs = (await chrome.storage.local.get(['keypairs'])).keypairs || [];
|
||||||
|
keypairs.push({ id: keyId, label: `Secp256k1-${keypairs.length + 1}` });
|
||||||
|
await chrome.storage.local.set({ keypairs });
|
||||||
|
sendResponse({ keyId });
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse({ error: e.message });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Select keypair
|
||||||
|
if (request.action === 'select_keypair') {
|
||||||
|
try {
|
||||||
|
await wasm.select_keypair(request.keyId);
|
||||||
|
await chrome.storage.local.set({ selectedKeypair: request.keyId });
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse({ error: e.message });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Sign
|
||||||
|
if (request.action === 'sign') {
|
||||||
|
try {
|
||||||
|
// Convert plaintext to Uint8Array
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const msgBytes = encoder.encode(request.message);
|
||||||
|
const signature = await wasm.sign(msgBytes);
|
||||||
|
sendResponse({ signature });
|
||||||
|
} catch (e) {
|
||||||
|
sendResponse({ error: e.message });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Query status
|
||||||
|
if (request.action === 'get_status') {
|
||||||
|
const { currentKeyspace, keypairs, selectedKeypair } = await chrome.storage.local.get(['currentKeyspace', 'keypairs', 'selectedKeypair']);
|
||||||
|
sendResponse({ currentKeyspace, keypairs: keypairs || [], selectedKeypair });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
765
extension/public/wasm/wasm_app.js
Normal file
@ -0,0 +1,765 @@
|
|||||||
|
import * as __wbg_star0 from 'env';
|
||||||
|
|
||||||
|
let wasm;
|
||||||
|
|
||||||
|
function addToExternrefTable0(obj) {
|
||||||
|
const idx = wasm.__externref_table_alloc();
|
||||||
|
wasm.__wbindgen_export_2.set(idx, obj);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(f, args) {
|
||||||
|
try {
|
||||||
|
return f.apply(this, args);
|
||||||
|
} catch (e) {
|
||||||
|
const idx = addToExternrefTable0(e);
|
||||||
|
wasm.__wbindgen_exn_store(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||||
|
|
||||||
|
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||||
|
|
||||||
|
let cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
|
function getUint8ArrayMemory0() {
|
||||||
|
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||||
|
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedUint8ArrayMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikeNone(x) {
|
||||||
|
return x === undefined || x === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayU8FromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||||
|
|
||||||
|
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||||
|
? function (arg, view) {
|
||||||
|
return cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
}
|
||||||
|
: function (arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len, 1) >>> 0;
|
||||||
|
|
||||||
|
const mem = getUint8ArrayMemory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||||
|
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = encodeString(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedDataViewMemory0 = null;
|
||||||
|
|
||||||
|
function getDataViewMemory0() {
|
||||||
|
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||||
|
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedDataViewMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(state => {
|
||||||
|
wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b)
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeMutClosure(arg0, arg1, dtor, f) {
|
||||||
|
const state = { a: arg0, b: arg1, cnt: 1, dtor };
|
||||||
|
const real = (...args) => {
|
||||||
|
// First up with a closure we increment the internal reference
|
||||||
|
// count. This ensures that the Rust closure environment won't
|
||||||
|
// be deallocated while we're invoking it.
|
||||||
|
state.cnt++;
|
||||||
|
const a = state.a;
|
||||||
|
state.a = 0;
|
||||||
|
try {
|
||||||
|
return f(a, state.b, ...args);
|
||||||
|
} finally {
|
||||||
|
if (--state.cnt === 0) {
|
||||||
|
wasm.__wbindgen_export_5.get(state.dtor)(a, state.b);
|
||||||
|
CLOSURE_DTORS.unregister(state);
|
||||||
|
} else {
|
||||||
|
state.a = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
real.original = state;
|
||||||
|
CLOSURE_DTORS.register(real, state, state);
|
||||||
|
return real;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugString(val) {
|
||||||
|
// primitive types
|
||||||
|
const type = typeof val;
|
||||||
|
if (type == 'number' || type == 'boolean' || val == null) {
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
if (type == 'string') {
|
||||||
|
return `"${val}"`;
|
||||||
|
}
|
||||||
|
if (type == 'symbol') {
|
||||||
|
const description = val.description;
|
||||||
|
if (description == null) {
|
||||||
|
return 'Symbol';
|
||||||
|
} else {
|
||||||
|
return `Symbol(${description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 'function') {
|
||||||
|
const name = val.name;
|
||||||
|
if (typeof name == 'string' && name.length > 0) {
|
||||||
|
return `Function(${name})`;
|
||||||
|
} else {
|
||||||
|
return 'Function';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// objects
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const length = val.length;
|
||||||
|
let debug = '[';
|
||||||
|
if (length > 0) {
|
||||||
|
debug += debugString(val[0]);
|
||||||
|
}
|
||||||
|
for(let i = 1; i < length; i++) {
|
||||||
|
debug += ', ' + debugString(val[i]);
|
||||||
|
}
|
||||||
|
debug += ']';
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
// Test for built-in
|
||||||
|
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||||
|
let className;
|
||||||
|
if (builtInMatches && builtInMatches.length > 1) {
|
||||||
|
className = builtInMatches[1];
|
||||||
|
} else {
|
||||||
|
// Failed to match the standard '[object ClassName]'
|
||||||
|
return toString.call(val);
|
||||||
|
}
|
||||||
|
if (className == 'Object') {
|
||||||
|
// we're a user defined class or Object
|
||||||
|
// JSON.stringify avoids problems with cycles, and is generally much
|
||||||
|
// easier than looping through ownProperties of `val`.
|
||||||
|
try {
|
||||||
|
return 'Object(' + JSON.stringify(val) + ')';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// errors
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||||
|
}
|
||||||
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Initialize the scripting environment (must be called before run_rhai)
|
||||||
|
*/
|
||||||
|
export function init_rhai_env() {
|
||||||
|
wasm.init_rhai_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeFromExternrefTable0(idx) {
|
||||||
|
const value = wasm.__wbindgen_export_2.get(idx);
|
||||||
|
wasm.__externref_table_dealloc(idx);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||||
|
* @param {string} script
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export function run_rhai(script) {
|
||||||
|
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.run_rhai(ptr0, len0);
|
||||||
|
if (ret[2]) {
|
||||||
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
|
}
|
||||||
|
return takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize session with keyspace and password
|
||||||
|
* @param {string} keyspace
|
||||||
|
* @param {string} password
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export function init_session(keyspace, password) {
|
||||||
|
const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.init_session(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the session (zeroize password and session)
|
||||||
|
*/
|
||||||
|
export function lock_session() {
|
||||||
|
wasm.lock_session();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all keypairs from the current session
|
||||||
|
* Returns an array of keypair objects with id, type, and metadata
|
||||||
|
* Select keypair for the session
|
||||||
|
* @param {string} key_id
|
||||||
|
*/
|
||||||
|
export function select_keypair(key_id) {
|
||||||
|
const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.select_keypair(ptr0, len0);
|
||||||
|
if (ret[1]) {
|
||||||
|
throw takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List keypairs in the current session's keyspace
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function list_keypairs() {
|
||||||
|
const ret = wasm.list_keypairs();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a keypair to the current keyspace
|
||||||
|
* @param {string | null} [key_type]
|
||||||
|
* @param {string | null} [metadata]
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function add_keypair(key_type, metadata) {
|
||||||
|
var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len0 = WASM_VECTOR_LEN;
|
||||||
|
var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.add_keypair(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function passArray8ToWasm0(arg, malloc) {
|
||||||
|
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||||
|
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||||
|
WASM_VECTOR_LEN = arg.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sign message with current session
|
||||||
|
* @param {Uint8Array} message
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function sign(message) {
|
||||||
|
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.sign(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||||
|
wasm.closure77_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||||
|
wasm.closure126_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||||
|
wasm.closure188_externref_shim(arg0, arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_adapter_123(arg0, arg1, arg2, arg3) {
|
||||||
|
wasm.closure213_externref_shim(arg0, arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||||
|
|
||||||
|
async function __wbg_load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
|
try {
|
||||||
|
return await WebAssembly.instantiateStreaming(module, imports);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||||
|
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return { instance, module };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_get_imports() {
|
||||||
|
const imports = {};
|
||||||
|
imports.wbg = {};
|
||||||
|
imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) {
|
||||||
|
const ret = arg0.buffer;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.call(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.call(arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
|
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) {
|
||||||
|
const ret = arg0.crypto;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||||
|
console.error(arg0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) {
|
||||||
|
const ret = arg0.error;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) {
|
||||||
|
arg0.getRandomValues(arg1);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) {
|
||||||
|
const ret = arg1[arg2 >>> 0];
|
||||||
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = Reflect.get(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.get(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBDatabase;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBFactory;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBOpenDBRequest;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof IDBRequest;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||||
|
const ret = arg0.length;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||||
|
const ret = arg0.msCrypto;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
|
||||||
|
try {
|
||||||
|
var state0 = {a: arg0, b: arg1};
|
||||||
|
var cb0 = (arg0, arg1) => {
|
||||||
|
const a = state0.a;
|
||||||
|
state0.a = 0;
|
||||||
|
try {
|
||||||
|
return __wbg_adapter_123(a, state0.b, arg0, arg1);
|
||||||
|
} finally {
|
||||||
|
state0.a = a;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ret = new Promise(cb0);
|
||||||
|
return ret;
|
||||||
|
} finally {
|
||||||
|
state0.a = state0.b = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_405e22f390576ce2 = function() {
|
||||||
|
const ret = new Object();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_78feb108b6472713 = function() {
|
||||||
|
const ret = new Array();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||||
|
const ret = new Uint8Array(arg0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
|
||||||
|
const ret = new Function(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) {
|
||||||
|
const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) {
|
||||||
|
const ret = new Uint8Array(arg0 >>> 0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) {
|
||||||
|
const ret = arg0.node;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) {
|
||||||
|
const ret = arg0.objectStoreNames;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
|
const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) {
|
||||||
|
const ret = arg0.process;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) {
|
||||||
|
const ret = arg0.push(arg1);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.put(arg1, arg2);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.put(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
|
||||||
|
queueMicrotask(arg0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
|
||||||
|
const ret = arg0.queueMicrotask;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||||
|
arg0.randomFillSync(arg1);
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||||
|
const ret = module.require;
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
|
||||||
|
const ret = Promise.resolve(arg0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) {
|
||||||
|
const ret = arg0.result;
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||||
|
arg0.set(arg1, arg2 >>> 0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||||
|
arg0.onerror = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||||
|
arg0.onsuccess = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) {
|
||||||
|
arg0.onupgradeneeded = arg1;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
|
||||||
|
const ret = typeof global === 'undefined' ? null : global;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
|
||||||
|
const ret = typeof globalThis === 'undefined' ? null : globalThis;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
|
||||||
|
const ret = typeof self === 'undefined' ? null : self;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
|
||||||
|
const ret = typeof window === 'undefined' ? null : window;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) {
|
||||||
|
const ret = arg0.target;
|
||||||
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
|
||||||
|
const ret = arg0.then(arg1);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
|
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
||||||
|
return ret;
|
||||||
|
}, arguments) };
|
||||||
|
imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) {
|
||||||
|
const ret = arg0.versions;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||||
|
const obj = arg0.original;
|
||||||
|
if (obj.cnt-- == 1) {
|
||||||
|
obj.a = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ret = false;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) {
|
||||||
|
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||||
|
const ret = debugString(arg1);
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||||
|
const table = wasm.__wbindgen_export_2;
|
||||||
|
const offset = table.grow(4);
|
||||||
|
table.set(0, undefined);
|
||||||
|
table.set(offset + 0, undefined);
|
||||||
|
table.set(offset + 1, null);
|
||||||
|
table.set(offset + 2, true);
|
||||||
|
table.set(offset + 3, false);
|
||||||
|
;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_function = function(arg0) {
|
||||||
|
const ret = typeof(arg0) === 'function';
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_null = function(arg0) {
|
||||||
|
const ret = arg0 === null;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_object = function(arg0) {
|
||||||
|
const val = arg0;
|
||||||
|
const ret = typeof(val) === 'object' && val !== null;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_string = function(arg0) {
|
||||||
|
const ret = typeof(arg0) === 'string';
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_is_undefined = function(arg0) {
|
||||||
|
const ret = arg0 === undefined;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_json_parse = function(arg0, arg1) {
|
||||||
|
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
|
||||||
|
const obj = arg1;
|
||||||
|
const ret = JSON.stringify(obj === undefined ? null : obj);
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_memory = function() {
|
||||||
|
const ret = wasm.memory;
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||||
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
};
|
||||||
|
imports['env'] = __wbg_star0;
|
||||||
|
|
||||||
|
return imports;
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_init_memory(imports, memory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function __wbg_finalize_init(instance, module) {
|
||||||
|
wasm = instance.exports;
|
||||||
|
__wbg_init.__wbindgen_wasm_module = module;
|
||||||
|
cachedDataViewMemory0 = null;
|
||||||
|
cachedUint8ArrayMemory0 = null;
|
||||||
|
|
||||||
|
|
||||||
|
wasm.__wbindgen_start();
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSync(module) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined') {
|
||||||
|
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||||
|
({module} = module)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
__wbg_init_memory(imports);
|
||||||
|
|
||||||
|
if (!(module instanceof WebAssembly.Module)) {
|
||||||
|
module = new WebAssembly.Module(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new WebAssembly.Instance(module, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function __wbg_init(module_or_path) {
|
||||||
|
if (wasm !== undefined) return wasm;
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof module_or_path !== 'undefined') {
|
||||||
|
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||||
|
({module_or_path} = module_or_path)
|
||||||
|
} else {
|
||||||
|
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'undefined') {
|
||||||
|
module_or_path = new URL('wasm_app_bg.wasm', import.meta.url);
|
||||||
|
}
|
||||||
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
|
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||||
|
module_or_path = fetch(module_or_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
__wbg_init_memory(imports);
|
||||||
|
|
||||||
|
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||||
|
|
||||||
|
return __wbg_finalize_init(instance, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initSync };
|
||||||
|
export default __wbg_init;
|
BIN
extension/public/wasm/wasm_app_bg.wasm
Normal file
120
extension/vite.config.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import wasm from 'vite-plugin-wasm';
|
||||||
|
import topLevelAwait from 'vite-plugin-top-level-await';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { Plugin } from 'vite';
|
||||||
|
|
||||||
|
// Custom plugin to copy extension files directly to the dist directory
|
||||||
|
const copyExtensionFiles = () => {
|
||||||
|
return {
|
||||||
|
name: 'copy-extension-files',
|
||||||
|
closeBundle() {
|
||||||
|
// Create the wasm directory in dist if it doesn't exist
|
||||||
|
const wasmDistDir = resolve(__dirname, 'dist/wasm');
|
||||||
|
if (!fs.existsSync(wasmDistDir)) {
|
||||||
|
fs.mkdirSync(wasmDistDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the wasm.js file
|
||||||
|
const wasmJsSource = resolve(__dirname, 'wasm/wasm_app.js');
|
||||||
|
const wasmJsDest = resolve(wasmDistDir, 'wasm_app.js');
|
||||||
|
fs.copyFileSync(wasmJsSource, wasmJsDest);
|
||||||
|
|
||||||
|
// Copy the wasm binary file
|
||||||
|
const wasmBinSource = resolve(__dirname, 'wasm/wasm_app_bg.wasm');
|
||||||
|
const wasmBinDest = resolve(wasmDistDir, 'wasm_app_bg.wasm');
|
||||||
|
fs.copyFileSync(wasmBinSource, wasmBinDest);
|
||||||
|
|
||||||
|
// Create background directory and copy the background script
|
||||||
|
const bgDistDir = resolve(__dirname, 'dist/background');
|
||||||
|
if (!fs.existsSync(bgDistDir)) {
|
||||||
|
fs.mkdirSync(bgDistDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgSource = resolve(__dirname, 'background/index.js');
|
||||||
|
const bgDest = resolve(bgDistDir, 'index.js');
|
||||||
|
fs.copyFileSync(bgSource, bgDest);
|
||||||
|
|
||||||
|
// Create popup directory and copy the popup files
|
||||||
|
const popupDistDir = resolve(__dirname, 'dist/popup');
|
||||||
|
if (!fs.existsSync(popupDistDir)) {
|
||||||
|
fs.mkdirSync(popupDistDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy HTML file
|
||||||
|
const htmlSource = resolve(__dirname, 'popup/index.html');
|
||||||
|
const htmlDest = resolve(popupDistDir, 'index.html');
|
||||||
|
fs.copyFileSync(htmlSource, htmlDest);
|
||||||
|
|
||||||
|
// Copy JS file
|
||||||
|
const jsSource = resolve(__dirname, 'popup/popup.js');
|
||||||
|
const jsDest = resolve(popupDistDir, 'popup.js');
|
||||||
|
fs.copyFileSync(jsSource, jsDest);
|
||||||
|
|
||||||
|
// Copy CSS file
|
||||||
|
const cssSource = resolve(__dirname, 'popup/popup.css');
|
||||||
|
const cssDest = resolve(popupDistDir, 'popup.css');
|
||||||
|
fs.copyFileSync(cssSource, cssDest);
|
||||||
|
|
||||||
|
// Also copy the manifest.json file
|
||||||
|
const manifestSource = resolve(__dirname, 'manifest.json');
|
||||||
|
const manifestDest = resolve(__dirname, 'dist/manifest.json');
|
||||||
|
fs.copyFileSync(manifestSource, manifestDest);
|
||||||
|
|
||||||
|
// Copy assets directory
|
||||||
|
const assetsDistDir = resolve(__dirname, 'dist/assets');
|
||||||
|
if (!fs.existsSync(assetsDistDir)) {
|
||||||
|
fs.mkdirSync(assetsDistDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy icon files
|
||||||
|
const iconSizes = [16, 32, 48, 128];
|
||||||
|
iconSizes.forEach(size => {
|
||||||
|
const iconSource = resolve(__dirname, `assets/icon-${size}.png`);
|
||||||
|
const iconDest = resolve(assetsDistDir, `icon-${size}.png`);
|
||||||
|
if (fs.existsSync(iconSource)) {
|
||||||
|
fs.copyFileSync(iconSource, iconDest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Extension files copied to dist directory');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
wasm(),
|
||||||
|
topLevelAwait(),
|
||||||
|
copyExtensionFiles()
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
// Simplify the build output for browser extension
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
popup: resolve(__dirname, 'popup/index.html')
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
// Use a simpler output format without hash values
|
||||||
|
entryFileNames: 'assets/[name].js',
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name].[ext]',
|
||||||
|
// Make sure output is compatible with browser extensions
|
||||||
|
format: 'iife',
|
||||||
|
// Don't generate separate code-split chunks
|
||||||
|
manualChunks: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Provide a simple dev server config
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
allow: ['../']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -9,8 +9,9 @@ path = "src/lib.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
js-sys = "0.3"
|
|
||||||
wasm-bindgen = "0.2"
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
@ -22,7 +23,9 @@ tempfile = "3"
|
|||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
idb = { version = "0.4" }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
|
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||||
|
idb = { version = "0.6" }
|
||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -22,3 +22,12 @@ pub enum KVError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, KVError>;
|
pub type Result<T> = std::result::Result<T, KVError>;
|
||||||
|
|
||||||
|
// Allow automatic conversion from idb::Error to KVError
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl From<idb::Error> for KVError {
|
||||||
|
fn from(e: idb::Error) -> Self {
|
||||||
|
KVError::Other(format!("idb error: {e:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -26,8 +26,7 @@ use async_trait::async_trait;
|
|||||||
use idb::{Database, TransactionMode, Factory};
|
use idb::{Database, TransactionMode, Factory};
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
#[cfg(target_arch = "wasm32")]
|
// use wasm-bindgen directly for Uint8Array if needed
|
||||||
use js_sys::Uint8Array;
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@ -47,6 +46,7 @@ impl WasmStore {
|
|||||||
let mut open_req = factory.open(name, None)
|
let mut open_req = factory.open(name, None)
|
||||||
.map_err(|e| KVError::Other(format!("IndexedDB factory open error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("IndexedDB factory open error: {e:?}")))?;
|
||||||
open_req.on_upgrade_needed(|event| {
|
open_req.on_upgrade_needed(|event| {
|
||||||
|
use idb::DatabaseEvent;
|
||||||
let db = event.database().expect("Failed to get database in upgrade event");
|
let db = event.database().expect("Failed to get database in upgrade event");
|
||||||
if !db.store_names().iter().any(|n| n == STORE_NAME) {
|
if !db.store_names().iter().any(|n| n == STORE_NAME) {
|
||||||
db.create_object_store(STORE_NAME, Default::default()).unwrap();
|
db.create_object_store(STORE_NAME, Default::default()).unwrap();
|
||||||
@ -66,11 +66,13 @@ impl KVStore for WasmStore {
|
|||||||
let store = tx.object_store(STORE_NAME)
|
let store = tx.object_store(STORE_NAME)
|
||||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||||
use idb::Query;
|
use idb::Query;
|
||||||
let val = store.get(Query::from(JsValue::from_str(key))).await
|
let val = store.get(Query::from(JsValue::from_str(key)))?.await
|
||||||
.map_err(|e| KVError::Other(format!("idb get await error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb get error: {e:?}")))?;
|
||||||
if let Some(jsval) = val {
|
if let Some(jsval) = val {
|
||||||
let arr = Uint8Array::new(&jsval);
|
match jsval.into_serde::<Vec<u8>>() {
|
||||||
Ok(Some(arr.to_vec()))
|
Ok(bytes) => Ok(Some(bytes)),
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
@ -80,8 +82,9 @@ impl KVStore for WasmStore {
|
|||||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||||
let store = tx.object_store(STORE_NAME)
|
let store = tx.object_store(STORE_NAME)
|
||||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||||
store.put(&Uint8Array::from(value).into(), Some(&JsValue::from_str(key))).await
|
let js_value = JsValue::from_serde(&value).map_err(|e| KVError::Other(format!("serde error: {e:?}")))?;
|
||||||
.map_err(|e| KVError::Other(format!("idb put await error: {e:?}")))?;
|
store.put(&js_value, Some(&JsValue::from_str(key)))?.await
|
||||||
|
.map_err(|e| KVError::Other(format!("idb put error: {e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn remove(&self, key: &str) -> Result<()> {
|
async fn remove(&self, key: &str) -> Result<()> {
|
||||||
@ -90,8 +93,8 @@ impl KVStore for WasmStore {
|
|||||||
let store = tx.object_store(STORE_NAME)
|
let store = tx.object_store(STORE_NAME)
|
||||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||||
use idb::Query;
|
use idb::Query;
|
||||||
store.delete(Query::from(JsValue::from_str(key))).await
|
store.delete(Query::from(JsValue::from_str(key)))?.await
|
||||||
.map_err(|e| KVError::Other(format!("idb delete await error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb delete error: {e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
async fn contains_key(&self, key: &str) -> Result<bool> {
|
async fn contains_key(&self, key: &str) -> Result<bool> {
|
||||||
@ -103,12 +106,11 @@ impl KVStore for WasmStore {
|
|||||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||||
let store = tx.object_store(STORE_NAME)
|
let store = tx.object_store(STORE_NAME)
|
||||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||||
let js_keys = store.get_all_keys(None, None).await
|
let js_keys = store.get_all_keys(None, None)?.await
|
||||||
.map_err(|e| KVError::Other(format!("idb get_all_keys error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb get_all_keys error: {e:?}")))?;
|
||||||
let arr = js_sys::Array::from(&JsValue::from(js_keys));
|
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
for i in 0..arr.length() {
|
for key in js_keys.iter() {
|
||||||
if let Some(s) = arr.get(i).as_string() {
|
if let Some(s) = key.as_string() {
|
||||||
keys.push(s);
|
keys.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -120,7 +122,7 @@ impl KVStore for WasmStore {
|
|||||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||||
let store = tx.object_store(STORE_NAME)
|
let store = tx.object_store(STORE_NAME)
|
||||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||||
store.clear().await
|
store.clear()?.await
|
||||||
.map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?;
|
.map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -31,3 +31,22 @@ async fn test_native_store_basic() {
|
|||||||
let keys = store.keys().await.unwrap();
|
let keys = store.keys().await.unwrap();
|
||||||
assert_eq!(keys.len(), 0);
|
assert_eq!(keys.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_native_store_persistence() {
|
||||||
|
let tmp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp_dir.path().join("persistdb");
|
||||||
|
let db_path = path.to_str().unwrap();
|
||||||
|
// First open, set value
|
||||||
|
{
|
||||||
|
let store = NativeStore::open(db_path).unwrap();
|
||||||
|
store.set("persist", b"value").await.unwrap();
|
||||||
|
}
|
||||||
|
// Reopen and check value
|
||||||
|
{
|
||||||
|
let store = NativeStore::open(db_path).unwrap();
|
||||||
|
let val = store.get("persist").await.unwrap();
|
||||||
|
assert_eq!(val, Some(b"value".to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ wasm_bindgen_test_configure!(run_in_browser);
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
async fn test_set_and_get() {
|
async fn test_set_and_get() {
|
||||||
let store = WasmStore::open("test-db").await.expect("open");
|
let store = WasmStore::open("vault").await.expect("open");
|
||||||
store.set("foo", b"bar").await.expect("set");
|
store.set("foo", b"bar").await.expect("set");
|
||||||
let val = store.get("foo").await.expect("get");
|
let val = store.get("foo").await.expect("get");
|
||||||
assert_eq!(val, Some(b"bar".to_vec()));
|
assert_eq!(val, Some(b"bar".to_vec()));
|
||||||
@ -16,7 +16,7 @@ async fn test_set_and_get() {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
async fn test_delete_and_exists() {
|
async fn test_delete_and_exists() {
|
||||||
let store = WasmStore::open("test-db").await.expect("open");
|
let store = WasmStore::open("vault").await.expect("open");
|
||||||
store.set("foo", b"bar").await.expect("set");
|
store.set("foo", b"bar").await.expect("set");
|
||||||
assert_eq!(store.contains_key("foo").await.unwrap(), true);
|
assert_eq!(store.contains_key("foo").await.unwrap(), true);
|
||||||
assert_eq!(store.contains_key("bar").await.unwrap(), false);
|
assert_eq!(store.contains_key("bar").await.unwrap(), false);
|
||||||
@ -26,7 +26,7 @@ async fn test_delete_and_exists() {
|
|||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
async fn test_keys() {
|
async fn test_keys() {
|
||||||
let store = WasmStore::open("test-db").await.expect("open");
|
let store = WasmStore::open("vault").await.expect("open");
|
||||||
store.set("foo", b"bar").await.expect("set");
|
store.set("foo", b"bar").await.expect("set");
|
||||||
store.set("baz", b"qux").await.expect("set");
|
store.set("baz", b"qux").await.expect("set");
|
||||||
let keys = store.keys().await.unwrap();
|
let keys = store.keys().await.unwrap();
|
||||||
@ -35,9 +35,26 @@ async fn test_keys() {
|
|||||||
assert!(keys.contains(&"baz".to_string()));
|
assert!(keys.contains(&"baz".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
async fn test_wasm_store_persistence() {
|
||||||
|
// Use a unique store name to avoid collisions
|
||||||
|
let store_name = "persist_test_store";
|
||||||
|
// First open, set value
|
||||||
|
{
|
||||||
|
let store = WasmStore::open(store_name).await.expect("open");
|
||||||
|
store.set("persist", b"value").await.expect("set");
|
||||||
|
}
|
||||||
|
// Reopen and check value
|
||||||
|
{
|
||||||
|
let store = WasmStore::open(store_name).await.expect("open");
|
||||||
|
let val = store.get("persist").await.expect("get");
|
||||||
|
assert_eq!(val, Some(b"value".to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen_test]
|
#[wasm_bindgen_test]
|
||||||
async fn test_clear() {
|
async fn test_clear() {
|
||||||
let store = WasmStore::open("test-db").await.expect("open");
|
let store = WasmStore::open("vault").await.expect("open");
|
||||||
store.set("foo", b"bar").await.expect("set");
|
store.set("foo", b"bar").await.expect("set");
|
||||||
store.set("baz", b"qux").await.expect("set");
|
store.set("baz", b"qux").await.expect("set");
|
||||||
store.clear().await.unwrap();
|
store.clear().await.unwrap();
|
||||||
|
@ -12,7 +12,7 @@ tokio = { version = "1.37", features = ["rt", "macros"] }
|
|||||||
kvstore = { path = "../kvstore" }
|
kvstore = { path = "../kvstore" }
|
||||||
scrypt = "0.11"
|
scrypt = "0.11"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
aes-gcm = "0.10"
|
# aes-gcm = "0.10"
|
||||||
pbkdf2 = "0.12"
|
pbkdf2 = "0.12"
|
||||||
signature = "2.2"
|
signature = "2.2"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@ -22,17 +22,20 @@ ed25519-dalek = "2.1"
|
|||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
env_logger = "0.11"
|
|
||||||
console_log = "1"
|
console_log = "1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
zeroize = "1.8.1"
|
zeroize = "1.8.1"
|
||||||
rhai = "1.21.0"
|
rhai = "1.21.0"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
console_error_panic_hook = "0.1"
|
# console_error_panic_hook = "0.1"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||||
tempfile = "3.10"
|
tempfile = "3.10"
|
||||||
@ -42,7 +45,16 @@ chrono = "0.4"
|
|||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
console_error_panic_hook = "0.1"
|
# console_error_panic_hook = "0.1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
hex = "0.4"
|
||||||
|
rhai = "1.21.0"
|
||||||
|
zeroize = "1.8.1"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
native = []
|
@ -1,5 +1,7 @@
|
|||||||
//! Data models for the vault crate
|
//! Data models for the vault crate
|
||||||
|
|
||||||
|
// Only keep serde derives on structs, remove unused imports
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct VaultMetadata {
|
pub struct VaultMetadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -7,7 +9,7 @@ pub struct VaultMetadata {
|
|||||||
// ... other vault-level metadata
|
// ... other vault-level metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct KeyspaceMetadata {
|
pub struct KeyspaceMetadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub salt: [u8; 16], // Unique salt for this keyspace
|
pub salt: [u8; 16], // Unique salt for this keyspace
|
||||||
@ -17,12 +19,28 @@ pub struct KeyspaceMetadata {
|
|||||||
// ... other keyspace metadata
|
// ... other keyspace metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct KeyspaceData {
|
pub struct KeyspaceData {
|
||||||
pub keypairs: Vec<KeyEntry>,
|
pub keypairs: Vec<KeyEntry>,
|
||||||
// ... other keyspace-level metadata
|
// ... other keyspace-level metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl zeroize::Zeroize for KeyspaceData {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
for key in &mut self.keypairs {
|
||||||
|
key.zeroize();
|
||||||
|
}
|
||||||
|
self.keypairs.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl zeroize::Zeroize for KeyEntry {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.private_key.zeroize();
|
||||||
|
// Optionally, zeroize other fields if needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct KeyEntry {
|
pub struct KeyEntry {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -39,7 +57,7 @@ pub enum KeyType {
|
|||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct KeyMetadata {
|
pub struct KeyMetadata {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub created_at: Option<u64>,
|
pub created_at: Option<u64>,
|
||||||
|
@ -39,7 +39,7 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan
|
|||||||
pub fn sign(&self, message: rhai::Blob) -> Result<rhai::Blob, String> {
|
pub fn sign(&self, message: rhai::Blob) -> Result<rhai::Blob, String> {
|
||||||
let sm = self.inner.lock().unwrap();
|
let sm = self.inner.lock().unwrap();
|
||||||
// Try to get the current keyspace name from session state if possible
|
// Try to get the current keyspace name from session state if possible
|
||||||
let keypair = sm.current_keypair().ok_or("No keypair selected")?;
|
let _keypair = sm.current_keypair().ok_or("No keypair selected")?;
|
||||||
// Sign using the session manager; password and keyspace are not needed (already unlocked)
|
// Sign using the session manager; password and keyspace are not needed (already unlocked)
|
||||||
crate::rhai_sync_helpers::sign_sync::<S>(
|
crate::rhai_sync_helpers::sign_sync::<S>(
|
||||||
&sm,
|
&sm,
|
||||||
@ -50,14 +50,46 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan
|
|||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
impl<S: kvstore::traits::KVStore + Clone + 'static> RhaiSessionManager<S> {
|
impl<S: kvstore::traits::KVStore + Clone + 'static> RhaiSessionManager<S> {
|
||||||
// WASM-specific implementation (stub for now)
|
pub fn select_keypair(&self, key_id: String) -> Result<(), String> {
|
||||||
|
// Use the global singleton for session management
|
||||||
|
crate::session_singleton::SESSION_MANAGER.with(|cell| {
|
||||||
|
let mut opt = cell.borrow_mut();
|
||||||
|
if let Some(session) = opt.as_mut() {
|
||||||
|
session.select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}"))
|
||||||
|
} else {
|
||||||
|
Err("Session not initialized".to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_keypair(&self) -> Option<String> {
|
||||||
|
crate::session_singleton::SESSION_MANAGER.with(|cell| {
|
||||||
|
let opt = cell.borrow();
|
||||||
|
opt.as_ref()
|
||||||
|
.and_then(|session| session.current_keypair().map(|k| k.id.clone()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logout(&self) {
|
||||||
|
crate::session_singleton::SESSION_MANAGER.with(|cell| {
|
||||||
|
let mut opt = cell.borrow_mut();
|
||||||
|
if let Some(session) = opt.as_mut() {
|
||||||
|
session.logout();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign(&self, _message: rhai::Blob) -> Result<rhai::Blob, String> {
|
||||||
|
// Signing is async in WASM; must be called from JS/wasm-bindgen, not Rhai
|
||||||
|
Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM-specific API: no Arc/Mutex, just a reference
|
// WASM-specific API: no Arc/Mutex, just a reference
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>(
|
pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
session_manager: &SessionManager<S>,
|
// session_manager: &SessionManager<S>,
|
||||||
) {
|
) {
|
||||||
// WASM registration logic (adapt as needed)
|
// WASM registration logic (adapt as needed)
|
||||||
// Example: engine.register_type::<RhaiSessionManager<S>>();
|
// Example: engine.register_type::<RhaiSessionManager<S>>();
|
||||||
@ -66,7 +98,7 @@ pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>(
|
|||||||
engine.register_fn("select_keypair", |key_id: String| {
|
engine.register_fn("select_keypair", |key_id: String| {
|
||||||
crate::wasm_helpers::select_keypair_global(&key_id)
|
crate::wasm_helpers::select_keypair_global(&key_id)
|
||||||
}); // Calls the shared WASM session singleton
|
}); // Calls the shared WASM session singleton
|
||||||
engine.register_fn("sign", |message: rhai::Blob| -> Result<rhai::Blob, String> {
|
engine.register_fn("sign", |_message: rhai::Blob| -> Result<rhai::Blob, String> {
|
||||||
Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string())
|
Err("sign is async in WASM; use the WASM sign() API from JS instead".to_string())
|
||||||
});
|
});
|
||||||
// No global session object in WASM; use JS/WASM API for session ops
|
// No global session object in WASM; use JS/WASM API for session ops
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
//! Synchronous wrappers for async Vault and EVM client APIs for use in Rhai bindings.
|
|
||||||
//! These use block_on for native, and spawn_local for WASM if needed.
|
|
||||||
|
|
||||||
use crate::session::SessionManager;
|
use crate::session::SessionManager;
|
||||||
|
|
||||||
|
// Synchronous wrappers for async Vault and EVM client APIs for use in Rhai bindings.
|
||||||
|
// These use block_on for native, and spawn_local for WASM if needed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
@ -2,79 +2,60 @@
|
|||||||
//! Provides ergonomic, stateful access to unlocked keyspaces and keypairs for interactive applications.
|
//! Provides ergonomic, stateful access to unlocked keyspaces and keypairs for interactive applications.
|
||||||
//! All state is local to the SessionManager instance. No global state.
|
//! All state is local to the SessionManager instance. No global state.
|
||||||
|
|
||||||
|
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
use crate::{Vault, KeyspaceData, KeyEntry, VaultError, KVStore};
|
|
||||||
|
|
||||||
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct SessionManager<S: KVStore + Send + Sync> {
|
pub struct SessionManager<S: KVStore + Send + Sync> {
|
||||||
vault: Vault<S>,
|
vault: Vault<S>,
|
||||||
unlocked_keyspaces: HashMap<String, (Vec<u8>, KeyspaceData)>, // name -> (password, data)
|
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
|
||||||
current_keyspace: Option<String>,
|
|
||||||
current_keypair: Option<String>,
|
current_keypair: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub struct SessionManager<S: KVStore> {
|
pub struct SessionManager<S: KVStore> {
|
||||||
vault: Vault<S>,
|
vault: Vault<S>,
|
||||||
unlocked_keyspaces: HashMap<String, (Vec<u8>, KeyspaceData)>, // name -> (password, data)
|
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
|
||||||
current_keyspace: Option<String>,
|
|
||||||
current_keypair: Option<String>,
|
current_keypair: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
impl<S: KVStore + Send + Sync> SessionManager<S> {
|
|
||||||
/// Create a new session manager from a Vault instance.
|
|
||||||
pub fn new(vault: Vault<S>) -> Self {
|
|
||||||
Self {
|
|
||||||
vault,
|
|
||||||
unlocked_keyspaces: HashMap::new(),
|
|
||||||
current_keyspace: None,
|
|
||||||
current_keypair: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
impl<S: KVStore> SessionManager<S> {
|
impl<S: KVStore> SessionManager<S> {
|
||||||
/// Create a new session manager from a Vault instance.
|
pub fn get_vault_mut(&mut self) -> &mut Vault<S> {
|
||||||
pub fn new(vault: Vault<S>) -> Self {
|
&mut self.vault
|
||||||
Self {
|
|
||||||
vault,
|
|
||||||
unlocked_keyspaces: HashMap::new(),
|
|
||||||
current_keyspace: None,
|
|
||||||
current_keypair: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native impl for all methods
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl<S: KVStore + Send + Sync> SessionManager<S> {
|
impl<S: KVStore + Send + Sync> SessionManager<S> {
|
||||||
/// Unlock a keyspace and store its decrypted data in memory.
|
pub fn new(vault: Vault<S>) -> Self {
|
||||||
|
Self {
|
||||||
|
vault,
|
||||||
|
unlocked_keyspace: None,
|
||||||
|
current_keypair: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
|
||||||
|
self.vault.create_keyspace(name, password, tags).await?;
|
||||||
|
self.unlock_keyspace(name, password).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
|
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
|
||||||
let data = self.vault.unlock_keyspace(name, password).await?;
|
let data = self.vault.unlock_keyspace(name, password).await?;
|
||||||
self.unlocked_keyspaces.insert(name.to_string(), (password.to_vec(), data));
|
self.unlocked_keyspace = Some((name.to_string(), password.to_vec(), data));
|
||||||
self.current_keyspace = Some(name.to_string());
|
self.current_keypair = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select a previously unlocked keyspace as the current context.
|
|
||||||
pub fn select_keyspace(&mut self, name: &str) -> Result<(), VaultError> {
|
|
||||||
if self.unlocked_keyspaces.contains_key(name) {
|
|
||||||
self.current_keyspace = Some(name.to_string());
|
|
||||||
self.current_keypair = None;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(VaultError::Crypto("Keyspace not unlocked".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select a keypair within the current keyspace.
|
|
||||||
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
|
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
|
||||||
let keyspace = self.current_keyspace.as_ref().ok_or_else(|| VaultError::Crypto("No keyspace selected".to_string()))?;
|
let data = self
|
||||||
let (_, data) = self.unlocked_keyspaces.get(keyspace).ok_or_else(|| VaultError::Crypto("Keyspace not unlocked".to_string()))?;
|
.unlocked_keyspace
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, _, d)| d)
|
||||||
|
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||||
if data.keypairs.iter().any(|k| k.id == key_id) {
|
if data.keypairs.iter().any(|k| k.id == key_id) {
|
||||||
self.current_keypair = Some(key_id.to_string());
|
self.current_keypair = Some(key_id.to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -83,146 +64,164 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the currently selected keyspace data (if any).
|
pub async fn add_keypair(
|
||||||
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
|
&mut self,
|
||||||
self.current_keyspace.as_ref()
|
key_type: Option<crate::KeyType>,
|
||||||
.and_then(|name| self.unlocked_keyspaces.get(name))
|
metadata: Option<crate::KeyMetadata>,
|
||||||
.map(|(_, data)| data)
|
) -> Result<String, VaultError> {
|
||||||
}
|
let (name, password, _) = self
|
||||||
|
.unlocked_keyspace
|
||||||
/// Get the currently selected keypair (if any).
|
.as_ref()
|
||||||
pub fn current_keypair(&self) -> Option<&KeyEntry> {
|
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||||
let keyspace = self.current_keyspace()?;
|
let id = self
|
||||||
let key_id = self.current_keypair.as_ref()?;
|
.vault
|
||||||
keyspace.keypairs.iter().find(|k| &k.id == key_id)
|
.add_keypair(name, password, key_type, metadata.clone())
|
||||||
}
|
.await?;
|
||||||
|
|
||||||
/// Sign a message with the currently selected keypair.
|
|
||||||
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
|
||||||
let _keyspace = self.current_keyspace().ok_or(VaultError::Crypto("No keyspace selected".to_string()))?;
|
|
||||||
let keypair = self.current_keypair().ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
|
|
||||||
let (password, _) = self.unlocked_keyspaces.get(self.current_keyspace.as_ref().unwrap()).unwrap();
|
|
||||||
self.vault.sign(
|
|
||||||
self.current_keyspace.as_ref().unwrap(),
|
|
||||||
password,
|
|
||||||
&keypair.id,
|
|
||||||
message,
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a reference to the underlying Vault (for stateless operations in tests).
|
|
||||||
pub fn get_vault(&self) -> &Vault<S> {
|
|
||||||
&self.vault
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WASM impl for all methods
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
impl<S: KVStore> SessionManager<S> {
|
|
||||||
/// Unlock a keyspace and store its decrypted data in memory.
|
|
||||||
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
|
|
||||||
let data = self.vault.unlock_keyspace(name, password).await?;
|
let data = self.vault.unlock_keyspace(name, password).await?;
|
||||||
self.unlocked_keyspaces.insert(name.to_string(), (password.to_vec(), data));
|
self.unlocked_keyspace = Some((name.clone(), password.clone(), data));
|
||||||
self.current_keyspace = Some(name.to_string());
|
Ok(id)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select a previously unlocked keyspace as the current context.
|
pub fn list_keypairs(&self) -> Option<&[KeyEntry]> {
|
||||||
pub fn select_keyspace(&mut self, name: &str) -> Result<(), VaultError> {
|
self.current_keyspace().map(|ks| ks.keypairs.as_slice())
|
||||||
if self.unlocked_keyspaces.contains_key(name) {
|
|
||||||
self.current_keyspace = Some(name.to_string());
|
|
||||||
self.current_keypair = None;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(VaultError::Crypto("Keyspace not unlocked".to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select a keypair within the current keyspace.
|
|
||||||
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
|
|
||||||
let keyspace = self.current_keyspace.as_ref().ok_or_else(|| VaultError::Crypto("No keyspace selected".to_string()))?;
|
|
||||||
let (_, data) = self.unlocked_keyspaces.get(keyspace).ok_or_else(|| VaultError::Crypto("Keyspace not unlocked".to_string()))?;
|
|
||||||
if data.keypairs.iter().any(|k| k.id == key_id) {
|
|
||||||
self.current_keypair = Some(key_id.to_string());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(VaultError::Crypto("Keypair not found".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the currently selected keyspace data (if any).
|
|
||||||
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
|
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
|
||||||
self.current_keyspace.as_ref()
|
self.unlocked_keyspace.as_ref().map(|(_, _, data)| data)
|
||||||
.and_then(|name| self.unlocked_keyspaces.get(name))
|
|
||||||
.map(|(_, data)| data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the currently selected keypair (if any).
|
|
||||||
pub fn current_keypair(&self) -> Option<&KeyEntry> {
|
pub fn current_keypair(&self) -> Option<&KeyEntry> {
|
||||||
let keyspace = self.current_keyspace()?;
|
let keyspace = self.current_keyspace()?;
|
||||||
let key_id = self.current_keypair.as_ref()?;
|
let key_id = self.current_keypair.as_ref()?;
|
||||||
keyspace.keypairs.iter().find(|k| &k.id == key_id)
|
keyspace.keypairs.iter().find(|k| &k.id == key_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign a message with the currently selected keypair.
|
|
||||||
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||||
let _keyspace = self.current_keyspace().ok_or(VaultError::Crypto("No keyspace selected".to_string()))?;
|
let (name, password, _) = self
|
||||||
let keypair = self.current_keypair().ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
|
.unlocked_keyspace
|
||||||
let (password, _) = self.unlocked_keyspaces.get(self.current_keyspace.as_ref().unwrap()).unwrap();
|
.as_ref()
|
||||||
self.vault.sign(
|
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||||
self.current_keyspace.as_ref().unwrap(),
|
let keypair = self
|
||||||
password,
|
.current_keypair()
|
||||||
&keypair.id,
|
.ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
|
||||||
message,
|
self.vault.sign(name, password, &keypair.id, message).await
|
||||||
).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the underlying Vault (for stateless operations in tests).
|
|
||||||
pub fn get_vault(&self) -> &Vault<S> {
|
pub fn get_vault(&self) -> &Vault<S> {
|
||||||
&self.vault
|
&self.vault
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Shared impl for methods needed by Drop
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
impl<S: KVStore + Send + Sync> SessionManager<S> {
|
|
||||||
/// Wipe all unlocked keyspaces and secrets from memory.
|
|
||||||
pub fn logout(&mut self) {
|
pub fn logout(&mut self) {
|
||||||
for (pw, data) in self.unlocked_keyspaces.values_mut() {
|
if let Some((_, mut password, mut data)) = self.unlocked_keyspace.take() {
|
||||||
pw.zeroize();
|
password.zeroize();
|
||||||
// KeyspaceData and KeyEntry use Vec<u8> for secrets, drop will clear
|
data.zeroize();
|
||||||
for k in &mut data.keypairs {
|
|
||||||
k.private_key.zeroize();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.unlocked_keyspaces.clear();
|
|
||||||
self.current_keyspace = None;
|
|
||||||
self.current_keypair = None;
|
self.current_keypair = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
impl<S: KVStore> SessionManager<S> {
|
|
||||||
/// Wipe all unlocked keyspaces and secrets from memory.
|
|
||||||
pub fn logout(&mut self) {
|
|
||||||
for (pw, data) in self.unlocked_keyspaces.values_mut() {
|
|
||||||
pw.zeroize();
|
|
||||||
// KeyspaceData and KeyEntry use Vec<u8> for secrets, drop will clear
|
|
||||||
for k in &mut data.keypairs {
|
|
||||||
k.private_key.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.unlocked_keyspaces.clear();
|
|
||||||
self.current_keyspace = None;
|
|
||||||
self.current_keypair = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// END wasm32 impl
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl<S: KVStore + Send + Sync> Drop for SessionManager<S> {
|
impl<S: KVStore + Send + Sync> Drop for SessionManager<S> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.logout();
|
self.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl<S: KVStore> SessionManager<S> {
|
||||||
|
pub fn new(vault: Vault<S>) -> Self {
|
||||||
|
Self {
|
||||||
|
vault,
|
||||||
|
unlocked_keyspace: None,
|
||||||
|
current_keypair: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
|
||||||
|
self.vault.create_keyspace(name, password, tags).await?;
|
||||||
|
self.unlock_keyspace(name, password).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError> {
|
||||||
|
let data = self.vault.unlock_keyspace(name, password).await?;
|
||||||
|
self.unlocked_keyspace = Some((name.to_string(), password.to_vec(), data));
|
||||||
|
self.current_keypair = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_keypair(&mut self, key_id: &str) -> Result<(), VaultError> {
|
||||||
|
let data = self
|
||||||
|
.unlocked_keyspace
|
||||||
|
.as_ref()
|
||||||
|
.map(|(_, _, d)| d)
|
||||||
|
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||||
|
if data.keypairs.iter().any(|k| k.id == key_id) {
|
||||||
|
self.current_keypair = Some(key_id.to_string());
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(VaultError::Crypto("Keypair not found".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_keypair(
|
||||||
|
&mut self,
|
||||||
|
key_type: Option<crate::KeyType>,
|
||||||
|
metadata: Option<crate::KeyMetadata>,
|
||||||
|
) -> Result<String, VaultError> {
|
||||||
|
let (name, password, _) = self
|
||||||
|
.unlocked_keyspace
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||||
|
let id = self
|
||||||
|
.vault
|
||||||
|
.add_keypair(name, password, key_type, metadata.clone())
|
||||||
|
.await?;
|
||||||
|
let data = self.vault.unlock_keyspace(name, password).await?;
|
||||||
|
self.unlocked_keyspace = Some((name.clone(), password.clone(), data));
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_keypairs(&self) -> Option<&[KeyEntry]> {
|
||||||
|
self.current_keyspace().map(|ks| ks.keypairs.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_keyspace(&self) -> Option<&KeyspaceData> {
|
||||||
|
self.unlocked_keyspace.as_ref().map(|(_, _, data)| data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_keypair(&self) -> Option<&KeyEntry> {
|
||||||
|
let keyspace = self.current_keyspace()?;
|
||||||
|
let key_id = self.current_keypair.as_ref()?;
|
||||||
|
keyspace.keypairs.iter().find(|k| &k.id == key_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||||
|
let (name, password, _) = self
|
||||||
|
.unlocked_keyspace
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||||
|
let keypair = self
|
||||||
|
.current_keypair()
|
||||||
|
.ok_or(VaultError::Crypto("No keypair selected".to_string()))?;
|
||||||
|
self.vault.sign(name, password, &keypair.id, message).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vault(&self) -> &Vault<S> {
|
||||||
|
&self.vault
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logout(&mut self) {
|
||||||
|
if let Some((_, mut password, mut data)) = self.unlocked_keyspace.take() {
|
||||||
|
password.zeroize();
|
||||||
|
data.zeroize();
|
||||||
|
}
|
||||||
|
self.current_keypair = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl<S: KVStore> Drop for SessionManager<S> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -15,17 +15,21 @@ async fn session_manager_end_to_end() {
|
|||||||
let keyspace = "personal";
|
let keyspace = "personal";
|
||||||
let password = b"testpass";
|
let password = b"testpass";
|
||||||
|
|
||||||
// Create keyspace
|
|
||||||
vault.create_keyspace(keyspace, password, None).await.expect("create_keyspace");
|
|
||||||
// Add keypair
|
|
||||||
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair");
|
|
||||||
|
|
||||||
// Create session manager
|
// Create session manager
|
||||||
let mut session = SessionManager::new(vault);
|
let mut session = SessionManager::new(vault);
|
||||||
session.unlock_keyspace(keyspace, password).await.expect("unlock_keyspace");
|
// Create and unlock keyspace in one step
|
||||||
session.select_keyspace(keyspace).expect("select_keyspace");
|
session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session");
|
||||||
|
// Add keypair using session API
|
||||||
|
let key_id = session.add_keypair(Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair via session");
|
||||||
session.select_keypair(&key_id).expect("select_keypair");
|
session.select_keypair(&key_id).expect("select_keypair");
|
||||||
|
|
||||||
|
// Test add_keypair with metadata via SessionManager
|
||||||
|
let meta = KeyMetadata { name: Some("user1-key".to_string()), created_at: None, tags: Some(vec!["tag1".to_string()]) };
|
||||||
|
let key_id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta.clone())).await.expect("add_keypair via session");
|
||||||
|
// List keypairs and check metadata
|
||||||
|
let keypairs = session.list_keypairs().expect("list_keypairs");
|
||||||
|
assert!(keypairs.iter().any(|k| k.id == key_id2 && k.metadata.as_ref().unwrap().name.as_deref() == Some("user1-key")), "metadata name should be present");
|
||||||
|
|
||||||
// Sign and verify
|
// Sign and verify
|
||||||
let msg = b"hello world";
|
let msg = b"hello world";
|
||||||
let sig = session.sign(msg).await.expect("sign");
|
let sig = session.sign(msg).await.expect("sign");
|
||||||
@ -55,7 +59,8 @@ async fn session_manager_errors() {
|
|||||||
let vault = Vault::new(store);
|
let vault = Vault::new(store);
|
||||||
let mut session = SessionManager::new(vault);
|
let mut session = SessionManager::new(vault);
|
||||||
// No keyspace unlocked
|
// No keyspace unlocked
|
||||||
assert!(session.select_keyspace("none").is_err());
|
// select_keyspace removed; test unlocking a non-existent keyspace or selecting a keypair from an empty keyspace instead.
|
||||||
|
assert!(session.select_keypair("none").is_err());
|
||||||
assert!(session.select_keypair("none").is_err());
|
assert!(session.select_keypair("none").is_err());
|
||||||
assert!(session.sign(b"fail").await.is_err());
|
assert!(session.sign(b"fail").await.is_err());
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
// All imports are WASM-specific and local to the test function
|
// All imports are WASM-specific and local to the test function
|
||||||
use kvstore::wasm::WasmStore;
|
use kvstore::wasm::WasmStore;
|
||||||
use vault::Vault;
|
use vault::Vault;
|
||||||
let store = WasmStore::open("testdb_wasm_keypair_management").await.unwrap();
|
let store = WasmStore::open("vault").await.unwrap();
|
||||||
let mut vault = Vault::new(store);
|
let mut vault = Vault::new(store);
|
||||||
vault.create_keyspace("testspace", b"pw", None).await.unwrap();
|
vault.create_keyspace("testspace", b"pw", None).await.unwrap();
|
||||||
let key_id = vault.add_keypair("testspace", b"pw", None, None).await.unwrap();
|
let key_id = vault.add_keypair("testspace", b"pw", None, None).await.unwrap();
|
||||||
|
@ -11,19 +11,65 @@ use vault::Vault;
|
|||||||
wasm_bindgen_test_configure!(run_in_browser);
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
#[wasm_bindgen_test(async)]
|
#[wasm_bindgen_test(async)]
|
||||||
async fn test_session_manager_end_to_end() {
|
async fn test_session_manager_lock_unlock_keypairs_persistence() {
|
||||||
// Example: test session manager logic in WASM
|
|
||||||
// This is a placeholder for your real test logic.
|
|
||||||
// All imports are WASM-specific and local to the test function
|
|
||||||
use kvstore::wasm::WasmStore;
|
use kvstore::wasm::WasmStore;
|
||||||
|
use vault::{Vault, KeyType, KeyMetadata};
|
||||||
use vault::session::SessionManager;
|
use vault::session::SessionManager;
|
||||||
let store = WasmStore::open("testdb_wasm_session_manager").await.unwrap();
|
let store = WasmStore::open("test-session-manager-lock-unlock").await.unwrap();
|
||||||
let vault = Vault::new(store);
|
let mut vault = Vault::new(store);
|
||||||
let mut manager = SessionManager::new(vault);
|
let keyspace = "testspace2";
|
||||||
let keyspace = "testspace";
|
let password = b"testpass2";
|
||||||
// This test can only check session initialization/select_keyspace logic as SessionManager does not create keypairs directly.
|
|
||||||
// manager.select_keyspace(keyspace) would fail unless the keyspace exists.
|
// 1. Create session manager
|
||||||
// For a true end-to-end test, use Vault to create the keyspace and keypair, then test SessionManager.
|
let mut session = SessionManager::new(vault);
|
||||||
// For now, just test that SessionManager can be constructed.
|
// Create and unlock keyspace in one step
|
||||||
assert!(manager.current_keyspace().is_none());
|
session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session");
|
||||||
|
// 2. Add two keypairs with names using session API
|
||||||
|
let meta1 = KeyMetadata { name: Some("keypair-one".to_string()), created_at: None, tags: None };
|
||||||
|
let meta2 = KeyMetadata { name: Some("keypair-two".to_string()), created_at: None, tags: None };
|
||||||
|
let id1 = session.add_keypair(Some(KeyType::Secp256k1), Some(meta1.clone())).await.expect("add_keypair1 via session");
|
||||||
|
let id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta2.clone())).await.expect("add_keypair2 via session");
|
||||||
|
|
||||||
|
// 3. List, store keys and names
|
||||||
|
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||||
|
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||||
|
assert_eq!(keypairs_before.len(), 2);
|
||||||
|
assert!(keypairs_before.iter().any(|k| k.0 == id1 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-one")));
|
||||||
|
assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two")));
|
||||||
|
|
||||||
|
// 4. Lock (logout)
|
||||||
|
session.logout();
|
||||||
|
assert!(session.current_keyspace().is_none());
|
||||||
|
|
||||||
|
// 5. Unlock again
|
||||||
|
session.unlock_keyspace(keyspace, password).await.expect("unlock_keyspace again");
|
||||||
|
// select_keyspace removed; unlocking a keyspace is sufficient after refactor.
|
||||||
|
|
||||||
|
// 6. List and check keys/names match
|
||||||
|
let keypairs_after = session.list_keypairs().expect("list_keypairs after").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
|
||||||
|
assert_eq!(keypairs_before, keypairs_after, "Keypairs before and after lock/unlock should match");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test(async)]
|
||||||
|
async fn test_session_manager_end_to_end() {
|
||||||
|
use kvstore::wasm::WasmStore;
|
||||||
|
use vault::{Vault, KeyType, KeyMetadata};
|
||||||
|
use vault::session::SessionManager;
|
||||||
|
let store = WasmStore::open("test-session-manager").await.unwrap();
|
||||||
|
let keyspace = "testspace";
|
||||||
|
let password = b"testpass";
|
||||||
|
|
||||||
|
// Create session manager
|
||||||
|
let mut session = SessionManager::new(Vault::new(store));
|
||||||
|
// Create and unlock keyspace in one step
|
||||||
|
session.create_keyspace(keyspace, password, None).await.expect("create_keyspace via session");
|
||||||
|
// Add keypair using session API
|
||||||
|
let key_id = session.add_keypair(Some(KeyType::Secp256k1), Some(KeyMetadata { name: Some("main".to_string()), created_at: None, tags: None })).await.expect("add_keypair via session");
|
||||||
|
|
||||||
|
// Test add_keypair with metadata via SessionManager
|
||||||
|
let meta = KeyMetadata { name: Some("user1-key".to_string()), created_at: None, tags: Some(vec!["tag1".to_string()]) };
|
||||||
|
let key_id2 = session.add_keypair(Some(KeyType::Ed25519), Some(meta.clone())).await.expect("add_keypair via session");
|
||||||
|
// List keypairs and check metadata
|
||||||
|
let keypairs = session.list_keypairs().expect("list_keypairs");
|
||||||
|
assert!(keypairs.iter().any(|k| k.id == key_id2 && k.metadata.as_ref().unwrap().name.as_deref() == Some("user1-key")), "metadata name should be present");
|
||||||
}
|
}
|
||||||
|
153
wasm/src/lib.rs
@ -1,153 +0,0 @@
|
|||||||
//! WASM entrypoint for Rhai scripting integration for the extension.
|
|
||||||
//! Composes vault and evm_client Rhai bindings and exposes a secure run_rhai API.
|
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
|
||||||
use once_cell::unsync::Lazy;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
|
|
||||||
use rhai::Engine;
|
|
||||||
use vault::session::SessionManager;
|
|
||||||
use vault::rhai_bindings as vault_rhai_bindings;
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use kvstore::wasm::WasmStore;
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
// Global singleton engine/session/client (for demonstration; production should scope per user/session)
|
|
||||||
thread_local! {
|
|
||||||
static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new()));
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
static SESSION_MANAGER: RefCell<Option<Arc<Mutex<SessionManager<kvstore::native::NativeStore>>>>> = RefCell::new(None);
|
|
||||||
static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
pub use vault::session_singleton::SESSION_MANAGER;
|
|
||||||
|
|
||||||
|
|
||||||
/// Initialize the scripting environment (must be called before run_rhai)
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn init_rhai_env() {
|
|
||||||
ENGINE.with(|engine_cell| {
|
|
||||||
let mut engine = engine_cell.borrow_mut();
|
|
||||||
// Register APIs with dummy session; will be replaced by real session after init
|
|
||||||
SESSION_MANAGER.with(|cell| {
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
if let Some(ref session) = cell.borrow().as_ref() {
|
|
||||||
vault_rhai_bindings::register_rhai_api(&mut engine, session);
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if let Some(session) = cell.borrow().as_ref() {
|
|
||||||
vault_rhai_bindings::register_rhai_api(&mut engine, session.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// TODO: Register EVM APIs with session if needed
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize session with keyspace and password
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn init_session(keyspace: &str, password: &str) -> Result<(), JsValue> {
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
use wasm_bindgen_futures::spawn_local;
|
|
||||||
use kvstore::wasm::WasmStore;
|
|
||||||
use vault::session::SessionManager;
|
|
||||||
let keyspace = keyspace.to_string();
|
|
||||||
let password_vec = password.as_bytes().to_vec();
|
|
||||||
spawn_local(async move {
|
|
||||||
match WasmStore::open(&keyspace).await {
|
|
||||||
Ok(store) => {
|
|
||||||
let vault = vault::Vault::new(store);
|
|
||||||
let mut manager = SessionManager::new(vault);
|
|
||||||
if let Err(e) = manager.unlock_keyspace(&keyspace, &password_vec).await {
|
|
||||||
web_sys::console::error_1(&format!("Failed to unlock keyspace: {e}").into());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SESSION_MANAGER.with(|cell| cell.replace(Some(manager)));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
web_sys::console::error_1(&format!("Failed to open WasmStore: {e}").into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
let store = kvstore::native::NativeStore::open("testdb").expect("open native store");
|
|
||||||
let vault = vault::Vault::new(store);
|
|
||||||
let manager = SessionManager::new(vault);
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
let arc_manager = Arc::new(Mutex::new(manager));
|
|
||||||
SESSION_MANAGER.with(|cell| cell.replace(Some(arc_manager)));
|
|
||||||
}
|
|
||||||
SESSION_PASSWORD.with(|cell| cell.replace(Some(password.as_bytes().to_vec())));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select keypair for the session
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn select_keypair(key_id: &str) -> Result<(), JsValue> {
|
|
||||||
let mut result = Err(JsValue::from_str("Session not initialized"));
|
|
||||||
SESSION_MANAGER.with(|cell| {
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
if let Some(session) = cell.borrow_mut().as_mut() {
|
|
||||||
result = session.select_keypair(key_id)
|
|
||||||
.map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}")));
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if let Some(session_arc) = cell.borrow_mut().as_mut() {
|
|
||||||
let mut session = session_arc.lock().unwrap();
|
|
||||||
result = session.select_keypair(key_id)
|
|
||||||
.map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}")));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lock the session (zeroize password and session)
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn lock_session() {
|
|
||||||
SESSION_MANAGER.with(|cell| *cell.borrow_mut() = None);
|
|
||||||
SESSION_PASSWORD.with(|cell| *cell.borrow_mut() = None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sign message with current session
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
|
||||||
let mut result: Option<Result<JsValue, JsValue>> = None;
|
|
||||||
SESSION_MANAGER.with(|cell| {
|
|
||||||
if let Some(session) = cell.borrow().as_ref() {
|
|
||||||
let password = SESSION_PASSWORD.with(|pw| pw.borrow().clone());
|
|
||||||
// ...rest of sign logic here, using session and password...
|
|
||||||
// For now, just set result = Ok(JsValue::from_str("signed")); as a placeholder
|
|
||||||
result = Some(Ok(JsValue::from_str("signed")));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
result.unwrap_or_else(|| Err(JsValue::from_str("Session not initialized")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Securely run a Rhai script in the extension context (must be called only after user approval)
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> {
|
|
||||||
ENGINE.with(|engine_cell| {
|
|
||||||
let mut engine = engine_cell.borrow_mut();
|
|
||||||
SESSION_MANAGER.with(|cell| {
|
|
||||||
if let Some(ref mut session) = cell.borrow_mut().as_mut() {
|
|
||||||
let mut scope = rhai::Scope::new();
|
|
||||||
engine.eval_with_scope::<rhai::Dynamic>(&mut scope, script)
|
|
||||||
.map(|result| JsValue::from_str(&result.to_string()))
|
|
||||||
.map_err(|e| JsValue::from_str(&format!("Rhai error: {e}")))
|
|
||||||
} else {
|
|
||||||
Err(JsValue::from_str("Session not initialized"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wasm"
|
name = "wasm_app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@ -7,11 +7,13 @@ edition = "2021"
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
kvstore = { path = "../kvstore" }
|
|
||||||
wasm-bindgen = "0.2"
|
|
||||||
gloo-utils = "0.1"
|
|
||||||
js-sys = "0.3"
|
|
||||||
web-sys = { version = "0.3", features = ["console"] }
|
web-sys = { version = "0.3", features = ["console"] }
|
||||||
|
kvstore = { path = "../kvstore" }
|
||||||
|
hex = "0.4"
|
||||||
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
|
gloo-utils = "0.1"
|
||||||
|
|
||||||
|
#
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
rhai = { version = "1.16", features = ["serde"] }
|
rhai = { version = "1.16", features = ["serde"] }
|
||||||
@ -25,4 +27,4 @@ wasm-bindgen-test = "0.3"
|
|||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] }
|
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
172
wasm_app/src/debug_bindings.rs
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
//! WASM-only debug bindings for the vault extension
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use crate::{SESSION_MANAGER, SESSION_PASSWORD};
|
||||||
|
|
||||||
|
/// Debugging function to check if keypairs can be listed
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn list_keypairs_debug() -> Result<JsValue, JsValue> {
|
||||||
|
use js_sys::{Array, Object};
|
||||||
|
use web_sys::console;
|
||||||
|
console::log_1(&"Debug listing keypairs...".into());
|
||||||
|
let session_ptr = SESSION_MANAGER.with(|cell| {
|
||||||
|
let has_session = cell.borrow().is_some();
|
||||||
|
console::log_1(&format!("Has session: {}", has_session).into());
|
||||||
|
cell.borrow().as_ref().map(|s| s as *const _)
|
||||||
|
});
|
||||||
|
let password_opt = SESSION_PASSWORD.with(|pw| {
|
||||||
|
let has_pw = pw.borrow().is_some();
|
||||||
|
console::log_1(&format!("Has password: {}", has_pw).into());
|
||||||
|
pw.borrow().clone()
|
||||||
|
});
|
||||||
|
if session_ptr.is_none() {
|
||||||
|
return Err(JsValue::from_str("Session not initialized in debug function"));
|
||||||
|
}
|
||||||
|
if password_opt.is_none() {
|
||||||
|
return Err(JsValue::from_str("Session password not set in debug function"));
|
||||||
|
}
|
||||||
|
let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = unsafe { &*session_ptr.unwrap() };
|
||||||
|
let password = password_opt.unwrap();
|
||||||
|
match session.current_keyspace_name() {
|
||||||
|
Some(ks) => {
|
||||||
|
let vault = session.get_vault();
|
||||||
|
match vault.list_keypairs(ks, &password).await {
|
||||||
|
Ok(keypairs) => {
|
||||||
|
console::log_1(&format!("Found {} keypairs", keypairs.len()).into());
|
||||||
|
let array = Array::new();
|
||||||
|
for (id, key_type) in keypairs {
|
||||||
|
let obj = Object::new();
|
||||||
|
js_sys::Reflect::set(&obj, &JsValue::from_str("id"), &JsValue::from_str(&id)).unwrap();
|
||||||
|
js_sys::Reflect::set(&obj, &JsValue::from_str("type"), &JsValue::from_str(&format!("{:?}", key_type))).unwrap();
|
||||||
|
array.push(&obj);
|
||||||
|
}
|
||||||
|
return Ok(array.into());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console::error_1(&format!("Error listing keypairs in debug function: {}", e).into());
|
||||||
|
return Err(JsValue::from_str(&format!("Error listing keypairs: {}", e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
console::error_1(&"No keyspace selected in debug function".into());
|
||||||
|
return Err(JsValue::from_str("No keyspace selected"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn check_indexeddb() -> Result<JsValue, JsValue> {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
return Err(JsValue::from_str(
|
||||||
|
"IndexedDB check only available in browser context",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
use js_sys::Object;
|
||||||
|
use kvstore::traits::KVStore;
|
||||||
|
use kvstore::wasm::WasmStore;
|
||||||
|
use web_sys::console; // Import the trait so we can use its methods
|
||||||
|
|
||||||
|
console::log_1(&"Checking IndexedDB availability...".into());
|
||||||
|
|
||||||
|
// Check if window.indexedDB is available
|
||||||
|
if js_sys::eval("typeof window.indexedDB")
|
||||||
|
.map_err(|e| {
|
||||||
|
console::error_1(&format!("Error checking IndexedDB: {:?}", e).into());
|
||||||
|
JsValue::from_str(&format!("Error checking IndexedDB: {:?}", e))
|
||||||
|
})?
|
||||||
|
.as_string()
|
||||||
|
.unwrap_or_default()
|
||||||
|
== "undefined"
|
||||||
|
{
|
||||||
|
console::error_1(&"IndexedDB is not available in this browser".into());
|
||||||
|
return Err(JsValue::from_str(
|
||||||
|
"IndexedDB is not available in this browser",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create a test database
|
||||||
|
match WasmStore::open("db_test").await {
|
||||||
|
Ok(store) => {
|
||||||
|
console::log_1(&"Successfully opened test database".into());
|
||||||
|
|
||||||
|
// Try to write and read a value to ensure it works
|
||||||
|
let test_key = "test_key";
|
||||||
|
let test_value = "test_value";
|
||||||
|
|
||||||
|
// Use the KVStore trait methods
|
||||||
|
if let Err(e) = store.set(test_key, test_value.as_bytes()).await {
|
||||||
|
console::error_1(&format!("Failed to write to IndexedDB: {}", e).into());
|
||||||
|
return Err(JsValue::from_str(&format!(
|
||||||
|
"Failed to write to IndexedDB: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value and handle the Option<Vec<u8>> properly
|
||||||
|
match store.get(test_key).await {
|
||||||
|
Ok(maybe_value) => match maybe_value {
|
||||||
|
Some(value) => {
|
||||||
|
let value_str = String::from_utf8_lossy(&value);
|
||||||
|
if value_str == test_value {
|
||||||
|
console::log_1(
|
||||||
|
&"Successfully read test value from IndexedDB".into(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console::error_1(
|
||||||
|
&format!(
|
||||||
|
"IndexedDB test value mismatch: expected {}, got {}",
|
||||||
|
test_value, value_str
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
return Err(JsValue::from_str("IndexedDB test value mismatch"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
console::error_1(&"IndexedDB test key not found after writing".into());
|
||||||
|
return Err(JsValue::from_str(
|
||||||
|
"IndexedDB test key not found after writing",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
console::error_1(&format!("Failed to read from IndexedDB: {}", e).into());
|
||||||
|
return Err(JsValue::from_str(&format!(
|
||||||
|
"Failed to read from IndexedDB: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success with the available database names
|
||||||
|
let result = Object::new();
|
||||||
|
js_sys::Reflect::set(
|
||||||
|
&result,
|
||||||
|
&JsValue::from_str("status"),
|
||||||
|
&JsValue::from_str("success"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
js_sys::Reflect::set(
|
||||||
|
&result,
|
||||||
|
&JsValue::from_str("message"),
|
||||||
|
&JsValue::from_str("IndexedDB is working properly"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return Ok(result.into());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
console::error_1(&format!("Failed to open IndexedDB test database: {}", e).into());
|
||||||
|
return Err(JsValue::from_str(&format!(
|
||||||
|
"Failed to open test database: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
wasm_app/src/lib.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
//! WASM entrypoint for Rhai scripting integration for the extension.
|
||||||
|
//! Composes vault and evm_client Rhai bindings and exposes a secure run_rhai API.
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
|
use once_cell::unsync::Lazy;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
use rhai::Engine;
|
||||||
|
use vault::rhai_bindings as vault_rhai_bindings;
|
||||||
|
use vault::session::SessionManager;
|
||||||
|
|
||||||
|
use kvstore::wasm::WasmStore;
|
||||||
|
|
||||||
|
// Global singleton engine/session/client (for demonstration; production should scope per user/session)
|
||||||
|
thread_local! {
|
||||||
|
static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new()));
|
||||||
|
static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use vault::session_singleton::SESSION_MANAGER;
|
||||||
|
|
||||||
|
// Include the keypair bindings module
|
||||||
|
mod vault_bindings;
|
||||||
|
pub use vault_bindings::*;
|
||||||
|
|
||||||
|
/// Initialize the scripting environment (must be called before run_rhai)
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init_rhai_env() {
|
||||||
|
ENGINE.with(|engine_cell| {
|
||||||
|
let mut engine = engine_cell.borrow_mut();
|
||||||
|
// Register APIs with dummy session; will be replaced by real session after init
|
||||||
|
SESSION_MANAGER.with(|cell| {
|
||||||
|
if let Some(ref session) = cell.borrow().as_ref() {
|
||||||
|
vault_rhai_bindings::register_rhai_api::<WasmStore>(&mut engine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_rhai(script: &str) -> Result<JsValue, JsValue> {
|
||||||
|
ENGINE.with(|engine_cell| {
|
||||||
|
let mut engine = engine_cell.borrow_mut();
|
||||||
|
SESSION_MANAGER.with(|cell| {
|
||||||
|
if let Some(ref mut session) = cell.borrow_mut().as_mut() {
|
||||||
|
let mut scope = rhai::Scope::new();
|
||||||
|
engine
|
||||||
|
.eval_with_scope::<rhai::Dynamic>(&mut scope, script)
|
||||||
|
.map(|res| JsValue::from_str(&format!("{:?}", res)))
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("{}", e)))
|
||||||
|
} else {
|
||||||
|
Err(JsValue::from_str("Session not initialized"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wasm_helpers {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Global function to select keypair (used in Rhai)
|
||||||
|
pub fn select_keypair_global(key_id: &str) -> Result<(), String> {
|
||||||
|
SESSION_MANAGER.with(|cell| {
|
||||||
|
if let Some(session) = cell.borrow_mut().as_mut() {
|
||||||
|
session
|
||||||
|
.select_keypair(key_id)
|
||||||
|
.map_err(|e| format!("select_keypair error: {e}"))
|
||||||
|
} else {
|
||||||
|
Err("Session not initialized".to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
185
wasm_app/src/vault_bindings.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
//! WebAssembly bindings for accessing vault operations (session, keypairs, signing, scripting, etc)
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
|
||||||
|
use kvstore::wasm::WasmStore;
|
||||||
|
use once_cell::unsync::Lazy;
|
||||||
|
use rhai::Engine;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use vault::rhai_bindings as vault_rhai_bindings;
|
||||||
|
use vault::session::SessionManager;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new()));
|
||||||
|
static SESSION_PASSWORD: RefCell<Option<Vec<u8>>> = RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use vault::session_singleton::SESSION_MANAGER;
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Session Lifecycle
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
/// Initialize session with keyspace and password
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn init_session(keyspace: &str, password: &str) -> Result<(), JsValue> {
|
||||||
|
let keyspace = keyspace.to_string();
|
||||||
|
let password_vec = password.as_bytes().to_vec();
|
||||||
|
match WasmStore::open(&keyspace).await {
|
||||||
|
Ok(store) => {
|
||||||
|
let vault = vault::Vault::new(store);
|
||||||
|
let mut manager = SessionManager::new(vault);
|
||||||
|
match manager.unlock_keyspace(&keyspace, &password_vec).await {
|
||||||
|
Ok(_) => {
|
||||||
|
SESSION_MANAGER.with(|cell| cell.replace(Some(manager)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::error_1(&format!("Failed to unlock keyspace: {e}").into());
|
||||||
|
return Err(JsValue::from_str(&format!("Failed to unlock keyspace: {e}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
web_sys::console::error_1(&format!("Failed to open WasmStore: {e}").into());
|
||||||
|
return Err(JsValue::from_str(&format!("Failed to open WasmStore: {e}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SESSION_PASSWORD.with(|cell| cell.replace(Some(password.as_bytes().to_vec())));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Lock the session (zeroize password and session)
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn lock_session() {
|
||||||
|
SESSION_MANAGER.with(|cell| *cell.borrow_mut() = None);
|
||||||
|
SESSION_PASSWORD.with(|cell| *cell.borrow_mut() = None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =====================
|
||||||
|
// Keypair Management
|
||||||
|
// =====================
|
||||||
|
|
||||||
|
/// Get all keypairs from the current session
|
||||||
|
/// Returns an array of keypair objects with id, type, and metadata
|
||||||
|
// #[wasm_bindgen]
|
||||||
|
// pub async fn list_keypairs() -> Result<JsValue, JsValue> {
|
||||||
|
// // [Function body commented out to resolve duplicate symbol error]
|
||||||
|
// // (Original implementation moved to keypair_bindings.rs)
|
||||||
|
// unreachable!("This function is disabled. Use the export from keypair_bindings.rs.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// [Function body commented out to resolve duplicate symbol error]
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
/// Select keypair for the session
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn select_keypair(key_id: &str) -> Result<(), JsValue> {
|
||||||
|
let mut result = Err(JsValue::from_str("Session not initialized"));
|
||||||
|
SESSION_MANAGER.with(|cell| {
|
||||||
|
if let Some(session) = cell.borrow_mut().as_mut() {
|
||||||
|
result = session
|
||||||
|
.select_keypair(key_id)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("select_keypair error: {e}")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// List keypairs in the current session's keyspace
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn list_keypairs() -> Result<JsValue, JsValue> {
|
||||||
|
SESSION_MANAGER.with(|cell| {
|
||||||
|
if let Some(session) = cell.borrow().as_ref() {
|
||||||
|
if let Some(keyspace) = session.current_keyspace() {
|
||||||
|
let keypairs = &keyspace.keypairs;
|
||||||
|
serde_json::to_string(keypairs)
|
||||||
|
.map(|s| JsValue::from_str(&s))
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
|
||||||
|
} else {
|
||||||
|
Err(JsValue::from_str("No keyspace unlocked"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(JsValue::from_str("Session not initialized"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a keypair to the current keyspace
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn add_keypair(
|
||||||
|
key_type: Option<String>,
|
||||||
|
metadata: Option<String>,
|
||||||
|
) -> Result<JsValue, JsValue> {
|
||||||
|
use vault::{KeyMetadata, KeyType};
|
||||||
|
let password = SESSION_PASSWORD
|
||||||
|
.with(|pw| pw.borrow().clone())
|
||||||
|
.ok_or_else(|| JsValue::from_str("Session password not set"))?;
|
||||||
|
let (keyspace_name, session_exists) = SESSION_MANAGER.with(|cell| {
|
||||||
|
if let Some(ref session) = cell.borrow().as_ref() {
|
||||||
|
let keyspace_name = session.current_keyspace().map(|_| "".to_string()); // TODO: replace with actual keyspace name if available;
|
||||||
|
(keyspace_name, true)
|
||||||
|
} else {
|
||||||
|
(None, false)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let keyspace_name = keyspace_name.ok_or_else(|| JsValue::from_str("No keyspace selected"))?;
|
||||||
|
if !session_exists {
|
||||||
|
return Err(JsValue::from_str("Session not initialized"));
|
||||||
|
}
|
||||||
|
let key_type = key_type
|
||||||
|
.as_deref()
|
||||||
|
.map(|s| match s {
|
||||||
|
"Ed25519" => KeyType::Ed25519,
|
||||||
|
"Secp256k1" => KeyType::Secp256k1,
|
||||||
|
_ => KeyType::Secp256k1,
|
||||||
|
})
|
||||||
|
.unwrap_or(KeyType::Secp256k1);
|
||||||
|
let metadata = match metadata {
|
||||||
|
Some(ref meta_str) => Some(
|
||||||
|
serde_json::from_str::<KeyMetadata>(meta_str)
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {e}")))?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
// Take session out, do async work, then put it back
|
||||||
|
let mut session_opt = SESSION_MANAGER.with(|cell| cell.borrow_mut().take());
|
||||||
|
let session = session_opt.as_mut().ok_or_else(|| JsValue::from_str("Session not initialized"))?;
|
||||||
|
let key_id = session
|
||||||
|
.get_vault_mut()
|
||||||
|
.add_keypair(&keyspace_name, &password, Some(key_type), metadata)
|
||||||
|
.await
|
||||||
|
.map_err(|e| JsValue::from_str(&format!("add_keypair error: {e}")))?;
|
||||||
|
// Put session back
|
||||||
|
SESSION_MANAGER.with(|cell| *cell.borrow_mut() = Some(session_opt.take().unwrap()));
|
||||||
|
Ok(JsValue::from_str(&key_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign message with current session
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
||||||
|
{
|
||||||
|
// SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope.
|
||||||
|
let session_ptr =
|
||||||
|
SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _));
|
||||||
|
let password_opt = SESSION_PASSWORD.with(|pw| pw.borrow().clone());
|
||||||
|
let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr {
|
||||||
|
Some(ptr) => unsafe { &*ptr },
|
||||||
|
None => return Err(JsValue::from_str("Session not initialized")),
|
||||||
|
};
|
||||||
|
let password = match password_opt {
|
||||||
|
Some(p) => p,
|
||||||
|
None => return Err(JsValue::from_str("Session password not set")),
|
||||||
|
};
|
||||||
|
match session.sign(message).await {
|
||||||
|
Ok(sig_bytes) => {
|
||||||
|
let hex_sig = hex::encode(&sig_bytes);
|
||||||
|
Ok(JsValue::from_str(&hex_sig))
|
||||||
|
}
|
||||||
|
Err(e) => Err(JsValue::from_str(&format!("Sign error: {e}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|