feat: Add basic project structure and initial crates
- Added basic project structure with workspace and crates: `kvstore`, `vault`, `evm_client`, `cli_app`, `web_app`. - Created initial `Cargo.toml` files for each crate. - Added placeholder implementations for key components. - Included initial documentation files (`README.md`, architecture docs, repo structure). - Included initial implementaion for kvstore crate(async API, backend abstraction, separation of concerns, WASM/native support, testability) - Included native and browser tests for the kvstore crate
This commit is contained in:
commit
9dce815daa
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Ignore target directory
|
||||
/target/
|
||||
# Ignore Cargo.lock for libraries
|
||||
**/Cargo.lock
|
||||
# Ignore IDE files
|
||||
.idea/
|
||||
.vscode/
|
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"kvstore",
|
||||
"vault",
|
||||
"evm_client"
|
||||
]
|
57
README.md
Normal file
57
README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# 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.
|
||||
|
||||
## Crate Overview
|
||||
|
||||
- **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.
|
||||
- **evm_client/**: EVM RPC client, integrates with `vault` for signing and secure key management.
|
||||
- **cli_app/**: (Planned) Command-line interface for scripting and automation.
|
||||
- **web_app/**: (Planned) WASM web app exposing the same APIs to JavaScript or browser scripting.
|
||||
|
||||
## Architecture Highlights
|
||||
- **Async everywhere:** All APIs are async and runtime-agnostic.
|
||||
- **Conditional backends:** Uses Cargo features and `cfg` to select the appropriate backend for each environment.
|
||||
- **Secure by design:** Vault encrypts all key material at rest and leverages modern cryptography.
|
||||
- **Tested natively and in browser:** WASM and native backends are both covered by tests.
|
||||
|
||||
## Building and Testing
|
||||
|
||||
### Prerequisites
|
||||
- Rust (latest stable recommended)
|
||||
- For WASM: `wasm-pack`, Firefox or Chrome (for browser tests)
|
||||
|
||||
### Native
|
||||
```sh
|
||||
cargo check --workspace --features kvstore/native
|
||||
```
|
||||
|
||||
### WASM (kvstore only)
|
||||
```sh
|
||||
cd kvstore
|
||||
wasm-pack test --headless --firefox --features web
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
```
|
||||
.
|
||||
├── kvstore/ # Key-value store trait and backends
|
||||
├── vault/ # Cryptographic vault
|
||||
├── evm_client/ # EVM RPC client
|
||||
├── cli_app/ # CLI (planned)
|
||||
├── web_app/ # Web app (planned)
|
||||
├── docs/ # Architecture docs
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
- [x] Unified async trait for key-value storage
|
||||
- [x] Native and WASM backends for kvstore
|
||||
- [x] Cryptographic vault with password-protected keyspace
|
||||
- [x] EVM client with vault integration
|
||||
- [ ] CLI and web app targets
|
||||
- [ ] Full end-to-end integration
|
||||
|
||||
## License
|
||||
MIT OR Apache-2.0
|
384
docs/Architecture.md
Normal file
384
docs/Architecture.md
Normal file
@ -0,0 +1,384 @@
|
||||
# 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"
|
131
docs/RepoStructure.md
Normal file
131
docs/RepoStructure.md
Normal file
@ -0,0 +1,131 @@
|
||||
# SAL Repository Structure
|
||||
|
||||
## Introduction
|
||||
|
||||
This document describes the recommended structure for the SAL repository, reflecting the new modular architecture. The goal is to enable robust, maintainable, and WASM-compatible development, while supporting a CLI binary and Rhai scripting.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Directory Layout
|
||||
|
||||
```
|
||||
sal/
|
||||
├── arch/
|
||||
│ ├── Architecture.md
|
||||
│ └── RepoStructure.md # ← this file
|
||||
├── Cargo.toml # Workspace manifest
|
||||
├── kvstore/
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
├── vault/
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
├── evm_client/
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
├── cli_app/
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ ├── main.rs # CLI binary entry point
|
||||
│ └── commands.rs
|
||||
├── web_app/
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
├── rhai/
|
||||
│ └── src/
|
||||
│ └── lib.rs # Rhai bindings and script API
|
||||
└── README.md
|
||||
```
|
||||
|
||||
- **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.
|
||||
- **WebAssembly target** is in `web_app`.
|
||||
- **Rhai bindings** live in their own crate (`rhai/`), so both CLI and WASM can depend on them.
|
||||
|
||||
---
|
||||
|
||||
## Sample Workspace `Cargo.toml`
|
||||
|
||||
```toml
|
||||
[workspace]
|
||||
members = [
|
||||
"kvstore",
|
||||
"vault",
|
||||
"evm_client",
|
||||
"cli_app",
|
||||
"web_app",
|
||||
"rhai",
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Details
|
||||
|
||||
### kvstore
|
||||
- Async trait for key-value storage.
|
||||
- Native and WASM backends selected by features or `cfg`.
|
||||
|
||||
### vault
|
||||
- Manages encrypted keyspaces and cryptographic operations.
|
||||
- Uses `kvstore` for persistence.
|
||||
|
||||
### evm_client
|
||||
- Handles EVM RPC, uses `vault` for signing.
|
||||
|
||||
### cli_app (Binary)
|
||||
- Provides CLI interface using `vault`, `evm_client`, and `rhai`.
|
||||
- Entry point: `cli_app/src/main.rs`.
|
||||
|
||||
### web_app (WASM)
|
||||
- Exposes Rust APIs to JS using `wasm-bindgen`.
|
||||
- Depends on `vault`, `evm_client`, and `rhai`.
|
||||
|
||||
### rhai
|
||||
- Contains all Rhai script bindings and API glue.
|
||||
- Shared by both CLI and WASM targets.
|
||||
|
||||
---
|
||||
|
||||
## Integration & Migration Notes
|
||||
|
||||
- Move code from `src/hero_vault` and related modules into the new crates.
|
||||
- The CLI binary is now in `cli_app/src/main.rs`, depending on all core crates.
|
||||
- Rhai bindings are centralized in the `rhai/` crate, making them reusable.
|
||||
- If you need to keep a legacy `src/` for migration, do so temporarily—plan to remove it once the transition is complete.
|
||||
|
||||
---
|
||||
|
||||
## Rationale
|
||||
|
||||
- **Idiomatic Rust**: Each crate at the root, easy to manage, test, and publish.
|
||||
- **Maintainability**: Clear separation of concerns, easy to extend.
|
||||
- **WASM & CLI**: Both targets are first-class, sharing as much code as possible.
|
||||
- **Rhai Integration**: Centralized, reusable scripting interface.
|
||||
|
||||
---
|
||||
|
||||
## Example: How to Build the CLI Binary
|
||||
|
||||
```sh
|
||||
cd cli_app
|
||||
cargo build --release
|
||||
cargo run -- <args>
|
||||
```
|
||||
|
||||
The CLI binary will be compiled as `target/release/cli_app`.
|
||||
|
||||
---
|
||||
|
||||
## Example: How to Build the WASM Target
|
||||
|
||||
```sh
|
||||
cd web_app
|
||||
wasm-pack build --target web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This structure enables modular, scalable development for both CLI and browser targets, with robust support for async, WASM, and scripting via Rhai.
|
73
docs/kvstore-vault-architecture.md
Normal file
73
docs/kvstore-vault-architecture.md
Normal file
@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
13
evm_client/Cargo.toml
Normal file
13
evm_client/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "evm_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
vault = { path = "../vault" }
|
||||
async-trait = "0.1"
|
||||
alloy = "0.6"
|
||||
thiserror = "1"
|
23
evm_client/src/lib.rs
Normal file
23
evm_client/src/lib.rs
Normal file
@ -0,0 +1,23 @@
|
||||
//! evm_client: EVM RPC client and integration
|
||||
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EvmError {
|
||||
#[error("RPC error: {0}")]
|
||||
Rpc(String),
|
||||
#[error("Vault error: {0}")]
|
||||
Vault(String),
|
||||
}
|
||||
|
||||
pub struct EvmClient {
|
||||
// ... fields for RPC, vault, etc.
|
||||
}
|
||||
|
||||
impl EvmClient {
|
||||
pub async fn connect(_rpc_url: &str) -> Result<Self, EvmError> {
|
||||
todo!("Implement connect")
|
||||
}
|
||||
// ... other API stubs
|
||||
}
|
31
kvstore/Cargo.toml
Normal file
31
kvstore/Cargo.toml
Normal file
@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "kvstore"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1"
|
||||
sled = { version = "0.34", optional = true }
|
||||
idb = { version = "0.4", optional = true }
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = "1"
|
||||
tempfile = "3"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
native = ["sled", "tokio"]
|
||||
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"
|
52
kvstore/README.md
Normal file
52
kvstore/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# kvstore: Async Key-Value Store for Native and WASM
|
||||
|
||||
`kvstore` provides a runtime-agnostic, async trait for key-value storage, with robust implementations for both native (using `sled`) and browser/WASM (using IndexedDB via the `idb` crate) environments.
|
||||
|
||||
## Features
|
||||
- **Unified async trait**: Same API for all platforms. Methods: `get`, `set`, `remove`, `contains_key`, `keys`, `clear`.
|
||||
- **Native backend**: Uses `sled` for fast, embedded storage. Blocking I/O is offloaded with `tokio::task::spawn_blocking`.
|
||||
- **WASM backend**: Uses IndexedDB via the `idb` crate for browser storage. Fully async and Promise-based.
|
||||
- **Error handling**: Consistent error types across platforms.
|
||||
- **Conditional compilation**: Uses Cargo features and `cfg` to select the backend.
|
||||
|
||||
## Usage
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
```toml
|
||||
[dependencies]
|
||||
kvstore = { path = "../kvstore" }
|
||||
```
|
||||
|
||||
### 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);
|
||||
}
|
||||
```
|
||||
|
||||
For WASM/browser, use `WasmStore` and run in a browser environment.
|
||||
|
||||
## Testing
|
||||
|
||||
### Native
|
||||
```sh
|
||||
cargo test -p kvstore --features native
|
||||
```
|
||||
|
||||
### WASM (browser)
|
||||
```sh
|
||||
cd kvstore
|
||||
wasm-pack test --headless --firefox --features web
|
||||
```
|
||||
|
||||
## Architecture
|
||||
- See `../docs/Architecture.md` for full design details.
|
||||
|
||||
## License
|
||||
MIT OR Apache-2.0
|
24
kvstore/src/error.rs
Normal file
24
kvstore/src/error.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//! Error types for the kvstore crate
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KVError {
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Key not found: {0}")]
|
||||
KeyNotFound(String),
|
||||
#[error("Store not found: {0}")]
|
||||
StoreNotFound(String),
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
#[error("Encryption error: {0}")]
|
||||
Encryption(String),
|
||||
#[error("Decryption error: {0}")]
|
||||
Decryption(String),
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, KVError>;
|
18
kvstore/src/lib.rs
Normal file
18
kvstore/src/lib.rs
Normal file
@ -0,0 +1,18 @@
|
||||
/// Error types for the kvstore crate
|
||||
pub mod error;
|
||||
/// Async trait for key-value storage
|
||||
pub mod traits;
|
||||
/// Native backend (e.g., sled or slatedb)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod native;
|
||||
/// WASM backend (IndexedDB)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
pub use error::{KVError, Result};
|
||||
pub use traits::KVStore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use native::NativeStore;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::WasmStore;
|
106
kvstore/src/native.rs
Normal file
106
kvstore/src/native.rs
Normal file
@ -0,0 +1,106 @@
|
||||
//! Native backend for kvstore using sled
|
||||
//!
|
||||
//! # Runtime Requirement
|
||||
//!
|
||||
//! **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`).
|
||||
//!
|
||||
//! All blocking I/O is offloaded using `tokio::task::spawn_blocking`.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
|
||||
use crate::traits::KVStore;
|
||||
use crate::error::{KVError, Result};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sled::Db;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeStore {
|
||||
db: Arc<Db>,
|
||||
}
|
||||
|
||||
impl NativeStore {
|
||||
pub fn open(path: &str) -> Result<Self> {
|
||||
let db = sled::open(path).map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(Self { db: Arc::new(db) })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[async_trait]
|
||||
impl KVStore for NativeStore {
|
||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.get(&key)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))
|
||||
.map(|opt| opt.map(|ivec| ivec.to_vec()))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
let value = value.to_vec();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.insert(&key, value)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
async fn remove(&self, key: &str) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.remove(&key)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
async fn contains_key(&self, key: &str) -> Result<bool> {
|
||||
let db = self.db.clone();
|
||||
let key = key.to_owned();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
Ok(db.contains_key(&key)
|
||||
.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
|
||||
async fn keys(&self) -> Result<Vec<String>> {
|
||||
let db = self.db.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut keys = Vec::new();
|
||||
for result in db.iter() {
|
||||
let (key, _) = result.map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
keys.push(String::from_utf8_lossy(&key).to_string());
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
|
||||
async fn clear(&self) -> Result<()> {
|
||||
let db = self.db.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
db.clear().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
db.flush().map_err(|e| KVError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| KVError::Other(format!("Join error: {e}")))?
|
||||
}
|
||||
}
|
26
kvstore/src/traits.rs
Normal file
26
kvstore/src/traits.rs
Normal file
@ -0,0 +1,26 @@
|
||||
//! Async trait for key-value storage
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
|
||||
/// Async key-value store interface.
|
||||
///
|
||||
/// For native (non-wasm32) backends, implementers should be `Send + Sync` to support runtime-agnostic async usage.
|
||||
/// For WASM (wasm32) backends, `Send + Sync` is not required and types may not implement them.
|
||||
///
|
||||
/// Methods:
|
||||
/// - get
|
||||
/// - set
|
||||
/// - remove (was delete)
|
||||
/// - contains_key (was exists)
|
||||
/// - keys
|
||||
/// - clear
|
||||
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<()>;
|
||||
}
|
140
kvstore/src/wasm.rs
Normal file
140
kvstore/src/wasm.rs
Normal file
@ -0,0 +1,140 @@
|
||||
//! WASM backend for kvstore using IndexedDB (idb crate)
|
||||
//!
|
||||
//! # Platform
|
||||
//!
|
||||
//! This backend is only available when compiling for `wasm32` (browser/WebAssembly).
|
||||
//! It uses the `idb` crate for async IndexedDB operations.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! This implementation is designed to run inside a browser environment and is not supported on native targets.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
|
||||
|
||||
use crate::traits::KVStore;
|
||||
use crate::error::{KVError, Result};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use idb::{Database, TransactionMode, Factory};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::JsValue;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use js_sys::Uint8Array;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
const STORE_NAME: &str = "kv";
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct WasmStore {
|
||||
db: Database,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl WasmStore {
|
||||
pub async fn open(name: &str) -> Result<Self> {
|
||||
let factory = Factory::new().map_err(|e| KVError::Other(format!("IndexedDB factory error: {e:?}")))?;
|
||||
let mut open_req = factory.open(name, None)
|
||||
.map_err(|e| KVError::Other(format!("IndexedDB factory open error: {e:?}")))?;
|
||||
open_req.on_upgrade_needed(|event| {
|
||||
let db = event.database().expect("Failed to get database in upgrade event");
|
||||
if !db.store_names().iter().any(|n| n == STORE_NAME) {
|
||||
db.create_object_store(STORE_NAME, Default::default()).unwrap();
|
||||
}
|
||||
});
|
||||
let db = open_req.await.map_err(|e| KVError::Other(format!("IndexedDB open error: {e:?}")))?;
|
||||
Ok(Self { db })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl KVStore for WasmStore {
|
||||
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
use idb::Query;
|
||||
let val = store.get(Query::from(JsValue::from_str(key))).await
|
||||
.map_err(|e| KVError::Other(format!("idb get await error: {e:?}")))?;
|
||||
if let Some(jsval) = val {
|
||||
let arr = Uint8Array::new(&jsval);
|
||||
Ok(Some(arr.to_vec()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
store.put(&Uint8Array::from(value).into(), Some(&JsValue::from_str(key))).await
|
||||
.map_err(|e| KVError::Other(format!("idb put await error: {e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
async fn remove(&self, key: &str) -> Result<()> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
use idb::Query;
|
||||
store.delete(Query::from(JsValue::from_str(key))).await
|
||||
.map_err(|e| KVError::Other(format!("idb delete await error: {e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
async fn contains_key(&self, key: &str) -> Result<bool> {
|
||||
Ok(self.get(key).await?.is_some())
|
||||
}
|
||||
|
||||
async fn keys(&self) -> Result<Vec<String>> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadOnly)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
let js_keys = store.get_all_keys(None, None).await
|
||||
.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();
|
||||
for i in 0..arr.length() {
|
||||
if let Some(s) = arr.get(i).as_string() {
|
||||
keys.push(s);
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
async fn clear(&self) -> Result<()> {
|
||||
let tx = self.db.transaction(&[STORE_NAME], TransactionMode::ReadWrite)
|
||||
.map_err(|e| KVError::Other(format!("idb transaction error: {e:?}")))?;
|
||||
let store = tx.object_store(STORE_NAME)
|
||||
.map_err(|e| KVError::Other(format!("idb object_store error: {e:?}")))?;
|
||||
store.clear().await
|
||||
.map_err(|e| KVError::Other(format!("idb clear error: {e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct WasmStore;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl KVStore for WasmStore {
|
||||
async fn get(&self, _key: &str) -> Result<Option<Vec<u8>>> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn delete(&self, _key: &str) -> Result<()> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn exists(&self, _key: &str) -> Result<bool> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
}
|
33
kvstore/tests/native.rs
Normal file
33
kvstore/tests/native.rs
Normal file
@ -0,0 +1,33 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
use kvstore::{NativeStore, KVStore};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_native_store_basic() {
|
||||
let tmp_dir = tempfile::tempdir().unwrap();
|
||||
let path = tmp_dir.path().join("testdb");
|
||||
let store = NativeStore::open(path.to_str().unwrap()).unwrap();
|
||||
|
||||
// Test set/get
|
||||
store.set("foo", b"bar").await.unwrap();
|
||||
let val = store.get("foo").await.unwrap();
|
||||
assert_eq!(val, Some(b"bar".to_vec()));
|
||||
|
||||
// Test exists
|
||||
assert_eq!(store.contains_key("foo").await.unwrap(), true);
|
||||
assert_eq!(store.contains_key("bar").await.unwrap(), false);
|
||||
store.remove("foo").await.unwrap();
|
||||
assert_eq!(store.get("foo").await.unwrap(), None);
|
||||
|
||||
// Test keys
|
||||
store.set("foo", b"bar").await.unwrap();
|
||||
store.set("baz", b"qux").await.unwrap();
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert!(keys.contains(&"foo".to_string()));
|
||||
assert!(keys.contains(&"baz".to_string()));
|
||||
|
||||
// Test clear
|
||||
store.clear().await.unwrap();
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 0);
|
||||
}
|
46
kvstore/tests/web.rs
Normal file
46
kvstore/tests/web.rs
Normal file
@ -0,0 +1,46 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
//! WASM/browser tests for kvstore using wasm-bindgen-test
|
||||
|
||||
use kvstore::{WasmStore, KVStore};
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_set_and_get() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
let val = store.get("foo").await.expect("get");
|
||||
assert_eq!(val, Some(b"bar".to_vec()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_delete_and_exists() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
assert_eq!(store.contains_key("foo").await.unwrap(), true);
|
||||
assert_eq!(store.contains_key("bar").await.unwrap(), false);
|
||||
store.remove("foo").await.unwrap();
|
||||
assert_eq!(store.get("foo").await.unwrap(), None);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_keys() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
store.set("baz", b"qux").await.expect("set");
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert!(keys.contains(&"foo".to_string()));
|
||||
assert!(keys.contains(&"baz".to_string()));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_clear() {
|
||||
let store = WasmStore::open("test-db").await.expect("open");
|
||||
store.set("foo", b"bar").await.expect("set");
|
||||
store.set("baz", b"qux").await.expect("set");
|
||||
store.clear().await.unwrap();
|
||||
let keys = store.keys().await.unwrap();
|
||||
assert_eq!(keys.len(), 0);
|
||||
}
|
15
vault/Cargo.toml
Normal file
15
vault/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "vault"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
kvstore = { path = "../kvstore" }
|
||||
async-trait = "0.1"
|
||||
chacha20poly1305 = "0.10"
|
||||
k256 = "0.13"
|
||||
rand_core = "0.6"
|
||||
thiserror = "1"
|
27
vault/src/lib.rs
Normal file
27
vault/src/lib.rs
Normal file
@ -0,0 +1,27 @@
|
||||
//! vault: Cryptographic keyspace and operations
|
||||
|
||||
|
||||
use kvstore::KVStore;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum VaultError {
|
||||
#[error("Storage error: {0}")]
|
||||
Storage(String),
|
||||
#[error("Crypto error: {0}")]
|
||||
Crypto(String),
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
pub struct Vault<S: KVStore + Send + Sync> {
|
||||
storage: S,
|
||||
// ... other fields
|
||||
}
|
||||
|
||||
impl<S: KVStore + Send + Sync> Vault<S> {
|
||||
/// Creates a new keyspace. Implementation pending.
|
||||
pub async fn create_keyspace(_dummy: ()) -> Result<(), VaultError> {
|
||||
todo!("Implement create_keyspace")
|
||||
}
|
||||
// ... other API stubs
|
||||
}
|
Loading…
Reference in New Issue
Block a user