Compare commits
10 Commits
09635f3a32
...
ed76ba3d8d
Author | SHA1 | Date | |
---|---|---|---|
|
ed76ba3d8d | ||
13945a8725 | |||
19f46d6edb | |||
85a15edaec | |||
017fc897f4 | |||
03533f9216 | |||
73233ec69b | |||
791752c3a5 | |||
cea2d7e655 | |||
7d7f94f114 |
2
.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ["--cfg", 'getrandom_backend="wasm_js"']
|
4
.gitignore
vendored
@ -5,3 +5,7 @@
|
|||||||
# Ignore IDE files
|
# Ignore IDE files
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Ignore test databases
|
||||||
|
/vault/vault_native_test/
|
||||||
|
node_modules/
|
@ -3,5 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"kvstore",
|
"kvstore",
|
||||||
"vault",
|
"vault",
|
||||||
"evm_client"
|
"evm_client",
|
||||||
|
"wasm_app",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
33
Makefile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Makefile to run all browser (WASM) tests for kvstore, vault, and evm_client
|
||||||
|
|
||||||
|
BROWSER ?= firefox
|
||||||
|
|
||||||
|
.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
|
||||||
|
|
||||||
|
# Run browser tests for kvstore
|
||||||
|
|
||||||
|
test-browser-kvstore:
|
||||||
|
cd kvstore && wasm-pack test --headless --$(BROWSER)
|
||||||
|
|
||||||
|
# Run browser tests for vault
|
||||||
|
|
||||||
|
test-browser-vault:
|
||||||
|
cd vault && wasm-pack test --headless --$(BROWSER)
|
||||||
|
|
||||||
|
# Run browser tests for evm_client
|
||||||
|
|
||||||
|
test-browser-evm-client:
|
||||||
|
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
|
||||||
|
|
161
README.md
@ -1,24 +1,80 @@
|
|||||||
# Modular Rust System: Key-Value Store, Vault, and EVM Client
|
# Modular Rust System: Key-Value Store, Vault, and EVM Client
|
||||||
|
|
||||||
This repository implements a modular, async, and cross-platform cryptographic stack in Rust. It is designed for use in both native (desktop/server) and WASM (browser) environments, supporting secure storage, cryptographic operations, and EVM (Ethereum) client functionality.
|
A modular, async, and cross-platform cryptographic stack in Rust. Built for both native (desktop/server) and WASM (browser) environments, this system provides secure storage, cryptographic operations, and Ethereum (EVM) client functionality—all with a focus on extensibility, testability, and scripting.
|
||||||
|
|
||||||
## Crate Overview
|
## Crate Overview
|
||||||
|
|
||||||
- **kvstore/**: Async key-value store trait and implementations (native: `sled`, WASM: IndexedDB).
|
- **kvstore/**: Async key-value store trait and implementations (native: `sled`, WASM: IndexedDB)
|
||||||
- **vault/**: Cryptographic vault for managing encrypted keyspaces and key operations. Uses `kvstore` for persistence.
|
- **vault/**: Cryptographic vault for encrypted keyspaces, key management, and signing; uses `kvstore` for persistence
|
||||||
- **evm_client/**: EVM RPC client, integrates with `vault` for signing and secure key management.
|
- **evm_client/**: Async EVM RPC client, integrates with `vault` for secure signing; supports trait-based signers and modular providers
|
||||||
- **cli_app/**: (Planned) Command-line interface for scripting and automation.
|
- **cli_app/** _(planned)_: Command-line interface for scripting, automation, and Rhai scripting
|
||||||
- **web_app/**: (Planned) WASM web app exposing the same APIs to JavaScript or browser scripting.
|
- **web_app/** _(planned)_: WASM web app exposing APIs to JavaScript/browser scripting
|
||||||
|
- **wasm/** _(planned)_: WebAssembly module for browser/extension integration
|
||||||
|
- **browser_extension/** _(planned)_: Browser extension for secure scripting and automation
|
||||||
|
- **rhai scripting** _(planned)_: Unified scripting API for both CLI and browser (see [`docs/rhai_architecture_plan.md`](docs/rhai_architecture_plan.md))
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
> **Note:** Some directories are planned for future extensibility and scripting, and may not exist yet in the current workspace.
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── kvstore/ # Key-value store trait and backends
|
||||||
|
├── vault/ # Cryptographic vault (shared core)
|
||||||
|
├── evm_client/ # EVM RPC client (shared core)
|
||||||
|
├── cli_app/ # Command-line tool for Rhai scripts (planned)
|
||||||
|
├── web_app/ # WASM web app exposing APIs (planned)
|
||||||
|
├── wasm/ # WebAssembly module for browser/extension (planned)
|
||||||
|
├── browser_extension/ # Extension source (planned)
|
||||||
|
├── docs/ # Architecture & usage docs
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture Highlights
|
## Architecture Highlights
|
||||||
- **Async everywhere:** All APIs are async and runtime-agnostic.
|
- **Modular and async:** All APIs are async and runtime-agnostic (works with both native and WASM targets)
|
||||||
- **Conditional backends:** Uses Cargo features and `cfg` to select the appropriate backend for each environment.
|
- **Conditional backends:** Uses Cargo features and `cfg` for platform-specific storage/networking
|
||||||
- **Secure by design:** Vault encrypts all key material at rest and leverages modern cryptography.
|
- **Secure by design:** Vault encrypts all key material at rest using modern cryptography
|
||||||
- **Tested natively and in browser:** WASM and native backends are both covered by tests.
|
- **Extensible:** Trait-based APIs for signers and providers, ready for scripting and new integrations
|
||||||
|
- **Tested everywhere:** Native and browser (WASM) backends are covered by automated tests and a unified Makefile
|
||||||
|
|
||||||
## Building and Testing
|
## Building and Testing
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
- Rust (latest stable recommended)
|
||||||
|
- wasm-pack (for browser tests)
|
||||||
|
- Firefox or Chrome (for browser testing)
|
||||||
|
|
||||||
|
### Native Build & Test
|
||||||
|
```sh
|
||||||
|
cargo build
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser (WASM) Tests
|
||||||
|
Run all browser tests for all modules:
|
||||||
|
```sh
|
||||||
|
make test-browser-all
|
||||||
|
```
|
||||||
|
Or run for a specific module:
|
||||||
|
```sh
|
||||||
|
make test-browser-kvstore
|
||||||
|
make test-browser-vault
|
||||||
|
make test-browser-evm-client
|
||||||
|
```
|
||||||
|
Set `BROWSER=chrome` to use Chrome instead of Firefox.
|
||||||
|
|
||||||
|
## Scripting & Extensibility
|
||||||
|
- **Rhai scripting**: The architecture is ready for ergonomic scripting via Rhai, both in CLI and browser (see [`docs/rhai_architecture_plan.md`](docs/rhai_architecture_plan.md))
|
||||||
|
- **Browser extension**: Planned support for browser extension scripting and secure key usage
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [Architecture Overview](docs/architecture.md)
|
||||||
|
- [EVM Client Plan](docs/evm_client_architecture_plan.md)
|
||||||
|
- [Rhai Scripting Plan](docs/rhai_architecture_plan.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For questions, contributions, or more details, see the architecture docs or open an issue!
|
||||||
|
|
||||||
- Rust (latest stable recommended)
|
- Rust (latest stable recommended)
|
||||||
- For WASM: `wasm-pack`, Firefox or Chrome (for browser tests)
|
- For WASM: `wasm-pack`, Firefox or Chrome (for browser tests)
|
||||||
|
|
||||||
@ -33,25 +89,92 @@ cd kvstore
|
|||||||
wasm-pack test --headless --firefox --features web
|
wasm-pack test --headless --firefox --features web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Rhai Scripting System
|
||||||
|
|
||||||
|
A unified system for writing and executing [Rhai](https://rhai.rs/) scripts, powered by shared Rust core logic. Supports both local CLI execution and secure browser extension use, with the same business logic compiled to WebAssembly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Goals
|
||||||
|
- **Write and run Rhai scripts** both locally (CLI) and in the browser (extension).
|
||||||
|
- **Reuse the same Rust core logic** (vault, evm_client) across all platforms.
|
||||||
|
- **Sandboxed, secure script execution** in both native and WASM environments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
- **Shared Rust Crates:**
|
||||||
|
- `vault/` and `evm_client/` implement business logic and expose APIs to Rhai.
|
||||||
|
- All logic is reusable in both native and WASM builds.
|
||||||
|
- **CLI Tool (`cli/`):**
|
||||||
|
- Runs Rhai scripts from files or stdin using the shared core.
|
||||||
|
- Outputs results to the terminal.
|
||||||
|
- **WebAssembly Module (`wasm/`):**
|
||||||
|
- Exposes `run_rhai(script: &str) -> String` via `wasm-bindgen`.
|
||||||
|
- Usable from browser JS and the extension.
|
||||||
|
- **Browser Extension (`browser_extension/`):**
|
||||||
|
- UI for entering and running Rhai scripts securely in the browser.
|
||||||
|
- Loads the WASM module and displays results.
|
||||||
|
- **Web App Integration:**
|
||||||
|
- Trusted web apps can send scripts to the extension for execution (via postMessage or WebSocket, with strict origin checks).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
```
|
||||||
|
sal-cli run my_script.rhai
|
||||||
|
# or
|
||||||
|
cat my_script.rhai | sal-cli run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browser/Extension
|
||||||
|
- Enter Rhai code in the extension popup or trusted website.
|
||||||
|
- Extension loads the WASM module and calls `run_rhai(script)`.
|
||||||
|
- Result is displayed in the UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
- All script execution is sandboxed via Rhai + WASM.
|
||||||
|
- Only accepts input from:
|
||||||
|
- Extension popup UI
|
||||||
|
- Approved websites (via content script)
|
||||||
|
- Trusted backend server (if using WebSocket)
|
||||||
|
- Strict origin and input validation.
|
||||||
|
- No internal APIs exposed beyond `run_rhai(script)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── kvstore/ # Key-value store trait and backends
|
├── kvstore/ # Key-value store trait and backends
|
||||||
├── vault/ # Cryptographic vault
|
├── vault/ # Cryptographic vault (shared core)
|
||||||
├── evm_client/ # EVM RPC client
|
├── evm_client/ # EVM RPC client (shared core)
|
||||||
├── cli_app/ # CLI (planned)
|
├── cli/ # Command-line tool for Rhai scripts
|
||||||
├── web_app/ # Web app (planned)
|
├── wasm/ # WebAssembly module for browser/extension
|
||||||
├── docs/ # Architecture docs
|
├── browser_extension/ # Extension source
|
||||||
|
├── docs/ # Architecture & usage docs
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
- [x] Unified async trait for key-value storage
|
- [x] Unified async trait for key-value storage
|
||||||
- [x] Native and WASM backends for kvstore
|
- [x] Native and WASM backends for kvstore
|
||||||
- [ ] Cryptographic vault with password-protected keyspace
|
- [x] Shared Rust core for vault and evm_client
|
||||||
- [ ] EVM client with vault integration
|
- [ ] WASM module exposing `run_rhai`
|
||||||
- [ ] CLI and web app targets
|
- [ ] CLI tool for local Rhai script execution
|
||||||
- [ ] Full end-to-end integration
|
- [ ] Browser extension for secure script execution
|
||||||
|
- [ ] Web app integration (postMessage/WebSocket)
|
||||||
|
- [ ] Full end-to-end integration and security review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
MIT OR Apache-2.0
|
MIT OR Apache-2.0
|
||||||
|
|
||||||
|
@ -1,384 +0,0 @@
|
|||||||
# Architecture and Implementation Plan for the Rust Modular System
|
|
||||||
|
|
||||||
The system is organized into three core Rust crates (`kvstore`, `vault`, `evm_client`) plus two front‐end targets (a CLI and a WASM web app). The **`kvstore`** crate defines an async `KVStore` trait and provides two implementations: on native platforms it uses **sled**, while in WASM/browser it uses IndexedDB via the `idb` crate (selected by Cargo feature flags or `#[cfg(target_arch = "wasm32")]`). For example, Wire’s core-crypto keystore uses IndexedDB with AES-GCM for WASM and SQLCipher on native platforms. The **`vault`** crate manages an encrypted keyspace of multiple keypairs (password-protected), performing cryptographic operations (sign/verify, sym/asym encryption) and persisting data through `kvstore`. The **`evm_client`** crate handles EVM RPC calls (using `alloy`), depending on `vault` to sign transactions with stored keys. A Rust **CLI** binary ties these together with a Rhai scripting engine: Rhai scripts invoke async APIs via a message-passing pattern. The **browser target** compiles to Wasm (with `wasm-bindgen`); it exposes the same APIs to JavaScript or to Rhai compiled for Wasm.
|
|
||||||
|
|
||||||
## Crate and Module Structure
|
|
||||||
|
|
||||||
* **Cargo workspace**: top-level `Cargo.toml` lists members `kvstore/`, `vault/`, `evm_client/`, `cli_app/`, `web_app/`. Common dev-dependencies and CI config are shared at the workspace root.
|
|
||||||
* **Features & cfg**: In `kvstore`, define Cargo features or use `#[cfg]` to toggle backends. E.g. `cfg(not(target_arch = "wasm32"))` for sled, and `cfg(target_arch = "wasm32")` for IndexedDB. Use `async_trait` for the `KVStore` trait so implementations can be async. Similar conditional compilation applies to any platform-specific code (e.g. using WebCrypto APIs only under WASM).
|
|
||||||
* **Dependencies**:
|
|
||||||
|
|
||||||
* `kvstore` depends on `sled` (native) and `idb` (WASM), and defines `async fn` methods. Blocking DB calls (sled) must be offloaded via a `spawn_blocking` provided by the caller.
|
|
||||||
* `vault` depends on `kvstore` and various crypto crates (e.g. `aes-gcm` or `chacha20poly1305` for symmetric encryption; `k256`/`rust-crypto` for signatures). For WASM compatibility, ensure chosen crypto crates support `wasm32-unknown-unknown`. Keys are encrypted at rest with a password-derived key (AES-256-GCM or similar).
|
|
||||||
* `evm_client` depends on `vault` (for signing) and an Ethereum library (e.g. `alloy` with an async HTTP provider). On WASM, use `wasm-bindgen-futures` to call JavaScript fetch or use a crate like `reqwest` with the `wasm` feature.
|
|
||||||
* The **CLI** (binary) depends on Rhai (`rhai` crate), `tokio` or similar for async execution, and the above libraries. It sets up an async runtime (e.g. Tokio) to run tasks.
|
|
||||||
* The **web\_app** (WASM target) depends on `wasm-bindgen`/`wasm-bindgen-futures` and `vault`/`evm_client`. It uses `wasm-bindgen` to expose Rust functions to JS. Rhai can also be compiled to WASM for scripting in-browser, but must be integrated via the same message-passing pattern (see below).
|
|
||||||
|
|
||||||
## `kvstore` Crate Design
|
|
||||||
|
|
||||||
The `kvstore` crate defines:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait KVStore {
|
|
||||||
async fn get(&self, key: &str) -> Option<Vec<u8>>;
|
|
||||||
async fn put(&self, key: &str, value: &[u8]) -> ();
|
|
||||||
async fn delete(&self, key: &str) -> ();
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
It then provides two modules implementing this trait:
|
|
||||||
|
|
||||||
* **Native backend (sled)**: A wrapper around `sled::Db`. Since `sled` I/O is blocking, each call should be executed in a blocking context (e.g. using `tokio::task::spawn_blocking`) so as not to block the async runtime.
|
|
||||||
* **WASM/browser backend (IndexedDB)**: Uses the `idb` crate (or `web-sys`/`gloo`) to store data in the browser’s IndexedDB. This implementation is inherently async (Promise-based) and works in `wasm32-unknown-unknown`. On compilation, one can use Cargo features like `default-features = false` and `features = ["native", "wasm"]`, or simply `#[cfg]` to select the correct backend.
|
|
||||||
|
|
||||||
Citing best practice: the pattern of having an encrypted keystore use IndexedDB on WASM is standard (e.g. Wire’s core-crypto keystore). We will mirror that by encrypting data before `put`-ting it. The `kvstore` implementation will automatically be runtime-agnostic (using only `std::future::Future` in its APIs).
|
|
||||||
|
|
||||||
## `vault` Crate Design
|
|
||||||
|
|
||||||
The `vault` crate implements a WebAssembly-compatible cryptographic keystore. It manages:
|
|
||||||
|
|
||||||
* **Encrypted keyspace**: A password protects all key material. On open, derive an encryption key (e.g. via scrypt or PBKDF2) and decrypt the stored vault (a blob in `kvstore`). Inside, multiple keypairs (e.g. Ethereum secp256k1 keys, Ed25519 keys, etc.) are stored.
|
|
||||||
* **Crypto APIs**: Expose async functions to create new keys, list keys, and to perform crypto operations: e.g. `async fn sign_transaction(&self, key_id: &str, tx: &Transaction) -> Signature`, `async fn verify(&self, ...) -> bool`, `async fn encrypt(&self, ...)->Ciphertext`, etc.
|
|
||||||
* **Storage**: Internally uses the `kvstore::KVStore` trait to persist the encrypted vault. For example, on each change, it re-encrypts the whole keyspace and `put`s it under a fixed ID key.
|
|
||||||
* **WASM Compatibility**: All operations must compile to Wasm. Use Rust crypto crates compatible with `no_std`/WASM (e.g. `aes-gcm`, `k256`, `rand_core` with `getrandom` support). Alternatively, one could use the browser’s WebCrypto via `wasm-bindgen` for symmetric operations, but for simplicity we can rely on Rust crates (AES-GCM implementations that compile to WASM).
|
|
||||||
|
|
||||||
Internally, `vault` ensures all operations return `Future`s. It will not assume any particular async runtime – for example, file I/O or crypto is fast in memory, but if any blocking work is needed (like PBKDF2 hashing), it should be done via a provided `spawn_blocking` (as recommended by forum answers). On WASM, such heavy work would yield to the JS event loop via `spawn_local` (see below).
|
|
||||||
|
|
||||||
When open, `vault` authenticates the user’s password, loads (via `kvstore`) the encrypted blob of keys, and allows operations. Fig. above illustrates a cryptographic network: keys stored securely (vault) are used for signing on behalf of the user. Internally, best practice is to use an authenticated cipher (e.g. AES-256-GCM) with a strong KDF, as noted in existing systems.
|
|
||||||
|
|
||||||
## `evm_client` Crate Design
|
|
||||||
|
|
||||||
The `evm_client` crate provides async interfaces to interact with an EVM blockchain:
|
|
||||||
|
|
||||||
* **Dependencies**: It uses the `alloy` crate for building transactions, ABI encoding, and an async HTTP provider for RPC calls.
|
|
||||||
* **Signing**: It calls into `vault` when a transaction must be signed. For example, `evm_client.sign_and_send(tx)` will invoke `vault.sign(key_id, tx_bytes)` to get a signature.
|
|
||||||
* **Async RPC**: All RPC calls (`eth_sendRawTransaction`, `eth_call`, etc.) are `async fn`s returning `Future`s. These futures must be runtime-agnostic: they use standard `async/await` and do not tie to Tokio specifically. For HTTP, on native targets use `reqwest` with Tokio, while on WASM use `reqwest` with its `wasm` feature or `gloo-net` with `wasm-bindgen-futures`.
|
|
||||||
* **Configuration**: Provide a flexible config (e.g. chain ID, gas price options) via plain structs. Errors should use a common error enum or `thiserror` crate.
|
|
||||||
* **Features**: Could have a feature flag to choose between `alloy` and `ethers`. Both are fully async.
|
|
||||||
|
|
||||||
The `evm_client` crate itself should be purely async and not block. It will typically run on Tokio in the CLI, and on the browser’s single-threaded event loop with `spawn_local` in the web app.
|
|
||||||
|
|
||||||
## CLI Binary (Rhai Scripting)
|
|
||||||
|
|
||||||
The CLI binary (`cli_app`) binds everything with a user interface. Its design:
|
|
||||||
|
|
||||||
* **Command loop**: On startup it spawns a Rhai `Engine` in a separate OS thread. The main thread runs a Tokio async runtime (or other) to handle network and I/O.
|
|
||||||
* **Message-passing**: Use two MPSC channels: one for messages *to* the engine, and one for replies *from* the engine. According to Rhai’s multi-threaded pattern, we register API functions in the engine that send commands via channel to the main thread. The main thread processes commands (e.g. “sign this tx”, “send transaction”, etc.) using `vault`/`evm_client`, then sends back results.
|
|
||||||
* **Blocking calls**: In Rhai, all calls are blocking from the script’s perspective. Under the hood, the registered API calls serialize the request (e.g. to JSON) and send it on the command channel. The Rhai engine will block until a reply arrives on the reply channel. This pattern ensures the script can call async Rust code seamlessly (step 6–8 in Rhai docs).
|
|
||||||
* **Example flow**: A Rhai script calls `let res = send_tx(data)`. The `send_tx` function (registered in the engine) captures the channel handles, packages `data` into a message, and sends it. The engine thread blocks. The main thread’s async runtime reads the message, calls `evm_client.send_transaction(data).await`, then sends the result back. The Rhai engine thread receives it and returns it to the script.
|
|
||||||
|
|
||||||
This design follows Rhai’s recommended “blocking/async” pattern. It keeps the library usage runtime-agnostic, while allowing user-defined scripts to trigger asynchronous operations.
|
|
||||||
|
|
||||||
## Browser Application (WASM)
|
|
||||||
|
|
||||||
The browser target (`web_app`) is compiled with `wasm-bindgen` to Wasm. It provides the same core functionality via a JS API (or Rhai in WASM). Key points:
|
|
||||||
|
|
||||||
* **Exports**: Use `#[wasm_bindgen]` to expose async functions to JavaScript. For example, expose `async fn create_key(name: String) -> JsValue` that returns a JavaScript `Promise`. The `wasm-bindgen-futures` crate will convert Rust `Future`s into JS Promises automatically.
|
|
||||||
* **Async runtime**: WebAssembly runs on the browser’s single thread. To perform async Rust code, we use `wasm_bindgen_futures::spawn_local` to drive futures on the JS event loop. For example, in an exported function we might do `spawn_local(async move { /* call vault, evm_client */ })`. According to docs, `spawn_local` “runs a Rust `Future` on the current thread” and schedules it as a microtask. This lets our async functions execute to completion without blocking the event loop.
|
|
||||||
* **Promises and interop**: Return types must be `JsValue` or types convertible by `wasm_bindgen`. Complex data (e.g. byte arrays) can be passed as `Uint8Array` or encoded (e.g. hex).
|
|
||||||
* **Rhai in WASM**: Optionally, we can compile Rhai to WebAssembly as well. In that case, we would run the Rhai engine in a WebWorker (since WASM threads are limited) and use `MessageChannel` for communication. The same message-passing pattern applies: a script call in the worker posts a message to the main thread with request data, and awaits a message back. The main thread (browser UI) handles the request using the exposed Rust APIs. This is analogous to the CLI pattern but using Web APIs. (Implementation note: enabling threading in WASM requires `wasm-bindgen` with the `--target bundler` or using `web-sys` `Worker` APIs.)
|
|
||||||
* **Integration tips**: Use the `wasm-bindgen` guide to share data types (strings, structs) between JS and Rust. For async tests, `wasm-bindgen-futures` has examples.
|
|
||||||
|
|
||||||
In summary, the web app compiles the same crates to Wasm and exposes them. The figure above (a network on a globe) conceptually represents the global connectivity: the browser connects to EVM nodes via WebAssembly modules, invoking Rust code. All async boundaries are handled with `spawn_local` and JS Promises (as `wasm-bindgen-futures` outlines).
|
|
||||||
|
|
||||||
## Async and Runtime-Agnostic Best Practices
|
|
||||||
|
|
||||||
Throughout all crates we adhere to runtime-agnostic async principles:
|
|
||||||
|
|
||||||
* **Use `std::future::Future`** in public APIs, not a specific runtime’s types. Internally, any async work (I/O, network) should be done with `async/await`.
|
|
||||||
* **Feature-gate runtime-specific code**: If we need to call `tokio::spawn` or `async-std`, isolate that behind `#[cfg(feature = "tokio")]` or similar. Initially, one can pick one runtime (e.g. Tokio) and make the library depend on it, then add cfg-features later.
|
|
||||||
* **Blocking calls**: Any blocking work (file I/O, heavy crypto) is executed via a passed-in executor (e.g. require a `spawn_blocking: Fn(Box<dyn FnOnce() + Send>)` callback), as recommended by Rust forum advice. This way the library never forces a specific thread pool. For example, in `kvstore`’s sled backend, all operations are done in `spawn_blocking`.
|
|
||||||
* **Testing**: Include tests for both native and WASM targets (using `wasm-pack test` or headless browser tests) to catch platform differences.
|
|
||||||
* **Error handling**: Use `Result` types, with a shared error enum. Avoid panic paths – return errors across FFI boundaries.
|
|
||||||
|
|
||||||
By decoupling logic from the runtime (using channels for Rhai, spawn\_local for WASM, cfg-features for backends), the libraries remain flexible. As one Rust discussion notes, “using `cfg(feature = "...")` to isolate the pieces that have to be runtime specific” is key. We ensure all public async APIs are `async fn` so they can be `await`ed in any context.
|
|
||||||
|
|
||||||
## Workspace Layout and Features
|
|
||||||
|
|
||||||
The recommended workspace layout is:
|
|
||||||
|
|
||||||
```
|
|
||||||
/Cargo.toml # workspace manifest
|
|
||||||
/kvstore/Cargo.toml # kvstore crate
|
|
||||||
/vault/Cargo.toml # vault crate
|
|
||||||
/evm_client/Cargo.toml
|
|
||||||
/cli_app/Cargo.toml # binary (depends on kvstore, vault, evm_client, rhai)
|
|
||||||
/web_app/Cargo.toml # cdylib (wasm) crate (depends on kvstore, vault, evm_client, wasm-bindgen)
|
|
||||||
```
|
|
||||||
|
|
||||||
Each crate’s `Cargo.toml` lists its dependencies. For `kvstore`, an example feature setup:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[features]
|
|
||||||
default = ["native"]
|
|
||||||
native = ["sled"]
|
|
||||||
web = ["idb"]
|
|
||||||
```
|
|
||||||
|
|
||||||
In code:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[cfg(feature = "native")]
|
|
||||||
mod sled_backend;
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
mod indexeddb_backend;
|
|
||||||
```
|
|
||||||
|
|
||||||
One could also omit features and just use `#[cfg(target_arch = "wasm32")]` for the web backend. The `wasm-bindgen` crate is included under the `web_app` for browser integration.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This plan lays out a clear, modular architecture. Diagrams (above) conceptually show how the crates interact: `kvstore` underlies `vault`, which together support `evm_client`; the CLI and WASM targets invoke them asynchronously. We use message-passing (channels) to bridge Rhai scripts with async Rust code, and `spawn_local` in the browser to schedule futures. By following Rust async best practices (runtime-agnostic Futures, careful use of `cfg` and spawn-blocking) and wasm-bindgen conventions, the system will work seamlessly both on the desktop/CLI and in the browser.
|
|
||||||
|
|
||||||
**Sources:** Concepts and patterns are drawn from Rust async and WASM guidelines. For example, using IndexedDB with AES-GCM in WASM keystores is inspired by existing systems. These sources guided the design of a flexible, secure architecture.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🔐 `kvstore` Crate: Pluggable Key-Value Storage Layer
|
|
||||||
|
|
||||||
**Purpose**: Provide an abstraction for key-value storage with async-compatible traits, supporting both native and WASM environments.
|
|
||||||
|
|
||||||
### Public API
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait KVStore {
|
|
||||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, KVError>;
|
|
||||||
async fn set(&self, key: &str, value: &[u8]) -> Result<(), KVError>;
|
|
||||||
async fn delete(&self, key: &str) -> Result<(), KVError>;
|
|
||||||
async fn exists(&self, key: &str) -> Result<bool, KVError>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Backends**:
|
|
||||||
|
|
||||||
* **Native**: [`sled`](https://crates.io/crates/sled)
|
|
||||||
* **WASM**: [`idb`](https://crates.io/crates/idb) (IndexedDB)
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
|
|
||||||
* Compile-time target detection via `#[cfg(target_arch = "wasm32")]`
|
|
||||||
* Enables usage in both CLI and browser environments
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛡️ `vault` Crate: Core Cryptography Module
|
|
||||||
|
|
||||||
**Purpose**: Manage secure key storage, cryptographic operations, and password-protected keyspaces.
|
|
||||||
|
|
||||||
### Public API
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct HeroVault;
|
|
||||||
|
|
||||||
impl HeroVault {
|
|
||||||
pub async fn create_keyspace(name: &str, password: &str) -> Result<(), VaultError>;
|
|
||||||
pub async fn load_keyspace(name: &str, password: &str) -> Result<(), VaultError>;
|
|
||||||
pub async fn logout() -> Result<(), VaultError>;
|
|
||||||
|
|
||||||
pub async fn create_keypair(label: &str) -> Result<(), VaultError>;
|
|
||||||
pub async fn select_keypair(label: &str) -> Result<(), VaultError>;
|
|
||||||
pub async fn list_keypairs() -> Result<Vec<String>, VaultError>;
|
|
||||||
pub async fn get_public_key(label: &str) -> Result<Vec<u8>, VaultError>;
|
|
||||||
|
|
||||||
pub async fn sign_message(message: &[u8]) -> Result<Vec<u8>, VaultError>;
|
|
||||||
pub async fn verify_signature(message: &[u8], signature: &[u8], public_key: &[u8]) -> Result<bool, VaultError>;
|
|
||||||
|
|
||||||
pub async fn encrypt(data: &[u8], password: &str) -> Result<Vec<u8>, VaultError>;
|
|
||||||
pub async fn decrypt(data: &[u8], password: &str) -> Result<Vec<u8>, VaultError>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Security**:
|
|
||||||
|
|
||||||
* All sensitive data encrypted at rest using AES-GCM or ChaCha20-Poly1305
|
|
||||||
* Passwords stretched via Argon2id or PBKDF2
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ `evm_client` Crate: EVM Integration Layer
|
|
||||||
|
|
||||||
**Purpose**: Interact with Ethereum-compatible chains using key material from `vault`.
|
|
||||||
|
|
||||||
### Public API
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct EvmClient;
|
|
||||||
|
|
||||||
impl EvmClient {
|
|
||||||
pub async fn connect(rpc_url: &str) -> Result<Self, EvmError>;
|
|
||||||
pub async fn get_balance(&self, address: &str) -> Result<U256, EvmError>;
|
|
||||||
pub async fn send_transaction(&self, to: &str, value: U256, data: &[u8]) -> Result<TxHash, EvmError>;
|
|
||||||
pub async fn call_contract(&self, to: &str, data: &[u8]) -> Result<Vec<u8>, EvmError>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Options**:
|
|
||||||
|
|
||||||
* `ethers-rs` (default, mature)
|
|
||||||
* `alloy` (alternative, lightweight and WASM-friendly)([Stack Overflow][1])
|
|
||||||
|
|
||||||
**Usage**:
|
|
||||||
|
|
||||||
* Transaction signing using vault keys
|
|
||||||
* Account management and EIP-1559 support
|
|
||||||
* Modular pluggability to support multiple networks([Medium][2])
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧰 CLI Interface
|
|
||||||
|
|
||||||
**Purpose**: Provide a command-line interface for interacting with the `vault` and `evm_client` crates, with scripting capabilities via Rhai.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Built with `rhai` scripting engine for dynamic workflows
|
|
||||||
* Thin wrapper over `vault` and `evm_client`
|
|
||||||
* Exposes custom functions to Rhai:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn sign_tx(...) -> Result<String, Box<EvalAltResult>>;
|
|
||||||
fn create_keyspace(...) -> ...;
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* Asynchronous operations managed via `tokio` or `async-std`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 WebAssembly (Browser) Target
|
|
||||||
|
|
||||||
**Purpose**: Provide a browser-compatible interface for the `vault` and `evm_client` crates, compiled to WebAssembly.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Exposed using `wasm-bindgen`
|
|
||||||
* No Rhai scripting in browser due to native-only dependencies
|
|
||||||
* Interaction model:
|
|
||||||
|
|
||||||
* Expose WebAssembly bindings (async `Promise`-compatible)
|
|
||||||
* Front-end (e.g., React) calls functions via JS bridge
|
|
||||||
* Keyspace and signing operations run within WASM memory
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧠 Rhai Integration Strategy
|
|
||||||
|
|
||||||
* Only used in CLI
|
|
||||||
* Bind only synchronous APIs
|
|
||||||
* Asynchronous work handled by sending commands to a background task([Deno][3])
|
|
||||||
|
|
||||||
```rust
|
|
||||||
rhai.register_fn("sign", move |input: String| -> String {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
command_sender.send(VaultCommand::SignMessage { input, resp: tx });
|
|
||||||
rx.blocking_recv().unwrap()
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Runtime Strategy
|
|
||||||
|
|
||||||
* **Library (`vault`, `kvstore`, `evm_client`)**:
|
|
||||||
|
|
||||||
* Must be async-runtime agnostic
|
|
||||||
* No global runtime should be spawned
|
|
||||||
* Use `async-trait`, `Send + 'static` futures
|
|
||||||
|
|
||||||
* **CLI & Web Targets**:
|
|
||||||
|
|
||||||
* CLI: Use `tokio` or `async-std`
|
|
||||||
* WASM: Use `wasm-bindgen-futures` and `spawn_local`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📐 Architecture Diagram
|
|
||||||
|
|
||||||
```
|
|
||||||
[ CLI (Rhai) ] [ Browser (WASM) ]
|
|
||||||
| |
|
|
||||||
[ Scripts ] [ JS / TS ]
|
|
||||||
| |
|
|
||||||
[ Runtime ] [ wasm-bindgen ]
|
|
||||||
| |
|
|
||||||
[ vault (async) ] [ vault (wasm32) ]
|
|
||||||
| |
|
|
||||||
[ kvstore (sled) ] [ kvstore (idb) ]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Dependency Overview
|
|
||||||
|
|
||||||
| Crate | Key Deps | WASM Support |
|
|
||||||
| ----------- | --------------------- | ---------------------------- |
|
|
||||||
| kvstore | sled, idb | ✅ |
|
|
||||||
| hero\_vault | aes-gcm, argon2, rand | ✅ |
|
|
||||||
| evm\_client | alloy | ✅ |
|
|
||||||
| CLI | rhai, tokio | ❌ |
|
|
||||||
| Web Target | wasm-bindgen, idb | ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 Implementation Plan
|
|
||||||
|
|
||||||
1. **Scaffold Crates**:
|
|
||||||
|
|
||||||
* `kvstore`
|
|
||||||
* `vault`
|
|
||||||
* `evm_client`
|
|
||||||
|
|
||||||
2. **Implement `KVStore` Trait**:
|
|
||||||
|
|
||||||
* Implement `sled` backend for native
|
|
||||||
* Implement `idb` backend for WASM
|
|
||||||
|
|
||||||
3. **Develop `vault`**:
|
|
||||||
|
|
||||||
* Implement password-based encrypted keyspaces
|
|
||||||
* Integrate with `kvstore` for persistence
|
|
||||||
* Implement cryptographic operations (signing, encryption, etc.)([GitHub][4])
|
|
||||||
|
|
||||||
4. **Develop `evm_client`**:
|
|
||||||
|
|
||||||
* Integrate with `alloy`
|
|
||||||
* Implement transaction signing using `vault` keys
|
|
||||||
* Implement account management and contract interaction
|
|
||||||
|
|
||||||
5. **Develop CLI Interface**:
|
|
||||||
|
|
||||||
* Integrate `rhai` scripting engine
|
|
||||||
* Expose `vault` and `evm_client` functionalities
|
|
||||||
* Implement message-passing for async operations
|
|
||||||
|
|
||||||
6. **Develop WebAssembly Target**:
|
|
||||||
|
|
||||||
* Compile `vault` and `evm_client` to WASM using `wasm-bindgen`
|
|
||||||
* Expose functionalities to JavaScript
|
|
||||||
* Implement frontend interface (e.g., React)
|
|
||||||
|
|
||||||
7. **Testing and Documentation**:
|
|
||||||
|
|
||||||
* Write unit and integration tests for all functionalities
|
|
||||||
* Document public APIs and usage examples
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This comprehensive plan ensures a modular, secure, and cross-platform cryptographic system, drawing inspiration from the `herocode/webassembly` project. The design facilitates both command-line and browser-based applications, providing
|
|
||||||
|
|
||||||
[1]: https://stackoverflow.com/questions/78979955/how-encrypt-on-blazor-wasm-wpa-using-aes-and-rfc2898?utm_source=chatgpt.com "how encrypt on blazor wasm wpa using Aes and Rfc2898"
|
|
||||||
[2]: https://medium.com/coderhack-com/coderhack-cryptography-libraries-and-uses-in-rust-31957242299f?utm_source=chatgpt.com "Cryptography with rust | by Amay B | CoderHack.com - Medium"
|
|
||||||
[3]: https://deno.com/blog/v1.12?utm_source=chatgpt.com "Deno 1.12 Release Notes"
|
|
||||||
[4]: https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/releases?utm_source=chatgpt.com "Releases · matrix-org/matrix-rust-sdk-crypto-wasm - GitHub"
|
|
79
docs/architecture.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
This document describes the architecture and design rationale for the modular Rust system, including the `kvstore`, `vault`, and `evm_client` crates, as well as the CLI and WASM/web-app targets.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Summary](#summary)
|
||||||
|
- [Crate and Module Structure](#crate-and-module-structure)
|
||||||
|
- [Design Rationale](#design-rationale)
|
||||||
|
- [Vault Crate Design](#vault-crate-design)
|
||||||
|
- [Async, WASM, and Integration](#async-wasm-and-integration)
|
||||||
|
- [Diagrams & References](#diagrams--references)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
This system is organized as a Rust workspace with three core crates:
|
||||||
|
- `kvstore`: Abstract, async key-value storage for both native and WASM environments.
|
||||||
|
- `vault`: Manages encrypted keyspaces and cryptographic operations, using `kvstore` for persistence.
|
||||||
|
- `evm_client`: Ethereum RPC client, signs transactions using keys from `vault`.
|
||||||
|
|
||||||
|
Front-end targets:
|
||||||
|
- CLI (with Rhai scripting)
|
||||||
|
- WASM/web-app (with JS interop)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Crate and Module Structure
|
||||||
|
- **Workspace:** Top-level `Cargo.toml` lists all member crates.
|
||||||
|
- **Features & cfg:** Use Cargo features and `#[cfg]` attributes for platform-specific code (e.g., `sled` for native, `idb` for WASM).
|
||||||
|
- **Dependencies:**
|
||||||
|
- `kvstore` uses `sled` (native) and `idb` (WASM).
|
||||||
|
- `vault` uses `kvstore` and crypto crates (`chacha20poly1305`, `pbkdf2`, etc.).
|
||||||
|
- `evm_client` uses `vault` and Ethereum libraries (e.g., `alloy`).
|
||||||
|
- CLI uses Rhai and async runtime (e.g., Tokio).
|
||||||
|
- Web app uses `wasm-bindgen` and exposes Rust APIs to JS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Rationale
|
||||||
|
### Improvements in the New Implementation
|
||||||
|
- **Async/Await API:** All operations are async, supporting non-blocking I/O for both WASM and native environments.
|
||||||
|
- **Backend Abstraction:** The `KVStore` trait abstracts over multiple storage backends, enabling cross-platform support and easier testing.
|
||||||
|
- **Separation of Concerns:**
|
||||||
|
- `kvstore` handles only storage.
|
||||||
|
- `vault` is responsible for encryption, decryption, and password management.
|
||||||
|
- **WASM and Native Support:** Out-of-the-box support for both browser (IndexedDB) and native (sled) environments.
|
||||||
|
- **Cleaner, Testable Design:** Each layer is independently testable and mockable.
|
||||||
|
|
||||||
|
### Why Encryption and Password Protection are in Vault
|
||||||
|
- **Single Responsibility:** `kvstore` is for storage; `vault` handles security.
|
||||||
|
- **Flexibility:** Encryption algorithms and policies can evolve in `vault` without changing storage.
|
||||||
|
- **Security:** Cryptography is isolated in `vault`, reducing attack surface and easing audits.
|
||||||
|
- **Cross-Platform Consistency:** Same vault logic regardless of storage backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vault Crate Design
|
||||||
|
- **Encrypted Keyspace:** Password-protected, supports multiple keypairs (e.g., secp256k1, Ed25519).
|
||||||
|
- **Crypto APIs:** Async functions for key management, signing, encryption/decryption.
|
||||||
|
- **Storage:** Uses `kvstore` for persistence; re-encrypts and stores the keyspace as a blob.
|
||||||
|
- **WASM Compatibility:** Uses Rust crypto crates that support `wasm32-unknown-unknown`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Async, WASM, and Integration
|
||||||
|
- **Exports:** Use `#[wasm_bindgen]` to expose async functions to JS.
|
||||||
|
- **Async runtime:** Use `wasm_bindgen_futures::spawn_local` in the browser.
|
||||||
|
- **Interop:** JS and Rust communicate via Promises and `JsValue`.
|
||||||
|
- **CLI:** Uses Rhai scripting and async runtime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagrams & References
|
||||||
|
- See included diagrams for crate relationships and message-passing patterns.
|
||||||
|
- Design patterns and best practices are drawn from Rust async and WASM guidelines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*This document merges and replaces content from the previous `Architecture.md` and `kvstore-vault-architecture.md`. For further details on implementation, see `vault_impl_plan.md`.*
|
98
docs/build_instructions.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Plan: Ensuring Native and WASM Builds Work for the Vault/KVStore System
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This document outlines the steps and requirements to guarantee that both native (desktop/server) and WASM (browser) builds of the `vault` and `kvstore` crates work seamlessly, securely, and efficiently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architecture Principles
|
||||||
|
- **Async/Await Everywhere:** All APIs must be async and runtime-agnostic (no Tokio requirement in library code).
|
||||||
|
- **KVStore Trait:** Use an async trait for storage, with platform-specific implementations (sled for native, IndexedDB/idb for WASM).
|
||||||
|
- **Conditional Compilation:** Use `#[cfg(target_arch = "wasm32")]` and `#[cfg(not(target_arch = "wasm32"))]` to select code and dependencies.
|
||||||
|
- **No Blocking in WASM:** All I/O and crypto operations must be async and non-blocking in browser builds.
|
||||||
|
- **WASM-Compatible Crypto:** Only use crypto crates that compile to WASM (e.g., `aes-gcm`, `chacha20poly1305`, `k256`, `rand_core`).
|
||||||
|
- **Separation of Concerns:** All encryption and password logic resides in `vault`, not `kvstore`.
|
||||||
|
- **Stateless and Session APIs:** Provide both stateless (context-passing) and session-based APIs in `vault`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Cargo.toml and Dependency Management
|
||||||
|
- **Native:**
|
||||||
|
- `[target.'cfg(not(target_arch = "wasm32"))'.dependencies]`
|
||||||
|
- `tokio` (with only supported features)
|
||||||
|
- `sled`
|
||||||
|
- **WASM:**
|
||||||
|
- `[target.'cfg(target_arch = "wasm32")'.dependencies]`
|
||||||
|
- `idb`
|
||||||
|
- `wasm-bindgen`, `wasm-bindgen-futures`
|
||||||
|
- **Crypto:**
|
||||||
|
- Only include crates that are WASM-compatible for both targets.
|
||||||
|
- **No unconditional `tokio`** in `vault` or `kvstore`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Code Organization
|
||||||
|
- **KVStore Trait:**
|
||||||
|
- Define as async trait (using `async_trait`).
|
||||||
|
- Implement for sled (native) and idb (WASM), using `#[cfg]`.
|
||||||
|
- **Vault:**
|
||||||
|
- All persistence must go through the KVStore trait.
|
||||||
|
- All cryptography must be WASM-compatible.
|
||||||
|
- No direct file or blocking I/O in WASM.
|
||||||
|
- **Runtime:**
|
||||||
|
- Only use `tokio` in binaries or native-specific code.
|
||||||
|
- In WASM, use `wasm-bindgen-futures::spawn_local` for async tasks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Platform-Specific Guidelines
|
||||||
|
- **Native (Desktop/Server):**
|
||||||
|
- Use `sled` for storage.
|
||||||
|
- Use `tokio::task::spawn_blocking` for blocking I/O if needed.
|
||||||
|
- All async code should work with any runtime.
|
||||||
|
- **WASM (Browser):**
|
||||||
|
- Use `idb` crate for IndexedDB storage.
|
||||||
|
- All code must be non-blocking and compatible with the browser event loop.
|
||||||
|
- Use `wasm-bindgen` and `wasm-bindgen-futures` for JS interop and async.
|
||||||
|
- Expose APIs with `#[wasm_bindgen]` for JS usage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Testing
|
||||||
|
- **Native:** `cargo test`
|
||||||
|
- **WASM:** `wasm-pack test --headless --firefox` (or `--chrome`) inside the crate directory
|
||||||
|
- **Separate tests** for native and WASM backends in `tests/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Browser (WASM) Testing for evm_client
|
||||||
|
|
||||||
|
To run browser-based tests for `evm_client`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd evm_client
|
||||||
|
wasm-pack test --headless --firefox
|
||||||
|
# or
|
||||||
|
wasm-pack test --headless --chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
This will compile your crate to WASM and run the tests in a real browser environment.
|
||||||
|
|
||||||
|
## 6. Checklist for Compliance
|
||||||
|
- [ ] No unconditional `tokio` usage in library code
|
||||||
|
- [ ] All dependencies are WASM-compatible (where needed)
|
||||||
|
- [ ] All storage goes through async KVStore trait
|
||||||
|
- [ ] No blocking I/O or native-only APIs in WASM
|
||||||
|
- [ ] All cryptography is WASM-compatible
|
||||||
|
- [ ] Both stateless and session APIs are available in `vault`
|
||||||
|
- [ ] All APIs are async and runtime-agnostic
|
||||||
|
- [ ] Native and WASM tests both pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. References
|
||||||
|
- See `docs/Architecture.md`, `docs/kvstore-vault-architecture.md`, and `docs/vault_impl_plan.md` for architectural background and rationale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
By following this plan, the codebase will be robust, portable, and secure on both native and browser platforms, and will adhere to all project architectural guidelines.
|
188
docs/evm_client_architecture_plan.md
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# EVM Client Architecture & Implementation Plan
|
||||||
|
|
||||||
|
## Project Goal
|
||||||
|
Build a cross-platform (native + WASM) EVM client that can:
|
||||||
|
- Interact with multiple EVM-compatible networks/providers
|
||||||
|
- Use pluggable signing backends (e.g., SessionManager, hardware wallets, mocks)
|
||||||
|
- Integrate seamlessly with Rhai scripting and the rest of the modular Rust workspace
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements & Principles
|
||||||
|
- **Async, modular, and testable**: All APIs are async and trait-based
|
||||||
|
- **Cross-platform**: Native (Rust) and WASM (browser) support
|
||||||
|
- **Multi-network**: Support for multiple EVM networks/providers, switchable at runtime
|
||||||
|
- **Pluggable signing**: No direct dependency on vault/session; uses a generic Signer trait
|
||||||
|
- **Consistency**: Follows conventions in architecture.md and other project docs
|
||||||
|
- **Scripting**: Exposes ergonomic API for both Rust and Rhai scripting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
evm_client/
|
||||||
|
├── Cargo.toml
|
||||||
|
└── src/
|
||||||
|
├── lib.rs # Public API
|
||||||
|
├── provider.rs # EvmProvider abstraction
|
||||||
|
├── client.rs # EvmClient struct
|
||||||
|
├── signer.rs # Signer trait
|
||||||
|
└── utils.rs # Helpers (e.g., HTTP, WASM glue)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Pluggable Signer Trait
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// signer.rs
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Signer {
|
||||||
|
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
|
||||||
|
fn address(&self) -> String;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `SessionManager` in vault implements this trait.
|
||||||
|
- Any other backend (mock, hardware wallet, etc.) can implement it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. EvmProvider Abstraction
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// provider.rs
|
||||||
|
pub enum EvmProvider {
|
||||||
|
Http { name: String, url: String, chain_id: u64 },
|
||||||
|
// Future: WebSocket, Infura, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EvmProvider {
|
||||||
|
pub async fn send_raw_transaction<S: Signer>(&self, tx: &Transaction, signer: &S) -> Result<TxHash, EvmError> {
|
||||||
|
let raw_tx = tx.sign(signer).await?;
|
||||||
|
let body = format!("{{\"raw\":\"{}\"}}", hex::encode(&raw_tx));
|
||||||
|
match self {
|
||||||
|
EvmProvider::Http { url, .. } => {
|
||||||
|
http_post(url, &body).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. EvmClient Struct & API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// client.rs
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct EvmClient<S: Signer> {
|
||||||
|
providers: HashMap<String, EvmProvider>,
|
||||||
|
current: String,
|
||||||
|
signer: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Signer> EvmClient<S> {
|
||||||
|
pub fn new(signer: S) -> Self {
|
||||||
|
Self {
|
||||||
|
providers: HashMap::new(),
|
||||||
|
current: String::new(),
|
||||||
|
signer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn add_provider(&mut self, key: String, provider: EvmProvider) {
|
||||||
|
self.providers.insert(key, provider);
|
||||||
|
}
|
||||||
|
pub fn set_current(&mut self, key: &str) -> Result<(), EvmError> {
|
||||||
|
if self.providers.contains_key(key) {
|
||||||
|
self.current = key.to_string();
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(EvmError::UnknownNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn current_provider(&self) -> Option<&EvmProvider> {
|
||||||
|
self.providers.get(&self.current)
|
||||||
|
}
|
||||||
|
pub async fn send_transaction(&self, tx: Transaction) -> Result<TxHash, EvmError> {
|
||||||
|
let provider = self.current_provider().ok_or(EvmError::NoNetwork)?;
|
||||||
|
provider.send_raw_transaction(&tx, &self.signer).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Cross-Platform Networking (Native + WASM)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// utils.rs
|
||||||
|
pub async fn http_post(url: &str, body: &str) -> Result<TxHash, EvmError> {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let resp = reqwest::Client::new().post(url).body(body.to_owned()).send().await?;
|
||||||
|
// parse response...
|
||||||
|
Ok(parse_tx_hash(resp.text().await?))
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let resp = gloo_net::http::Request::post(url).body(body).send().await?;
|
||||||
|
// parse response...
|
||||||
|
Ok(parse_tx_hash(resp.text().await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Rhai Scripting Integration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// rhai_bindings.rs
|
||||||
|
pub fn register_rhai_api(engine: &mut Engine) {
|
||||||
|
engine.register_type::<EvmClient<MySigner>>();
|
||||||
|
engine.register_fn("add_network", EvmClient::add_provider);
|
||||||
|
engine.register_fn("switch_network", EvmClient::set_current);
|
||||||
|
engine.register_fn("send_tx", EvmClient::send_transaction);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Usage Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use evm_client::{EvmClient, EvmProvider, Signer};
|
||||||
|
use vault::SessionManager;
|
||||||
|
|
||||||
|
let mut client = EvmClient::new(session_manager);
|
||||||
|
client.add_provider("mainnet".into(), EvmProvider::Http { name: "Ethereum Mainnet".into(), url: "...".into(), chain_id: 1 });
|
||||||
|
client.add_provider("polygon".into(), EvmProvider::Http { name: "Polygon".into(), url: "...".into(), chain_id: 137 });
|
||||||
|
client.set_current("polygon")?;
|
||||||
|
let tx_hash = client.send_transaction(tx).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Compliance & Consistency
|
||||||
|
- **Async/trait-based**: Like kvstore/vault, all APIs are async and trait-based
|
||||||
|
- **No direct dependencies**: Uses generic Signer, not vault/session directly
|
||||||
|
- **Cross-platform**: Uses conditional networking for native/WASM
|
||||||
|
- **Modular/testable**: Clear separation of provider, client, and signer logic
|
||||||
|
- **Rhai scripting**: Exposes ergonomic scripting API
|
||||||
|
- **Follows architecture.md**: Modular, layered, reusable, and extensible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Open Questions / TODOs
|
||||||
|
- How to handle provider-specific errors and retries?
|
||||||
|
- Should we support WebSocket providers in v1?
|
||||||
|
- What subset of EVM JSON-RPC should be exposed to Rhai?
|
||||||
|
- How to best test WASM networking in CI?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 2025-05-16*
|
111
docs/extension_architecture.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Browser Extension Architecture & Workflow
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The browser extension is the main user interface for interacting with the modular Rust cryptographic stack (vault, EVM client, key-value store) and for executing Rhai scripts securely. It is designed for both local (user-driven) scripting and remote (server-driven) workflows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features & Phases
|
||||||
|
|
||||||
|
### Phase 1: Local Session & Script Execution
|
||||||
|
- **Session Management**: User creates/unlocks a keyspace and selects/creates a keypair. Session state is required for all cryptographic operations.
|
||||||
|
- **Keypair Actions**:
|
||||||
|
- Sign, verify
|
||||||
|
- Asymmetric encrypt/decrypt
|
||||||
|
- Symmetric encrypt/decrypt (arbitrary messages/files, using password-derived key)
|
||||||
|
- Send transaction, check balance (with selected provider)
|
||||||
|
- Execute user-provided Rhai scripts (from extension input box)
|
||||||
|
- Scripts have access to the session manager's signer; explicit per-script approval is required.
|
||||||
|
|
||||||
|
### Phase 2: WebSocket Server Integration
|
||||||
|
- **Connection**: User connects to a websocket server using the selected keypair's public key. Connection persists as long as the extension is loaded (i.e., its background logic/service worker is active), regardless of whether the popup/UI is open.
|
||||||
|
- **Script Delivery & Approval**:
|
||||||
|
- Server can send Rhai scripts (with title, description, tags: `local`/`remote`).
|
||||||
|
- Extension notifies user of incoming scripts, displays metadata, allows viewing and approval.
|
||||||
|
- User must unlock keyspace and select the correct keypair to approve/execute.
|
||||||
|
- For `remote` scripts: user signs the script hash and sends signature to server (for consent/authorization; server may execute script).
|
||||||
|
- For `local` scripts: script executes locally, and the extension logs and reports the result back to the server.
|
||||||
|
- For user-pasted scripts: logs only; server connection not required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- **Approval Model**: Every script execution (local or remote) requires user approval.
|
||||||
|
- **No global permissions**: Permissions are not granted globally or permanently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI/UX Guidelines
|
||||||
|
- Use any robust, modern, and fast UI framework (React, Svelte, etc.).
|
||||||
|
- Dark mode is recommended.
|
||||||
|
- UI should be responsive, intuitive, and secure.
|
||||||
|
- All cryptographic operations and script executions must be clearly auditable and user-approved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Developer Notes
|
||||||
|
- Extension is the canonical interface for scripting and secure automation.
|
||||||
|
- CLI and additional server features are planned for future phases.
|
||||||
|
- For vault and scripting details, see [rhai_architecture_plan.md].
|
||||||
|
- For EVM client integration, see [evm_client_architecture_plan.md].
|
@ -1,73 +0,0 @@
|
|||||||
# What’s Improved in the New Implementation
|
|
||||||
|
|
||||||
_and why Encryption and Password Protection should be implemented in the Vault crate_
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Introduction
|
|
||||||
|
|
||||||
This document compares the old and new designs of the key-value store (kvstore) module, highlights improvements in the new implementation, and explains the architectural decision to move encryption and password protection to the vault crate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Improvements in the New Implementation
|
|
||||||
|
|
||||||
### a. **Async/Await API**
|
|
||||||
- All operations are asynchronous, enabling non-blocking I/O.
|
|
||||||
- Essential for WASM/browser and scalable server environments.
|
|
||||||
|
|
||||||
### b. **Backend Abstraction**
|
|
||||||
- The `KVStore` trait abstracts over multiple storage backends (native and WASM).
|
|
||||||
- Enables cross-platform support and easier testing.
|
|
||||||
|
|
||||||
### c. **Separation of Concerns**
|
|
||||||
- The storage layer (`kvstore`) is now focused solely on key-value persistence.
|
|
||||||
- No longer mixes storage with cryptography or user authentication.
|
|
||||||
|
|
||||||
### d. **WASM and Native Support**
|
|
||||||
- Out-of-the-box support for both browser (IndexedDB) and native (sled) environments.
|
|
||||||
- Easy to extend with new backends in the future.
|
|
||||||
|
|
||||||
### e. **Cleaner, More Testable Design**
|
|
||||||
- Each layer is independently testable and mockable.
|
|
||||||
- Simpler to reason about and maintain.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Why Encryption and Password Protection Belong in the Vault Crate
|
|
||||||
|
|
||||||
### a. **Single Responsibility Principle**
|
|
||||||
- `kvstore` should only handle storage, not cryptographic operations or user authentication.
|
|
||||||
- `vault` is responsible for security: encryption, decryption, password management.
|
|
||||||
|
|
||||||
### b. **Flexibility and Extensibility**
|
|
||||||
- Different applications may require different encryption schemes or policies.
|
|
||||||
- By implementing encryption in `vault`, you can easily swap algorithms, add multi-user support, or support new crypto features without touching the storage backend.
|
|
||||||
|
|
||||||
### c. **Security Best Practices**
|
|
||||||
- Keeping cryptography separate from storage reduces the attack surface and risk of subtle bugs.
|
|
||||||
- All key material and secrets are encrypted before being handed to the storage layer.
|
|
||||||
|
|
||||||
### d. **Cross-Platform Consistency**
|
|
||||||
- The same vault logic can be used regardless of storage backend (sled, IndexedDB, etc).
|
|
||||||
- Ensures consistent encryption and password handling on all platforms.
|
|
||||||
|
|
||||||
### e. **Easier Upgrades and Auditing**
|
|
||||||
- Security code is isolated in one place (`vault`), making it easier to audit and upgrade.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Summary Table
|
|
||||||
|
|
||||||
| Layer | Responsibility | Encryption | Passwords | Storage Backend |
|
|
||||||
|-----------|------------------------|------------|-----------|----------------|
|
|
||||||
| kvstore | Persistence/Storage | ❌ | ❌ | sled, IndexedDB|
|
|
||||||
| vault | Security, Key Mgmt | ✅ | ✅ | Uses kvstore |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Conclusion
|
|
||||||
|
|
||||||
- The new design is more modular, secure, and maintainable.
|
|
||||||
- Encryption and password logic in `vault` enables strong, flexible security while keeping storage simple and robust.
|
|
||||||
|
|
123
docs/kvstore.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# kvstore Crate Overview
|
||||||
|
|
||||||
|
`kvstore` is a runtime-agnostic, async key-value storage crate designed for both native (using `sled`) and WASM/browser (using IndexedDB via the `idb` crate) environments. It provides a unified API for all platforms, enabling seamless storage abstraction for Rust applications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Summary](#summary)
|
||||||
|
- [Main Components](#main-components)
|
||||||
|
- [Supported Environments](#supported-environments)
|
||||||
|
- [Quickstart & Usage Examples](#quickstart--usage-examples)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [More Information](#more-information)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
The `kvstore` crate defines an async trait for key-value storage, with robust implementations for native and browser environments. It is the storage backend for higher-level crates such as `vault`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Components
|
||||||
|
- **KVStore Trait**: Async interface for key-value operations (`get`, `set`, `remove`, `contains_key`, `keys`, `clear`).
|
||||||
|
- **NativeStore**: Native backend using `sled` (requires Tokio runtime).
|
||||||
|
- **WasmStore**: WASM/browser backend using IndexedDB via the `idb` crate.
|
||||||
|
- **KVError**: Error type covering I/O, serialization, encryption, and backend-specific issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported Environments
|
||||||
|
- **Native:** Uses `sled` for fast, embedded storage. Blocking I/O is offloaded to background threads using Tokio.
|
||||||
|
- **Browser (WASM):** Uses IndexedDB via the `idb` crate. Fully async and Promise-based.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quickstart & Usage Examples
|
||||||
|
|
||||||
|
### Native Example
|
||||||
|
```rust
|
||||||
|
use kvstore::{KVStore, NativeStore};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let store = NativeStore::open("/tmp/mydb").unwrap();
|
||||||
|
store.set("foo", b"bar").await.unwrap();
|
||||||
|
let val = store.get("foo").await.unwrap();
|
||||||
|
println!("Got: {:?}", val);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM/Browser Example
|
||||||
|
```rust
|
||||||
|
// In a browser/WASM environment:
|
||||||
|
use kvstore::{KVStore, WasmStore};
|
||||||
|
|
||||||
|
// Must be called from an async context (e.g., JS Promise)
|
||||||
|
let store = WasmStore::open("vault").await.unwrap();
|
||||||
|
store.set("foo", b"bar").await.unwrap();
|
||||||
|
let val = store.get("foo").await.unwrap();
|
||||||
|
// Use the value as needed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### KVStore Trait
|
||||||
|
```rust
|
||||||
|
#[async_trait]
|
||||||
|
pub trait KVStore {
|
||||||
|
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
|
||||||
|
async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
|
||||||
|
async fn remove(&self, key: &str) -> Result<()>;
|
||||||
|
async fn contains_key(&self, key: &str) -> Result<bool>;
|
||||||
|
async fn keys(&self) -> Result<Vec<String>>;
|
||||||
|
async fn clear(&self) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NativeStore
|
||||||
|
```rust
|
||||||
|
pub struct NativeStore { /* ... */ }
|
||||||
|
|
||||||
|
impl NativeStore {
|
||||||
|
pub fn open(path: &str) -> Result<Self>;
|
||||||
|
// Implements KVStore trait
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WasmStore
|
||||||
|
```rust
|
||||||
|
pub struct WasmStore { /* ... */ }
|
||||||
|
|
||||||
|
impl WasmStore {
|
||||||
|
pub async fn open(name: &str) -> Result<Self>;
|
||||||
|
// Implements KVStore trait
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum KVError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
KeyNotFound(String),
|
||||||
|
StoreNotFound(String),
|
||||||
|
Serialization(String),
|
||||||
|
Deserialization(String),
|
||||||
|
Encryption(String),
|
||||||
|
Decryption(String),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## More Information
|
||||||
|
- For architecture and design, see [`architecture.md`](architecture.md)
|
||||||
|
- For integration examples, see [`build_instructions.md`](build_instructions.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For advanced usage, backend customization, and WASM integration, see the linked documents above.*
|
@ -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.
|
||||||
|
|
||||||
---
|
---
|
397
docs/rhai_architecture_plan.md
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
# Rhai Scripting Architecture Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the architecture and integration plan for Rhai scripting within the modular Rust cryptographic system. The goal is to enable secure, extensible scripting for both browser and (future) CLI environments, with the browser extension as the main user interface.
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
- **Browser Extension**: The primary and recommended user interface for all modules, scripting, and automation.
|
||||||
|
- **CLI**: Planned as a future feature; not a primary interface.
|
||||||
|
|
||||||
|
## Vault & Scripting Capabilities
|
||||||
|
- All cryptographic operations (sign, verify, encrypt, decrypt) are exposed to Rhai scripts via the extension.
|
||||||
|
- Symmetric encryption/decryption of arbitrary messages/files is supported using a key derived from the keyspace password (see `Vault::encrypt`/`Vault::decrypt`).
|
||||||
|
- User-provided Rhai scripts can access the current session's signer (with explicit approval).
|
||||||
|
|
||||||
|
## Extension UI/UX & Workflow
|
||||||
|
|
||||||
|
### Phase 1: Local Session & Script Execution
|
||||||
|
1. **Session Management**
|
||||||
|
- User is prompted to create/unlock a keyspace and select/create a keypair.
|
||||||
|
- The session (unlocked keyspace + selected keypair) is required for all cryptographic actions and script execution.
|
||||||
|
2. **Per-Keypair Actions**
|
||||||
|
- Sign, verify
|
||||||
|
- Asymmetric encrypt/decrypt
|
||||||
|
- Symmetric encrypt/decrypt (using password-derived key)
|
||||||
|
- Send transaction, check balance (with selected provider)
|
||||||
|
- Execute user-provided Rhai script (from input box)
|
||||||
|
- Scripts have access to the session manager's current signer and can send transactions on behalf of the user, but require explicit approval per script execution.
|
||||||
|
|
||||||
|
### Phase 2: WebSocket Server Integration
|
||||||
|
1. **Connection**
|
||||||
|
- User must have an active session to connect to the server (connects using selected keypair's public key).
|
||||||
|
- Connection is persistent while the extension is open; user may lock keyspace but remain connected.
|
||||||
|
2. **Script Delivery & Approval**
|
||||||
|
- Server can send Rhai scripts to the extension, each with a title, description, and tags (e.g., `local`, `remote`).
|
||||||
|
- Extension notifies user of incoming script, displays metadata, and allows user to view the script.
|
||||||
|
- User must unlock their keyspace and select the correct keypair to approve/execute the script.
|
||||||
|
- For `remote` scripts: user signs the script hash (consent/authorization) and sends the signature to the server. The server may then execute the script.
|
||||||
|
- For `local` scripts: script executes locally, and the extension logs and reports the result back to the server.
|
||||||
|
- For user-pasted scripts (from input box): logs only; server connection not required.
|
||||||
|
|
||||||
|
## Script Permissions & Security
|
||||||
|
- **Session Password Handling**: The session password (or a derived key) is kept in memory only for the duration of the unlocked session, never persisted, and is zeroized from memory on session lock/logout. This follows best practices for cryptographic applications and browser extensions.
|
||||||
|
- **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.
|
||||||
|
- **No global permissions**: Permissions are not granted globally or permanently.
|
||||||
|
|
||||||
|
## UI Framework & UX
|
||||||
|
- Any robust, modern, and fast UI framework may be used (React, Svelte, etc.).
|
||||||
|
- Dark mode is recommended.
|
||||||
|
- UI should be responsive, intuitive, and secure.
|
||||||
|
|
||||||
|
## Developer Notes
|
||||||
|
- The extension is the canonical interface for scripting and secure automation.
|
||||||
|
- CLI support and additional server features are planned for future phases.
|
||||||
|
- See also: [EVM Client Plan](evm_client_architecture_plan.md) and [README.md] for architecture overview.
|
||||||
|
|
||||||
|
## Project Goal
|
||||||
|
|
||||||
|
Build a system that allows users to write and execute Rhai scripts both:
|
||||||
|
- **Locally via a CLI**, and
|
||||||
|
- **In the browser via a browser extension**
|
||||||
|
|
||||||
|
using the same core logic (the `vault` and `evm_client` crates), powered by a Rust WebAssembly module.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements & Architecture
|
||||||
|
|
||||||
|
### 1. Shared Rust Libraries
|
||||||
|
- **Core Libraries:**
|
||||||
|
- `vault/`: Cryptographic vault and session management
|
||||||
|
- `evm_client/`: EVM RPC client
|
||||||
|
- **Responsibilities:**
|
||||||
|
- Implement business logic
|
||||||
|
- Expose functions to the Rhai scripting engine
|
||||||
|
- Reusable in both native CLI and WebAssembly builds
|
||||||
|
|
||||||
|
### 2. Recommended File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
rhai_sandbox_workspace/
|
||||||
|
├── Cargo.toml # Workspace manifest
|
||||||
|
├── vault/ # Shared logic + Rhai bindings
|
||||||
|
│ ├── Cargo.toml
|
||||||
|
│ └── src/
|
||||||
|
│ ├── lib.rs # Public API (all core logic)
|
||||||
|
│ ├── rhai_bindings.rs# Rhai registration (shared)
|
||||||
|
│ └── utils.rs # Any helpers
|
||||||
|
├── cli/ # CLI runner
|
||||||
|
│ ├── Cargo.toml
|
||||||
|
│ └── src/
|
||||||
|
│ └── main.rs
|
||||||
|
├── wasm/ # Wasm runner using same API
|
||||||
|
│ ├── Cargo.toml
|
||||||
|
│ └── src/
|
||||||
|
│ └── lib.rs
|
||||||
|
├── browser-extension/ # (optional) Extension frontend
|
||||||
|
│ ├── manifest.json
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Code Organization for Shared Rhai Bindings
|
||||||
|
|
||||||
|
**In `vault/src/lib.rs`:**
|
||||||
|
```rust
|
||||||
|
pub mod rhai_bindings;
|
||||||
|
pub use rhai_bindings::register_rhai_api;
|
||||||
|
|
||||||
|
pub fn fib(n: i64) -> i64 {
|
||||||
|
if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**In `vault/src/rhai_bindings.rs`:**
|
||||||
|
```rust
|
||||||
|
use rhai::{Engine, RegisterFn};
|
||||||
|
use crate::fib;
|
||||||
|
|
||||||
|
pub fn register_rhai_api(engine: &mut Engine) {
|
||||||
|
engine.register_fn("fib", fib);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using in CLI (`cli/src/main.rs`):**
|
||||||
|
```rust
|
||||||
|
use rhai::Engine;
|
||||||
|
use vault::register_rhai_api;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 2 {
|
||||||
|
eprintln!("Usage: cli <script.rhai>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let script = fs::read_to_string(&args[1]).expect("Failed to read script");
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
register_rhai_api(&mut engine);
|
||||||
|
match engine.eval::<i64>(&script) {
|
||||||
|
Ok(result) => println!("Result: {}", result),
|
||||||
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using in WASM (`wasm/src/lib.rs`):**
|
||||||
|
```rust
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use rhai::Engine;
|
||||||
|
use vault::register_rhai_api;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_rhai(script: &str) -> JsValue {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
register_rhai_api(&mut engine);
|
||||||
|
match engine.eval_expression::<i64>(script) {
|
||||||
|
Ok(res) => JsValue::from_f64(res as f64),
|
||||||
|
Err(e) => JsValue::from_str(&format!("Error: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Single source of truth for Rhai bindings (`register_rhai_api`)
|
||||||
|
- Easy to expand: add more Rust functions and register in one place
|
||||||
|
- Works seamlessly across CLI and WASM
|
||||||
|
- Encourages code reuse and maintainability
|
||||||
|
|
||||||
|
**This approach fully adheres to the principles in `architecture.md`**:
|
||||||
|
- Modular, layered design
|
||||||
|
- Code reuse across targets
|
||||||
|
- Shared business logic for both native and WASM
|
||||||
|
- Clean separation of platform-specific code
|
||||||
|
|
||||||
|
### 4. Native CLI Tool (`cli/`)
|
||||||
|
- Accepts `.rhai` script file or stdin
|
||||||
|
- Uses shared libraries to run the script via the Rhai engine
|
||||||
|
- Outputs the result to the terminal
|
||||||
|
|
||||||
|
### 5. WebAssembly Module (`wasm/`)
|
||||||
|
- Uses the same core library for Rhai logic
|
||||||
|
- Exposes a `run_rhai(script: &str) -> String` function via `wasm_bindgen`
|
||||||
|
- Usable from browser-based JS (e.g., `import { run_rhai }`)
|
||||||
|
|
||||||
|
### 4. Browser Extension (`browser_extension/`)
|
||||||
|
- UI for user to enter Rhai code after loading keyspace and selecting keypair (using SessionManager)
|
||||||
|
- Loads the WebAssembly module
|
||||||
|
- Runs user input through `run_rhai(script)`
|
||||||
|
- Displays the result or error
|
||||||
|
- **Security:**
|
||||||
|
- Only allows script input from:
|
||||||
|
- Trusted websites (via content script injection)
|
||||||
|
- Extension popup UI---
|
||||||
|
|
||||||
|
## EVM Client Integration: Pluggable Signer Pattern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Platform, Multi-Network EvmClient Design
|
||||||
|
|
||||||
|
To be consistent with the rest of the project and adhere to the architecture and modularity principles, the `evm_client` crate should:
|
||||||
|
- Use async APIs and traits for all network and signing operations
|
||||||
|
- Support both native and WASM (browser) environments via conditional compilation
|
||||||
|
- Allow dynamic switching between multiple EVM networks/providers at runtime
|
||||||
|
- Avoid direct dependencies on vault/session, using the pluggable Signer trait
|
||||||
|
- Expose a clear, ergonomic API for both Rust and Rhai scripting
|
||||||
|
|
||||||
|
### 1. EvmProvider Abstraction
|
||||||
|
```rust
|
||||||
|
pub enum EvmProvider {
|
||||||
|
Http { name: String, url: String, chain_id: u64 },
|
||||||
|
// Future: WebSocket, Infura, etc.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. EvmClient Struct
|
||||||
|
```rust
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct EvmClient<S: Signer> {
|
||||||
|
providers: HashMap<String, EvmProvider>,
|
||||||
|
current: String,
|
||||||
|
signer: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Signer> EvmClient<S> {
|
||||||
|
pub fn new(signer: S) -> Self {
|
||||||
|
Self {
|
||||||
|
providers: HashMap::new(),
|
||||||
|
current: String::new(),
|
||||||
|
signer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn add_provider(&mut self, key: String, provider: EvmProvider) {
|
||||||
|
self.providers.insert(key, provider);
|
||||||
|
}
|
||||||
|
pub fn set_current(&mut self, key: &str) -> Result<(), EvmError> {
|
||||||
|
if self.providers.contains_key(key) {
|
||||||
|
self.current = key.to_string();
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(EvmError::UnknownNetwork)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn current_provider(&self) -> Option<&EvmProvider> {
|
||||||
|
self.providers.get(&self.current)
|
||||||
|
}
|
||||||
|
pub async fn send_transaction(&self, tx: Transaction) -> Result<TxHash, EvmError> {
|
||||||
|
let provider = self.current_provider().ok_or(EvmError::NoNetwork)?;
|
||||||
|
provider.send_raw_transaction(&tx, &self.signer).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Provider Networking (Native + WASM)
|
||||||
|
```rust
|
||||||
|
impl EvmProvider {
|
||||||
|
pub async fn send_raw_transaction<S: Signer>(&self, tx: &Transaction, signer: &S) -> Result<TxHash, EvmError> {
|
||||||
|
let raw_tx = tx.sign(signer).await?;
|
||||||
|
let body = format!("{{\"raw\":\"{}\"}}", hex::encode(&raw_tx));
|
||||||
|
match self {
|
||||||
|
EvmProvider::Http { url, .. } => {
|
||||||
|
http_post(url, &body).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-platform HTTP POST
|
||||||
|
pub async fn http_post(url: &str, body: &str) -> Result<TxHash, EvmError> {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let resp = reqwest::Client::new().post(url).body(body.to_owned()).send().await?;
|
||||||
|
// parse response...
|
||||||
|
Ok(parse_tx_hash(resp.text().await?))
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let resp = gloo_net::http::Request::post(url).body(body).send().await?;
|
||||||
|
// parse response...
|
||||||
|
Ok(parse_tx_hash(resp.text().await?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Rhai Scripting Integration
|
||||||
|
- Expose `add_network`, `switch_network`, and `send_tx` functions to the Rhai engine via the shared bindings pattern.
|
||||||
|
- Example:
|
||||||
|
```rust
|
||||||
|
pub fn register_rhai_api(engine: &mut Engine) {
|
||||||
|
engine.register_type::<EvmClient<MySigner>>();
|
||||||
|
engine.register_fn("add_network", EvmClient::add_provider);
|
||||||
|
engine.register_fn("switch_network", EvmClient::set_current);
|
||||||
|
engine.register_fn("send_tx", EvmClient::send_transaction);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Consistency and Compliance
|
||||||
|
- **Async, modular, and testable:** All APIs are async and trait-based, just like kvstore/vault.
|
||||||
|
- **No direct dependencies:** EvmClient is generic over signing backend, like other crates.
|
||||||
|
- **Cross-platform:** Uses conditional compilation for networking, ensuring WASM and native support.
|
||||||
|
- **Clear separation:** Network and signing logic are independent, allowing easy extension and testing.
|
||||||
|
|
||||||
|
This design fits seamlessly with your project’s architecture and modularity goals.
|
||||||
|
|
||||||
|
### 5. Web App Integration
|
||||||
|
- Enable trusted web apps to send Rhai scripts to the extension, using one or both of:
|
||||||
|
|
||||||
|
#### Option A: Direct (Client-side)
|
||||||
|
- Web apps use `window.postMessage()` or DOM events
|
||||||
|
- Extension listens via content script
|
||||||
|
- Validates origin before running the script
|
||||||
|
|
||||||
|
#### Option B: Server-based (WebSocket)
|
||||||
|
- Both extension and web app connect to a backend WebSocket server
|
||||||
|
- Web app sends script to server
|
||||||
|
- Server routes it to the right connected extension client
|
||||||
|
- Extension executes the script and returns the result
|
||||||
|
|
||||||
|
### 6. Security Considerations
|
||||||
|
- All script execution is sandboxed via Rhai + Wasm
|
||||||
|
- Only allow input from:
|
||||||
|
- Extension popup
|
||||||
|
- Approved websites or servers
|
||||||
|
- Validate origins and inputs strictly
|
||||||
|
- Do not expose internal APIs beyond `run_rhai(script)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-Level Component Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
+-------------------+ +-------------------+
|
||||||
|
| CLI Tool | | Browser Extension|
|
||||||
|
| (cli/) | | (browser_ext/) |
|
||||||
|
+---------+---------+ +---------+---------+
|
||||||
|
| |
|
||||||
|
| +----------------+
|
||||||
|
| |
|
||||||
|
+---------v---------+
|
||||||
|
| WASM Module | <--- Shared Rust Core (vault, evm_client)
|
||||||
|
| (wasm/) |
|
||||||
|
+---------+---------+
|
||||||
|
|
|
||||||
|
+---------v---------+
|
||||||
|
| Rhai Engine |
|
||||||
|
+-------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Core Library Integration
|
||||||
|
- Ensure all business logic (vault & evm_client) is accessible from both native and WASM targets.
|
||||||
|
- Expose required functions to Rhai engine.
|
||||||
|
|
||||||
|
### Phase 2: CLI Tool
|
||||||
|
- Implement CLI that loads and runs Rhai scripts using the shared core.
|
||||||
|
- Add support for stdin and file input.
|
||||||
|
|
||||||
|
### Phase 3: WASM Module
|
||||||
|
- Build WASM module exposing `run_rhai`.
|
||||||
|
- Integrate with browser JS via `wasm_bindgen`.
|
||||||
|
|
||||||
|
### Phase 4: Browser Extension
|
||||||
|
- UI for script input and result display.
|
||||||
|
- Integrate WASM module and SessionManager.
|
||||||
|
- Secure script input (popup and trusted sites only).
|
||||||
|
|
||||||
|
### Phase 5: Web App Integration
|
||||||
|
- Implement postMessage and/or WebSocket protocol for trusted web apps to send scripts.
|
||||||
|
- Validate origins and handle results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions / TODOs
|
||||||
|
- What subset of the vault/evm_client API should be exposed to Rhai?
|
||||||
|
- What are the best practices for sandboxing Rhai in WASM?
|
||||||
|
- How will user authentication/session be handled between extension and web app?
|
||||||
|
- How will error reporting and logging work across boundaries?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
- [Rhai scripting engine](https://rhai.rs/)
|
||||||
|
- [wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/)
|
||||||
|
- [WebExtension APIs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions)
|
||||||
|
- [Rust + WASM Book](https://rustwasm.github.io/book/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Last updated: 2025-05-15*
|
48
docs/user_stories.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# User Stories: Modular Cryptographic Extension & Scripting
|
||||||
|
|
||||||
|
## As a User, I want to...
|
||||||
|
|
||||||
|
### Session & Key Management
|
||||||
|
- Create a new encrypted keyspace with a password so that only I can access my secrets.
|
||||||
|
- Unlock an existing keyspace by entering my password.
|
||||||
|
- Create, select, and manage multiple keypairs within a keyspace.
|
||||||
|
- Clearly see which keyspace and keypair are currently active in my session.
|
||||||
|
|
||||||
|
### Cryptographic Operations
|
||||||
|
- Sign and verify messages using my selected keypair.
|
||||||
|
- Encrypt and decrypt messages or files using asymmetric cryptography (public/private keypair).
|
||||||
|
- Encrypt and decrypt messages or files using symmetric encryption (derived from my keyspace password).
|
||||||
|
- Export or back up my keypairs securely.
|
||||||
|
|
||||||
|
### EVM Client Actions
|
||||||
|
- Connect to an Ethereum provider and check my account balance.
|
||||||
|
- Send transactions using my selected keypair.
|
||||||
|
|
||||||
|
### Scripting (Rhai)
|
||||||
|
- Paste or write a Rhai script into the extension UI and execute it securely.
|
||||||
|
- Approve or deny each script execution, with a clear understanding of what the script will access (e.g., signing, sending transactions).
|
||||||
|
- See script logs/output in the extension UI.
|
||||||
|
|
||||||
|
### Security & Permissions
|
||||||
|
- Be prompted for approval before any script can access my keypair or perform sensitive operations.
|
||||||
|
- See a clear audit trail/log of all cryptographic and scripting actions performed in my session.
|
||||||
|
|
||||||
|
### WebSocket Integration (Future)
|
||||||
|
- Connect to a server using my keypair's public key and receive Rhai scripts from the server.
|
||||||
|
- Review and approve/reject incoming scripts, with clear metadata (title, description, tags).
|
||||||
|
- For remote scripts, sign the script hash and send my signature to the server as consent.
|
||||||
|
- For local scripts, execute them in the extension and have the results reported back to the server.
|
||||||
|
|
||||||
|
### UI/UX
|
||||||
|
- Use a fast, modern, and intuitive extension interface, with dark mode support.
|
||||||
|
- Always know the current security state (locked/unlocked, connected/disconnected, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As a Developer, I want to...
|
||||||
|
|
||||||
|
- Expose all vault and EVM client APIs to WASM so they are callable from JavaScript/TypeScript.
|
||||||
|
- Provide ergonomic Rust-to-Rhai bindings for all key cryptographic and EVM actions.
|
||||||
|
- Ensure clear error reporting and logging for all extension and scripting operations.
|
||||||
|
- Write tests for both WASM and native environments.
|
||||||
|
- Easily add new cryptographic algorithms, providers, or scripting APIs as the system evolves.
|
138
docs/vault.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Vault Crate Overview
|
||||||
|
|
||||||
|
Welcome to the Vault crate! This document provides a high-level overview, usage examples, and a guide to the main components of the Vault system. For deeper technical details, see [`architecture.md`](architecture.md) and [`vault_impl_plan.md`](vault_impl_plan.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Summary](#summary)
|
||||||
|
- [Main Components](#main-components)
|
||||||
|
- [Security Model](#security-model)
|
||||||
|
- [Quickstart & Usage Examples](#quickstart--usage-examples)
|
||||||
|
- [More Information](#more-information)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
The Vault crate is a modular, async, and WASM-compatible cryptographic keystore. It manages encrypted keyspaces (each with multiple keypairs), provides cryptographic APIs, and persists all data via a pluggable key-value store. All sensitive material is always encrypted at rest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main Components
|
||||||
|
- **VaultStore**: Central manager for all keyspaces. Handles creation, loading, and metadata.
|
||||||
|
- **KeySpace**: Isolated environment containing multiple keypairs, encrypted with its own password.
|
||||||
|
- **KeyPair**: Represents an individual cryptographic keypair (e.g., secp256k1, Ed25519).
|
||||||
|
- **Symmetric Encryption Module**: Handles encryption/decryption using ChaCha20Poly1305 and PBKDF2.
|
||||||
|
- **SessionManager** (optional): Manages active context for ergonomic API usage (not required for stateless usage).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
- **Per-KeySpace Encryption:** Each keyspace is encrypted independently using a password-derived key.
|
||||||
|
- **VaultStore Metadata:** Stores non-sensitive metadata about keyspaces (names, creation dates, etc.).
|
||||||
|
- **Zero-Knowledge:** Passwords and keys are never stored in plaintext; all cryptographic operations are performed in memory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quickstart & Usage Examples
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Create a new VaultStore
|
||||||
|
let mut vault = VaultStore::new();
|
||||||
|
|
||||||
|
// Create a new keyspace
|
||||||
|
vault.create_keyspace("personal", "password123")?;
|
||||||
|
|
||||||
|
// List available keyspaces
|
||||||
|
let keyspaces = vault.list_keyspaces();
|
||||||
|
|
||||||
|
// Unlock a keyspace
|
||||||
|
let keyspace = vault.load_keyspace("personal", "password123")?;
|
||||||
|
|
||||||
|
// Create a new keypair
|
||||||
|
let key_id = keyspace.create_key(KeyType::Secp256k1, "mykey")?;
|
||||||
|
|
||||||
|
// Sign a message
|
||||||
|
let signature = keyspace.sign(&key_id, b"hello world")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## More Information
|
||||||
|
- **Architecture & Design:** See [`architecture.md`](architecture.md)
|
||||||
|
- **Implementation Details:** See [`vault_impl_plan.md`](vault_impl_plan.md)
|
||||||
|
- **Build Instructions:** See [`build_instructions.md`](build_instructions.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*For advanced usage (stateless/session APIs, custom backends, WASM integration), see the linked documents above.*
|
||||||
|
}
|
||||||
|
|
||||||
|
### KeySpace API Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl KeySpace {
|
||||||
|
pub fn new(name: &str, password: &str) -> Result<Self, VaultError>;
|
||||||
|
pub fn save(&self) -> Result<(), VaultError>;
|
||||||
|
pub fn list_keypairs(&self) -> Vec<String>;
|
||||||
|
pub fn create_keypair(&mut self, name: &str) -> Result<(), VaultError>;
|
||||||
|
pub fn delete_keypair(&mut self, name: &str) -> Result<(), VaultError>;
|
||||||
|
pub fn rename_keypair(&mut self, old_name: &str, new_name: &str) -> Result<(), VaultError>;
|
||||||
|
pub fn get_keypair(&self, name: &str) -> Result<KeyPair, VaultError>;
|
||||||
|
pub fn sign(&self, keypair_name: &str, message: &[u8]) -> Result<Vec<u8>, VaultError>;
|
||||||
|
pub fn verify(&self, keypair_name: &str, message: &[u8], signature: &[u8]) -> Result<bool, VaultError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### KeyPair API Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct KeyPair {
|
||||||
|
// Internal fields
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyPair {
|
||||||
|
pub fn new() -> Self;
|
||||||
|
pub fn from_private_key(private_key: &[u8]) -> Result<Self, VaultError>;
|
||||||
|
pub fn public_key(&self) -> Vec<u8>;
|
||||||
|
pub fn private_key(&self) -> Vec<u8>;
|
||||||
|
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError>;
|
||||||
|
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, VaultError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SessionManager API Example (Optional)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SessionManager {
|
||||||
|
keyspace: KeySpace,
|
||||||
|
active_keypair: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionManager {
|
||||||
|
pub fn new(keyspace: KeySpace, keypair_name: &str) -> Result<Self, VaultError>;
|
||||||
|
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError>;
|
||||||
|
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, VaultError>;
|
||||||
|
pub fn switch_keypair(&mut self, keypair_name: &str) -> Result<(), VaultError>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
vault_store/
|
||||||
|
├── metadata.json
|
||||||
|
└── keyspaces/
|
||||||
|
├── alice.ksp
|
||||||
|
├── bob.ksp
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
- `metadata.json`: Contains metadata about each keyspace, such as name and creation date.
|
||||||
|
|
||||||
|
|
||||||
|
## Supported Environments
|
||||||
|
|
||||||
|
- **Native:** Uses filesystem or a database (e.g., SQLite) for storage.
|
||||||
|
- **Browser (WASM):** Uses IndexedDB or localStorage via the kvstore abstraction.
|
||||||
|
|
||||||
|
For full build and integration instructions, see [build_instructions.md](build_instructions.md).
|
234
docs/vault_impl_plan.md
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
# Vault Implementation Plan (Technical Appendix)
|
||||||
|
|
||||||
|
This document is a technical reference for contributors and maintainers of the Vault crate. It covers advanced implementation details, design rationale, and data models. For a high-level overview and usage, see [`vault.md`](vault.md) and [`architecture.md`](architecture.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Design Principle: Stateless & Session APIs](#design-principle-stateless--session-apis)
|
||||||
|
- [Data Model](#data-model)
|
||||||
|
- [Module & File Structure](#module--file-structure)
|
||||||
|
- [Advanced Notes](#advanced-notes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
> **Design Principle:**
|
||||||
|
> **The vault crate will provide both a stateless (context-passing) API and an ergonomic session-based API.**
|
||||||
|
> This ensures maximum flexibility for both library developers and application builders, supporting both functional and stateful usage patterns.
|
||||||
|
|
||||||
|
## Design Principle: Stateless & Session APIs
|
||||||
|
|
||||||
|
The `vault` crate is a modular, async, and WASM-compatible cryptographic keystore. It manages an encrypted keyspace (multiple keypairs), provides cryptographic APIs, and persists all data via the `kvstore` trait. The design ensures all sensitive material is encrypted at rest and is portable across native and browser environments.
|
||||||
|
|
||||||
|
**Core Components:**
|
||||||
|
- **Vault:** Main manager for encrypted keyspace and cryptographic operations.
|
||||||
|
- **KeyPair:** Represents individual asymmetric keypairs (e.g., secp256k1, Ed25519).
|
||||||
|
- **Symmetric Encryption Module:** Handles encryption/decryption and key derivation.
|
||||||
|
- **SessionManager (Optional):** Maintains current context (e.g., selected keypair) for user sessions.
|
||||||
|
- **KVStore:** Async trait for backend-agnostic persistence (sled on native, IndexedDB on WASM).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
You can design the vault crate to support both stateless and session-based (stateful) usage patterns. This gives maximum flexibility to both library developers and application builders.
|
||||||
|
|
||||||
|
### Stateless API
|
||||||
|
- All operations require explicit context (unlocked keyspace, keypair, etc.) as arguments.
|
||||||
|
- No hidden or global state; maximally testable and concurrency-friendly.
|
||||||
|
- Example:
|
||||||
|
```rust
|
||||||
|
let keyspace = vault.unlock_keyspace("personal", b"password").await?;
|
||||||
|
let signature = keyspace.sign("key1", &msg).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Manager API
|
||||||
|
- Maintains in-memory state of unlocked keyspaces and current selections.
|
||||||
|
- Provides ergonomic methods for interactive apps (CLI, desktop, browser).
|
||||||
|
- Example:
|
||||||
|
```rust
|
||||||
|
let mut session = SessionManager::new();
|
||||||
|
session.unlock_keyspace("personal", b"password", &vault)?;
|
||||||
|
session.select_keypair("key1");
|
||||||
|
let signature = session.current_keypair().unwrap().sign(&msg)?;
|
||||||
|
session.logout(); // wipes all secrets from memory
|
||||||
|
```
|
||||||
|
|
||||||
|
### How They Work Together
|
||||||
|
- The **stateless API** is the core, always available and used internally by the session manager.
|
||||||
|
- The **session manager** is a thin, optional layer that wraps the stateless API for convenience.
|
||||||
|
- Applications can choose which pattern fits their needs, or even mix both (e.g., use stateless for background jobs, session manager for user sessions).
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
- **Flexibility:** Library users can pick the best model for their use case.
|
||||||
|
- **Security:** Session manager can enforce auto-lock, timeouts, and secure memory wiping.
|
||||||
|
- **Simplicity:** Stateless API is easy to test and reason about, while session manager improves UX for interactive flows.
|
||||||
|
|
||||||
|
### Commitment: Provide Both APIs
|
||||||
|
- **Both stateless and session-based APIs will be provided in the vault crate.**
|
||||||
|
- Stateless API: For backend, automation, or library contexts—explicit, functional, and concurrency-friendly.
|
||||||
|
- Session manager API: For UI/UX-focused applications—ergonomic, stateful, and user-friendly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### VaultMetadata & Keyspace Model
|
||||||
|
```rust
|
||||||
|
struct VaultMetadata {
|
||||||
|
name: String,
|
||||||
|
keyspaces: Vec<KeyspaceMetadata>,
|
||||||
|
// ... other vault-level metadata (optionally encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KeyspaceMetadata {
|
||||||
|
name: String,
|
||||||
|
salt: [u8; 16], // Unique salt for this keyspace
|
||||||
|
encrypted_blob: Vec<u8>, // All keypairs & secrets, encrypted with keyspace password
|
||||||
|
// ... other keyspace metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// The decrypted contents of a keyspace:
|
||||||
|
struct KeyspaceData {
|
||||||
|
keypairs: Vec<KeyEntry>,
|
||||||
|
// ... other keyspace-level metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KeyEntry {
|
||||||
|
id: String,
|
||||||
|
key_type: KeyType,
|
||||||
|
private_key: Vec<u8>, // Only present in memory after decryption
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
metadata: Option<KeyMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyType {
|
||||||
|
Secp256k1,
|
||||||
|
Ed25519,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- The vault contains a list of keyspaces, each with its own salt and encrypted blob.
|
||||||
|
- Each keyspace is unlocked independently using its password and salt.
|
||||||
|
- Key material is never stored unencrypted; only decrypted in memory after unlocking a keyspace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API Design (Keyspace Model)
|
||||||
|
|
||||||
|
### Vault
|
||||||
|
```rust
|
||||||
|
impl<S: KVStore + Send + Sync> Vault<S> {
|
||||||
|
async fn open(store: S) -> Result<Self, VaultError>;
|
||||||
|
async fn list_keyspaces(&self) -> Result<Vec<KeyspaceInfo>, VaultError>;
|
||||||
|
async fn create_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError>;
|
||||||
|
async fn delete_keyspace(&mut self, name: &str) -> Result<(), VaultError>;
|
||||||
|
async fn unlock_keyspace(&mut self, name: &str, password: &[u8]) -> Result<(), VaultError>;
|
||||||
|
async fn lock_keyspace(&mut self, name: &str);
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyspace Management
|
||||||
|
```rust
|
||||||
|
impl Keyspace {
|
||||||
|
fn is_unlocked(&self) -> bool;
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
async fn create_key(&mut self, key_type: KeyType, name: &str) -> Result<String, VaultError>;
|
||||||
|
async fn list_keys(&self) -> Result<Vec<KeyInfo>, VaultError>;
|
||||||
|
async fn sign(&self, key_id: &str, msg: &[u8]) -> Result<Signature, VaultError>;
|
||||||
|
async fn encrypt(&self, key_id: &str, plaintext: &[u8]) -> Result<Ciphertext, VaultError>;
|
||||||
|
async fn decrypt(&self, key_id: &str, ciphertext: &[u8]) -> Result<Vec<u8>, VaultError>;
|
||||||
|
async fn change_password(&mut self, old: &[u8], new: &[u8]) -> Result<(), VaultError>;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SessionManager
|
||||||
|
```rust
|
||||||
|
impl SessionManager {
|
||||||
|
fn select_key(&mut self, key_id: &str);
|
||||||
|
fn current_key(&self) -> Option<&KeyPair>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
vault/
|
||||||
|
├── src/
|
||||||
|
│ ├── lib.rs # Vault API and main logic
|
||||||
|
│ ├── data.rs # Data models: VaultData, KeyEntry, etc.
|
||||||
|
│ ├── crypto.rs # Symmetric/asymmetric crypto, key derivation
|
||||||
|
│ ├── session.rs # SessionManager
|
||||||
|
│ ├── error.rs # VaultError and error handling
|
||||||
|
│ └── utils.rs # Helpers, serialization, etc.
|
||||||
|
├── tests/
|
||||||
|
│ ├── native.rs # Native (sled) tests
|
||||||
|
│ └── wasm.rs # WASM (IndexedDB) tests
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Notes
|
||||||
|
|
||||||
|
- For further context on cryptographic choices, async patterns, and WASM compatibility, see `architecture.md`.
|
||||||
|
- This appendix is intended for developers extending or maintaining the Vault implementation.
|
||||||
|
|
||||||
|
### Cryptography: Crates and Algorithms
|
||||||
|
|
||||||
|
**Crates:**
|
||||||
|
- [`aes-gcm`](https://crates.io/crates/aes-gcm): AES-GCM authenticated encryption (WASM-compatible)
|
||||||
|
- [`chacha20poly1305`](https://crates.io/crates/chacha20poly1305): ChaCha20Poly1305 authenticated encryption (WASM-compatible)
|
||||||
|
- [`pbkdf2`](https://crates.io/crates/pbkdf2): Password-based key derivation (WASM-compatible)
|
||||||
|
- [`scrypt`](https://crates.io/crates/scrypt): Alternative KDF, strong and WASM-compatible
|
||||||
|
- [`k256`](https://crates.io/crates/k256): secp256k1 ECDSA (Ethereum keys)
|
||||||
|
- [`ed25519-dalek`](https://crates.io/crates/ed25519-dalek): Ed25519 keypairs
|
||||||
|
- [`rand_core`](https://crates.io/crates/rand_core): Randomness, WASM-compatible
|
||||||
|
- [`getrandom`](https://crates.io/crates/getrandom): Platform-agnostic RNG
|
||||||
|
|
||||||
|
**Algorithm Choices:**
|
||||||
|
- **Vault Encryption:**
|
||||||
|
- AES-256-GCM (default, via `aes-gcm`)
|
||||||
|
- Optionally ChaCha20Poly1305 (via `chacha20poly1305`)
|
||||||
|
- **Password Key Derivation:**
|
||||||
|
- PBKDF2-HMAC-SHA256 (via `pbkdf2`)
|
||||||
|
- Optionally scrypt (via `scrypt`)
|
||||||
|
- **Asymmetric Keypairs:**
|
||||||
|
- secp256k1 (via `k256`) for Ethereum/EVM
|
||||||
|
- Ed25519 (via `ed25519-dalek`) for general-purpose signatures
|
||||||
|
- **Randomness:**
|
||||||
|
- Use `rand_core` and `getrandom` for secure RNG in both native and WASM
|
||||||
|
|
||||||
|
**Feature-to-Algorithm Mapping:**
|
||||||
|
| Feature | Crate(s) | Algorithm(s) |
|
||||||
|
|------------------------|-----------------------|---------------------------|
|
||||||
|
| Vault encryption | aes-gcm, chacha20poly1305 | AES-256-GCM, ChaCha20Poly1305 |
|
||||||
|
| Password KDF | pbkdf2, scrypt | PBKDF2-HMAC-SHA256, scrypt|
|
||||||
|
| Symmetric encryption | aes-gcm, chacha20poly1305 | AES-256-GCM, ChaCha20Poly1305 |
|
||||||
|
| secp256k1 keypairs | k256 | secp256k1 ECDSA |
|
||||||
|
| Ed25519 keypairs | ed25519-dalek | Ed25519 |
|
||||||
|
| Randomness | rand_core, getrandom | OS RNG |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. WASM & Native Considerations
|
||||||
|
- Use only WASM-compatible crypto crates (`aes-gcm`, `chacha20poly1305`, `k256`, `ed25519-dalek`, etc).
|
||||||
|
- Use `wasm-bindgen`/`wasm-bindgen-futures` for browser interop.
|
||||||
|
- Use `tokio::task::spawn_blocking` for blocking crypto on native.
|
||||||
|
- All APIs are async and runtime-agnostic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Future Extensions
|
||||||
|
- Multi-user vaults (multi-password, access control)
|
||||||
|
- Hardware-backed key storage (YubiKey, WebAuthn)
|
||||||
|
- Key rotation and auditing
|
||||||
|
- Pluggable crypto algorithms
|
||||||
|
- Advanced metadata and tagging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. References
|
||||||
|
- See `docs/Architecture.md` and `docs/kvstore-vault-architecture.md` for high-level design and rationale.
|
||||||
|
- Crypto patterns inspired by industry best practices (e.g., Wire, Signal, Bitwarden).
|
@ -7,7 +7,38 @@ edition = "2021"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
vault = { path = "../vault" }
|
# Only universal/core dependencies here
|
||||||
|
|
||||||
|
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||||
|
rhai = "1.16"
|
||||||
|
ethers-core = "2.0"
|
||||||
|
rlp = "0.5"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
alloy = "0.6"
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
log = "0.4"
|
||||||
|
hex = "0.4"
|
||||||
|
k256 = { version = "0.13", features = ["ecdsa"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3.10"
|
||||||
|
wasm-bindgen-test = "0.3"
|
||||||
|
web-sys = { version = "0.3", features = ["console"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
|
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
|
# console_error_panic_hook = "0.1"
|
||||||
|
gloo-net = { version = "0.5", features = ["http"] }
|
||||||
|
console_log = "1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||||
|
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),
|
||||||
|
}
|
@ -2,22 +2,163 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//! evm_client: Minimal EVM JSON-RPC client abstraction
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
//! evm_client: Minimal EVM JSON-RPC client abstraction
|
||||||
pub enum EvmError {
|
|
||||||
#[error("RPC error: {0}")]
|
//! evm_client: Minimal EVM JSON-RPC client abstraction
|
||||||
Rpc(String),
|
|
||||||
#[error("Vault error: {0}")]
|
//! evm_client: Minimal EVM JSON-RPC client abstraction
|
||||||
Vault(String),
|
|
||||||
|
//! evm_client: Minimal EVM JSON-RPC client abstraction
|
||||||
|
|
||||||
|
pub use ethers_core::types::*;
|
||||||
|
pub mod provider;
|
||||||
|
pub mod signer;
|
||||||
|
pub mod rhai_bindings;
|
||||||
|
pub mod rhai_sync_helpers;
|
||||||
|
pub mod error;
|
||||||
|
pub use provider::send_rpc;
|
||||||
|
pub use error::EvmError;
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
// ... fields for RPC, vault, etc.
|
pub provider: Provider,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EvmClient {
|
impl EvmClient {
|
||||||
pub async fn connect(_rpc_url: &str) -> Result<Self, EvmError> {
|
pub fn new(provider: Provider) -> Self {
|
||||||
todo!("Implement connect")
|
Self { provider }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize logging for the current target (native: env_logger, WASM: console_log)
|
||||||
|
/// 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)
|
||||||
}
|
}
|
||||||
// ... other API stubs
|
|
||||||
}
|
}
|
||||||
|
97
evm_client/src/provider.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Minimal provider abstraction for EVM JSON-RPC
|
||||||
|
// Uses ethers-core for types and signing
|
||||||
|
// Uses gloo-net (WASM) or reqwest (native) for HTTP
|
||||||
|
use std::error::Error;
|
||||||
|
use ethers_core::types::{U256, Address, Bytes};
|
||||||
|
use rlp::RlpStream;
|
||||||
|
|
||||||
|
/// Send a JSON-RPC POST request to an EVM node.
|
||||||
|
pub async fn send_rpc(url: &str, body: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
use gloo_net::http::Request;
|
||||||
|
let resp = Request::post(url)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(body)?
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(resp.text().await?)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = client
|
||||||
|
.post(url)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(body.to_string())
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
Ok(resp.text().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct Transaction {
|
||||||
|
pub to: Address,
|
||||||
|
pub value: U256,
|
||||||
|
pub data: Bytes,
|
||||||
|
pub gas: Option<U256>,
|
||||||
|
pub gas_price: Option<U256>,
|
||||||
|
pub nonce: Option<U256>,
|
||||||
|
pub chain_id: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transaction {
|
||||||
|
pub fn rlp_encode_unsigned(&self) -> Vec<u8> {
|
||||||
|
let mut s = RlpStream::new_list(9);
|
||||||
|
s.append(&self.nonce);
|
||||||
|
s.append(&self.gas_price);
|
||||||
|
s.append(&self.gas);
|
||||||
|
s.append(&self.to);
|
||||||
|
s.append(&self.value);
|
||||||
|
s.append(&self.data.to_vec());
|
||||||
|
s.append(&self.chain_id);
|
||||||
|
s.append(&0u8);
|
||||||
|
s.append(&0u8);
|
||||||
|
s.out().to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to parse a 65-byte secp256k1 signature into (r, s, v) for EVM.
|
||||||
|
/// Assumes signature is [r (32 bytes) | s (32 bytes) | v (1 byte)]
|
||||||
|
pub fn parse_signature_rs_v(sig: &[u8], chain_id: u64) -> Option<(U256, U256, u64)> {
|
||||||
|
if sig.len() != 65 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut r_bytes = [0u8; 32];
|
||||||
|
r_bytes.copy_from_slice(&sig[0..32]);
|
||||||
|
let r = U256::from_big_endian(&r_bytes);
|
||||||
|
let mut s_bytes = [0u8; 32];
|
||||||
|
s_bytes.copy_from_slice(&sig[32..64]);
|
||||||
|
let s = U256::from_big_endian(&s_bytes);
|
||||||
|
let mut v = sig[64] as u64;
|
||||||
|
// EIP-155: v = recid + 35 + chain_id * 2
|
||||||
|
if v < 27 { v += 27; }
|
||||||
|
v = v + chain_id * 2 + 8;
|
||||||
|
Some((r, s, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
// let (r, s, v) = parse_signature_rs_v(&signature, tx.chain_id).unwrap();
|
||||||
|
// Use these for EVM transaction serialization.
|
||||||
|
|
||||||
|
/// Query the balance of an Ethereum address using eth_getBalance
|
||||||
|
pub async fn get_balance(url: &str, address: Address) -> Result<U256, Box<dyn std::error::Error>> {
|
||||||
|
use serde_json::json;
|
||||||
|
let body = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "eth_getBalance",
|
||||||
|
"params": [format!("0x{:x}", address), "latest"],
|
||||||
|
"id": 1
|
||||||
|
}).to_string();
|
||||||
|
let resp = send_rpc(url, &body).await?;
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&resp)?;
|
||||||
|
let hex = v["result"].as_str().ok_or("No result field in RPC response")?;
|
||||||
|
let balance = U256::from_str_radix(hex.trim_start_matches("0x"), 16)?;
|
||||||
|
Ok(balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
45
evm_client/src/rhai_bindings.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
//! Rhai bindings for EVM Client module
|
||||||
|
//! Provides a single source of truth for scripting integration for EVM actions.
|
||||||
|
|
||||||
|
use rhai::{Engine, Map};
|
||||||
|
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
|
||||||
|
|
||||||
|
/// Register EVM Client APIs with the Rhai scripting engine.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClient>) {
|
||||||
|
/// Rhai-friendly wrapper for EvmClient, allowing method registration and instance sharing.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RhaiEvmClient {
|
||||||
|
inner: std::sync::Arc<EvmClient>,
|
||||||
|
}
|
||||||
|
impl RhaiEvmClient {
|
||||||
|
/// Get balance using the EVM client.
|
||||||
|
pub fn get_balance(&self, address_hex: String) -> Result<String, String> {
|
||||||
|
use ethers_core::types::Address;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
engine.register_type::<RhaiEvmClient>();
|
||||||
|
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
||||||
|
// Register instance for scripts
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn register_rhai_api(engine: &mut Engine) {
|
||||||
|
// In WASM, register global functions that operate on the singleton/global EvmClient
|
||||||
|
engine.register_fn("get_balance", |provider_url: String, public_key: rhai::Blob| -> Result<String, String> {
|
||||||
|
// WASM: get_balance is async, so error if called from Rhai
|
||||||
|
Err("get_balance is async in WASM; use the WASM get_balance() API from JS instead".to_string())
|
||||||
|
});
|
||||||
|
engine.register_fn("send_transaction", |provider_url: String, key_id: String, password: rhai::Blob, tx_data: rhai::Map| -> Result<String, String> {
|
||||||
|
// WASM: send_transaction is async, so error if called from Rhai
|
||||||
|
Err("send_transaction is async in WASM; use the WASM send_transaction() API from JS instead".to_string())
|
||||||
|
});
|
||||||
|
// No global evm object in WASM; use JS/WASM API for EVM ops
|
||||||
|
}
|
||||||
|
|
37
evm_client/src/rhai_sync_helpers.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//! Synchronous wrappers for async EVM client APIs for use in Rhai bindings.
|
||||||
|
//! These use block_on for native, and should be adapted for WASM as needed.
|
||||||
|
|
||||||
|
use crate::EvmClient;
|
||||||
|
use rhai::Map;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
/// Synchronously get the balance using the EVM client.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn get_balance_sync(
|
||||||
|
evm_client: &EvmClient,
|
||||||
|
address: ethers_core::types::Address,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
Handle::current().block_on(async {
|
||||||
|
evm_client.get_balance(address)
|
||||||
|
.await
|
||||||
|
.map(|b| b.to_string())
|
||||||
|
.map_err(|e| format!("get_balance error: {e}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronously send a transaction using the EVM client.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn send_transaction_sync(
|
||||||
|
evm_client: &EvmClient,
|
||||||
|
tx: crate::provider::Transaction,
|
||||||
|
signer: &dyn crate::signer::Signer,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
Handle::current().block_on(async {
|
||||||
|
evm_client.send_transaction(tx, signer)
|
||||||
|
.await
|
||||||
|
.map(|tx| format!("0x{:x}", tx))
|
||||||
|
.map_err(|e| format!("send_transaction error: {e}"))
|
||||||
|
})
|
||||||
|
}
|
8
evm_client/src/signer.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use super::error::EvmError;
|
||||||
|
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||||
|
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||||
|
pub trait Signer: Send + Sync {
|
||||||
|
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, EvmError>;
|
||||||
|
fn address(&self) -> String;
|
||||||
|
}
|
25
evm_client/src/utils.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// No longer needed: use serde_json and ethers-core utilities directly.
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub async fn http_post(url: &str, body: &str) -> Result<serde_json::Value, EvmError> {
|
||||||
|
let resp = reqwest::Client::new()
|
||||||
|
.post(url)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(body.to_owned())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let json = resp.json().await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn http_post(url: &str, body: &str) -> Result<serde_json::Value, EvmError> {
|
||||||
|
use gloo_net::http::Request;
|
||||||
|
let resp = Request::post(url)
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(body).map_err(|e| EvmError::Rpc(e.to_string()))?
|
||||||
|
.send().await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
let json = resp.json().await.map_err(|e| EvmError::Rpc(e.to_string()))?;
|
||||||
|
Ok(json)
|
||||||
|
}
|
49
evm_client/tests/balance.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rlp_encode_unsigned() {
|
||||||
|
use ethers_core::types::{Address, U256, Bytes};
|
||||||
|
use evm_client::provider::Transaction;
|
||||||
|
|
||||||
|
let tx = Transaction {
|
||||||
|
to: Address::zero(),
|
||||||
|
value: U256::from(100),
|
||||||
|
data: Bytes::new(),
|
||||||
|
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();
|
||||||
|
assert!(!rlp.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_signature_rs_v() {
|
||||||
|
use ethers_core::types::U256;
|
||||||
|
use evm_client::provider::parse_signature_rs_v;
|
||||||
|
|
||||||
|
let mut sig = [0u8; 65];
|
||||||
|
sig[31] = 1; sig[63] = 2; sig[64] = 27;
|
||||||
|
let (r, s, v) = parse_signature_rs_v(&sig, 1).unwrap();
|
||||||
|
assert_eq!(r, U256::from(1));
|
||||||
|
assert_eq!(s, U256::from(2));
|
||||||
|
assert_eq!(v, 27 + 1 * 2 + 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_balance_real_address() {
|
||||||
|
use ethers_core::types::{Address, U256};
|
||||||
|
use evm_client::provider::get_balance;
|
||||||
|
|
||||||
|
// Vitalik's address
|
||||||
|
let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
||||||
|
let address = ethers_core::types::Address::from_slice(&hex::decode(address).unwrap());
|
||||||
|
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
||||||
|
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");
|
||||||
|
}
|
11
evm_client/tests/evm_client.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
// tests/evm_client.rs
|
||||||
|
use evm_client::send_rpc;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_send_rpc_smoke() {
|
||||||
|
// This test just checks the function compiles and can be called.
|
||||||
|
let url = "http://localhost:8545";
|
||||||
|
let body = r#"{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}"#;
|
||||||
|
let _ = send_rpc(url, body).await;
|
||||||
|
}
|
47
evm_client/tests/wasm.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// This file contains WASM-only integration tests for EVM client balance and signing logic.
|
||||||
|
// All code is strictly separated from native using cfg attributes.
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
use evm_client::provider::{Transaction, parse_signature_rs_v, get_balance};
|
||||||
|
use ethers_core::types::{U256, Address, Bytes};
|
||||||
|
use hex;
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_rlp_encode_unsigned() {
|
||||||
|
let tx = Transaction {
|
||||||
|
to: Address::zero(),
|
||||||
|
value: U256::from(100),
|
||||||
|
data: Bytes::new(),
|
||||||
|
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();
|
||||||
|
assert!(!rlp.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[wasm_bindgen_test(async)]
|
||||||
|
pub async fn test_get_balance_real_address_wasm_unique() {
|
||||||
|
web_sys::console::log_1(&"WASM balance test running!".into());
|
||||||
|
// Vitalik's address
|
||||||
|
let address = "d8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
|
||||||
|
let address = Address::from_slice(&hex::decode(address).unwrap());
|
||||||
|
let url = "https://ethereum.blockpi.network/v1/rpc/public";
|
||||||
|
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());
|
||||||
|
assert!(balance > U256::zero(), "Vitalik's balance should be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen_test]
|
||||||
|
fn test_parse_signature_rs_v() {
|
||||||
|
let mut sig = [0u8; 65];
|
||||||
|
sig[31] = 1; sig[63] = 2; sig[64] = 27;
|
||||||
|
let (r, s, v) = parse_signature_rs_v(&sig, 1).unwrap();
|
||||||
|
assert_eq!(r, U256::from(1));
|
||||||
|
assert_eq!(s, U256::from(2));
|
||||||
|
assert_eq!(v, 27 + 1 * 2 + 8);
|
||||||
|
}
|
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: ['../']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -7,25 +7,27 @@ edition = "2021"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
sled = { version = "0.34", optional = true }
|
|
||||||
idb = { version = "0.4", optional = true }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
js-sys = "0.3"
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
wasm-bindgen = "0.2"
|
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
sled = { version = "0.34" }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
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"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
native = ["sled", "tokio"]
|
native = []
|
||||||
web = ["idb"]
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
|
||||||
tokio = { version = "1.45", optional = true, default-features = false, features = ["rt-multi-thread", "macros"] }
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
|
||||||
idb = "0.4"
|
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
|
||||||
wasm-bindgen-test = "0.3"
|
|
||||||
|
@ -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:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! # Runtime Requirement
|
//! # Runtime Requirement
|
||||||
//!
|
//!
|
||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
//! **A Tokio runtime must be running to use this backend.**
|
//! **A Tokio runtime must be running to use this backend.**
|
||||||
//! This library does not start or manage a runtime; it assumes that all async methods are called from within an existing Tokio runtime context (e.g., via `#[tokio::main]` or `tokio::test`).
|
//! This library does not start or manage a runtime; it assumes that all async methods are called from within an existing Tokio runtime context (e.g., via `#[tokio::main]` or `tokio::test`).
|
||||||
//!
|
//!
|
||||||
@ -10,11 +11,18 @@
|
|||||||
//! # Example
|
//! # Example
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use crate::traits::KVStore;
|
//! Native backend for kvstore using sled
|
||||||
use crate::error::{KVError, Result};
|
//! Only compiled for non-wasm32 targets
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use crate::traits::KVStore;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use crate::error::{KVError, Result};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use sled::Db;
|
use sled::Db;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -16,6 +16,11 @@ use crate::error::Result;
|
|||||||
/// - contains_key (was exists)
|
/// - contains_key (was exists)
|
||||||
/// - keys
|
/// - keys
|
||||||
/// - clear
|
/// - clear
|
||||||
|
/// Async key-value store interface for both native and WASM backends.
|
||||||
|
///
|
||||||
|
/// For native (non-wasm32) backends, implementers should be `Send + Sync` to support async usage.
|
||||||
|
/// For WASM (wasm32) backends, `Send + Sync` is not required.
|
||||||
|
#[async_trait::async_trait]
|
||||||
pub trait KVStore {
|
pub trait KVStore {
|
||||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
|
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
|
||||||
async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
|
async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
|
||||||
|
@ -13,25 +13,30 @@
|
|||||||
//!
|
//!
|
||||||
|
|
||||||
|
|
||||||
|
//! WASM backend for kvstore using IndexedDB (idb crate)
|
||||||
|
//! Only compiled for wasm32 targets
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::traits::KVStore;
|
use crate::traits::KVStore;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::error::{KVError, Result};
|
use crate::error::{KVError, Result};
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
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;
|
||||||
|
// use wasm-bindgen directly for Uint8Array if needed
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use js_sys::Uint8Array;
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
const STORE_NAME: &str = "kv";
|
const STORE_NAME: &str = "kv";
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct WasmStore {
|
pub struct WasmStore {
|
||||||
db: Database,
|
db: Rc<Database>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -41,13 +46,14 @@ 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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let db = open_req.await.map_err(|e| KVError::Other(format!("IndexedDB open error: {e:?}")))?;
|
let db = open_req.await.map_err(|e| KVError::Other(format!("IndexedDB open error: {e:?}")))?;
|
||||||
Ok(Self { db })
|
Ok(Self { db: Rc::new(db) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,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)
|
||||||
}
|
}
|
||||||
@ -74,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<()> {
|
||||||
@ -84,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> {
|
||||||
@ -97,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,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();
|
||||||
|
@ -7,9 +7,54 @@ edition = "2021"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
once_cell = "1.18"
|
||||||
|
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||||
kvstore = { path = "../kvstore" }
|
kvstore = { path = "../kvstore" }
|
||||||
|
scrypt = "0.11"
|
||||||
|
sha2 = "0.10"
|
||||||
|
# aes-gcm = "0.10"
|
||||||
|
pbkdf2 = "0.12"
|
||||||
|
signature = "2.2"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
chacha20poly1305 = "0.10"
|
chacha20poly1305 = "0.10"
|
||||||
k256 = "0.13"
|
k256 = { version = "0.13", features = ["ecdsa"] }
|
||||||
|
ed25519-dalek = "2.1"
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
|
log = "0.4"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
console_log = "1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
env_logger = "0.11"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
hex = "0.4"
|
||||||
|
zeroize = "1.8.1"
|
||||||
|
rhai = "1.21.0"
|
||||||
|
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen-test = "0.3"
|
||||||
|
# console_error_panic_hook = "0.1"
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||||
|
tempfile = "3.10"
|
||||||
|
tokio = { version = "1.0", features = ["rt", "macros"] }
|
||||||
|
async-std = { version = "1", features = ["attributes"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
|
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
js-sys = "0.3"
|
||||||
|
# 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 = []
|
68
vault/README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# vault: Cryptographic Vault for Native and WASM
|
||||||
|
|
||||||
|
`vault` provides a secure, async, and cross-platform cryptographic key management system. It leverages the `kvstore` crate for persistent storage and supports both native (desktop/server) and WASM (browser) environments.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Keyspace management**: Create, unlock, and manage encrypted keyspaces.
|
||||||
|
- **Keypair operations**: Add, remove, list, export, and use keypairs for signing and verification.
|
||||||
|
- **End-to-end encryption**: All key material is encrypted at rest using modern ciphers (ChaCha20Poly1305, AES-GCM).
|
||||||
|
- **Async API**: All operations are async and runtime-agnostic.
|
||||||
|
- **Cross-platform**: Native uses `sled` via `kvstore::native::NativeStore`, WASM uses IndexedDB via `kvstore::wasm::WasmStore`.
|
||||||
|
- **Pluggable logging**: Uses the standard `log` crate for logging, with recommended backends for native (`env_logger`) and WASM (`console_log`).
|
||||||
|
|
||||||
|
## Logging Best Practices
|
||||||
|
|
||||||
|
This crate uses the [`log`](https://docs.rs/log) crate for logging. For native tests, use [`env_logger`](https://docs.rs/env_logger); for WASM tests, use [`console_log`](https://docs.rs/console_log).
|
||||||
|
|
||||||
|
- Native (in tests):
|
||||||
|
```rust
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
log::info!("test started");
|
||||||
|
```
|
||||||
|
- WASM (in tests):
|
||||||
|
```rust
|
||||||
|
console_log::init_with_level(log::Level::Debug).expect("error initializing logger");
|
||||||
|
log::debug!("wasm test started");
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `log::debug!`, `log::info!`, `log::error!`, etc., throughout the codebase for consistent and idiomatic logging. Do not prefix messages with [DEBUG], [ERROR], etc. The log level is handled by the logger.
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use vault::{Vault, KeyType, KeyMetadata};
|
||||||
|
use kvstore::native::NativeStore;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let store = NativeStore::open("/tmp/vaultdb").unwrap();
|
||||||
|
let mut vault = Vault::new(store);
|
||||||
|
let keyspace = "myspace";
|
||||||
|
let password = b"secret";
|
||||||
|
vault.create_keyspace(keyspace, password, "pbkdf2", "chacha20poly1305", None).await.unwrap();
|
||||||
|
let key_id = vault.add_keypair(keyspace, password, KeyType::Ed25519, None).await.unwrap();
|
||||||
|
println!("Created keypair: {}", key_id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For WASM/browser, use `kvstore::wasm::WasmStore` and initialize logging with `console_log`.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Native
|
||||||
|
```sh
|
||||||
|
cargo test -p vault --features native
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM
|
||||||
|
```sh
|
||||||
|
wasm-pack test --headless --firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
- All cryptographic operations use vetted RustCrypto crates.
|
||||||
|
- Password-based key derivation uses PBKDF2 by default (10,000 iterations).
|
||||||
|
- All sensitive data is encrypted before storage.
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT OR Apache-2.0
|
49
vault/src/crypto.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//! Crypto utilities for the vault crate
|
||||||
|
|
||||||
|
//! Crypto utilities for the vault crate
|
||||||
|
use chacha20poly1305::{ChaCha20Poly1305, KeyInit as ChaChaKeyInit, aead::{Aead, generic_array::GenericArray}};
|
||||||
|
|
||||||
|
use pbkdf2::pbkdf2_hmac;
|
||||||
|
|
||||||
|
use sha2::Sha256;
|
||||||
|
use rand_core::{RngCore, OsRng as RandOsRng};
|
||||||
|
|
||||||
|
pub mod kdf {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn derive_key_pbkdf2(password: &[u8], salt: &[u8], key_len: usize, iterations: u32) -> Vec<u8> {
|
||||||
|
let mut key = vec![0u8; key_len];
|
||||||
|
pbkdf2_hmac::<Sha256>(password, salt, iterations, &mut key);
|
||||||
|
key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod cipher {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
pub fn encrypt_chacha20(key: &[u8], plaintext: &[u8], nonce: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
|
let cipher = ChaCha20Poly1305::new(GenericArray::from_slice(key));
|
||||||
|
cipher.encrypt(GenericArray::from_slice(nonce), plaintext)
|
||||||
|
.map_err(|e| format!("encryption error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_chacha20(key: &[u8], ciphertext: &[u8], nonce: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
|
let cipher = ChaCha20Poly1305::new(GenericArray::from_slice(key));
|
||||||
|
cipher.decrypt(GenericArray::from_slice(nonce), ciphertext)
|
||||||
|
.map_err(|e| format!("decryption error: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn random_salt(len: usize) -> Vec<u8> {
|
||||||
|
let mut salt = vec![0u8; len];
|
||||||
|
RandOsRng.fill_bytes(&mut salt);
|
||||||
|
salt
|
||||||
|
}
|
66
vault/src/data.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! Data models for the vault crate
|
||||||
|
|
||||||
|
// Only keep serde derives on structs, remove unused imports
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct VaultMetadata {
|
||||||
|
pub name: String,
|
||||||
|
pub keyspaces: Vec<KeyspaceMetadata>,
|
||||||
|
// ... other vault-level metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct KeyspaceMetadata {
|
||||||
|
pub name: String,
|
||||||
|
pub salt: [u8; 16], // Unique salt for this keyspace
|
||||||
|
pub encrypted_blob: Vec<u8>,
|
||||||
|
pub created_at: Option<u64>, // Unix timestamp
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
// ... other keyspace metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
|
pub struct KeyspaceData {
|
||||||
|
pub keypairs: Vec<KeyEntry>,
|
||||||
|
// ... 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)]
|
||||||
|
pub struct KeyEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub key_type: KeyType,
|
||||||
|
pub private_key: Vec<u8>, // Only present in memory after decryption
|
||||||
|
pub public_key: Vec<u8>,
|
||||||
|
pub metadata: Option<KeyMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum KeyType {
|
||||||
|
Secp256k1,
|
||||||
|
Ed25519,
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct KeyMetadata {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub created_at: Option<u64>,
|
||||||
|
pub tags: Option<Vec<String>>,
|
||||||
|
// ...
|
||||||
|
}
|
21
vault/src/error.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//! Error types for the vault crate
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum VaultError {
|
||||||
|
#[error("Storage error: {0}")]
|
||||||
|
Storage(String),
|
||||||
|
#[error("Crypto error: {0}")]
|
||||||
|
Crypto(String),
|
||||||
|
#[error("Unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
#[error("Keyspace not found: {0}")]
|
||||||
|
KeyspaceNotFound(String),
|
||||||
|
#[error("Key not found: {0}")]
|
||||||
|
KeyNotFound(String),
|
||||||
|
#[error("Invalid password")]
|
||||||
|
InvalidPassword,
|
||||||
|
#[error("Serialization error: {0}")]
|
||||||
|
Serialization(String),
|
||||||
|
#[error("Other: {0}")]
|
||||||
|
Other(String),
|
||||||
|
}
|
428
vault/src/lib.rs
@ -1,27 +1,421 @@
|
|||||||
//! vault: Cryptographic keyspace and operations
|
//! vault: Cryptographic keyspace and operations
|
||||||
|
|
||||||
|
|
||||||
use kvstore::KVStore;
|
//! vault: Cryptographic keyspace and operations
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
pub mod data;
|
||||||
pub enum VaultError {
|
pub use crate::session::SessionManager;
|
||||||
#[error("Storage error: {0}")]
|
pub use crate::data::{KeyType, KeyMetadata, KeyEntry};
|
||||||
Storage(String),
|
mod error;
|
||||||
#[error("Crypto error: {0}")]
|
mod crypto;
|
||||||
Crypto(String),
|
pub mod session;
|
||||||
#[error("Unauthorized")]
|
mod utils;
|
||||||
Unauthorized,
|
mod rhai_sync_helpers;
|
||||||
}
|
pub mod rhai_bindings;
|
||||||
|
|
||||||
pub struct Vault<S: KVStore + Send + Sync> {
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub mod session_singleton;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub mod wasm_helpers;
|
||||||
|
|
||||||
|
|
||||||
|
pub use kvstore::traits::KVStore;
|
||||||
|
use data::*;
|
||||||
|
use error::VaultError;
|
||||||
|
use crate::crypto::random_salt;
|
||||||
|
use crate::crypto::kdf;
|
||||||
|
|
||||||
|
use crate::crypto::cipher::{encrypt_chacha20, decrypt_chacha20};
|
||||||
|
use signature::SignatureEncoding;
|
||||||
|
// TEMP: File-based debug logger for crypto troubleshooting
|
||||||
|
use log::{debug};
|
||||||
|
|
||||||
|
/// Vault: Cryptographic keyspace and operations
|
||||||
|
pub struct Vault<S: KVStore> {
|
||||||
storage: S,
|
storage: S,
|
||||||
// ... other fields
|
// Optionally: cache of unlocked keyspaces, etc.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: KVStore + Send + Sync> Vault<S> {
|
/// Helper to encrypt and prepend nonce to ciphertext for keyspace storage
|
||||||
/// Creates a new keyspace. Implementation pending.
|
/// Helper to encrypt and prepend nonce to ciphertext for keyspace storage
|
||||||
pub async fn create_keyspace(_dummy: ()) -> Result<(), VaultError> {
|
/// Always uses ChaCha20Poly1305.
|
||||||
todo!("Implement create_keyspace")
|
fn encrypt_with_nonce_prepended(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||||
|
let nonce = random_salt(12);
|
||||||
|
debug!("nonce: {}", hex::encode(&nonce));
|
||||||
|
// Always use ChaCha20Poly1305 for encryption
|
||||||
|
let ct = encrypt_chacha20(key, plaintext, &nonce)
|
||||||
|
.map_err(|e| VaultError::Crypto(e))?;
|
||||||
|
debug!("ct: {}", hex::encode(&ct));
|
||||||
|
debug!("key: {}", hex::encode(key));
|
||||||
|
let mut blob = nonce.clone();
|
||||||
|
blob.extend_from_slice(&ct);
|
||||||
|
debug!("ENCRYPTED (nonce|ct): {}", hex::encode(&blob));
|
||||||
|
Ok(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: KVStore> Vault<S> {
|
||||||
|
pub fn new(storage: S) -> Self {
|
||||||
|
Self { storage }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new keyspace with the given name, password, and options.
|
||||||
|
/// Create a new keyspace with the given name and password. Always uses PBKDF2 and ChaCha20Poly1305.
|
||||||
|
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
|
||||||
|
// Check if keyspace already exists
|
||||||
|
if self.storage.get(name).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?.is_some() {
|
||||||
|
debug!("keyspace '{}' already exists", name);
|
||||||
|
return Err(VaultError::Crypto("Keyspace already exists".to_string()));
|
||||||
|
}
|
||||||
|
debug!("entry: name={}", name);
|
||||||
|
use crate::crypto::{random_salt, kdf};
|
||||||
|
use crate::data::{KeyspaceMetadata, KeyspaceData};
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
// 1. Generate salt
|
||||||
|
let salt = random_salt(16);
|
||||||
|
debug!("salt: {:?}", salt);
|
||||||
|
// 2. Derive key
|
||||||
|
// Always use PBKDF2 for key derivation
|
||||||
|
let key = kdf::derive_key_pbkdf2(password, &salt, 32, 10_000);
|
||||||
|
debug!("derived key: {} bytes", key.len());
|
||||||
|
// 3. Prepare initial keyspace data
|
||||||
|
let keyspace_data = KeyspaceData { keypairs: vec![] };
|
||||||
|
let plaintext = match serde_json::to_vec(&keyspace_data) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serde_json error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("plaintext serialized: {} bytes", plaintext.len());
|
||||||
|
// 4. Generate nonce (12 bytes for both ciphers)
|
||||||
|
let nonce = random_salt(12);
|
||||||
|
debug!("nonce: {}", hex::encode(&nonce));
|
||||||
|
// 5. Encrypt
|
||||||
|
// Always use ChaCha20Poly1305 for encryption
|
||||||
|
let encrypted_blob = encrypt_with_nonce_prepended(&key, &plaintext)?;
|
||||||
|
debug!("encrypted_blob: {} bytes", encrypted_blob.len());
|
||||||
|
debug!("encrypted_blob (hex): {}", hex::encode(&encrypted_blob));
|
||||||
|
// 6. Compose metadata
|
||||||
|
let metadata = KeyspaceMetadata {
|
||||||
|
name: name.to_string(),
|
||||||
|
salt: salt.try_into().unwrap_or([0u8; 16]),
|
||||||
|
encrypted_blob,
|
||||||
|
created_at: Some(crate::utils::now()),
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
// 7. Store in kvstore (keyed by keyspace name)
|
||||||
|
let meta_bytes = match serde_json::to_vec(&metadata) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serde_json metadata error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.storage.set(name, &meta_bytes).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
debug!("success");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all keyspaces (metadata only, not decrypted)
|
||||||
|
pub async fn list_keyspaces(&self) -> Result<Vec<KeyspaceMetadata>, VaultError> {
|
||||||
|
use serde_json;
|
||||||
|
// 1. List all keys in kvstore
|
||||||
|
let keys = self.storage.keys().await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
let mut keyspaces = Vec::new();
|
||||||
|
for key in keys {
|
||||||
|
if let Some(bytes) = self.storage.get(&key).await.map_err(|e| VaultError::Storage(format!("{e:?}")))? {
|
||||||
|
if let Ok(meta) = serde_json::from_slice::<KeyspaceMetadata>(&bytes) {
|
||||||
|
keyspaces.push(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(keyspaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unlock a keyspace by name and password, returning the decrypted data
|
||||||
|
/// Unlock a keyspace by name and password, returning the decrypted data
|
||||||
|
/// Always uses PBKDF2 and ChaCha20Poly1305.
|
||||||
|
pub async fn unlock_keyspace(&self, name: &str, password: &[u8]) -> Result<KeyspaceData, VaultError> {
|
||||||
|
debug!("unlock_keyspace entry: name={}", name);
|
||||||
|
// use crate::crypto::kdf; // removed if not needed
|
||||||
|
use serde_json;
|
||||||
|
// 1. Fetch keyspace metadata
|
||||||
|
let meta_bytes = self.storage.get(name).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
let meta_bytes = meta_bytes.ok_or(VaultError::KeyspaceNotFound(name.to_string()))?;
|
||||||
|
let metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes).map_err(|e| VaultError::Serialization(e.to_string()))?;
|
||||||
|
if metadata.salt.len() != 16 {
|
||||||
|
debug!("salt length {} != 16", metadata.salt.len());
|
||||||
|
return Err(VaultError::Crypto("Salt length must be 16 bytes".to_string()));
|
||||||
|
}
|
||||||
|
// 2. Derive key
|
||||||
|
let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000);
|
||||||
|
debug!("derived key: {} bytes", key.len());
|
||||||
|
|
||||||
|
let ciphertext = &metadata.encrypted_blob;
|
||||||
|
if ciphertext.len() < 12 {
|
||||||
|
debug!("ciphertext too short: {}", ciphertext.len());
|
||||||
|
return Err(VaultError::Crypto("Ciphertext too short".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (nonce, ct) = ciphertext.split_at(12);
|
||||||
|
debug!("nonce: {}", hex::encode(nonce));
|
||||||
|
let plaintext = decrypt_chacha20(&key, ct, nonce).map_err(VaultError::Crypto)?;
|
||||||
|
debug!("plaintext decrypted: {} bytes", plaintext.len());
|
||||||
|
// 4. Deserialize keyspace data
|
||||||
|
let keyspace_data: KeyspaceData = match serde_json::from_slice(&plaintext) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serde_json data error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("success");
|
||||||
|
Ok(keyspace_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lock a keyspace (remove from cache, if any)
|
||||||
|
/// Lock a keyspace (remove from cache, if any)
|
||||||
|
pub fn lock_keyspace(&mut self, _name: &str) {
|
||||||
|
// Optional: clear from in-memory cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Keypair Management APIs ---
|
||||||
|
|
||||||
|
/// Add a new keypair to a keyspace (generates and stores a new keypair)
|
||||||
|
/// Add a new keypair to a keyspace (generates and stores a new keypair)
|
||||||
|
/// If key_type is None, defaults to Secp256k1.
|
||||||
|
pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: Option<KeyType>, metadata: Option<KeyMetadata>) -> Result<String, VaultError> {
|
||||||
|
use crate::data::KeyEntry;
|
||||||
|
use rand_core::OsRng;
|
||||||
|
use rand_core::RngCore;
|
||||||
|
|
||||||
|
// 1. Unlock keyspace
|
||||||
|
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
// 2. Generate keypair
|
||||||
|
let key_type = key_type.unwrap_or(KeyType::Secp256k1);
|
||||||
|
let (private_key, public_key, id) = match key_type {
|
||||||
|
KeyType::Ed25519 => {
|
||||||
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut bytes);
|
||||||
|
let signing = SigningKey::from_bytes(&bytes);
|
||||||
|
let verifying: VerifyingKey = (&signing).into();
|
||||||
|
let priv_bytes = signing.to_bytes().to_vec();
|
||||||
|
let pub_bytes = verifying.to_bytes().to_vec();
|
||||||
|
let id = hex::encode(&pub_bytes);
|
||||||
|
(priv_bytes, pub_bytes, id)
|
||||||
|
},
|
||||||
|
KeyType::Secp256k1 => {
|
||||||
|
use k256::ecdsa::SigningKey;
|
||||||
|
|
||||||
|
let sk = SigningKey::random(&mut OsRng);
|
||||||
|
let pk = sk.verifying_key();
|
||||||
|
let priv_bytes = sk.to_bytes().to_vec();
|
||||||
|
let pub_bytes = pk.to_encoded_point(false).as_bytes().to_vec();
|
||||||
|
let id = hex::encode(&pub_bytes);
|
||||||
|
(priv_bytes, pub_bytes, id)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// 3. Add to keypairs
|
||||||
|
let entry = KeyEntry {
|
||||||
|
id: id.clone(),
|
||||||
|
key_type,
|
||||||
|
private_key,
|
||||||
|
public_key,
|
||||||
|
metadata,
|
||||||
|
};
|
||||||
|
data.keypairs.push(entry);
|
||||||
|
// 4. Re-encrypt and store
|
||||||
|
self.save_keyspace(keyspace, password, &data).await?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a keypair by id from a keyspace
|
||||||
|
pub async fn remove_keypair(&mut self, keyspace: &str, password: &[u8], key_id: &str) -> Result<(), VaultError> {
|
||||||
|
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
data.keypairs.retain(|k| k.id != key_id);
|
||||||
|
self.save_keyspace(keyspace, password, &data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all keypairs in a keyspace (public info only)
|
||||||
|
pub async fn list_keypairs(&self, keyspace: &str, password: &[u8]) -> Result<Vec<(String, KeyType)>, VaultError> {
|
||||||
|
let data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
Ok(data.keypairs.iter().map(|k| (k.id.clone(), k.key_type.clone())).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export a keypair's private and public key by id
|
||||||
|
pub async fn export_keypair(&self, keyspace: &str, password: &[u8], key_id: &str) -> Result<(Vec<u8>, Vec<u8>), VaultError> {
|
||||||
|
let data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?;
|
||||||
|
Ok((key.private_key.clone(), key.public_key.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save the updated keyspace data (helper)
|
||||||
|
async fn save_keyspace(&mut self, keyspace: &str, password: &[u8], data: &KeyspaceData) -> Result<(), VaultError> {
|
||||||
|
debug!("save_keyspace entry: keyspace={}", keyspace);
|
||||||
|
use crate::crypto::kdf;
|
||||||
|
use serde_json;
|
||||||
|
|
||||||
|
let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
debug!("got meta_bytes: {}", meta_bytes.as_ref().map(|v| v.len()).unwrap_or(0));
|
||||||
|
let meta_bytes = meta_bytes.ok_or(VaultError::KeyspaceNotFound(keyspace.to_string()))?;
|
||||||
|
let mut metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes).map_err(|e| VaultError::Serialization(e.to_string()))?;
|
||||||
|
debug!("metadata: salt={:?}", metadata.salt);
|
||||||
|
if metadata.salt.len() != 16 {
|
||||||
|
debug!("salt length {} != 16", metadata.salt.len());
|
||||||
|
return Err(VaultError::Crypto("Salt length must be 16 bytes".to_string()));
|
||||||
|
}
|
||||||
|
// 2. Derive key
|
||||||
|
let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000);
|
||||||
|
debug!("derived key: {} bytes", key.len());
|
||||||
|
// 3. Serialize plaintext
|
||||||
|
let plaintext = match serde_json::to_vec(data) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serde_json data error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("plaintext serialized: {} bytes", plaintext.len());
|
||||||
|
// 4. Generate nonce
|
||||||
|
let nonce = random_salt(12);
|
||||||
|
debug!("nonce: {}", hex::encode(&nonce));
|
||||||
|
// 5. Encrypt
|
||||||
|
let encrypted_blob = encrypt_with_nonce_prepended(&key, &plaintext)?;
|
||||||
|
debug!("encrypted_blob: {} bytes", encrypted_blob.len());
|
||||||
|
// 6. Store new encrypted blob
|
||||||
|
metadata.encrypted_blob = encrypted_blob;
|
||||||
|
let meta_bytes = match serde_json::to_vec(&metadata) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serde_json metadata error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.storage.set(keyspace, &meta_bytes).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
debug!("success");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign a message with a stored keypair in a keyspace
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `keyspace` - Keyspace name
|
||||||
|
/// * `password` - Keyspace password
|
||||||
|
/// * `key_id` - Keypair ID
|
||||||
|
/// * `message` - Message to sign
|
||||||
|
pub async fn sign(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||||
|
let data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?;
|
||||||
|
match key.key_type {
|
||||||
|
KeyType::Ed25519 => {
|
||||||
|
use ed25519_dalek::{SigningKey, Signer};
|
||||||
|
let signing = SigningKey::from_bytes(&key.private_key.clone().try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 private key length".to_string()))?);
|
||||||
|
let sig = signing.sign(message);
|
||||||
|
Ok(sig.to_bytes().to_vec())
|
||||||
|
}
|
||||||
|
KeyType::Secp256k1 => {
|
||||||
|
use k256::ecdsa::{SigningKey, signature::Signer};
|
||||||
|
let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| VaultError::Crypto("Invalid secp256k1 private key length".to_string()))?;
|
||||||
|
let sk = SigningKey::from_bytes(arr.into()).map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
|
let sig: k256::ecdsa::DerSignature = sk.sign(message);
|
||||||
|
Ok(sig.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a signature with a stored keypair in a keyspace
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `keyspace` - Keyspace name
|
||||||
|
/// * `password` - Keyspace password
|
||||||
|
/// * `key_id` - Keypair ID
|
||||||
|
/// * `message` - Message that was signed
|
||||||
|
/// * `signature` - Signature to verify
|
||||||
|
pub async fn verify(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8], signature: &[u8]) -> Result<bool, VaultError> {
|
||||||
|
let data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?;
|
||||||
|
match key.key_type {
|
||||||
|
KeyType::Ed25519 => {
|
||||||
|
use ed25519_dalek::{VerifyingKey, Signature, Verifier};
|
||||||
|
let verifying = VerifyingKey::from_bytes(&key.public_key.clone().try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 public key length".to_string()))?)
|
||||||
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
|
let sig = Signature::from_bytes(&signature.try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 signature length".to_string()))?);
|
||||||
|
Ok(verifying.verify(message, &sig).is_ok())
|
||||||
|
}
|
||||||
|
KeyType::Secp256k1 => {
|
||||||
|
use k256::ecdsa::{VerifyingKey, Signature, signature::Verifier};
|
||||||
|
let pk = VerifyingKey::from_sec1_bytes(&key.public_key).map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
|
let sig = Signature::from_der(signature).map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
|
Ok(pk.verify(message, &sig).is_ok())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypt a message using the keyspace symmetric cipher
|
||||||
|
/// (for simplicity, uses keyspace password-derived key)
|
||||||
|
pub async fn encrypt(&self, keyspace: &str, password: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||||
|
debug!("encrypt");
|
||||||
|
|
||||||
|
// 1. Load keyspace metadata
|
||||||
|
let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
let meta_bytes = match meta_bytes {
|
||||||
|
Some(val) => val,
|
||||||
|
None => {
|
||||||
|
debug!("keyspace not found");
|
||||||
|
return Err(VaultError::Other("Keyspace not found".to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serialization error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("salt={:?} (hex salt: {})", meta.salt, hex::encode(&meta.salt));
|
||||||
|
// 2. Derive key
|
||||||
|
let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000);
|
||||||
|
// 3. Generate nonce
|
||||||
|
let nonce = random_salt(12);
|
||||||
|
debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(&nonce));
|
||||||
|
// 4. Encrypt
|
||||||
|
let ciphertext = encrypt_chacha20(&key, plaintext, &nonce).map_err(VaultError::Crypto)?;
|
||||||
|
let mut out = nonce;
|
||||||
|
out.extend_from_slice(&ciphertext);
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt a message using the keyspace symmetric cipher
|
||||||
|
/// (for simplicity, uses keyspace password-derived key)
|
||||||
|
pub async fn decrypt(&self, keyspace: &str, password: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||||
|
debug!("decrypt");
|
||||||
|
|
||||||
|
// 1. Load keyspace metadata
|
||||||
|
let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||||
|
let meta_bytes = match meta_bytes {
|
||||||
|
Some(val) => val,
|
||||||
|
None => {
|
||||||
|
debug!("keyspace not found");
|
||||||
|
return Err(VaultError::Other("Keyspace not found".to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("serialization error: {}", e);
|
||||||
|
return Err(VaultError::Serialization(e.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
debug!("salt={:?} (hex salt: {})", meta.salt, hex::encode(&meta.salt));
|
||||||
|
// 2. Derive key
|
||||||
|
let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000);
|
||||||
|
// 3. Extract nonce
|
||||||
|
let nonce = &ciphertext[..12];
|
||||||
|
debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce));
|
||||||
|
// 4. Decrypt
|
||||||
|
let plaintext = decrypt_chacha20(&key, &ciphertext[12..], nonce).map_err(VaultError::Crypto)?;
|
||||||
|
Ok(plaintext)
|
||||||
}
|
}
|
||||||
// ... other API stubs
|
|
||||||
}
|
}
|
109
vault/src/rhai_bindings.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
//! Rhai bindings for Vault and EVM Client modules
|
||||||
|
//! Provides a single source of truth for scripting integration.
|
||||||
|
|
||||||
|
use rhai::Engine;
|
||||||
|
use crate::session::SessionManager;
|
||||||
|
|
||||||
|
|
||||||
|
/// Register core Vault and EVM Client APIs with the Rhai scripting engine.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static>(
|
||||||
|
engine: &mut Engine,
|
||||||
|
session_manager: std::sync::Arc<std::sync::Mutex<SessionManager<S>>>,
|
||||||
|
) {
|
||||||
|
engine.register_type::<RhaiSessionManager<S>>();
|
||||||
|
engine.register_fn("select_keypair", RhaiSessionManager::<S>::select_keypair);
|
||||||
|
engine.register_fn("sign", RhaiSessionManager::<S>::sign);
|
||||||
|
// No global constant registration: Rhai does not support this directly.
|
||||||
|
// Scripts should receive the session manager as a parameter or via module scope.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RhaiSessionManager<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> {
|
||||||
|
inner: std::sync::Arc<std::sync::Mutex<SessionManager<S>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RhaiSessionManager<S: kvstore::traits::KVStore + Clone + 'static> {
|
||||||
|
inner: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionManager<S> {
|
||||||
|
pub fn select_keypair(&self, key_id: String) -> Result<(), String> {
|
||||||
|
// Use Mutex for interior mutability, &self is sufficient
|
||||||
|
self.inner.lock().unwrap().select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}"))
|
||||||
|
}
|
||||||
|
pub fn sign(&self, message: rhai::Blob) -> Result<rhai::Blob, String> {
|
||||||
|
let sm = self.inner.lock().unwrap();
|
||||||
|
// Try to get the current keyspace name from session state if possible
|
||||||
|
let _keypair = sm.current_keypair().ok_or("No keypair selected")?;
|
||||||
|
// Sign using the session manager; password and keyspace are not needed (already unlocked)
|
||||||
|
crate::rhai_sync_helpers::sign_sync::<S>(
|
||||||
|
&sm,
|
||||||
|
&message,
|
||||||
|
).map_err(|e| format!("sign error: {e}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl<S: kvstore::traits::KVStore + Clone + 'static> RhaiSessionManager<S> {
|
||||||
|
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
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn register_rhai_api<S: kvstore::traits::KVStore + Clone + 'static>(
|
||||||
|
engine: &mut Engine,
|
||||||
|
// session_manager: &SessionManager<S>,
|
||||||
|
) {
|
||||||
|
// WASM registration logic (adapt as needed)
|
||||||
|
// Example: engine.register_type::<RhaiSessionManager<S>>();
|
||||||
|
// engine.register_fn(...);
|
||||||
|
// In WASM, register global functions that operate on the singleton
|
||||||
|
engine.register_fn("select_keypair", |key_id: String| {
|
||||||
|
crate::wasm_helpers::select_keypair_global(&key_id)
|
||||||
|
}); // Calls the shared WASM session singleton
|
||||||
|
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())
|
||||||
|
});
|
||||||
|
// No global session object in WASM; use JS/WASM API for session ops
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sync wrappers for async Rust APIs (to be implemented with block_on or similar) ---
|
||||||
|
// These should be implemented in a separate module (rhai_sync_helpers.rs)
|
||||||
|
// and use block_on or spawn_local for WASM compatibility.
|
22
vault/src/rhai_sync_helpers.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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"))]
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
// Synchronous sign wrapper for Rhai: only supports signing the currently selected keypair in the unlocked keyspace
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn sign_sync<S: kvstore::traits::KVStore + Send + Sync + 'static>(
|
||||||
|
session_manager: &SessionManager<S>,
|
||||||
|
message: &[u8],
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
Handle::current().block_on(async {
|
||||||
|
session_manager.sign(message).await.map_err(|e| format!("sign error: {e}"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
227
vault/src/session.rs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
//! Session manager for the vault crate (optional)
|
||||||
|
//! Provides ergonomic, stateful access to unlocked keyspaces and keypairs for interactive applications.
|
||||||
|
//! All state is local to the SessionManager instance. No global state.
|
||||||
|
|
||||||
|
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub struct SessionManager<S: KVStore + Send + Sync> {
|
||||||
|
vault: Vault<S>,
|
||||||
|
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
|
||||||
|
current_keypair: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub struct SessionManager<S: KVStore> {
|
||||||
|
vault: Vault<S>,
|
||||||
|
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
|
||||||
|
current_keypair: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl<S: KVStore> SessionManager<S> {
|
||||||
|
pub fn get_vault_mut(&mut self) -> &mut Vault<S> {
|
||||||
|
&mut self.vault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl<S: KVStore + Send + Sync> 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(not(target_arch = "wasm32"))]
|
||||||
|
impl<S: KVStore + Send + Sync> Drop for SessionManager<S> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
12
vault/src/session_singleton.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//! WASM session singleton for the vault crate
|
||||||
|
//! This file defines the global SessionManager singleton for WASM builds.
|
||||||
|
|
||||||
|
use once_cell::unsync::Lazy;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use crate::session::SessionManager;
|
||||||
|
use kvstore::wasm::WasmStore;
|
||||||
|
|
||||||
|
// Thread-local singleton for WASM session management
|
||||||
|
thread_local! {
|
||||||
|
pub static SESSION_MANAGER: Lazy<RefCell<Option<SessionManager<WasmStore>>>> = Lazy::new(|| RefCell::new(None));
|
||||||
|
}
|
19
vault/src/utils.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//! Utility functions for the vault crate
|
||||||
|
|
||||||
|
// Add serialization helpers, random salt generation, etc.
|
||||||
|
|
||||||
|
/// Returns the current unix timestamp as u64 (seconds since epoch)
|
||||||
|
pub fn now() -> u64 {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
// Use JS Date.now() in milliseconds, convert to seconds
|
||||||
|
|
||||||
|
let date = js_sys::Date::new_0();
|
||||||
|
(date.get_time() / 1000.0) as u64
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
|
||||||
|
}
|
||||||
|
}
|
15
vault/src/wasm_helpers.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//! WASM-specific helpers for Rhai bindings and session management
|
||||||
|
//! Provides global functions for Rhai integration in WASM builds.
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub fn select_keypair_global(key_id: &str) -> Result<(), String> {
|
||||||
|
use crate::session_singleton::SESSION_MANAGER;
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
69
vault/tests/keypair_management.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
//! Tests for vault keypair management and crypto operations
|
||||||
|
use vault::{Vault, KeyType, KeyMetadata};
|
||||||
|
use kvstore::native::NativeStore;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_keypair_management_and_crypto() {
|
||||||
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
debug!("test_keypair_management_and_crypto started");
|
||||||
|
// Use NativeStore for native tests
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use tempfile::TempDir;
|
||||||
|
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");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let mut vault = Vault::new(store);
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
compile_error!("This test is not intended for wasm32 targets");
|
||||||
|
let keyspace = &format!("testspace_{}", chrono::Utc::now().timestamp_nanos_opt().unwrap());
|
||||||
|
let password = b"supersecret";
|
||||||
|
|
||||||
|
debug!("keyspace: {} password: {}", keyspace, hex::encode(password));
|
||||||
|
debug!("before create_keyspace");
|
||||||
|
vault.create_keyspace(keyspace, password, None).await.unwrap();
|
||||||
|
|
||||||
|
debug!("after create_keyspace: keyspace={} password={}", keyspace, hex::encode(password));
|
||||||
|
debug!("before add Ed25519 keypair");
|
||||||
|
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Ed25519), Some(KeyMetadata { name: Some("edkey".into()), created_at: None, tags: None })).await;
|
||||||
|
match &key_id {
|
||||||
|
Ok(_) => debug!("after add Ed25519 keypair (Ok)"),
|
||||||
|
Err(e) => debug!("after add Ed25519 keypair (Err): {:?}", e),
|
||||||
|
}
|
||||||
|
let key_id = key_id.unwrap();
|
||||||
|
debug!("before add secp256k1 keypair");
|
||||||
|
let secp_id = vault.add_keypair(keyspace, password, None, Some(KeyMetadata { name: Some("secpkey".into()), created_at: None, tags: None })).await.unwrap();
|
||||||
|
|
||||||
|
debug!("before list_keypairs");
|
||||||
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
|
assert_eq!(keys.len(), 2);
|
||||||
|
|
||||||
|
debug!("before export Ed25519 keypair");
|
||||||
|
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
|
||||||
|
assert!(!priv_bytes.is_empty() && !pub_bytes.is_empty());
|
||||||
|
|
||||||
|
debug!("before sign Ed25519");
|
||||||
|
let msg = b"hello world";
|
||||||
|
let sig = vault.sign(keyspace, password, &key_id, msg).await.unwrap();
|
||||||
|
debug!("before verify Ed25519");
|
||||||
|
let ok = vault.verify(keyspace, password, &key_id, msg, &sig).await.unwrap();
|
||||||
|
assert!(ok);
|
||||||
|
|
||||||
|
debug!("before sign secp256k1");
|
||||||
|
let sig2 = vault.sign(keyspace, password, &secp_id, msg).await.unwrap();
|
||||||
|
debug!("before verify secp256k1");
|
||||||
|
let ok2 = vault.verify(keyspace, password, &secp_id, msg, &sig2).await.unwrap();
|
||||||
|
assert!(ok2);
|
||||||
|
|
||||||
|
// Encrypt and decrypt
|
||||||
|
let ciphertext = vault.encrypt(keyspace, password, msg).await.unwrap();
|
||||||
|
let plaintext = vault.decrypt(keyspace, password, &ciphertext).await.unwrap();
|
||||||
|
assert_eq!(plaintext, msg);
|
||||||
|
|
||||||
|
// Remove a keypair
|
||||||
|
vault.remove_keypair(keyspace, password, &key_id).await.unwrap();
|
||||||
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
|
assert_eq!(keys.len(), 1);
|
||||||
|
}
|
35
vault/tests/mock_store.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//! In-memory mock key-value store for testing vault logic (native only)
|
||||||
|
use kvstore::KVStore;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct MockStore {
|
||||||
|
inner: Arc<Mutex<HashMap<String, Vec<u8>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||||
|
impl KVStore for MockStore {
|
||||||
|
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, kvstore::KVError> {
|
||||||
|
Ok(self.inner.lock().unwrap().get(key).cloned())
|
||||||
|
}
|
||||||
|
async fn set(&self, key: &str, value: &[u8]) -> Result<(), kvstore::KVError> {
|
||||||
|
self.inner.lock().unwrap().insert(key.to_string(), value.to_vec());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn remove(&self, key: &str) -> Result<(), kvstore::KVError> {
|
||||||
|
self.inner.lock().unwrap().remove(key);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn contains_key(&self, key: &str) -> Result<bool, kvstore::KVError> {
|
||||||
|
Ok(self.inner.lock().unwrap().contains_key(key))
|
||||||
|
}
|
||||||
|
async fn keys(&self) -> Result<Vec<String>, kvstore::KVError> {
|
||||||
|
Ok(self.inner.lock().unwrap().keys().cloned().collect())
|
||||||
|
}
|
||||||
|
async fn clear(&self) -> Result<(), kvstore::KVError> {
|
||||||
|
self.inner.lock().unwrap().clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
66
vault/tests/session_manager.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
//! Integration tests for SessionManager (stateful API) in the vault crate
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use vault::{Vault, KeyType, KeyMetadata, SessionManager};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
use kvstore::NativeStore;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn session_manager_end_to_end() {
|
||||||
|
use tempfile::TempDir;
|
||||||
|
let tmp_dir = TempDir::new().expect("create temp dir");
|
||||||
|
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
|
||||||
|
let mut vault = Vault::new(store);
|
||||||
|
let keyspace = "personal";
|
||||||
|
let password = b"testpass";
|
||||||
|
|
||||||
|
// Create session manager
|
||||||
|
let mut session = SessionManager::new(vault);
|
||||||
|
// 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");
|
||||||
|
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
|
||||||
|
let msg = b"hello world";
|
||||||
|
let sig = session.sign(msg).await.expect("sign");
|
||||||
|
let _keypair = session.current_keypair().expect("current_keypair");
|
||||||
|
// Use stateless API for verify: get password from test context, not from private fields
|
||||||
|
let password = b"testpass";
|
||||||
|
let verified = session
|
||||||
|
.get_vault()
|
||||||
|
.verify(keyspace, password, &key_id, msg, &sig)
|
||||||
|
.await
|
||||||
|
.expect("verify");
|
||||||
|
assert!(verified, "signature should verify");
|
||||||
|
|
||||||
|
// Logout wipes secrets
|
||||||
|
session.logout();
|
||||||
|
assert!(session.current_keyspace().is_none());
|
||||||
|
assert!(session.current_keypair().is_none());
|
||||||
|
// No public API for unlocked_keyspaces, but behavior is covered by above asserts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn session_manager_errors() {
|
||||||
|
use tempfile::TempDir;
|
||||||
|
let tmp_dir = TempDir::new().expect("create temp dir");
|
||||||
|
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
|
||||||
|
let vault = Vault::new(store);
|
||||||
|
let mut session = SessionManager::new(vault);
|
||||||
|
// No keyspace unlocked
|
||||||
|
// 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.sign(b"fail").await.is_err());
|
||||||
|
}
|
24
vault/tests/wasm_keypair_management.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// This file contains WASM-only tests for keypair management in the vault crate.
|
||||||
|
// All code is strictly separated from native using cfg attributes.
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
//! WASM test for keypair management in the vault crate.
|
||||||
|
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
use vault::Vault;
|
||||||
|
|
||||||
|
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
#[wasm_bindgen_test(async)]
|
||||||
|
async fn test_keypair_management_and_crypto() {
|
||||||
|
// Example: test keypair creation, selection, signing, etc.
|
||||||
|
// 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 vault::Vault;
|
||||||
|
let store = WasmStore::open("vault").await.unwrap();
|
||||||
|
let mut vault = Vault::new(store);
|
||||||
|
vault.create_keyspace("testspace", b"pw", None).await.unwrap();
|
||||||
|
let key_id = vault.add_keypair("testspace", b"pw", None, None).await.unwrap();
|
||||||
|
assert!(!key_id.is_empty(), "Keypair ID should not be empty");
|
||||||
|
}
|
75
vault/tests/wasm_session_manager.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// This file contains WASM-only tests for session manager logic in the vault crate.
|
||||||
|
// All code is strictly separated from native using cfg attributes.
|
||||||
|
#![cfg(target_arch = "wasm32")]
|
||||||
|
//! WASM test for session manager logic in the vault crate.
|
||||||
|
|
||||||
|
use wasm_bindgen_test::*;
|
||||||
|
use vault::session::SessionManager;
|
||||||
|
use vault::Vault;
|
||||||
|
|
||||||
|
|
||||||
|
wasm_bindgen_test_configure!(run_in_browser);
|
||||||
|
|
||||||
|
#[wasm_bindgen_test(async)]
|
||||||
|
async fn test_session_manager_lock_unlock_keypairs_persistence() {
|
||||||
|
use kvstore::wasm::WasmStore;
|
||||||
|
use vault::{Vault, KeyType, KeyMetadata};
|
||||||
|
use vault::session::SessionManager;
|
||||||
|
let store = WasmStore::open("test-session-manager-lock-unlock").await.unwrap();
|
||||||
|
let mut vault = Vault::new(store);
|
||||||
|
let keyspace = "testspace2";
|
||||||
|
let password = b"testpass2";
|
||||||
|
|
||||||
|
// 1. Create session manager
|
||||||
|
let mut session = SessionManager::new(vault);
|
||||||
|
// Create and unlock keyspace in one step
|
||||||
|
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");
|
||||||
|
}
|
30
wasm_app/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "wasm_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
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_json = "1.0"
|
||||||
|
rhai = { version = "1.16", features = ["serde"] }
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
once_cell = "1.21"
|
||||||
|
vault = { path = "../vault" }
|
||||||
|
evm_client = { path = "../evm_client" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wasm-bindgen-test = "0.3"
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
getrandom = { version = "0.3", features = ["wasm_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}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|