Compare commits
26 Commits
ed76ba3d8d
...
main_brows
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d3d0a4fa4 | ||
|
|
4f3f98a954 | ||
|
|
c641d0ae2e | ||
|
|
6f42e5ab8d | ||
|
|
203cde1cba | ||
|
|
6b037537bf | ||
|
|
580fd72dce | ||
|
|
a0622629ae | ||
|
|
4e1e707f85 | ||
|
|
9f143ded9d | ||
|
|
b0d0aaa53d | ||
|
|
e00c140396 | ||
|
|
4ba1e43f4e | ||
|
|
b82d457873 | ||
|
|
b0b6359be1 | ||
|
|
536c077fbf | ||
|
|
31975aa9d3 | ||
|
|
087720f61f | ||
|
|
c2c5be3409 | ||
|
|
37764e3861 | ||
|
|
5bc205b2f7 | ||
|
|
beba294054 | ||
|
|
0224755ba3 | ||
|
|
44b4dfd6a7 | ||
|
|
1e52c572d2 | ||
|
|
1f2d7e3fec |
@@ -5,5 +5,5 @@ members = [
|
||||
"vault",
|
||||
"evm_client",
|
||||
"wasm_app",
|
||||
"sigsocket_client",
|
||||
]
|
||||
|
||||
|
||||
13
Makefile
@@ -2,7 +2,7 @@
|
||||
|
||||
BROWSER ?= firefox
|
||||
|
||||
.PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client build-wasm-app
|
||||
.PHONY: test-browser-all test-browser-kvstore test-browser-vault test-browser-evm-client build-wasm-app build-hero-vault-extension
|
||||
|
||||
test-browser-all: test-browser-kvstore test-browser-vault test-browser-evm-client
|
||||
|
||||
@@ -25,9 +25,8 @@ test-browser-evm-client:
|
||||
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
|
||||
|
||||
# Build Hero Vault extension: wasm, copy, then extension
|
||||
build-crypto-vault-extension: build-wasm-app
|
||||
cp wasm_app/pkg/wasm_app* crypto_vault_extension/wasm/
|
||||
cp wasm_app/pkg/*.d.ts crypto_vault_extension/wasm/
|
||||
cp wasm_app/pkg/*.js crypto_vault_extension/wasm/
|
||||
|
||||
62
README.md
@@ -13,6 +13,24 @@ A modular, async, and cross-platform cryptographic stack in Rust. Built for both
|
||||
- **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))
|
||||
|
||||
---
|
||||
|
||||
## What is Rust Conditional Compilation?
|
||||
|
||||
Rust's conditional compilation allows you to write code that only gets included for certain platforms or configurations. This is done using attributes like `#[cfg(target_arch = "wasm32")]` for WebAssembly, or `#[cfg(not(target_arch = "wasm32"))]` for native platforms. It enables a single codebase to support multiple targets (such as desktop and browser) with platform-specific logic where needed.
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
// This code only compiles for WebAssembly targets
|
||||
```
|
||||
|
||||
## What is WASM (WebAssembly)?
|
||||
|
||||
WebAssembly (WASM) is a binary instruction format for a stack-based virtual machine. It allows code written in languages like Rust, C, or C++ to run at near-native speed in web browsers and other environments. In this project, WASM enables the cryptographic vault to work securely inside the browser, exposing Rust functions to JavaScript and web applications.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
> **Note:** Some directories are planned for future extensibility and scripting, and may not exist yet in the current workspace.
|
||||
|
||||
@@ -36,6 +54,42 @@ A modular, async, and cross-platform cryptographic stack in Rust. Built for both
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
## Conditional Compilation & WASM Support
|
||||
|
||||
This project makes extensive use of Rust's conditional compilation to support both native and WebAssembly (WASM) environments with a single codebase. Key points:
|
||||
|
||||
- **Platform-specific code:**
|
||||
- Rust's `#[cfg(target_arch = "wasm32")]` attribute is used to write WASM-specific code, while `#[cfg(not(target_arch = "wasm32"))]` is for native targets.
|
||||
- This pattern is used for struct definitions, method implementations, and even module imports.
|
||||
|
||||
- **Example: SessionManager**
|
||||
```rust
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct SessionManager<S: KVStore + Send + Sync> { ... }
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct SessionManager<S: KVStore> { ... }
|
||||
```
|
||||
|
||||
- **WASM Bindings:**
|
||||
- The `wasm_app` crate uses `wasm-bindgen` to expose Rust functions to JavaScript, enabling browser integration.
|
||||
- Functions are annotated with `#[wasm_bindgen]` and exported for use in JS/TS.
|
||||
|
||||
- **Storage Backends:**
|
||||
- Native uses `sled` or other file-based stores.
|
||||
- WASM uses IndexedDB (via `kvstore::wasm::WasmStore`).
|
||||
|
||||
- **Building for WASM:**
|
||||
- Use `wasm-pack build --target web` to build the WASM package.
|
||||
- Serve the resulting files with a static server for browser use.
|
||||
|
||||
- **Testing:**
|
||||
- Both native and WASM tests are supported. WASM tests can be run in headless browsers using `wasm-pack test --headless --firefox` or similar commands.
|
||||
|
||||
---
|
||||
|
||||
## Building and Testing
|
||||
|
||||
### Prerequisites
|
||||
@@ -80,13 +134,13 @@ For questions, contributions, or more details, see the architecture docs or open
|
||||
|
||||
### Native
|
||||
```sh
|
||||
cargo check --workspace --features kvstore/native
|
||||
cargo check --workspace
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
### WASM (kvstore only)
|
||||
### WASM
|
||||
```sh
|
||||
cd kvstore
|
||||
wasm-pack test --headless --firefox --features web
|
||||
make test-browser-all
|
||||
```
|
||||
|
||||
# Rhai Scripting System
|
||||
|
||||
38
build.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Main build script for Hero Vault Extension
|
||||
# This script handles the complete build process in one step
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for better readability
|
||||
GREEN="\033[0;32m"
|
||||
BLUE="\033[0;34m"
|
||||
RESET="\033[0m"
|
||||
|
||||
echo -e "${BLUE}=== Building Hero Vault Extension ===${RESET}"
|
||||
|
||||
# Step 1: Build the WASM package
|
||||
echo -e "${BLUE}Building WASM package...${RESET}"
|
||||
cd "$(dirname "$0")/wasm_app" || exit 1
|
||||
wasm-pack build --target web
|
||||
echo -e "${GREEN}✓ WASM build successful!${RESET}"
|
||||
|
||||
# Step 2: Prepare the frontend extension
|
||||
echo -e "${BLUE}Preparing frontend extension...${RESET}"
|
||||
cd ../crypto_vault_extension || exit 1
|
||||
|
||||
# Copy WASM files to the extension's public directory
|
||||
echo "Copying WASM files..."
|
||||
cp ../wasm_app/pkg/wasm_app* wasm/
|
||||
cp ../wasm_app/pkg/*.d.ts wasm/
|
||||
cp ../wasm_app/pkg/*.js wasm/
|
||||
|
||||
|
||||
echo -e "${GREEN}=== Build Complete ===${RESET}"
|
||||
echo "Extension is ready in: $(pwd)"
|
||||
echo ""
|
||||
echo -e "${BLUE}To load the extension in Chrome:${RESET}"
|
||||
echo "1. Go to chrome://extensions/"
|
||||
echo "2. Enable Developer mode (toggle in top-right)"
|
||||
echo "3. Click 'Load unpacked'"
|
||||
echo "4. Select the $(pwd) directory"
|
||||
670
crypto_vault_extension/background.js
Normal file
@@ -0,0 +1,670 @@
|
||||
let vault = null;
|
||||
let isInitialized = false;
|
||||
let currentSession = null;
|
||||
let keepAliveInterval = null;
|
||||
let sessionTimeoutDuration = 15; // Default 15 seconds
|
||||
let sessionTimeoutId = null; // Background timer
|
||||
let popupPort = null; // Track popup connection
|
||||
|
||||
// SigSocket service instance
|
||||
let sigSocketService = null;
|
||||
|
||||
// Utility function to convert Uint8Array to hex
|
||||
function toHex(uint8Array) {
|
||||
return Array.from(uint8Array)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
// Background session timeout management
|
||||
async function loadTimeoutSetting() {
|
||||
const result = await chrome.storage.local.get(['sessionTimeout']);
|
||||
sessionTimeoutDuration = result.sessionTimeout || 15;
|
||||
}
|
||||
|
||||
function startSessionTimeout() {
|
||||
clearSessionTimeout();
|
||||
|
||||
if (currentSession && sessionTimeoutDuration > 0) {
|
||||
sessionTimeoutId = setTimeout(async () => {
|
||||
if (vault && currentSession) {
|
||||
// Lock the session
|
||||
vault.lock_session();
|
||||
|
||||
// Keep the session info for SigSocket connection but mark it as timed out
|
||||
const keyspace = currentSession.keyspace;
|
||||
await sessionManager.clear();
|
||||
|
||||
// Maintain SigSocket connection for the locked keyspace to receive pending requests
|
||||
if (sigSocketService && keyspace) {
|
||||
try {
|
||||
// Keep SigSocket connected to receive requests even when locked
|
||||
console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`);
|
||||
} catch (error) {
|
||||
console.warn('Failed to maintain SigSocket connection after timeout:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify popup if it's open
|
||||
if (popupPort) {
|
||||
popupPort.postMessage({
|
||||
type: 'sessionTimeout',
|
||||
message: 'Session timed out due to inactivity'
|
||||
});
|
||||
}
|
||||
}
|
||||
}, sessionTimeoutDuration * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSessionTimeout() {
|
||||
if (sessionTimeoutId) {
|
||||
clearTimeout(sessionTimeoutId);
|
||||
sessionTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function resetSessionTimeout() {
|
||||
if (currentSession) {
|
||||
startSessionTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
// Session persistence functions
|
||||
async function saveSession(keyspace) {
|
||||
currentSession = { keyspace, timestamp: Date.now() };
|
||||
|
||||
// Save to both session and local storage for better persistence
|
||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
|
||||
}
|
||||
|
||||
async function loadSession() {
|
||||
// Try session storage first
|
||||
let result = await chrome.storage.session.get(['cryptoVaultSession']);
|
||||
if (result.cryptoVaultSession) {
|
||||
currentSession = result.cryptoVaultSession;
|
||||
return currentSession;
|
||||
}
|
||||
|
||||
// Fallback to local storage
|
||||
result = await chrome.storage.local.get(['cryptoVaultSessionBackup']);
|
||||
if (result.cryptoVaultSessionBackup) {
|
||||
currentSession = result.cryptoVaultSessionBackup;
|
||||
// Restore to session storage
|
||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||
return currentSession;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function clearSession() {
|
||||
currentSession = null;
|
||||
await chrome.storage.session.remove(['cryptoVaultSession']);
|
||||
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
|
||||
}
|
||||
|
||||
// Keep service worker alive
|
||||
function startKeepAlive() {
|
||||
if (keepAliveInterval) {
|
||||
clearInterval(keepAliveInterval);
|
||||
}
|
||||
|
||||
keepAliveInterval = setInterval(() => {
|
||||
chrome.storage.session.get(['keepAlive']).catch(() => {});
|
||||
}, 20000);
|
||||
}
|
||||
|
||||
function stopKeepAlive() {
|
||||
if (keepAliveInterval) {
|
||||
clearInterval(keepAliveInterval);
|
||||
keepAliveInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Consolidated session management
|
||||
const sessionManager = {
|
||||
async save(keyspace) {
|
||||
await saveSession(keyspace);
|
||||
startKeepAlive();
|
||||
await loadTimeoutSetting();
|
||||
startSessionTimeout();
|
||||
},
|
||||
async clear() {
|
||||
await clearSession();
|
||||
stopKeepAlive();
|
||||
clearSessionTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
async function restoreSession() {
|
||||
const session = await loadSession();
|
||||
if (session && vault) {
|
||||
// Check if the session is still valid by testing if vault is unlocked
|
||||
const isUnlocked = vault.is_unlocked();
|
||||
if (isUnlocked) {
|
||||
// Restart keep-alive for restored session
|
||||
startKeepAlive();
|
||||
|
||||
// Connect to SigSocket for the restored session
|
||||
if (sigSocketService) {
|
||||
try {
|
||||
const connected = await sigSocketService.connectToServer(session.keyspace);
|
||||
if (connected) {
|
||||
console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
|
||||
if (error.message && error.message.includes('Workspace not found')) {
|
||||
console.log(`ℹ️ SigSocket connection skipped for restored session: No workspace available yet`);
|
||||
} else {
|
||||
console.warn('Failed to reconnect SigSocket for restored session:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
} else {
|
||||
// Session exists but is locked - still try to connect SigSocket to receive pending requests
|
||||
if (sigSocketService && session.keyspace) {
|
||||
try {
|
||||
const connected = await sigSocketService.connectToServer(session.keyspace);
|
||||
if (connected) {
|
||||
console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
|
||||
if (error.message && error.message.includes('Workspace not found')) {
|
||||
console.log(`ℹ️ SigSocket connection skipped for locked session: No workspace available yet`);
|
||||
} else {
|
||||
console.warn('Failed to connect SigSocket for locked session:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't clear the session - keep it for SigSocket connection
|
||||
// await sessionManager.clear();
|
||||
}
|
||||
}
|
||||
return session; // Return session even if locked, so we know which keyspace to use
|
||||
}
|
||||
|
||||
// Import WASM module functions and SigSocket service
|
||||
import init, * as wasmFunctions from './wasm/wasm_app.js';
|
||||
import SigSocketService from './background/sigsocket.js';
|
||||
|
||||
// Initialize WASM module
|
||||
async function initVault() {
|
||||
try {
|
||||
if (vault && isInitialized) return vault;
|
||||
|
||||
// Initialize with the WASM file
|
||||
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
||||
await init(wasmUrl);
|
||||
|
||||
// Use imported functions directly
|
||||
vault = wasmFunctions;
|
||||
isInitialized = true;
|
||||
|
||||
// Initialize SigSocket service
|
||||
if (!sigSocketService) {
|
||||
sigSocketService = new SigSocketService();
|
||||
await sigSocketService.initialize(vault);
|
||||
console.log('🔌 SigSocket service initialized');
|
||||
}
|
||||
|
||||
// Try to restore previous session
|
||||
await restoreSession();
|
||||
|
||||
return vault;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize CryptoVault:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Consolidated message handlers
|
||||
const messageHandlers = {
|
||||
createKeyspace: async (request) => {
|
||||
await vault.create_keyspace(request.keyspace, request.password);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
initSession: async (request) => {
|
||||
await vault.init_session(request.keyspace, request.password);
|
||||
await sessionManager.save(request.keyspace);
|
||||
|
||||
// Smart auto-connect to SigSocket when session is initialized
|
||||
if (sigSocketService) {
|
||||
try {
|
||||
console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`);
|
||||
|
||||
// This will reuse existing connection if same workspace, or switch if different
|
||||
const connected = await sigSocketService.connectToServer(request.keyspace);
|
||||
if (connected) {
|
||||
console.log(`✅ SigSocket ready for workspace: ${request.keyspace}`);
|
||||
} else {
|
||||
console.warn(`⚠️ SigSocket connection failed for workspace: ${request.keyspace}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to auto-connect to SigSocket:', error);
|
||||
|
||||
// If connection fails, try once more after a short delay
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`);
|
||||
await sigSocketService.connectToServer(request.keyspace);
|
||||
} catch (retryError) {
|
||||
console.warn('SigSocket retry connection also failed:', retryError);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Notify SigSocket service that keyspace is now unlocked
|
||||
await sigSocketService.onKeypaceUnlocked();
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
isUnlocked: () => ({ success: true, unlocked: vault.is_unlocked() }),
|
||||
|
||||
addKeypair: async (request) => {
|
||||
const result = await vault.add_keypair(request.keyType, request.metadata);
|
||||
return { success: true, result };
|
||||
},
|
||||
|
||||
listKeypairs: async () => {
|
||||
if (!vault.is_unlocked()) {
|
||||
return { success: false, error: 'Session is not unlocked' };
|
||||
}
|
||||
const keypairsRaw = await vault.list_keypairs();
|
||||
const keypairs = typeof keypairsRaw === 'string' ? JSON.parse(keypairsRaw) : keypairsRaw;
|
||||
return { success: true, keypairs };
|
||||
},
|
||||
|
||||
selectKeypair: (request) => {
|
||||
vault.select_keypair(request.keyId);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
getCurrentKeypairMetadata: () => ({ success: true, metadata: vault.current_keypair_metadata() }),
|
||||
|
||||
getCurrentKeypairPublicKey: () => ({ success: true, publicKey: toHex(vault.current_keypair_public_key()) }),
|
||||
|
||||
sign: async (request) => {
|
||||
const signature = await vault.sign(new Uint8Array(request.message));
|
||||
return { success: true, signature };
|
||||
},
|
||||
|
||||
encrypt: async (request) => {
|
||||
if (!vault.is_unlocked()) {
|
||||
return { success: false, error: 'Session is not unlocked' };
|
||||
}
|
||||
const messageBytes = new TextEncoder().encode(request.message);
|
||||
const encryptedData = await vault.encrypt_data(messageBytes);
|
||||
const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
|
||||
return { success: true, encryptedMessage };
|
||||
},
|
||||
|
||||
decrypt: async (request) => {
|
||||
if (!vault.is_unlocked()) {
|
||||
return { success: false, error: 'Session is not unlocked' };
|
||||
}
|
||||
const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0)));
|
||||
const decryptedData = await vault.decrypt_data(encryptedBytes);
|
||||
const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData));
|
||||
return { success: true, decryptedMessage };
|
||||
},
|
||||
|
||||
verify: async (request) => {
|
||||
const metadata = vault.current_keypair_metadata();
|
||||
if (!metadata) {
|
||||
return { success: false, error: 'No keypair selected' };
|
||||
}
|
||||
const isValid = await vault.verify(new Uint8Array(request.message), request.signature);
|
||||
return { success: true, isValid };
|
||||
},
|
||||
|
||||
lockSession: async () => {
|
||||
vault.lock_session();
|
||||
await sessionManager.clear();
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
getStatus: async () => {
|
||||
const status = vault ? vault.is_unlocked() : false;
|
||||
const session = await loadSession();
|
||||
return {
|
||||
success: true,
|
||||
status,
|
||||
session: session ? { keyspace: session.keyspace } : null
|
||||
};
|
||||
},
|
||||
|
||||
// Timeout management handlers
|
||||
resetTimeout: async () => {
|
||||
resetSessionTimeout();
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
updateTimeout: async (request) => {
|
||||
sessionTimeoutDuration = request.timeout;
|
||||
await chrome.storage.local.set({ sessionTimeout: request.timeout });
|
||||
resetSessionTimeout(); // Restart with new duration
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
updateSigSocketUrl: async (request) => {
|
||||
if (sigSocketService) {
|
||||
// Update the server URL in the SigSocket service
|
||||
sigSocketService.defaultServerUrl = request.serverUrl;
|
||||
|
||||
// Save to storage (already done in popup, but ensure consistency)
|
||||
await chrome.storage.local.set({ sigSocketUrl: request.serverUrl });
|
||||
|
||||
console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`);
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
// SigSocket handlers
|
||||
connectSigSocket: async (request) => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
const connected = await sigSocketService.connectToServer(request.workspace);
|
||||
return { success: connected };
|
||||
},
|
||||
|
||||
disconnectSigSocket: async () => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
await sigSocketService.disconnect();
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
getSigSocketStatus: async () => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
const status = await sigSocketService.getStatus();
|
||||
return { success: true, status };
|
||||
},
|
||||
|
||||
getSigSocketStatusWithTest: async () => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
// Use the enhanced connection testing method
|
||||
const status = await sigSocketService.getStatusWithConnectionTest();
|
||||
return { success: true, status };
|
||||
},
|
||||
|
||||
getPendingSignRequests: async () => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Use WASM filtered requests which handles workspace filtering
|
||||
const requests = await sigSocketService.getFilteredRequests();
|
||||
return { success: true, requests };
|
||||
} catch (error) {
|
||||
console.error('Failed to get pending requests:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
approveSignRequest: async (request) => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
const approved = await sigSocketService.approveSignRequest(request.requestId);
|
||||
return { success: approved };
|
||||
},
|
||||
|
||||
rejectSignRequest: async (request) => {
|
||||
if (!sigSocketService) {
|
||||
return { success: false, error: 'SigSocket service not initialized' };
|
||||
}
|
||||
const rejected = await sigSocketService.rejectSignRequest(request.requestId, request.reason);
|
||||
return { success: rejected };
|
||||
}
|
||||
};
|
||||
|
||||
// Handle messages from popup and content scripts
|
||||
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||
const handleRequest = async () => {
|
||||
try {
|
||||
if (!vault) {
|
||||
await initVault();
|
||||
}
|
||||
|
||||
const handler = messageHandlers[request.action];
|
||||
if (handler) {
|
||||
return await handler(request);
|
||||
} else {
|
||||
throw new Error('Unknown action: ' + request.action);
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
handleRequest().then(sendResponse);
|
||||
return true; // Keep the message channel open for async response
|
||||
});
|
||||
|
||||
// Initialize vault when extension starts
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
initVault();
|
||||
});
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
initVault();
|
||||
});
|
||||
|
||||
// Handle popup connection for keep-alive and timeout notifications
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === 'popup') {
|
||||
// Track popup connection
|
||||
popupPort = port;
|
||||
|
||||
// Connect SigSocket service to popup
|
||||
if (sigSocketService) {
|
||||
sigSocketService.setPopupPort(port);
|
||||
}
|
||||
|
||||
// If we have an active session, ensure keep-alive is running
|
||||
if (currentSession) {
|
||||
startKeepAlive();
|
||||
}
|
||||
|
||||
// Handle messages from popup
|
||||
port.onMessage.addListener(async (message) => {
|
||||
if (message.type === 'REQUEST_IMMEDIATE_STATUS') {
|
||||
// Immediately send current SigSocket status to popup
|
||||
if (sigSocketService) {
|
||||
try {
|
||||
const status = await sigSocketService.getStatus();
|
||||
port.postMessage({
|
||||
type: 'CONNECTION_STATUS_CHANGED',
|
||||
status: status
|
||||
});
|
||||
console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected');
|
||||
} catch (error) {
|
||||
console.warn('Failed to send immediate status:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
// Popup closed, clear reference and stop keep-alive
|
||||
popupPort = null;
|
||||
stopKeepAlive();
|
||||
|
||||
// Disconnect SigSocket service from popup
|
||||
if (sigSocketService) {
|
||||
sigSocketService.setPopupPort(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle notification clicks to open extension (notifications are now clickable without buttons)
|
||||
chrome.notifications.onClicked.addListener(async (notificationId) => {
|
||||
console.log(`🔔 Notification clicked: ${notificationId}`);
|
||||
|
||||
// Check if this is a SigSocket notification
|
||||
if (notificationId.startsWith('sigsocket-request-')) {
|
||||
console.log('🔔 SigSocket notification clicked, opening extension...');
|
||||
try {
|
||||
await openExtensionPopup();
|
||||
// Clear the notification after successfully opening
|
||||
chrome.notifications.clear(notificationId);
|
||||
console.log('✅ Notification cleared after opening extension');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to handle notification click:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('🔔 Non-SigSocket notification clicked, ignoring');
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Notification button handler removed - notifications are now clickable without buttons
|
||||
|
||||
// Function to open extension popup with best UX
|
||||
async function openExtensionPopup() {
|
||||
try {
|
||||
console.log('🔔 Opening extension popup from notification...');
|
||||
|
||||
// First, check if there's already a popup window open
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
const existingPopup = windows.find(window =>
|
||||
window.type === 'popup' &&
|
||||
window.tabs?.some(tab => tab.url?.includes('popup.html'))
|
||||
);
|
||||
|
||||
if (existingPopup) {
|
||||
// Focus existing popup and send focus message
|
||||
await chrome.windows.update(existingPopup.id, { focused: true });
|
||||
console.log('✅ Focused existing popup window');
|
||||
|
||||
// Send message to focus on SigSocket section
|
||||
if (popupPort) {
|
||||
popupPort.postMessage({
|
||||
type: 'FOCUS_SIGSOCKET',
|
||||
fromNotification: true
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Best UX: Try to use the normal popup experience
|
||||
// The action API gives the same popup as clicking the extension icon
|
||||
try {
|
||||
if (chrome.action && chrome.action.openPopup) {
|
||||
await chrome.action.openPopup();
|
||||
console.log('✅ Extension popup opened via action API (best UX - normal popup)');
|
||||
|
||||
// Send focus message after popup opens
|
||||
setTimeout(() => {
|
||||
if (popupPort) {
|
||||
popupPort.postMessage({
|
||||
type: 'FOCUS_SIGSOCKET',
|
||||
fromNotification: true
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (actionError) {
|
||||
// The action API fails when there's no active browser window
|
||||
// This is common when all browser windows are closed but extension is still running
|
||||
console.log('⚠️ Action API failed (likely no active window):', actionError.message);
|
||||
|
||||
// Check if we have any normal browser windows
|
||||
const allWindows = await chrome.windows.getAll();
|
||||
const normalWindows = allWindows.filter(w => w.type === 'normal');
|
||||
|
||||
if (normalWindows.length > 0) {
|
||||
// We have browser windows, try to focus one and retry action API
|
||||
try {
|
||||
const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0];
|
||||
await chrome.windows.update(targetWindow.id, { focused: true });
|
||||
|
||||
// Small delay and retry
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await chrome.action.openPopup();
|
||||
console.log('✅ Extension popup opened via action API after focusing window');
|
||||
|
||||
setTimeout(() => {
|
||||
if (popupPort) {
|
||||
popupPort.postMessage({
|
||||
type: 'FOCUS_SIGSOCKET',
|
||||
fromNotification: true
|
||||
});
|
||||
}
|
||||
}, 200);
|
||||
|
||||
return;
|
||||
} catch (retryError) {
|
||||
console.log('⚠️ Action API retry also failed:', retryError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If action API fails completely, we need to create a window
|
||||
// But let's make it as close to the normal popup experience as possible
|
||||
console.log('⚠️ Creating popup window as fallback (action API unavailable)');
|
||||
|
||||
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
|
||||
|
||||
// Position the popup where the extension icon would normally show its popup
|
||||
// Try to position it in the top-right area like a normal extension popup
|
||||
let left = screen.width - 420; // 400px width + 20px margin
|
||||
let top = 80; // Below browser toolbar area
|
||||
|
||||
try {
|
||||
// If we have a browser window, position relative to it
|
||||
const allWindows = await chrome.windows.getAll();
|
||||
const normalWindows = allWindows.filter(w => w.type === 'normal');
|
||||
|
||||
if (normalWindows.length > 0) {
|
||||
const referenceWindow = normalWindows[0];
|
||||
left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420;
|
||||
top = (referenceWindow.top || 0) + 80;
|
||||
}
|
||||
} catch (positionError) {
|
||||
console.log('⚠️ Could not get window position, using screen-based positioning');
|
||||
}
|
||||
|
||||
const newWindow = await chrome.windows.create({
|
||||
url: popupUrl,
|
||||
type: 'popup',
|
||||
width: 400,
|
||||
height: 600,
|
||||
left: Math.max(0, left),
|
||||
top: Math.max(0, top),
|
||||
focused: true
|
||||
});
|
||||
|
||||
console.log(`✅ Extension popup window created: ${newWindow.id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to open extension popup:', error);
|
||||
|
||||
// Final fallback: open in new tab (least ideal but still functional)
|
||||
try {
|
||||
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
|
||||
await chrome.tabs.create({ url: popupUrl, active: true });
|
||||
console.log('✅ Opened extension in new tab as final fallback');
|
||||
} catch (tabError) {
|
||||
console.error('❌ All popup opening methods failed:', tabError);
|
||||
}
|
||||
}
|
||||
}
|
||||
876
crypto_vault_extension/background/sigsocket.js
Normal file
@@ -0,0 +1,876 @@
|
||||
/**
|
||||
* SigSocket Service - Clean Implementation with New WASM APIs
|
||||
*
|
||||
* This service provides a clean interface for SigSocket functionality using
|
||||
* the new WASM-based APIs that handle all WebSocket management, request storage,
|
||||
* and security validation internally.
|
||||
*
|
||||
* Architecture:
|
||||
* - WASM handles: WebSocket connection, message parsing, request storage, security
|
||||
* - Extension handles: UI notifications, badge updates, user interactions
|
||||
*/
|
||||
|
||||
class SigSocketService {
|
||||
constructor() {
|
||||
// Connection state
|
||||
this.isConnected = false;
|
||||
this.currentWorkspace = null;
|
||||
this.connectedPublicKey = null;
|
||||
|
||||
// Configuration
|
||||
this.defaultServerUrl = "ws://localhost:8080/ws";
|
||||
|
||||
// WASM module reference
|
||||
this.wasmModule = null;
|
||||
|
||||
// UI communication
|
||||
this.popupPort = null;
|
||||
|
||||
// Status monitoring
|
||||
this.statusMonitorInterval = null;
|
||||
this.lastKnownConnectionState = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the service with WASM module
|
||||
* @param {Object} wasmModule - The loaded WASM module with SigSocketManager
|
||||
*/
|
||||
async initialize(wasmModule) {
|
||||
this.wasmModule = wasmModule;
|
||||
|
||||
// Load server URL from storage
|
||||
try {
|
||||
const result = await chrome.storage.local.get(['sigSocketUrl']);
|
||||
if (result.sigSocketUrl) {
|
||||
this.defaultServerUrl = result.sigSocketUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load SigSocket URL from storage:', error);
|
||||
}
|
||||
|
||||
// Restore any persisted pending requests
|
||||
await this.restorePendingRequests();
|
||||
|
||||
console.log('🔌 SigSocket service initialized with WASM APIs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore pending requests from persistent storage
|
||||
* Only restore requests that match the current workspace
|
||||
*/
|
||||
async restorePendingRequests() {
|
||||
try {
|
||||
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
|
||||
if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) {
|
||||
console.log(`🔄 Found ${result.sigSocketPendingRequests.length} stored requests`);
|
||||
|
||||
// Filter requests for current workspace only
|
||||
const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request =>
|
||||
request.target_public_key === this.connectedPublicKey
|
||||
);
|
||||
|
||||
console.log(`🔄 Restoring ${currentWorkspaceRequests.length} requests for current workspace`);
|
||||
|
||||
// Add each workspace-specific request back to WASM storage
|
||||
for (const request of currentWorkspaceRequests) {
|
||||
try {
|
||||
await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request));
|
||||
console.log(`✅ Restored request: ${request.id || request.request?.id}`);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update badge after restoration
|
||||
this.updateBadge();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore pending requests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist pending requests to storage with workspace isolation
|
||||
*/
|
||||
async persistPendingRequests() {
|
||||
try {
|
||||
const requests = await this.getFilteredRequests();
|
||||
|
||||
// Get existing storage to merge with other workspaces
|
||||
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
|
||||
const existingRequests = result.sigSocketPendingRequests || [];
|
||||
|
||||
// Remove old requests for current workspace
|
||||
const otherWorkspaceRequests = existingRequests.filter(request =>
|
||||
request.target_public_key !== this.connectedPublicKey
|
||||
);
|
||||
|
||||
// Combine with current workspace requests
|
||||
const allRequests = [...otherWorkspaceRequests, ...requests];
|
||||
|
||||
await chrome.storage.local.set({ sigSocketPendingRequests: allRequests });
|
||||
console.log(`💾 Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`);
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist pending requests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to SigSocket server using WASM APIs
|
||||
* WASM handles all connection logic (reuse, switching, etc.)
|
||||
* @param {string} workspaceId - The workspace/keyspace identifier
|
||||
* @param {number} retryCount - Number of retry attempts (default: 3)
|
||||
* @returns {Promise<boolean>} - True if connected successfully
|
||||
*/
|
||||
async connectToServer(workspaceId, retryCount = 3) {
|
||||
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
||||
try {
|
||||
if (!this.wasmModule?.SigSocketManager) {
|
||||
throw new Error('WASM SigSocketManager not available');
|
||||
}
|
||||
|
||||
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`);
|
||||
|
||||
// Clean workspace switching
|
||||
if (this.currentWorkspace && this.currentWorkspace !== workspaceId) {
|
||||
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`);
|
||||
await this.cleanWorkspaceSwitch(workspaceId);
|
||||
// Small delay to ensure clean state transition
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
|
||||
// Let WASM handle all connection logic (reuse, switching, etc.)
|
||||
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
|
||||
workspaceId,
|
||||
this.defaultServerUrl,
|
||||
(event) => this.handleSigSocketEvent(event)
|
||||
);
|
||||
|
||||
// Parse connection info
|
||||
const info = JSON.parse(connectionInfo);
|
||||
this.currentWorkspace = workspaceId; // Use the parameter we passed, not WASM response
|
||||
this.connectedPublicKey = info.public_key;
|
||||
this.isConnected = info.is_connected;
|
||||
|
||||
console.log(`✅ SigSocket connection result:`, {
|
||||
workspace: this.currentWorkspace,
|
||||
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
|
||||
connected: this.isConnected,
|
||||
serverUrl: this.defaultServerUrl
|
||||
});
|
||||
|
||||
// Validate that we have a public key if connected
|
||||
if (this.isConnected && !this.connectedPublicKey) {
|
||||
console.warn('⚠️ Connected but no public key received - this may cause request issues');
|
||||
}
|
||||
|
||||
// Update badge to show current state
|
||||
this.updateBadge();
|
||||
|
||||
if (this.isConnected) {
|
||||
// Clean flow: Connect -> Restore workspace requests -> Update UI
|
||||
console.log(`🔗 Connected to workspace: ${workspaceId}, restoring pending requests...`);
|
||||
|
||||
// 1. Restore requests for this specific workspace
|
||||
await this.restorePendingRequests();
|
||||
|
||||
// 2. Update badge with current count
|
||||
this.updateBadge();
|
||||
|
||||
console.log(`✅ Workspace ${workspaceId} ready with restored requests`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not connected but no error, try again
|
||||
if (attempt < retryCount) {
|
||||
console.log(`⏳ Connection not established, retrying in 1 second...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Check if this is an expected "no workspace" error during startup
|
||||
const isExpectedStartupError = error.message &&
|
||||
(error.message.includes('Workspace not found') ||
|
||||
error.message.includes('no keypairs available'));
|
||||
|
||||
if (isExpectedStartupError && attempt === 1) {
|
||||
console.log(`⏳ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`);
|
||||
}
|
||||
|
||||
// Check if this is a public key related error
|
||||
if (error.message && error.message.includes('public key')) {
|
||||
console.error(`🔑 Public key error detected: ${error.message}`);
|
||||
// For public key errors, don't retry immediately - might need workspace change
|
||||
if (attempt === 1) {
|
||||
console.log(`🔄 Public key error on first attempt, trying to disconnect and reconnect...`);
|
||||
await this.disconnect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt < retryCount) {
|
||||
if (!isExpectedStartupError) {
|
||||
console.log(`⏳ Retrying connection in 2 seconds...`);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
} else {
|
||||
// Final attempt failed
|
||||
this.isConnected = false;
|
||||
this.currentWorkspace = null;
|
||||
this.connectedPublicKey = null;
|
||||
|
||||
if (isExpectedStartupError) {
|
||||
console.log(`ℹ️ SigSocket connection failed: No active workspace. Will connect when user logs in.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle events from the WASM SigSocket client
|
||||
* This is called automatically when requests arrive
|
||||
* @param {Object} event - Event from WASM layer
|
||||
*/
|
||||
async handleSigSocketEvent(event) {
|
||||
console.log('📨 Received SigSocket event:', event);
|
||||
|
||||
if (event.type === 'sign_request') {
|
||||
console.log(`🔐 New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`);
|
||||
|
||||
// Clean flow: Request arrives -> Store -> Persist -> Update UI
|
||||
try {
|
||||
// 1. Request is automatically stored in WASM (already done by WASM layer)
|
||||
|
||||
// 2. Persist to storage with workspace isolation
|
||||
await this.persistPendingRequests();
|
||||
|
||||
// 3. Update badge count
|
||||
this.updateBadge();
|
||||
|
||||
// 4. Show notification
|
||||
this.showSignRequestNotification();
|
||||
|
||||
// 5. Notify popup if connected
|
||||
this.notifyPopupOfNewRequest();
|
||||
|
||||
console.log(`✅ Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to process request ${event.request_id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a sign request using WASM APIs
|
||||
* @param {string} requestId - Request to approve
|
||||
* @returns {Promise<boolean>} - True if approved successfully
|
||||
*/
|
||||
async approveSignRequest(requestId) {
|
||||
try {
|
||||
if (!this.wasmModule?.SigSocketManager) {
|
||||
throw new Error('WASM SigSocketManager not available');
|
||||
}
|
||||
|
||||
// Check if we're connected before attempting approval
|
||||
if (!this.isConnected) {
|
||||
console.warn(`⚠️ Not connected to SigSocket server, cannot approve request: ${requestId}`);
|
||||
throw new Error('Not connected to SigSocket server');
|
||||
}
|
||||
|
||||
// Verify we can approve this request
|
||||
const canApprove = await this.canApproveRequest(requestId);
|
||||
if (!canApprove) {
|
||||
console.warn(`⚠️ Cannot approve request ${requestId} - keyspace may be locked or request not found`);
|
||||
throw new Error('Cannot approve request - keyspace may be locked or request not found');
|
||||
}
|
||||
|
||||
console.log(`✅ Approving request: ${requestId}`);
|
||||
|
||||
// WASM handles all validation, signing, and server communication
|
||||
await this.wasmModule.SigSocketManager.approve_request(requestId);
|
||||
|
||||
console.log(`🎉 Request approved successfully: ${requestId}`);
|
||||
|
||||
// Clean flow: Approve -> Remove from storage -> Update UI
|
||||
// 1. Remove from persistent storage (WASM already removed it)
|
||||
await this.persistPendingRequests();
|
||||
|
||||
// 2. Update badge count
|
||||
this.updateBadge();
|
||||
|
||||
// 3. Notify popup of updated state
|
||||
this.notifyPopupOfRequestUpdate();
|
||||
|
||||
console.log(`✅ Request ${requestId} approved and cleaned up`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to approve request ${requestId}:`, error);
|
||||
|
||||
// Check if this is a connection-related error
|
||||
if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) {
|
||||
console.error(`🔑 Connection/public key error during approval. Current state:`, {
|
||||
connected: this.isConnected,
|
||||
workspace: this.currentWorkspace,
|
||||
publicKey: this.connectedPublicKey?.substring(0, 16) + '...'
|
||||
});
|
||||
|
||||
// Try to reconnect for next time
|
||||
if (this.currentWorkspace) {
|
||||
console.log(`🔄 Attempting to reconnect to workspace: ${this.currentWorkspace}`);
|
||||
setTimeout(() => this.connectToServer(this.currentWorkspace), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a sign request using WASM APIs
|
||||
* @param {string} requestId - Request to reject
|
||||
* @param {string} reason - Reason for rejection
|
||||
* @returns {Promise<boolean>} - True if rejected successfully
|
||||
*/
|
||||
async rejectSignRequest(requestId, reason = 'User rejected') {
|
||||
try {
|
||||
if (!this.wasmModule?.SigSocketManager) {
|
||||
throw new Error('WASM SigSocketManager not available');
|
||||
}
|
||||
|
||||
console.log(`❌ Rejecting request: ${requestId}, reason: ${reason}`);
|
||||
|
||||
// WASM handles rejection and server communication
|
||||
await this.wasmModule.SigSocketManager.reject_request(requestId, reason);
|
||||
|
||||
console.log(`✅ Request rejected successfully: ${requestId}`);
|
||||
|
||||
// Clean flow: Reject -> Remove from storage -> Update UI
|
||||
// 1. Remove from persistent storage (WASM already removed it)
|
||||
await this.persistPendingRequests();
|
||||
|
||||
// 2. Update badge count
|
||||
this.updateBadge();
|
||||
|
||||
// 3. Notify popup of updated state
|
||||
this.notifyPopupOfRequestUpdate();
|
||||
|
||||
console.log(`✅ Request ${requestId} rejected and cleaned up`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to reject request ${requestId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending requests from WASM (filtered by current workspace)
|
||||
* @returns {Promise<Array>} - Array of pending requests for current workspace
|
||||
*/
|
||||
async getPendingRequests() {
|
||||
return this.getFilteredRequests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered requests from WASM (workspace-aware)
|
||||
* @returns {Promise<Array>} - Array of filtered requests
|
||||
*/
|
||||
async getFilteredRequests() {
|
||||
try {
|
||||
if (!this.wasmModule?.SigSocketManager) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const requestsJson = await this.wasmModule.SigSocketManager.get_filtered_requests();
|
||||
const requests = JSON.parse(requestsJson);
|
||||
|
||||
console.log(`📋 Retrieved ${requests.length} filtered requests for current workspace`);
|
||||
return requests;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to get filtered requests:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a request can be approved (keyspace validation)
|
||||
* @param {string} requestId - Request ID to check
|
||||
* @returns {Promise<boolean>} - True if can be approved
|
||||
*/
|
||||
async canApproveRequest(requestId) {
|
||||
try {
|
||||
if (!this.wasmModule?.SigSocketManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await this.wasmModule.SigSocketManager.can_approve_request(requestId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to check request approval status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show clickable notification for new sign request
|
||||
* Call this AFTER the request has been stored and persisted
|
||||
*/
|
||||
async showSignRequestNotification() {
|
||||
try {
|
||||
if (chrome.notifications && chrome.notifications.create) {
|
||||
// Small delay to ensure request is fully stored
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
console.log(`📢 Preparing notification for new signature request`);
|
||||
|
||||
// Check if keyspace is currently unlocked to customize message
|
||||
let message = 'New signature request received. Click to review and approve.';
|
||||
let title = 'SigSocket Sign Request';
|
||||
|
||||
// Try to determine if keyspace is locked
|
||||
try {
|
||||
const requests = await this.getPendingRequests();
|
||||
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
|
||||
if (!canApprove) {
|
||||
message = 'New signature request received. Click to unlock keyspace and approve.';
|
||||
title = 'SigSocket Request';
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't check, use generic message
|
||||
message = 'New signature request received. Click to open extension.';
|
||||
}
|
||||
|
||||
// Create clickable notification with unique ID
|
||||
const notificationId = `sigsocket-request-${Date.now()}`;
|
||||
|
||||
const notificationOptions = {
|
||||
type: 'basic',
|
||||
iconUrl: 'icons/icon48.png',
|
||||
title: title,
|
||||
message: message,
|
||||
requireInteraction: true // Keep notification visible until user interacts
|
||||
};
|
||||
|
||||
console.log(`📢 Creating notification: ${notificationId}`, notificationOptions);
|
||||
|
||||
chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('❌ Failed to create notification:', chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log(`✅ Notification created successfully: ${createdId}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('📢 Notifications not available, skipping notification');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to show notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update extension badge with pending request count
|
||||
*/
|
||||
async updateBadge() {
|
||||
try {
|
||||
const requests = await this.getPendingRequests();
|
||||
const count = requests.length;
|
||||
const badgeText = count > 0 ? count.toString() : '';
|
||||
|
||||
console.log(`🔢 Updating badge: ${count} pending requests`);
|
||||
|
||||
chrome.action.setBadgeText({ text: badgeText });
|
||||
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to update badge:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify popup about new request
|
||||
*/
|
||||
async notifyPopupOfNewRequest() {
|
||||
if (!this.popupPort) {
|
||||
console.log('No popup connected, skipping notification');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const requests = await this.getPendingRequests();
|
||||
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
|
||||
|
||||
this.popupPort.postMessage({
|
||||
type: 'NEW_SIGN_REQUEST',
|
||||
canApprove,
|
||||
pendingRequests: requests
|
||||
});
|
||||
|
||||
console.log(`📤 Notified popup: ${requests.length} requests, canApprove: ${canApprove}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to notify popup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify popup about request updates
|
||||
*/
|
||||
async notifyPopupOfRequestUpdate() {
|
||||
if (!this.popupPort) return;
|
||||
|
||||
try {
|
||||
const requests = await this.getPendingRequests();
|
||||
|
||||
this.popupPort.postMessage({
|
||||
type: 'REQUESTS_UPDATED',
|
||||
pendingRequests: requests
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to notify popup of update:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from SigSocket server
|
||||
* WASM handles all disconnection logic
|
||||
*/
|
||||
async disconnect() {
|
||||
try {
|
||||
if (this.wasmModule?.SigSocketManager) {
|
||||
await this.wasmModule.SigSocketManager.disconnect();
|
||||
}
|
||||
|
||||
// Clear local state
|
||||
this.isConnected = false;
|
||||
this.currentWorkspace = null;
|
||||
this.connectedPublicKey = null;
|
||||
this.lastKnownConnectionState = false;
|
||||
|
||||
// Stop status monitoring
|
||||
this.stopStatusMonitoring();
|
||||
|
||||
this.updateBadge();
|
||||
|
||||
console.log('🔌 SigSocket disconnection requested');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear persisted pending requests from storage
|
||||
*/
|
||||
async clearPersistedRequests() {
|
||||
try {
|
||||
await chrome.storage.local.remove(['sigSocketPendingRequests']);
|
||||
console.log('🗑️ Cleared persisted pending requests from storage');
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear persisted requests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean workspace switch - clear current workspace requests only
|
||||
*/
|
||||
async cleanWorkspaceSwitch(newWorkspace) {
|
||||
try {
|
||||
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`);
|
||||
|
||||
// 1. Persist current workspace requests before switching
|
||||
if (this.currentWorkspace && this.isConnected) {
|
||||
await this.persistPendingRequests();
|
||||
console.log(`💾 Saved requests for workspace: ${this.currentWorkspace}`);
|
||||
}
|
||||
|
||||
// 2. Clear WASM state (will be restored for new workspace)
|
||||
if (this.wasmModule?.SigSocketManager) {
|
||||
await this.wasmModule.SigSocketManager.clear_pending_requests();
|
||||
console.log('🧹 Cleared WASM request state');
|
||||
}
|
||||
|
||||
// 3. Reset local state
|
||||
this.currentWorkspace = null;
|
||||
this.connectedPublicKey = null;
|
||||
this.isConnected = false;
|
||||
|
||||
console.log('✅ Workspace switch cleanup completed');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clean workspace switch:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status with real connection verification
|
||||
* @returns {Promise<Object>} - Connection status information
|
||||
*/
|
||||
async getStatus() {
|
||||
try {
|
||||
if (!this.wasmModule?.SigSocketManager) {
|
||||
return {
|
||||
isConnected: false,
|
||||
workspace: null,
|
||||
publicKey: null,
|
||||
pendingRequestCount: 0,
|
||||
serverUrl: this.defaultServerUrl
|
||||
};
|
||||
}
|
||||
|
||||
// Get WASM status first
|
||||
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
|
||||
const status = JSON.parse(statusJson);
|
||||
|
||||
// Verify connection by trying to get requests (this will fail if not connected)
|
||||
let actuallyConnected = false;
|
||||
let requests = [];
|
||||
|
||||
try {
|
||||
requests = await this.getPendingRequests();
|
||||
// If we can get requests and WASM says connected, we're probably connected
|
||||
actuallyConnected = status.is_connected && Array.isArray(requests);
|
||||
} catch (error) {
|
||||
// If getting requests fails, we're definitely not connected
|
||||
console.warn('Connection verification failed:', error);
|
||||
actuallyConnected = false;
|
||||
}
|
||||
|
||||
// Update our internal state
|
||||
this.isConnected = actuallyConnected;
|
||||
|
||||
if (status.connected_public_key && actuallyConnected) {
|
||||
this.connectedPublicKey = status.connected_public_key;
|
||||
} else {
|
||||
this.connectedPublicKey = null;
|
||||
}
|
||||
|
||||
// If we're disconnected, clear our workspace
|
||||
if (!actuallyConnected) {
|
||||
this.currentWorkspace = null;
|
||||
}
|
||||
|
||||
const statusResult = {
|
||||
isConnected: actuallyConnected,
|
||||
workspace: this.currentWorkspace,
|
||||
publicKey: status.connected_public_key,
|
||||
pendingRequestCount: requests.length,
|
||||
serverUrl: this.defaultServerUrl,
|
||||
// Clean flow status indicators
|
||||
cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key
|
||||
};
|
||||
|
||||
console.log('📊 Clean flow status:', {
|
||||
connected: statusResult.isConnected,
|
||||
workspace: statusResult.workspace,
|
||||
requestCount: statusResult.pendingRequestCount,
|
||||
flowReady: statusResult.cleanFlowReady
|
||||
});
|
||||
|
||||
return statusResult;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to get status:', error);
|
||||
// Clear state on error
|
||||
this.isConnected = false;
|
||||
this.currentWorkspace = null;
|
||||
this.connectedPublicKey = null;
|
||||
return {
|
||||
isConnected: false,
|
||||
workspace: null,
|
||||
publicKey: null,
|
||||
pendingRequestCount: 0,
|
||||
serverUrl: this.defaultServerUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the popup port for communication
|
||||
* @param {chrome.runtime.Port|null} port - The popup port or null to disconnect
|
||||
*/
|
||||
setPopupPort(port) {
|
||||
this.popupPort = port;
|
||||
|
||||
if (port) {
|
||||
console.log('📱 Popup connected to SigSocket service');
|
||||
|
||||
// Immediately check connection status when popup opens
|
||||
this.checkConnectionStatusNow();
|
||||
|
||||
// Start monitoring connection status when popup connects
|
||||
this.startStatusMonitoring();
|
||||
} else {
|
||||
console.log('📱 Popup disconnected from SigSocket service');
|
||||
// Stop monitoring when popup disconnects
|
||||
this.stopStatusMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately check and update connection status
|
||||
*/
|
||||
async checkConnectionStatusNow() {
|
||||
try {
|
||||
// Force a fresh connection check
|
||||
const currentStatus = await this.getStatusWithConnectionTest();
|
||||
this.lastKnownConnectionState = currentStatus.isConnected;
|
||||
|
||||
// Notify popup of current status
|
||||
this.notifyPopupOfStatusChange(currentStatus);
|
||||
|
||||
console.log(`🔍 Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`);
|
||||
} catch (error) {
|
||||
console.warn('Failed to check connection status immediately:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status with additional connection testing
|
||||
*/
|
||||
async getStatusWithConnectionTest() {
|
||||
const status = await this.getStatus();
|
||||
|
||||
// If WASM claims we're connected, do an additional verification
|
||||
if (status.isConnected) {
|
||||
try {
|
||||
// Try to get connection status again - if this fails, we're not really connected
|
||||
const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status();
|
||||
const verifyStatus = JSON.parse(verifyJson);
|
||||
|
||||
if (!verifyStatus.is_connected) {
|
||||
console.log('🔍 Connection verification failed - marking as disconnected');
|
||||
status.isConnected = false;
|
||||
this.isConnected = false;
|
||||
this.currentWorkspace = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('🔍 Connection test failed - marking as disconnected:', error.message);
|
||||
status.isConnected = false;
|
||||
this.isConnected = false;
|
||||
this.currentWorkspace = null;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic status monitoring to detect connection changes
|
||||
*/
|
||||
startStatusMonitoring() {
|
||||
// Clear any existing monitoring
|
||||
if (this.statusMonitorInterval) {
|
||||
clearInterval(this.statusMonitorInterval);
|
||||
}
|
||||
|
||||
// Check status every 2 seconds when popup is open (more responsive)
|
||||
this.statusMonitorInterval = setInterval(async () => {
|
||||
if (this.popupPort) {
|
||||
try {
|
||||
const currentStatus = await this.getStatusWithConnectionTest();
|
||||
|
||||
// Check if connection status changed
|
||||
if (currentStatus.isConnected !== this.lastKnownConnectionState) {
|
||||
console.log(`🔄 Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`);
|
||||
this.lastKnownConnectionState = currentStatus.isConnected;
|
||||
|
||||
// Notify popup of status change
|
||||
this.notifyPopupOfStatusChange(currentStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Status monitoring error:', error);
|
||||
// On error, assume disconnected
|
||||
if (this.lastKnownConnectionState !== false) {
|
||||
console.log('🔄 Status monitoring error - marking as disconnected');
|
||||
this.lastKnownConnectionState = false;
|
||||
this.notifyPopupOfStatusChange({
|
||||
isConnected: false,
|
||||
workspace: null,
|
||||
publicKey: null,
|
||||
pendingRequestCount: 0,
|
||||
serverUrl: this.defaultServerUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stop monitoring when popup is closed
|
||||
this.stopStatusMonitoring();
|
||||
}
|
||||
}, 2000); // 2 seconds for better responsiveness
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop status monitoring
|
||||
*/
|
||||
stopStatusMonitoring() {
|
||||
if (this.statusMonitorInterval) {
|
||||
clearInterval(this.statusMonitorInterval);
|
||||
this.statusMonitorInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify popup of connection status change
|
||||
* @param {Object} status - Current connection status
|
||||
*/
|
||||
notifyPopupOfStatusChange(status) {
|
||||
if (this.popupPort) {
|
||||
this.popupPort.postMessage({
|
||||
type: 'CONNECTION_STATUS_CHANGED',
|
||||
status: status
|
||||
});
|
||||
console.log(`📡 Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when keyspace is unlocked - clean approach to show pending requests
|
||||
*/
|
||||
async onKeypaceUnlocked() {
|
||||
try {
|
||||
console.log('🔓 Keyspace unlocked - preparing to show pending requests');
|
||||
|
||||
// 1. Restore any persisted requests for this workspace
|
||||
await this.restorePendingRequests();
|
||||
|
||||
// 2. Get current requests (includes restored + any new ones)
|
||||
const requests = await this.getPendingRequests();
|
||||
|
||||
// 3. Check if we can approve requests (keyspace should be unlocked now)
|
||||
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true;
|
||||
|
||||
// 4. Update badge with current count
|
||||
this.updateBadge();
|
||||
|
||||
// 5. Notify popup if connected
|
||||
if (this.popupPort) {
|
||||
this.popupPort.postMessage({
|
||||
type: 'KEYSPACE_UNLOCKED',
|
||||
canApprove,
|
||||
pendingRequests: requests
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔓 Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`);
|
||||
|
||||
return requests;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to handle keyspace unlock:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in background script
|
||||
export default SigSocketService;
|
||||
75
crypto_vault_extension/demo/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Mock SigSocket Server Demo
|
||||
|
||||
This directory contains a mock SigSocket server for testing the browser extension functionality.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the mock server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
The server will listen on `ws://localhost:8080/ws`
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Commands
|
||||
|
||||
Once the server is running, you can use these commands:
|
||||
|
||||
- `test` - Send a test sign request to all connected clients
|
||||
- `status` - Show server status and connected clients
|
||||
- `quit` - Shutdown the server
|
||||
|
||||
### Testing Flow
|
||||
|
||||
1. Start the mock server
|
||||
2. Load the browser extension in Chrome
|
||||
3. Create a keyspace and keypair in the extension
|
||||
4. The extension should automatically connect to the server
|
||||
5. The server will send a test sign request after 3 seconds
|
||||
6. Use the extension popup to approve or reject the request
|
||||
7. The server will log the response and send another request after 10 seconds
|
||||
|
||||
### Expected Output
|
||||
|
||||
When a client connects:
|
||||
```
|
||||
New WebSocket connection from: ::1
|
||||
Received message: 04a8b2c3d4e5f6...
|
||||
Client registered: client_1234567890_abc123 with public key: 04a8b2c3d4e5f6...
|
||||
📝 Sending sign request to client_1234567890_abc123: req_1_1234567890
|
||||
Message: "Test message 1 - 2024-01-01T12:00:00.000Z"
|
||||
```
|
||||
|
||||
When a sign response is received:
|
||||
```
|
||||
Received sign response from client_1234567890_abc123: {
|
||||
id: 'req_1_1234567890',
|
||||
message: 'VGVzdCBtZXNzYWdlIDEgLSAyMDI0LTAxLTAxVDEyOjAwOjAwLjAwMFo=',
|
||||
signature: '3045022100...'
|
||||
}
|
||||
✅ Sign request req_1_1234567890 completed successfully
|
||||
Signature: 3045022100...
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
The mock server implements a simplified version of the SigSocket protocol:
|
||||
|
||||
1. **Client Introduction**: Client sends hex-encoded public key
|
||||
2. **Welcome Message**: Server responds with welcome JSON
|
||||
3. **Sign Requests**: Server sends JSON with `id` and `message` (base64)
|
||||
4. **Sign Responses**: Client sends JSON with `id`, `message`, and `signature`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Connection refused**: Make sure the server is running on port 8080
|
||||
- **No sign requests**: Check that the extension is properly connected
|
||||
- **Extension errors**: Check the browser console for JavaScript errors
|
||||
- **WASM errors**: Ensure the WASM files are properly built and loaded
|
||||
232
crypto_vault_extension/demo/mock_sigsocket_server.js
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Mock SigSocket Server for Testing Browser Extension
|
||||
*
|
||||
* This is a simple WebSocket server that simulates the SigSocket protocol
|
||||
* for testing the browser extension functionality.
|
||||
*
|
||||
* Usage:
|
||||
* node mock_sigsocket_server.js
|
||||
*
|
||||
* The server will listen on ws://localhost:8080/ws
|
||||
*/
|
||||
|
||||
const WebSocket = require('ws');
|
||||
const http = require('http');
|
||||
|
||||
class MockSigSocketServer {
|
||||
constructor(port = 8080) {
|
||||
this.port = port;
|
||||
this.clients = new Map(); // clientId -> { ws, publicKey }
|
||||
this.requestCounter = 0;
|
||||
|
||||
this.setupServer();
|
||||
}
|
||||
|
||||
setupServer() {
|
||||
// Create HTTP server
|
||||
this.httpServer = http.createServer();
|
||||
|
||||
// Create WebSocket server
|
||||
this.wss = new WebSocket.Server({
|
||||
server: this.httpServer,
|
||||
path: '/ws'
|
||||
});
|
||||
|
||||
this.wss.on('connection', (ws, req) => {
|
||||
console.log('New WebSocket connection from:', req.socket.remoteAddress);
|
||||
this.handleConnection(ws);
|
||||
});
|
||||
|
||||
this.httpServer.listen(this.port, () => {
|
||||
console.log(`Mock SigSocket Server listening on ws://localhost:${this.port}/ws`);
|
||||
console.log('Waiting for browser extension connections...');
|
||||
});
|
||||
}
|
||||
|
||||
handleConnection(ws) {
|
||||
let clientId = null;
|
||||
let publicKey = null;
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const message = data.toString();
|
||||
console.log('Received message:', message);
|
||||
|
||||
// Check if this is a client introduction (hex-encoded public key)
|
||||
if (!clientId && this.isHexString(message)) {
|
||||
publicKey = message;
|
||||
clientId = this.generateClientId();
|
||||
|
||||
this.clients.set(clientId, { ws, publicKey });
|
||||
|
||||
console.log(`Client registered: ${clientId} with public key: ${publicKey.substring(0, 16)}...`);
|
||||
|
||||
// Send welcome message
|
||||
ws.send(JSON.stringify({
|
||||
type: 'welcome',
|
||||
clientId: clientId,
|
||||
message: 'Connected to Mock SigSocket Server'
|
||||
}));
|
||||
|
||||
// Schedule a test sign request after 3 seconds
|
||||
setTimeout(() => {
|
||||
this.sendTestSignRequest(clientId);
|
||||
}, 3000);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse as JSON (sign response)
|
||||
try {
|
||||
const jsonMessage = JSON.parse(message);
|
||||
this.handleSignResponse(clientId, jsonMessage);
|
||||
} catch (e) {
|
||||
console.log('Received non-JSON message:', message);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
if (clientId) {
|
||||
this.clients.delete(clientId);
|
||||
console.log(`Client disconnected: ${clientId}`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
handleSignResponse(clientId, response) {
|
||||
console.log(`Received sign response from ${clientId}:`, response);
|
||||
|
||||
if (response.id && response.signature) {
|
||||
console.log(`✅ Sign request ${response.id} completed successfully`);
|
||||
console.log(` Signature: ${response.signature.substring(0, 32)}...`);
|
||||
|
||||
// Send another test request after 10 seconds
|
||||
setTimeout(() => {
|
||||
this.sendTestSignRequest(clientId);
|
||||
}, 10000);
|
||||
} else {
|
||||
console.log('❌ Invalid sign response format');
|
||||
}
|
||||
}
|
||||
|
||||
sendTestSignRequest(clientId) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (!client) {
|
||||
console.log(`Client ${clientId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestCounter++;
|
||||
const requestId = `req_${this.requestCounter}_${Date.now()}`;
|
||||
const testMessage = `Test message ${this.requestCounter} - ${new Date().toISOString()}`;
|
||||
const messageBase64 = Buffer.from(testMessage).toString('base64');
|
||||
|
||||
const signRequest = {
|
||||
id: requestId,
|
||||
message: messageBase64
|
||||
};
|
||||
|
||||
console.log(`📝 Sending sign request to ${clientId}:`, requestId);
|
||||
console.log(` Message: "${testMessage}"`);
|
||||
|
||||
try {
|
||||
client.ws.send(JSON.stringify(signRequest));
|
||||
} catch (error) {
|
||||
console.error(`Failed to send sign request to ${clientId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
isHexString(str) {
|
||||
return /^[0-9a-fA-F]+$/.test(str) && str.length >= 32; // At least 16 bytes
|
||||
}
|
||||
|
||||
generateClientId() {
|
||||
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
||||
}
|
||||
|
||||
// Send a test request to all connected clients
|
||||
broadcastTestRequest() {
|
||||
console.log('\n📢 Broadcasting test sign request to all clients...');
|
||||
for (const [clientId] of this.clients) {
|
||||
this.sendTestSignRequest(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
// Get server status
|
||||
getStatus() {
|
||||
return {
|
||||
port: this.port,
|
||||
connectedClients: this.clients.size,
|
||||
clients: Array.from(this.clients.keys())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create and start the server
|
||||
const server = new MockSigSocketServer();
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 Shutting down Mock SigSocket Server...');
|
||||
server.httpServer.close(() => {
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Add some interactive commands
|
||||
process.stdin.setEncoding('utf8');
|
||||
console.log('\n📋 Available commands:');
|
||||
console.log(' "test" - Send test sign request to all clients');
|
||||
console.log(' "status" - Show server status');
|
||||
console.log(' "quit" - Shutdown server');
|
||||
console.log(' Type a command and press Enter\n');
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
const chunk = process.stdin.read();
|
||||
if (chunk !== null) {
|
||||
const command = chunk.trim().toLowerCase();
|
||||
|
||||
switch (command) {
|
||||
case 'test':
|
||||
server.broadcastTestRequest();
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
const status = server.getStatus();
|
||||
console.log('\n📊 Server Status:');
|
||||
console.log(` Port: ${status.port}`);
|
||||
console.log(` Connected clients: ${status.connectedClients}`);
|
||||
if (status.clients.length > 0) {
|
||||
console.log(` Client IDs: ${status.clients.join(', ')}`);
|
||||
}
|
||||
console.log('');
|
||||
break;
|
||||
|
||||
case 'quit':
|
||||
case 'exit':
|
||||
process.emit('SIGINT');
|
||||
break;
|
||||
|
||||
case '':
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`Unknown command: ${command}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Export for testing
|
||||
module.exports = MockSigSocketServer;
|
||||
21
crypto_vault_extension/demo/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "mock-sigsocket-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Mock SigSocket server for testing browser extension",
|
||||
"main": "mock_sigsocket_server.js",
|
||||
"scripts": {
|
||||
"start": "node mock_sigsocket_server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.14.0"
|
||||
},
|
||||
"keywords": [
|
||||
"websocket",
|
||||
"sigsocket",
|
||||
"testing",
|
||||
"mock"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
}
|
||||
BIN
crypto_vault_extension/icons/icon128.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
crypto_vault_extension/icons/icon16.png
Normal file
|
After Width: | Height: | Size: 676 B |
BIN
crypto_vault_extension/icons/icon32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
crypto_vault_extension/icons/icon36.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
crypto_vault_extension/icons/icon48.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
247
crypto_vault_extension/js/errorHandler.js
Normal file
@@ -0,0 +1,247 @@
|
||||
class CryptoVaultError extends Error {
|
||||
constructor(message, code, retryable = false, userMessage = null) {
|
||||
super(message);
|
||||
this.name = 'CryptoVaultError';
|
||||
this.code = code;
|
||||
this.retryable = retryable;
|
||||
this.userMessage = userMessage || message;
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
const ERROR_CODES = {
|
||||
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
||||
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
||||
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
||||
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||
CRYPTO_ERROR: 'CRYPTO_ERROR',
|
||||
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
||||
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
|
||||
INVALID_INPUT: 'INVALID_INPUT',
|
||||
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
|
||||
INVALID_FORMAT: 'INVALID_FORMAT',
|
||||
WASM_ERROR: 'WASM_ERROR',
|
||||
STORAGE_ERROR: 'STORAGE_ERROR',
|
||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||
};
|
||||
|
||||
const ERROR_MESSAGES = {
|
||||
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
|
||||
[ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.',
|
||||
[ERROR_CODES.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable. Please try again later.',
|
||||
|
||||
[ERROR_CODES.INVALID_PASSWORD]: 'Invalid password. Please check your password and try again.',
|
||||
[ERROR_CODES.SESSION_EXPIRED]: 'Your session has expired. Please log in again.',
|
||||
[ERROR_CODES.UNAUTHORIZED]: 'You are not authorized to perform this action.',
|
||||
|
||||
[ERROR_CODES.CRYPTO_ERROR]: 'Cryptographic operation failed. Please try again.',
|
||||
[ERROR_CODES.INVALID_SIGNATURE]: 'Invalid signature. Please verify your input.',
|
||||
[ERROR_CODES.ENCRYPTION_FAILED]: 'Encryption failed. Please try again.',
|
||||
|
||||
[ERROR_CODES.INVALID_INPUT]: 'Invalid input. Please check your data and try again.',
|
||||
[ERROR_CODES.MISSING_KEYPAIR]: 'No keypair selected. Please select a keypair first.',
|
||||
[ERROR_CODES.INVALID_FORMAT]: 'Invalid data format. Please check your input.',
|
||||
|
||||
[ERROR_CODES.WASM_ERROR]: 'System error occurred. Please refresh and try again.',
|
||||
[ERROR_CODES.STORAGE_ERROR]: 'Storage error occurred. Please try again.',
|
||||
[ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.'
|
||||
};
|
||||
|
||||
const RETRYABLE_ERRORS = new Set([
|
||||
ERROR_CODES.NETWORK_ERROR,
|
||||
ERROR_CODES.TIMEOUT_ERROR,
|
||||
ERROR_CODES.SERVICE_UNAVAILABLE,
|
||||
ERROR_CODES.WASM_ERROR,
|
||||
ERROR_CODES.STORAGE_ERROR
|
||||
]);
|
||||
|
||||
function classifyError(error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.NETWORK_ERROR,
|
||||
true,
|
||||
ERROR_MESSAGES[ERROR_CODES.NETWORK_ERROR]
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.INVALID_PASSWORD,
|
||||
false,
|
||||
ERROR_MESSAGES[ERROR_CODES.INVALID_PASSWORD]
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('session') || errorMessage.includes('not unlocked') || errorMessage.includes('expired')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.SESSION_EXPIRED,
|
||||
false,
|
||||
ERROR_MESSAGES[ERROR_CODES.SESSION_EXPIRED]
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.CRYPTO_ERROR,
|
||||
false,
|
||||
'Invalid password or corrupted data. Please check your password.'
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('Crypto error') || errorMessage.includes('encryption')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.CRYPTO_ERROR,
|
||||
false,
|
||||
ERROR_MESSAGES[ERROR_CODES.CRYPTO_ERROR]
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('No keypair selected')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.MISSING_KEYPAIR,
|
||||
false,
|
||||
ERROR_MESSAGES[ERROR_CODES.MISSING_KEYPAIR]
|
||||
);
|
||||
}
|
||||
|
||||
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.WASM_ERROR,
|
||||
true,
|
||||
ERROR_MESSAGES[ERROR_CODES.WASM_ERROR]
|
||||
);
|
||||
}
|
||||
|
||||
return new CryptoVaultError(
|
||||
errorMessage,
|
||||
ERROR_CODES.UNKNOWN_ERROR,
|
||||
false,
|
||||
ERROR_MESSAGES[ERROR_CODES.UNKNOWN_ERROR]
|
||||
);
|
||||
}
|
||||
|
||||
function getErrorMessage(error) {
|
||||
if (!error) return 'Unknown error';
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error.trim();
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (error.error) {
|
||||
return getErrorMessage(error.error);
|
||||
}
|
||||
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'object') {
|
||||
try {
|
||||
const stringified = JSON.stringify(error);
|
||||
if (stringified && stringified !== '{}') {
|
||||
return stringified;
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently handle JSON stringify errors
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
async function withRetry(operation, options = {}) {
|
||||
const {
|
||||
maxRetries = 3,
|
||||
baseDelay = 1000,
|
||||
maxDelay = 10000,
|
||||
backoffFactor = 2,
|
||||
onRetry = null
|
||||
} = options;
|
||||
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
const classifiedError = classifyError(error);
|
||||
lastError = classifiedError;
|
||||
|
||||
if (attempt === maxRetries || !classifiedError.retryable) {
|
||||
throw classifiedError;
|
||||
}
|
||||
|
||||
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
||||
|
||||
if (onRetry) {
|
||||
onRetry(attempt + 1, delay, classifiedError);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function executeOperation(operation, options = {}) {
|
||||
const {
|
||||
loadingElement = null,
|
||||
successMessage = null,
|
||||
showRetryProgress = false,
|
||||
onProgress = null
|
||||
} = options;
|
||||
|
||||
if (loadingElement) {
|
||||
setButtonLoading(loadingElement, true);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await withRetry(operation, {
|
||||
...options,
|
||||
onRetry: (attempt, delay, error) => {
|
||||
if (showRetryProgress && onProgress) {
|
||||
onProgress(`Retrying... (${attempt}/${options.maxRetries || 3})`);
|
||||
}
|
||||
if (options.onRetry) {
|
||||
options.onRetry(attempt, delay, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (successMessage) {
|
||||
showToast(successMessage, 'success');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
showToast(error.userMessage || error.message, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
if (loadingElement) {
|
||||
setButtonLoading(loadingElement, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.CryptoVaultError = CryptoVaultError;
|
||||
window.ERROR_CODES = ERROR_CODES;
|
||||
window.classifyError = classifyError;
|
||||
window.getErrorMessage = getErrorMessage;
|
||||
window.withRetry = withRetry;
|
||||
window.executeOperation = executeOperation;
|
||||
48
crypto_vault_extension/manifest.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "CryptoVault",
|
||||
"version": "1.0.0",
|
||||
"description": "Secure cryptographic key management and signing in your browser",
|
||||
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"notifications"
|
||||
],
|
||||
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"wasm/*.wasm",
|
||||
"wasm/*.js"
|
||||
],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self' 'unsafe-inline';"
|
||||
}
|
||||
}
|
||||
269
crypto_vault_extension/popup.html
Normal file
@@ -0,0 +1,269 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="styles/popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<div class="logo clickable-header" id="headerTitle">
|
||||
<div class="logo-icon">🔐</div>
|
||||
<h1>CryptoVault</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="settingsBtn" class="btn-icon-only" title="Settings">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Create/Login Section -->
|
||||
<section class="section" id="authSection">
|
||||
<div class="card">
|
||||
<h2>Access Your Vault</h2>
|
||||
<div class="form-group">
|
||||
<label for="keyspaceInput">Keyspace Name</label>
|
||||
<input type="text" id="keyspaceInput" placeholder="Enter keyspace name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="passwordInput">Password</label>
|
||||
<input type="password" id="passwordInput" placeholder="Enter password">
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button id="createKeyspaceBtn" class="btn btn-secondary">Create New</button>
|
||||
<button id="loginBtn" class="btn btn-primary">Unlock</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main Vault Section -->
|
||||
<section class="section hidden" id="vaultSection">
|
||||
<!-- Status Section -->
|
||||
<div class="vault-status" id="vaultStatus">
|
||||
<div class="status-indicator" id="statusIndicator">
|
||||
<span id="statusText"></span>
|
||||
<button id="lockBtn" class="btn btn-ghost btn-small hidden">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<circle cx="12" cy="16" r="1"></circle>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
Lock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SigSocket Requests Section -->
|
||||
<div class="card sigsocket-section" id="sigSocketSection">
|
||||
<div class="section-header">
|
||||
<h3>🔌 SigSocket Requests</h3>
|
||||
<div class="connection-status" id="connectionStatus">
|
||||
<span class="status-dot" id="connectionDot"></span>
|
||||
<span id="connectionText">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="requests-container" id="requestsContainer">
|
||||
<div class="loading-requests hidden" id="loadingRequestsMessage">
|
||||
<div class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading requests...</p>
|
||||
<small>Fetching pending signature requests</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-requests" id="noRequestsMessage">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📝</div>
|
||||
<p>No pending sign requests</p>
|
||||
<small>Requests will appear here when received from SigSocket server</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="requests-list hidden" id="requestsList">
|
||||
<!-- Requests will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sigsocket-actions">
|
||||
<button id="refreshRequestsBtn" class="btn btn-ghost btn-small">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<polyline points="1 20 1 14 7 14"></polyline>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vault-header">
|
||||
<h2>Your Keypairs</h2>
|
||||
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
||||
<span class="btn-icon">+</span>
|
||||
Add Keypair
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Keypair Form (Hidden by default) -->
|
||||
<div class="card add-keypair-form hidden" id="addKeypairCard">
|
||||
<div class="form-header">
|
||||
<h3>Add New Keypair</h3>
|
||||
<button id="cancelAddKeypairBtn" class="btn-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="form-content">
|
||||
<div class="form-group">
|
||||
<label for="keyTypeSelect">Key Type</label>
|
||||
<select id="keyTypeSelect" class="select">
|
||||
<option value="Secp256k1">Secp256k1</option>
|
||||
<option value="Ed25519">Ed25519</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="keyNameInput">Keypair Name</label>
|
||||
<input type="text" id="keyNameInput" placeholder="Enter a name for your keypair">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button id="addKeypairBtn" class="btn btn-primary">Create Keypair</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keypairs List -->
|
||||
<div class="card">
|
||||
<h3>Keypairs</h3>
|
||||
<div id="keypairsList" class="keypairs-list">
|
||||
<div class="loading">Loading keypairs...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Crypto Operations -->
|
||||
<div class="card">
|
||||
<h3>Crypto Operations</h3>
|
||||
|
||||
<!-- Operation Tabs -->
|
||||
<div class="operation-tabs">
|
||||
<button class="tab-btn active" data-tab="encrypt">Encrypt</button>
|
||||
<button class="tab-btn" data-tab="decrypt">Decrypt</button>
|
||||
<button class="tab-btn" data-tab="sign">Sign</button>
|
||||
<button class="tab-btn" data-tab="verify">Verify</button>
|
||||
</div>
|
||||
|
||||
<!-- Encrypt Tab -->
|
||||
<div class="tab-content active" id="encrypt-tab">
|
||||
<div class="form-group">
|
||||
<label for="encryptMessageInput">Message to Encrypt</label>
|
||||
<textarea id="encryptMessageInput" placeholder="Enter message to encrypt..." rows="3"></textarea>
|
||||
</div>
|
||||
<button id="encryptBtn" class="btn btn-primary" disabled>Encrypt Message</button>
|
||||
|
||||
<div class="encrypt-result hidden" id="encryptResult">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Decrypt Tab -->
|
||||
<div class="tab-content" id="decrypt-tab">
|
||||
<div class="form-group">
|
||||
<label for="encryptedMessageInput">Encrypted Message</label>
|
||||
<textarea id="encryptedMessageInput" placeholder="Enter encrypted message..." rows="3"></textarea>
|
||||
</div>
|
||||
<button id="decryptBtn" class="btn btn-primary" disabled>Decrypt Message</button>
|
||||
|
||||
<div class="decrypt-result hidden" id="decryptResult">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sign Tab -->
|
||||
<div class="tab-content" id="sign-tab">
|
||||
<div class="form-group">
|
||||
<label for="messageInput">Message to Sign</label>
|
||||
<textarea id="messageInput" placeholder="Enter your message here..." rows="3"></textarea>
|
||||
</div>
|
||||
<button id="signBtn" class="btn btn-primary" disabled>Sign Message</button>
|
||||
|
||||
<div class="signature-result hidden" id="signatureResult">
|
||||
<label>Signature:</label>
|
||||
<div class="signature-container">
|
||||
<code id="signatureValue">-</code>
|
||||
<button id="copySignatureBtn" class="btn-copy" title="Copy to clipboard">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verify Tab -->
|
||||
<div class="tab-content" id="verify-tab">
|
||||
<div class="form-group">
|
||||
<label for="verifyMessageInput">Original Message</label>
|
||||
<textarea id="verifyMessageInput" placeholder="Enter the original message..." rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="signatureToVerifyInput">Signature</label>
|
||||
<input type="text" id="signatureToVerifyInput" placeholder="Enter signature to verify...">
|
||||
</div>
|
||||
<button id="verifyBtn" class="btn btn-primary" disabled>Verify Signature</button>
|
||||
|
||||
<div class="verify-result hidden" id="verifyResult">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings Section -->
|
||||
<section class="section hidden" id="settingsSection">
|
||||
<div class="settings-header">
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
|
||||
<!-- Session Settings -->
|
||||
<div class="card">
|
||||
<h3>Session Settings</h3>
|
||||
<div class="settings-item">
|
||||
<label for="timeoutInput">Session Timeout</label>
|
||||
<div class="timeout-input-group">
|
||||
<input type="number" id="timeoutInput" min="3" max="300" value="15">
|
||||
<span>seconds</span>
|
||||
</div>
|
||||
<small class="settings-help">Automatically lock session after inactivity</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SigSocket Settings -->
|
||||
<div class="card">
|
||||
<h3>SigSocket Settings</h3>
|
||||
<div class="settings-item">
|
||||
<label for="serverUrlInput">Server URL</label>
|
||||
<div class="server-input-group">
|
||||
<input type="text" id="serverUrlInput" placeholder="ws://localhost:8080/ws" value="ws://localhost:8080/ws">
|
||||
<button id="saveServerUrlBtn" class="btn btn-small btn-primary">Save</button>
|
||||
</div>
|
||||
<small class="settings-help">WebSocket URL for SigSocket server (ws:// or wss://)</small>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Enhanced JavaScript modules -->
|
||||
<script src="js/errorHandler.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1651
crypto_vault_extension/popup.js
Normal file
443
crypto_vault_extension/popup/components/SignRequestManager.js
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Sign Request Manager Component
|
||||
*
|
||||
* Handles the display and management of SigSocket sign requests in the popup.
|
||||
* Manages different UI states:
|
||||
* 1. Keyspace locked: Show unlock form
|
||||
* 2. Wrong keyspace: Show mismatch message
|
||||
* 3. Correct keyspace: Show approval UI
|
||||
*/
|
||||
|
||||
class SignRequestManager {
|
||||
constructor() {
|
||||
this.pendingRequests = [];
|
||||
this.isKeypaceUnlocked = false;
|
||||
this.keypaceMatch = false;
|
||||
this.connectionStatus = { isConnected: false };
|
||||
|
||||
this.container = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
async initialize(container) {
|
||||
this.container = container;
|
||||
this.initialized = true;
|
||||
|
||||
// Load initial state
|
||||
await this.loadState();
|
||||
|
||||
// Render initial UI
|
||||
this.render();
|
||||
|
||||
// Set up event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Listen for background messages
|
||||
this.setupBackgroundListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current state from background script
|
||||
*/
|
||||
async loadState() {
|
||||
try {
|
||||
// Check if keyspace is unlocked
|
||||
const unlockedResponse = await this.sendMessage('isUnlocked');
|
||||
this.isKeypaceUnlocked = unlockedResponse?.unlocked || false;
|
||||
|
||||
// Get pending requests
|
||||
const requestsResponse = await this.sendMessage('getPendingRequests');
|
||||
this.pendingRequests = requestsResponse?.requests || [];
|
||||
|
||||
// Get SigSocket status
|
||||
const statusResponse = await this.sendMessage('getSigSocketStatus');
|
||||
this.connectionStatus = statusResponse?.status || { isConnected: false };
|
||||
|
||||
// If keyspace is unlocked, notify background to check keyspace match
|
||||
if (this.isKeypaceUnlocked) {
|
||||
await this.sendMessage('keypaceUnlocked');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load sign request state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the component UI
|
||||
*/
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
|
||||
const hasRequests = this.pendingRequests.length > 0;
|
||||
|
||||
if (!hasRequests) {
|
||||
this.renderNoRequests();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isKeypaceUnlocked) {
|
||||
this.renderUnlockPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.keypaceMatch) {
|
||||
this.renderKeypaceMismatch();
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderApprovalUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render no requests state
|
||||
*/
|
||||
renderNoRequests() {
|
||||
this.container.innerHTML = `
|
||||
<div class="sign-request-manager">
|
||||
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
|
||||
<span class="status-indicator"></span>
|
||||
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
|
||||
</div>
|
||||
<div class="no-requests">
|
||||
<p>No pending sign requests</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render unlock prompt
|
||||
*/
|
||||
renderUnlockPrompt() {
|
||||
const requestCount = this.pendingRequests.length;
|
||||
this.container.innerHTML = `
|
||||
<div class="sign-request-manager">
|
||||
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
|
||||
<span class="status-indicator"></span>
|
||||
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
|
||||
</div>
|
||||
<div class="unlock-prompt">
|
||||
<h3>🔒 Unlock Keyspace</h3>
|
||||
<p>Unlock your keyspace to see ${requestCount} pending sign request${requestCount !== 1 ? 's' : ''}.</p>
|
||||
<p class="hint">Use the login form above to unlock your keyspace.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render keyspace mismatch message
|
||||
*/
|
||||
renderKeypaceMismatch() {
|
||||
this.container.innerHTML = `
|
||||
<div class="sign-request-manager">
|
||||
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
|
||||
<span class="status-indicator"></span>
|
||||
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
|
||||
</div>
|
||||
<div class="keyspace-mismatch">
|
||||
<h3>⚠️ Wrong Keyspace</h3>
|
||||
<p>The unlocked keyspace doesn't match the connected SigSocket session.</p>
|
||||
<p class="hint">Please unlock the correct keyspace to approve sign requests.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render approval UI with pending requests
|
||||
*/
|
||||
renderApprovalUI() {
|
||||
const requestsHtml = this.pendingRequests.map(request => this.renderSignRequestCard(request)).join('');
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="sign-request-manager">
|
||||
<div class="connection-status connected">
|
||||
<span class="status-indicator"></span>
|
||||
SigSocket: Connected
|
||||
</div>
|
||||
<div class="requests-header">
|
||||
<h3>📝 Sign Requests (${this.pendingRequests.length})</h3>
|
||||
</div>
|
||||
<div class="requests-list">
|
||||
${requestsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render individual sign request card
|
||||
* @param {Object} request - Sign request data
|
||||
* @returns {string} - HTML string for the request card
|
||||
*/
|
||||
renderSignRequestCard(request) {
|
||||
const timestamp = new Date(request.timestamp).toLocaleTimeString();
|
||||
const messagePreview = this.getMessagePreview(request.message);
|
||||
|
||||
return `
|
||||
<div class="sign-request-card" data-request-id="${request.id}">
|
||||
<div class="request-header">
|
||||
<div class="request-id">Request: ${request.id.substring(0, 8)}...</div>
|
||||
<div class="request-time">${timestamp}</div>
|
||||
</div>
|
||||
<div class="request-message">
|
||||
<label>Message:</label>
|
||||
<div class="message-content">
|
||||
<div class="message-preview">${messagePreview}</div>
|
||||
<button class="expand-message" data-request-id="${request.id}">
|
||||
<span class="expand-text">Show Full</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="request-actions">
|
||||
<button class="btn-reject" data-request-id="${request.id}">
|
||||
❌ Reject
|
||||
</button>
|
||||
<button class="btn-approve" data-request-id="${request.id}">
|
||||
✅ Approve & Sign
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a preview of the message content
|
||||
* @param {string} messageBase64 - Base64 encoded message
|
||||
* @returns {string} - Preview text
|
||||
*/
|
||||
getMessagePreview(messageBase64) {
|
||||
try {
|
||||
const decoded = atob(messageBase64);
|
||||
const preview = decoded.length > 50 ? decoded.substring(0, 50) + '...' : decoded;
|
||||
return preview;
|
||||
} catch (error) {
|
||||
return `Base64: ${messageBase64.substring(0, 20)}...`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
if (!this.container) return;
|
||||
|
||||
// Use event delegation for dynamic content
|
||||
this.container.addEventListener('click', (e) => {
|
||||
const target = e.target;
|
||||
|
||||
if (target.classList.contains('btn-approve')) {
|
||||
const requestId = target.getAttribute('data-request-id');
|
||||
this.approveRequest(requestId);
|
||||
} else if (target.classList.contains('btn-reject')) {
|
||||
const requestId = target.getAttribute('data-request-id');
|
||||
this.rejectRequest(requestId);
|
||||
} else if (target.classList.contains('expand-message')) {
|
||||
const requestId = target.getAttribute('data-request-id');
|
||||
this.toggleMessageExpansion(requestId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up listener for background script messages
|
||||
*/
|
||||
setupBackgroundListener() {
|
||||
// Listen for keyspace unlock events
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'KEYSPACE_UNLOCKED') {
|
||||
this.isKeypaceUnlocked = true;
|
||||
this.keypaceMatch = message.keypaceMatches;
|
||||
this.pendingRequests = message.pendingRequests || [];
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve a sign request
|
||||
* @param {string} requestId - Request ID to approve
|
||||
*/
|
||||
async approveRequest(requestId) {
|
||||
try {
|
||||
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`);
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.textContent = 'Signing...';
|
||||
}
|
||||
|
||||
const response = await this.sendMessage('approveSignRequest', { requestId });
|
||||
|
||||
if (response?.success) {
|
||||
// Remove the request from UI
|
||||
this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId);
|
||||
this.render();
|
||||
|
||||
// Show success message
|
||||
this.showToast('Sign request approved successfully!', 'success');
|
||||
} else {
|
||||
throw new Error(response?.error || 'Failed to approve request');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to approve request:', error);
|
||||
this.showToast('Failed to approve request: ' + error.message, 'error');
|
||||
|
||||
// Re-enable button
|
||||
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`);
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.textContent = '✅ Approve & Sign';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a sign request
|
||||
* @param {string} requestId - Request ID to reject
|
||||
*/
|
||||
async rejectRequest(requestId) {
|
||||
try {
|
||||
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`);
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.textContent = 'Rejecting...';
|
||||
}
|
||||
|
||||
const response = await this.sendMessage('rejectSignRequest', {
|
||||
requestId,
|
||||
reason: 'User rejected'
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
// Remove the request from UI
|
||||
this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId);
|
||||
this.render();
|
||||
|
||||
// Show success message
|
||||
this.showToast('Sign request rejected', 'info');
|
||||
} else {
|
||||
throw new Error(response?.error || 'Failed to reject request');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to reject request:', error);
|
||||
this.showToast('Failed to reject request: ' + error.message, 'error');
|
||||
|
||||
// Re-enable button
|
||||
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`);
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.textContent = '❌ Reject';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle message expansion
|
||||
* @param {string} requestId - Request ID
|
||||
*/
|
||||
toggleMessageExpansion(requestId) {
|
||||
const request = this.pendingRequests.find(r => r.id === requestId);
|
||||
if (!request) return;
|
||||
|
||||
const card = this.container.querySelector(`[data-request-id="${requestId}"]`);
|
||||
const messageContent = card.querySelector('.message-content');
|
||||
const expandButton = card.querySelector('.expand-message');
|
||||
|
||||
const isExpanded = messageContent.classList.contains('expanded');
|
||||
|
||||
if (isExpanded) {
|
||||
messageContent.classList.remove('expanded');
|
||||
messageContent.querySelector('.message-preview').textContent = this.getMessagePreview(request.message);
|
||||
expandButton.querySelector('.expand-text').textContent = 'Show Full';
|
||||
} else {
|
||||
messageContent.classList.add('expanded');
|
||||
try {
|
||||
const fullMessage = atob(request.message);
|
||||
messageContent.querySelector('.message-preview').textContent = fullMessage;
|
||||
} catch (error) {
|
||||
messageContent.querySelector('.message-preview').textContent = `Base64: ${request.message}`;
|
||||
}
|
||||
expandButton.querySelector('.expand-text').textContent = 'Show Less';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to background script
|
||||
* @param {string} action - Action to perform
|
||||
* @param {Object} data - Additional data
|
||||
* @returns {Promise<Object>} - Response from background script
|
||||
*/
|
||||
async sendMessage(action, data = {}) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ action, ...data }, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show toast notification
|
||||
* @param {string} message - Message to show
|
||||
* @param {string} type - Toast type (success, error, info)
|
||||
*/
|
||||
showToast(message, type = 'info') {
|
||||
// Use the existing toast system from popup.js
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(message, type);
|
||||
} else {
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update component state
|
||||
* @param {Object} newState - New state data
|
||||
*/
|
||||
updateState(newState) {
|
||||
console.log('SignRequestManager.updateState called with:', newState);
|
||||
console.log('Current state before update:', {
|
||||
isKeypaceUnlocked: this.isKeypaceUnlocked,
|
||||
keypaceMatch: this.keypaceMatch,
|
||||
pendingRequests: this.pendingRequests.length
|
||||
});
|
||||
|
||||
Object.assign(this, newState);
|
||||
|
||||
// Fix the property name mismatch
|
||||
if (newState.keypaceMatches !== undefined) {
|
||||
this.keypaceMatch = newState.keypaceMatches;
|
||||
}
|
||||
|
||||
console.log('State after update:', {
|
||||
isKeypaceUnlocked: this.isKeypaceUnlocked,
|
||||
keypaceMatch: this.keypaceMatch,
|
||||
pendingRequests: this.pendingRequests.length
|
||||
});
|
||||
|
||||
if (this.initialized) {
|
||||
console.log('Rendering SignRequestManager with new state');
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh component data
|
||||
*/
|
||||
async refresh() {
|
||||
await this.loadState();
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in popup
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = SignRequestManager;
|
||||
} else {
|
||||
window.SignRequestManager = SignRequestManager;
|
||||
}
|
||||
1419
crypto_vault_extension/styles/popup.css
Normal file
114
crypto_vault_extension/test_extension.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Testing the SigSocket Browser Extension
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **SigSocket Server**: You need a running SigSocket server at `ws://localhost:8080/ws`
|
||||
2. **Browser**: Chrome or Chromium-based browser with developer mode enabled
|
||||
|
||||
## Test Steps
|
||||
|
||||
### 1. Load the Extension
|
||||
|
||||
1. Open Chrome and go to `chrome://extensions/`
|
||||
2. Enable "Developer mode" in the top right
|
||||
3. Click "Load unpacked" and select the `crypto_vault_extension` directory
|
||||
4. The CryptoVault extension should appear in your extensions list
|
||||
|
||||
### 2. Basic Functionality Test
|
||||
|
||||
1. Click the CryptoVault extension icon in the toolbar
|
||||
2. Create a new keyspace:
|
||||
- Enter a keyspace name (e.g., "test-workspace")
|
||||
- Enter a password
|
||||
- Click "Create New"
|
||||
3. The extension should automatically connect to the SigSocket server
|
||||
4. Add a keypair:
|
||||
- Click "Add Keypair"
|
||||
- Enter a name for the keypair
|
||||
- Click "Create Keypair"
|
||||
|
||||
### 3. SigSocket Integration Test
|
||||
|
||||
1. **Check Connection Status**:
|
||||
- Look for the SigSocket connection status at the bottom of the popup
|
||||
- It should show "SigSocket: Connected" with a green indicator
|
||||
|
||||
2. **Test Sign Request Flow**:
|
||||
- Send a sign request to the SigSocket server (you'll need to implement this on the server side)
|
||||
- The extension should show a notification
|
||||
- The extension badge should show the number of pending requests
|
||||
- Open the extension popup to see the sign request
|
||||
|
||||
3. **Test Approval Flow**:
|
||||
- If keyspace is locked, you should see "Unlock keyspace to see X pending requests"
|
||||
- Unlock the keyspace using the login form
|
||||
- You should see the sign request details
|
||||
- Click "Approve & Sign" to approve the request
|
||||
- The request should be signed and sent back to the server
|
||||
|
||||
### 4. Settings Test
|
||||
|
||||
1. Click the settings gear icon in the extension popup
|
||||
2. Change the SigSocket server URL if needed
|
||||
3. Adjust the session timeout if desired
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
- ✅ Extension loads without errors
|
||||
- ✅ Can create keyspaces and keypairs
|
||||
- ✅ SigSocket connection is established automatically
|
||||
- ✅ Sign requests are received and displayed
|
||||
- ✅ Approval flow works correctly
|
||||
- ✅ Settings can be configured
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Extension won't load**: Check the console for JavaScript errors
|
||||
2. **SigSocket won't connect**: Verify the server is running and the URL is correct
|
||||
3. **WASM errors**: Check that the WASM files are properly built and copied
|
||||
4. **Sign requests not appearing**: Check the browser console for callback errors
|
||||
|
||||
### Debug Steps
|
||||
|
||||
1. Open Chrome DevTools
|
||||
2. Go to the Extensions tab
|
||||
3. Find CryptoVault and click "Inspect views: background page"
|
||||
4. Check the console for any errors
|
||||
5. Also inspect the popup by right-clicking the extension icon and selecting "Inspect popup"
|
||||
|
||||
## Server-Side Testing
|
||||
|
||||
To fully test the extension, you'll need a SigSocket server that can:
|
||||
|
||||
1. Accept WebSocket connections at `/ws`
|
||||
2. Handle client introduction messages (hex-encoded public keys)
|
||||
3. Send sign requests in the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "unique-request-id",
|
||||
"message": "base64-encoded-message"
|
||||
}
|
||||
```
|
||||
|
||||
4. Receive sign responses in the format:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "request-id",
|
||||
"message": "base64-encoded-message",
|
||||
"signature": "base64-encoded-signature"
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
If basic functionality works:
|
||||
|
||||
1. Test with multiple concurrent sign requests
|
||||
2. Test connection recovery after network issues
|
||||
3. Test with different keyspace configurations
|
||||
4. Test the rejection flow
|
||||
5. Test session timeout behavior
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as __wbg_star0 from 'env';
|
||||
|
||||
let wasm;
|
||||
|
||||
function addToExternrefTable0(obj) {
|
||||
@@ -205,30 +203,18 @@ function debugString(val) {
|
||||
return className;
|
||||
}
|
||||
/**
|
||||
* Initialize the scripting environment (must be called before run_rhai)
|
||||
* Create and unlock a new keyspace with the given name and password
|
||||
* @param {string} keyspace
|
||||
* @param {string} password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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);
|
||||
export function create_keyspace(keyspace, password) {
|
||||
const ptr0 = passStringToWasm0(keyspace, 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]);
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.create_keyspace(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,6 +239,80 @@ export function lock_session() {
|
||||
wasm.lock_session();
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_export_2.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* Get metadata of the currently selected keypair
|
||||
* @returns {any}
|
||||
*/
|
||||
export function current_keypair_metadata() {
|
||||
const ret = wasm.current_keypair_metadata();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key of the currently selected keypair as Uint8Array
|
||||
* @returns {any}
|
||||
*/
|
||||
export function current_keypair_public_key() {
|
||||
const ret = wasm.current_keypair_public_key();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a keyspace is currently unlocked
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function is_unlocked() {
|
||||
const ret = wasm.is_unlocked();
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default public key for a workspace (keyspace)
|
||||
* This returns the public key of the first keypair in the keyspace
|
||||
* @param {string} workspace_id
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function get_workspace_default_public_key(workspace_id) {
|
||||
const ptr0 = passStringToWasm0(workspace_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.get_workspace_default_public_key(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current unlocked public key as hex string
|
||||
* @returns {string}
|
||||
*/
|
||||
export function get_current_unlocked_public_key() {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.get_current_unlocked_public_key();
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keypairs from the current session
|
||||
* Returns an array of keypair objects with id, type, and metadata
|
||||
@@ -299,7 +359,7 @@ function passArray8ToWasm0(arg, malloc) {
|
||||
return ptr;
|
||||
}
|
||||
/**
|
||||
* Sign message with current session
|
||||
* Sign message with current session (requires selected keypair)
|
||||
* @param {Uint8Array} message
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
@@ -310,24 +370,487 @@ export function sign(message) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||
wasm.closure77_externref_shim(arg0, arg1, arg2);
|
||||
/**
|
||||
* Get the current keyspace name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function get_current_keyspace_name() {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.get_current_keyspace_name();
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||
wasm.closure126_externref_shim(arg0, arg1, arg2);
|
||||
/**
|
||||
* Sign message with default keypair (first keypair in keyspace) without changing session state
|
||||
* @param {Uint8Array} message
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function sign_with_default_keypair(message) {
|
||||
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sign_with_default_keypair(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||
wasm.closure188_externref_shim(arg0, arg1, arg2);
|
||||
/**
|
||||
* Verify a signature with the current session's selected keypair
|
||||
* @param {Uint8Array} message
|
||||
* @param {string} signature
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function verify(message, signature) {
|
||||
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(signature, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.verify(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function __wbg_adapter_123(arg0, arg1, arg2, arg3) {
|
||||
wasm.closure213_externref_shim(arg0, arg1, arg2, arg3);
|
||||
/**
|
||||
* Encrypt data using the current session's keyspace symmetric cipher
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function encrypt_data(data) {
|
||||
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.encrypt_data(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt data using the current session's keyspace symmetric cipher
|
||||
* @param {Uint8Array} encrypted
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function decrypt_data(encrypted) {
|
||||
const ptr0 = passArray8ToWasm0(encrypted, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.decrypt_data(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the scripting environment (must be called before run_rhai)
|
||||
*/
|
||||
export function init_rhai_env() {
|
||||
wasm.init_rhai_env();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
|
||||
function __wbg_adapter_34(arg0, arg1, arg2) {
|
||||
wasm.closure203_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_39(arg0, arg1) {
|
||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79bf9f6d48e92f7(arg0, arg1);
|
||||
}
|
||||
|
||||
function __wbg_adapter_44(arg0, arg1, arg2) {
|
||||
wasm.closure239_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_49(arg0, arg1) {
|
||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf103de07b8856532(arg0, arg1);
|
||||
}
|
||||
|
||||
function __wbg_adapter_52(arg0, arg1, arg2) {
|
||||
wasm.closure319_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_55(arg0, arg1, arg2) {
|
||||
wasm.closure395_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_207(arg0, arg1, arg2, arg3) {
|
||||
wasm.closure2042_externref_shim(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"];
|
||||
|
||||
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||
|
||||
const SigSocketConnectionFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(ptr => wasm.__wbg_sigsocketconnection_free(ptr >>> 0, 1));
|
||||
/**
|
||||
* WASM-bindgen wrapper for SigSocket client
|
||||
*
|
||||
* This provides a clean JavaScript API for the browser extension to:
|
||||
* - Connect to SigSocket servers
|
||||
* - Send responses to sign requests
|
||||
* - Manage connection state
|
||||
*/
|
||||
export class SigSocketConnection {
|
||||
|
||||
__destroy_into_raw() {
|
||||
const ptr = this.__wbg_ptr;
|
||||
this.__wbg_ptr = 0;
|
||||
SigSocketConnectionFinalization.unregister(this);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
free() {
|
||||
const ptr = this.__destroy_into_raw();
|
||||
wasm.__wbg_sigsocketconnection_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Create a new SigSocket connection
|
||||
*/
|
||||
constructor() {
|
||||
const ret = wasm.sigsocketconnection_new();
|
||||
this.__wbg_ptr = ret >>> 0;
|
||||
SigSocketConnectionFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Connect to a SigSocket server
|
||||
*
|
||||
* # Arguments
|
||||
* * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
* * `public_key_hex` - Client's public key as hex string
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Successfully connected
|
||||
* * `Err(error)` - Connection failed
|
||||
* @param {string} server_url
|
||||
* @param {string} public_key_hex
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
connect(server_url, public_key_hex) {
|
||||
const ptr0 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(public_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketconnection_connect(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Send a response to a sign request
|
||||
*
|
||||
* This should be called by the extension after the user has approved
|
||||
* a sign request and the message has been signed.
|
||||
*
|
||||
* # Arguments
|
||||
* * `request_id` - ID of the original request
|
||||
* * `message_base64` - Original message (base64-encoded)
|
||||
* * `signature_hex` - Signature as hex string
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Response sent successfully
|
||||
* * `Err(error)` - Failed to send response
|
||||
* @param {string} request_id
|
||||
* @param {string} message_base64
|
||||
* @param {string} signature_hex
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
send_response(request_id, message_base64, signature_hex) {
|
||||
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(message_base64, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passStringToWasm0(signature_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketconnection_send_response(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Send a rejection for a sign request
|
||||
*
|
||||
* This should be called when the user rejects a sign request.
|
||||
*
|
||||
* # Arguments
|
||||
* * `request_id` - ID of the request to reject
|
||||
* * `reason` - Reason for rejection (optional)
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Rejection sent successfully
|
||||
* * `Err(error)` - Failed to send rejection
|
||||
* @param {string} request_id
|
||||
* @param {string} reason
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
send_rejection(request_id, reason) {
|
||||
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketconnection_send_rejection(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Disconnect from the SigSocket server
|
||||
*/
|
||||
disconnect() {
|
||||
wasm.sigsocketconnection_disconnect(this.__wbg_ptr);
|
||||
}
|
||||
/**
|
||||
* Check if connected to the server
|
||||
* @returns {boolean}
|
||||
*/
|
||||
is_connected() {
|
||||
const ret = wasm.sigsocketconnection_is_connected(this.__wbg_ptr);
|
||||
return ret !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
const SigSocketManagerFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(ptr => wasm.__wbg_sigsocketmanager_free(ptr >>> 0, 1));
|
||||
/**
|
||||
* SigSocket manager for high-level operations
|
||||
*/
|
||||
export class SigSocketManager {
|
||||
|
||||
__destroy_into_raw() {
|
||||
const ptr = this.__wbg_ptr;
|
||||
this.__wbg_ptr = 0;
|
||||
SigSocketManagerFinalization.unregister(this);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
free() {
|
||||
const ptr = this.__destroy_into_raw();
|
||||
wasm.__wbg_sigsocketmanager_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Connect to SigSocket server with smart connection management
|
||||
*
|
||||
* This handles all connection logic:
|
||||
* - Reuses existing connection if same workspace
|
||||
* - Switches connection if different workspace
|
||||
* - Creates new connection if none exists
|
||||
*
|
||||
* # Arguments
|
||||
* * `workspace` - The workspace name to connect with
|
||||
* * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
* * `event_callback` - JavaScript function to call when events occur
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(connection_info)` - JSON string with connection details
|
||||
* * `Err(error)` - If connection failed or workspace is invalid
|
||||
* @param {string} workspace
|
||||
* @param {string} server_url
|
||||
* @param {Function} event_callback
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static connect_workspace_with_events(workspace, server_url, event_callback) {
|
||||
const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketmanager_connect_workspace_with_events(ptr0, len0, ptr1, len1, event_callback);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Connect to SigSocket server with a specific workspace (backward compatibility)
|
||||
*
|
||||
* This is a simpler version that doesn't set up event callbacks.
|
||||
* Use connect_workspace_with_events for full functionality.
|
||||
*
|
||||
* # Arguments
|
||||
* * `workspace` - The workspace name to connect with
|
||||
* * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(connection_info)` - JSON string with connection details
|
||||
* * `Err(error)` - If connection failed or workspace is invalid
|
||||
* @param {string} workspace
|
||||
* @param {string} server_url
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static connect_workspace(workspace, server_url) {
|
||||
const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketmanager_connect_workspace(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Disconnect from SigSocket server
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Successfully disconnected
|
||||
* * `Err(error)` - If disconnect failed
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static disconnect() {
|
||||
const ret = wasm.sigsocketmanager_disconnect();
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Check if we can approve a specific sign request
|
||||
*
|
||||
* This validates that:
|
||||
* 1. The request exists
|
||||
* 2. The vault session is unlocked
|
||||
* 3. The current workspace matches the request's target
|
||||
*
|
||||
* # Arguments
|
||||
* * `request_id` - The ID of the request to validate
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(true)` - Request can be approved
|
||||
* * `Ok(false)` - Request cannot be approved
|
||||
* * `Err(error)` - Validation error
|
||||
* @param {string} request_id
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static can_approve_request(request_id) {
|
||||
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketmanager_can_approve_request(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Approve a sign request and send the signature to the server
|
||||
*
|
||||
* This performs the complete approval flow:
|
||||
* 1. Validates the request can be approved
|
||||
* 2. Signs the message using the vault
|
||||
* 3. Sends the signature to the SigSocket server
|
||||
* 4. Removes the request from pending list
|
||||
*
|
||||
* # Arguments
|
||||
* * `request_id` - The ID of the request to approve
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(signature)` - Base64-encoded signature that was sent
|
||||
* * `Err(error)` - If approval failed
|
||||
* @param {string} request_id
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static approve_request(request_id) {
|
||||
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketmanager_approve_request(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Reject a sign request
|
||||
*
|
||||
* # Arguments
|
||||
* * `request_id` - The ID of the request to reject
|
||||
* * `reason` - The reason for rejection
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Request rejected successfully
|
||||
* * `Err(error)` - If rejection failed
|
||||
* @param {string} request_id
|
||||
* @param {string} reason
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static reject_request(request_id, reason) {
|
||||
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketmanager_reject_request(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Get pending requests filtered by current workspace
|
||||
*
|
||||
* This returns only the requests that the current vault session can handle,
|
||||
* based on the unlocked workspace and its public key.
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(requests_json)` - JSON array of filtered requests
|
||||
* * `Err(error)` - If filtering failed
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
static get_filtered_requests() {
|
||||
const ret = wasm.sigsocketmanager_get_filtered_requests();
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Add a pending sign request (called when request arrives from server)
|
||||
*
|
||||
* # Arguments
|
||||
* * `request_json` - JSON string containing the sign request
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Request added successfully
|
||||
* * `Err(error)` - If adding failed
|
||||
* @param {string} request_json
|
||||
*/
|
||||
static add_pending_request(request_json) {
|
||||
const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sigsocketmanager_add_pending_request(ptr0, len0);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get connection status
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(status_json)` - JSON object with connection status
|
||||
* * `Err(error)` - If getting status failed
|
||||
* @returns {string}
|
||||
*/
|
||||
static get_connection_status() {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.sigsocketmanager_get_connection_status();
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear all pending requests
|
||||
*
|
||||
* # Returns
|
||||
* * `Ok(())` - Requests cleared successfully
|
||||
*/
|
||||
static clear_pending_requests() {
|
||||
const ret = wasm.sigsocketmanager_clear_pending_requests();
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
@@ -374,6 +897,9 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.call(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_close_2893b7d056a0627d = function() { return handleError(function (arg0) {
|
||||
arg0.close();
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||
return ret;
|
||||
@@ -382,6 +908,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.crypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_data_432d9c3df2630942 = function(arg0) {
|
||||
const ret = arg0.data;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||
console.error(arg0);
|
||||
};
|
||||
@@ -395,6 +925,10 @@ function __wbg_get_imports() {
|
||||
imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.getRandomValues(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getTime_46267b1c24877e30 = function(arg0) {
|
||||
const ret = arg0.getTime();
|
||||
return ret;
|
||||
};
|
||||
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);
|
||||
@@ -450,14 +984,31 @@ function __wbg_get_imports() {
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof Window;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_log_c222819a41e063d3 = function(arg0) {
|
||||
console.log(arg0);
|
||||
};
|
||||
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||
const ret = arg0.msCrypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new0_f788a2397c7ca929 = function() {
|
||||
const ret = new Date();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
|
||||
try {
|
||||
var state0 = {a: arg0, b: arg1};
|
||||
@@ -465,7 +1016,7 @@ function __wbg_get_imports() {
|
||||
const a = state0.a;
|
||||
state0.a = 0;
|
||||
try {
|
||||
return __wbg_adapter_123(a, state0.b, arg0, arg1);
|
||||
return __wbg_adapter_207(a, state0.b, arg0, arg1);
|
||||
} finally {
|
||||
state0.a = a;
|
||||
}
|
||||
@@ -484,6 +1035,10 @@ function __wbg_get_imports() {
|
||||
const ret = new Array();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_92c54fc74574ef55 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = new WebSocket(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||
const ret = new Uint8Array(arg0);
|
||||
return ret;
|
||||
@@ -504,6 +1059,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.node;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_now_d18023d54d4e5500 = function(arg0) {
|
||||
const ret = arg0.now();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) {
|
||||
const ret = arg0.objectStoreNames;
|
||||
return ret;
|
||||
@@ -512,6 +1071,12 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_onConnectionStateChanged_b0dc098522afadba = function(arg0) {
|
||||
onConnectionStateChanged(arg0 !== 0);
|
||||
};
|
||||
imports.wbg.__wbg_onSignRequestReceived_93232ba7a0919705 = function(arg0, arg1, arg2, arg3) {
|
||||
onSignRequestReceived(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
|
||||
};
|
||||
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
@@ -546,6 +1111,10 @@ function __wbg_get_imports() {
|
||||
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.randomFillSync(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_readyState_7ef6e63c349899ed = function(arg0) {
|
||||
const ret = arg0.readyState;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||
const ret = module.require;
|
||||
return ret;
|
||||
@@ -558,12 +1127,38 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.result;
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_send_0293179ba074ffb4 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
arg0.send(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_setTimeout_f2fe5af8e3debeb3 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.setTimeout(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||
arg0.set(arg1, arg2 >>> 0);
|
||||
};
|
||||
imports.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = Reflect.set(arg0, arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_setbinaryType_92fa1ffd873b327c = function(arg0, arg1) {
|
||||
arg0.binaryType = __wbindgen_enum_BinaryType[arg1];
|
||||
};
|
||||
imports.wbg.__wbg_setonclose_14fc475a49d488fc = function(arg0, arg1) {
|
||||
arg0.onclose = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonerror_8639efe354b947cd = function(arg0, arg1) {
|
||||
arg0.onerror = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||
arg0.onerror = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonmessage_6eccab530a8fb4c7 = function(arg0, arg1) {
|
||||
arg0.onmessage = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonopen_2da654e1f39745d5 = function(arg0, arg1) {
|
||||
arg0.onopen = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||
arg0.onsuccess = arg1;
|
||||
};
|
||||
@@ -598,6 +1193,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.then(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) {
|
||||
const ret = arg0.then(arg1, arg2);
|
||||
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;
|
||||
@@ -606,6 +1205,9 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.versions;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_warn_4ca3906c248c47c4 = function(arg0) {
|
||||
console.warn(arg0);
|
||||
};
|
||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||
const obj = arg0.original;
|
||||
if (obj.cnt-- == 1) {
|
||||
@@ -615,16 +1217,40 @@ function __wbg_get_imports() {
|
||||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32);
|
||||
imports.wbg.__wbindgen_closure_wrapper1036 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 320, __wbg_adapter_52);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35);
|
||||
imports.wbg.__wbindgen_closure_wrapper1329 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 396, __wbg_adapter_55);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
||||
imports.wbg.__wbindgen_closure_wrapper624 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper625 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper626 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_39);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper630 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper765 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper768 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_49);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||
@@ -681,6 +1307,14 @@ function __wbg_get_imports() {
|
||||
const ret = wasm.memory;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
|
||||
const obj = arg1;
|
||||
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||
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.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
@@ -688,7 +1322,6 @@ function __wbg_get_imports() {
|
||||
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports['env'] = __wbg_star0;
|
||||
|
||||
return imports;
|
||||
}
|
||||
BIN
crypto_vault_extension/wasm/wasm_app_bg.wasm
Normal file
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
# Only universal/core dependencies here
|
||||
|
||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||
@@ -29,7 +30,7 @@ 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"
|
||||
wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] }
|
||||
js-sys = "0.3"
|
||||
# console_error_panic_hook = "0.1"
|
||||
gloo-net = { version = "0.5", features = ["http"] }
|
||||
|
||||
@@ -68,7 +68,7 @@ impl EvmClient {
|
||||
mut tx: provider::Transaction,
|
||||
signer: &dyn crate::signer::Signer,
|
||||
) -> Result<ethers_core::types::H256, EvmError> {
|
||||
use ethers_core::types::{U256, H256, Bytes, Address};
|
||||
use ethers_core::types::{U256, H256};
|
||||
use std::str::FromStr;
|
||||
use serde_json::json;
|
||||
use crate::provider::{send_rpc, parse_signature_rs_v};
|
||||
@@ -131,7 +131,7 @@ impl EvmClient {
|
||||
|
||||
// 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()))?;
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Rhai bindings for EVM Client module
|
||||
//! Provides a single source of truth for scripting integration for EVM actions.
|
||||
|
||||
use rhai::{Engine, Map};
|
||||
use rhai::Engine;
|
||||
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
|
||||
|
||||
/// Register EVM Client APIs with the Rhai scripting engine.
|
||||
@@ -25,7 +25,7 @@ pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClie
|
||||
engine.register_type::<RhaiEvmClient>();
|
||||
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
||||
// Register instance for scripts
|
||||
let rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
|
||||
let _rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
|
||||
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
//! 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;
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
#[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
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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)
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 454 B |
|
Before Width: | Height: | Size: 712 B |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,81 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
// 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
|
Before Width: | Height: | Size: 1.9 KiB |
BIN
extension/dist/assets/icon-16.png
vendored
|
Before Width: | Height: | Size: 454 B |
BIN
extension/dist/assets/icon-32.png
vendored
|
Before Width: | Height: | Size: 712 B |
BIN
extension/dist/assets/icon-48.png
vendored
|
Before Width: | Height: | Size: 1.1 KiB |
2
extension/dist/assets/popup.js
vendored
@@ -1,2 +0,0 @@
|
||||
(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
@@ -1,81 +0,0 @@
|
||||
// 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
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,13 +0,0 @@
|
||||
<!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
@@ -1,117 +0,0 @@
|
||||
/* 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
@@ -1,306 +0,0 @@
|
||||
// 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!';
|
||||
});
|
||||
});
|
||||
BIN
extension/dist/wasm/wasm_app_bg.wasm
vendored
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"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
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
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;
|
||||
@@ -1,30 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,667 +0,0 @@
|
||||
/**
|
||||
* 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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,8 +0,0 @@
|
||||
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 />);
|
||||
@@ -1,117 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
// 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!';
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
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,11 +7,12 @@ edition = "2021"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||
async-trait = "0.1"
|
||||
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||
wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = "1"
|
||||
|
||||
@@ -27,6 +28,7 @@ getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
getrandom_02 = { package = "getrandom", version = "0.2.16", features = ["js"] }
|
||||
idb = { version = "0.6" }
|
||||
wasm-bindgen-test = "0.3"
|
||||
wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -130,6 +130,7 @@ store.put(&js_value, Some(&JsValue::from_str(key)))?.await
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct WasmStore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl KVStore for WasmStore {
|
||||
@@ -139,10 +140,16 @@ impl KVStore for WasmStore {
|
||||
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<()> {
|
||||
async fn remove(&self, _key: &str) -> Result<()> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn exists(&self, _key: &str) -> Result<bool> {
|
||||
async fn contains_key(&self, _key: &str) -> Result<bool> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn keys(&self) -> Result<Vec<String>> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
async fn clear(&self) -> Result<()> {
|
||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
53
sigsocket_client/Cargo.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "sigsocket_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "WebSocket client for sigsocket server with WASM-first support"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://git.ourworld.tf/samehabouelsaad/sal-modular"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# Core dependencies (both native and WASM)
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
hex = "0.4"
|
||||
base64 = "0.21"
|
||||
url = "2.5"
|
||||
async-trait = "0.1"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Native-only dependencies
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio-tungstenite = "0.21"
|
||||
futures-util = "0.3"
|
||||
thiserror = "1.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# WASM-only dependencies
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"WebSocket",
|
||||
"MessageEvent",
|
||||
"Event",
|
||||
"BinaryType",
|
||||
"CloseEvent",
|
||||
"ErrorEvent",
|
||||
"Window",
|
||||
] }
|
||||
js-sys = "0.3"
|
||||
|
||||
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
env_logger = "0.10"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
console_error_panic_hook = "0.1"
|
||||
214
sigsocket_client/IMPLEMENTATION.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# SigSocket Client Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of the `sigsocket_client` crate, a WebSocket client library designed for connecting to sigsocket servers with **WASM-first support**.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Design Principles
|
||||
|
||||
1. **WASM-First**: Designed primarily for browser environments with native support as a secondary target
|
||||
2. **No Signing Logic**: The client delegates all signing operations to the application
|
||||
3. **User Approval Flow**: Applications are notified about incoming requests and handle user approval
|
||||
4. **Protocol Compatibility**: Fully compatible with the sigsocket server protocol
|
||||
5. **Async/Await**: Modern async Rust API throughout
|
||||
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
sigsocket_client/
|
||||
├── src/
|
||||
│ ├── lib.rs # Main library entry point
|
||||
│ ├── error.rs # Error types (native + WASM versions)
|
||||
│ ├── protocol.rs # Protocol message definitions
|
||||
│ ├── client.rs # Main client interface
|
||||
│ ├── native.rs # Native (tokio) implementation
|
||||
│ └── wasm.rs # WASM (web-sys) implementation
|
||||
├── examples/
|
||||
│ ├── basic_usage.rs # Native usage example
|
||||
│ └── wasm_usage.rs # WASM usage example
|
||||
├── tests/
|
||||
│ └── integration_test.rs
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Protocol Implementation
|
||||
|
||||
The sigsocket protocol is simple and consists of three message types:
|
||||
|
||||
### 1. Introduction Message
|
||||
When connecting, the client sends its public key as a hex-encoded string:
|
||||
```
|
||||
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
|
||||
```
|
||||
|
||||
### 2. Sign Request (Server → Client)
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Sign Response (Client → Server)
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl", // original message
|
||||
"signature": "c2lnbmF0dXJl" // base64-encoded signature
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features Implemented
|
||||
|
||||
### ✅ Dual Platform Support
|
||||
- **Native**: Uses `tokio` and `tokio-tungstenite` for async WebSocket communication
|
||||
- **WASM**: Uses `web-sys` and `wasm-bindgen` for browser WebSocket API
|
||||
|
||||
### ✅ Type-Safe Protocol
|
||||
- `SignRequest` and `SignResponse` structs with serde serialization
|
||||
- Helper methods for base64 encoding/decoding
|
||||
- Comprehensive error handling
|
||||
|
||||
### ✅ Flexible Sign Handler Interface
|
||||
```rust
|
||||
trait SignRequestHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
```
|
||||
|
||||
### ✅ Connection Management
|
||||
- Automatic connection state tracking
|
||||
- Clean disconnect handling
|
||||
- Connection status queries
|
||||
|
||||
### ✅ Error Handling
|
||||
- Comprehensive error types for different failure modes
|
||||
- Platform-specific error conversions
|
||||
- WASM-compatible error handling (no `std::error::Error` dependency)
|
||||
|
||||
## Platform-Specific Implementations
|
||||
|
||||
### Native Implementation (`native.rs`)
|
||||
- Uses `tokio-tungstenite` for WebSocket communication
|
||||
- Spawns separate tasks for reading and writing
|
||||
- Thread-safe with `Arc<RwLock<T>>` for shared state
|
||||
- Supports `Send + Sync` trait bounds
|
||||
|
||||
### WASM Implementation (`wasm.rs`)
|
||||
- Uses `web-sys::WebSocket` for browser WebSocket API
|
||||
- Event-driven with JavaScript closures
|
||||
- Single-threaded (no `Send + Sync` requirements)
|
||||
- Browser console logging for debugging
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Native Usage
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let public_key = hex::decode("02f9308a...")?;
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
|
||||
client.set_sign_handler(MySignHandler);
|
||||
client.connect().await?;
|
||||
|
||||
// Client handles requests automatically
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### WASM Usage
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
|
||||
let public_key = get_user_public_key()?;
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
|
||||
client.set_sign_handler(WasmSignHandler);
|
||||
client.connect().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
- Protocol message serialization/deserialization
|
||||
- Error handling and conversion
|
||||
- Client creation and configuration
|
||||
|
||||
### Integration Tests
|
||||
- End-to-end usage patterns
|
||||
- Sign request/response cycles
|
||||
- Error scenarios
|
||||
|
||||
### Documentation Tests
|
||||
- Example code in documentation is verified to compile
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies (Both Platforms)
|
||||
- `serde` + `serde_json` - JSON serialization
|
||||
- `hex` - Hex encoding/decoding
|
||||
- `base64` - Base64 encoding/decoding
|
||||
- `url` - URL parsing and validation
|
||||
|
||||
### Native-Only Dependencies
|
||||
- `tokio` - Async runtime
|
||||
- `tokio-tungstenite` - WebSocket client
|
||||
- `futures-util` - Stream utilities
|
||||
- `thiserror` - Error derive macros
|
||||
|
||||
### WASM-Only Dependencies
|
||||
- `wasm-bindgen` - Rust/JavaScript interop
|
||||
- `web-sys` - Browser API bindings
|
||||
- `js-sys` - JavaScript type bindings
|
||||
- `wasm-bindgen-futures` - Async support
|
||||
|
||||
## Build Targets
|
||||
|
||||
### Native Build
|
||||
```bash
|
||||
cargo build --features native
|
||||
cargo test --features native
|
||||
cargo run --example basic_usage --features native
|
||||
```
|
||||
|
||||
### WASM Build
|
||||
```bash
|
||||
cargo check --target wasm32-unknown-unknown --features wasm
|
||||
wasm-pack build --target web --features wasm
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
1. **Reconnection Logic**: Automatic reconnection with exponential backoff
|
||||
2. **Request Queuing**: Queue multiple concurrent sign requests
|
||||
3. **Timeout Handling**: Configurable timeouts for requests
|
||||
4. **Metrics**: Connection and request metrics
|
||||
5. **Logging**: Structured logging with configurable levels
|
||||
|
||||
### WASM Enhancements
|
||||
1. **Better Callback System**: More ergonomic callback handling in WASM
|
||||
2. **Browser Wallet Integration**: Direct integration with MetaMask, etc.
|
||||
3. **Service Worker Support**: Background request handling
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **No Private Key Storage**: The client never handles private keys
|
||||
2. **User Approval Required**: All signing requires explicit user approval
|
||||
3. **Message Validation**: All incoming messages are validated
|
||||
4. **Secure Transport**: Requires WebSocket Secure (WSS) in production
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Rust Version**: 1.70+
|
||||
- **WASM Target**: `wasm32-unknown-unknown`
|
||||
- **Browser Support**: Modern browsers with WebSocket support
|
||||
- **Server Compatibility**: Compatible with sigsocket server protocol
|
||||
|
||||
This implementation provides a solid foundation for applications that need to connect to sigsocket servers while maintaining security and user control over signing operations.
|
||||
218
sigsocket_client/README.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# SigSocket Client
|
||||
|
||||
A WebSocket client library for connecting to sigsocket servers with **WASM-first support**.
|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 **WASM-first design**: Optimized for browser environments
|
||||
- 🖥️ **Native support**: Works in native Rust applications
|
||||
- 🔐 **No signing logic**: Delegates signing to the application
|
||||
- 👤 **User approval flow**: Notifies applications about incoming requests
|
||||
- 🔌 **sigsocket compatible**: Fully compatible with sigsocket server protocol
|
||||
- 🚀 **Async/await**: Modern async Rust API
|
||||
- 🔄 **Automatic reconnection**: Both platforms support reconnection with exponential backoff
|
||||
- ⏱️ **Connection timeouts**: Proper timeout handling and connection management
|
||||
- 🛡️ **Production ready**: Comprehensive error handling and reliability features
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Native Usage
|
||||
|
||||
```rust
|
||||
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
|
||||
|
||||
struct MySignHandler;
|
||||
|
||||
impl SignRequestHandler for MySignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
// 1. Present request to user
|
||||
println!("Sign request: {}", request.message);
|
||||
|
||||
// 2. Get user approval
|
||||
// ... your UI logic here ...
|
||||
|
||||
// 3. Sign the message (using your signing logic)
|
||||
let signature = your_signing_function(&request.message_bytes()?)?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Your public key bytes
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")?;
|
||||
|
||||
// Create and configure client
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
client.set_sign_handler(MySignHandler);
|
||||
|
||||
// Connect and handle requests
|
||||
client.connect().await?;
|
||||
|
||||
// Client will automatically handle incoming signature requests
|
||||
// Keep the connection alive...
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### WASM Usage
|
||||
|
||||
```rust
|
||||
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
struct WasmSignHandler;
|
||||
|
||||
impl SignRequestHandler for WasmSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
// Show request to user in browser
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.alert_with_message(&format!("Sign request: {}", request.id))
|
||||
.unwrap();
|
||||
|
||||
// Your signing logic here...
|
||||
let signature = sign_with_browser_wallet(&request.message_bytes()?)?;
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
|
||||
let public_key = get_user_public_key()?;
|
||||
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
client.set_sign_handler(WasmSignHandler);
|
||||
|
||||
client.connect().await
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Protocol
|
||||
|
||||
The sigsocket client implements a simple WebSocket protocol:
|
||||
|
||||
### 1. Introduction
|
||||
Upon connection, the client sends its public key as a hex-encoded string:
|
||||
```
|
||||
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388
|
||||
```
|
||||
|
||||
### 2. Sign Requests
|
||||
The server sends signature requests as JSON:
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Sign Responses
|
||||
The client responds with signatures as JSON:
|
||||
```json
|
||||
{
|
||||
"id": "req_123",
|
||||
"message": "dGVzdCBtZXNzYWdl", // original message
|
||||
"signature": "c2lnbmF0dXJl" // base64-encoded signature
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SigSocketClient`
|
||||
|
||||
Main client for connecting to sigsocket servers.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `new(url, public_key)` - Create a new client
|
||||
- `set_sign_handler(handler)` - Set the signature request handler
|
||||
- `connect()` - Connect to the server with automatic reconnection
|
||||
- `disconnect()` - Disconnect from the server
|
||||
- `send_sign_response(response)` - Manually send a signature response
|
||||
- `state()` - Get current connection state
|
||||
- `is_connected()` - Check if connected
|
||||
|
||||
#### Reconnection Configuration (WASM only)
|
||||
|
||||
- `set_auto_reconnect(enabled)` - Enable/disable automatic reconnection
|
||||
- `set_reconnect_config(max_attempts, initial_delay_ms)` - Configure reconnection parameters
|
||||
|
||||
**Default settings:**
|
||||
- Max attempts: 5
|
||||
- Initial delay: 1000ms (with exponential backoff: 1s, 2s, 4s, 8s, 16s)
|
||||
- Auto-reconnect: enabled
|
||||
|
||||
### `SignRequestHandler` Trait
|
||||
|
||||
Implement this trait to handle incoming signature requests.
|
||||
|
||||
```rust
|
||||
trait SignRequestHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
```
|
||||
|
||||
### `SignRequest`
|
||||
|
||||
Represents a signature request from the server.
|
||||
|
||||
#### Fields
|
||||
- `id: String` - Unique request identifier
|
||||
- `message: String` - Base64-encoded message to sign
|
||||
|
||||
#### Methods
|
||||
- `message_bytes()` - Decode message to bytes
|
||||
- `message_hex()` - Get message as hex string
|
||||
|
||||
### `SignResponse`
|
||||
|
||||
Represents a signature response to send to the server.
|
||||
|
||||
#### Methods
|
||||
- `new(id, message, signature)` - Create a new response
|
||||
- `from_request_and_signature(request, signature)` - Create from request and signature bytes
|
||||
|
||||
## Examples
|
||||
|
||||
Run the basic example:
|
||||
|
||||
```bash
|
||||
cargo run --example basic_usage
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Native Build
|
||||
```bash
|
||||
cargo build
|
||||
cargo test
|
||||
cargo run --example basic_usage
|
||||
```
|
||||
|
||||
### WASM Build
|
||||
```bash
|
||||
wasm-pack build --target web
|
||||
wasm-pack test --headless --firefox # Run WASM tests
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
### Native
|
||||
- Rust 1.70+
|
||||
- tokio runtime
|
||||
|
||||
### WASM
|
||||
- wasm-pack
|
||||
- Modern browser with WebSocket support
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
133
sigsocket_client/examples/basic_usage.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
//! Basic usage example for sigsocket_client
|
||||
//!
|
||||
//! This example demonstrates how to:
|
||||
//! 1. Create a sigsocket client
|
||||
//! 2. Set up a sign request handler
|
||||
//! 3. Connect to a sigsocket server
|
||||
//! 4. Handle incoming signature requests
|
||||
//!
|
||||
//! This example only runs on native (non-WASM) targets.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Example sign request handler
|
||||
///
|
||||
/// In a real application, this would:
|
||||
/// - Present the request to the user
|
||||
/// - Get user approval
|
||||
/// - Use a secure signing method (hardware wallet, etc.)
|
||||
/// - Return the signature
|
||||
struct ExampleSignHandler;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SignRequestHandler for ExampleSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
println!("📝 Received sign request:");
|
||||
println!(" ID: {}", request.id);
|
||||
println!(" Message (base64): {}", request.message);
|
||||
|
||||
// Decode the message to show what we're signing
|
||||
match request.message_bytes() {
|
||||
Ok(message_bytes) => {
|
||||
println!(" Message (hex): {}", hex::encode(&message_bytes));
|
||||
println!(" Message (text): {}", String::from_utf8_lossy(&message_bytes));
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ⚠️ Failed to decode message: {}", e);
|
||||
return Err(SigSocketError::Base64(e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// In a real implementation, you would:
|
||||
// 1. Show this to the user
|
||||
// 2. Get user approval
|
||||
// 3. Sign the message using a secure method
|
||||
|
||||
println!("🤔 Would you like to sign this message? (This is a simulation)");
|
||||
println!("✅ Auto-approving for demo purposes...");
|
||||
|
||||
// Simulate signing - in reality, this would be a real signature
|
||||
let fake_signature = format!("fake_signature_for_{}", request.id);
|
||||
Ok(fake_signature.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
|
||||
println!("🚀 SigSocket Client Example");
|
||||
println!("============================");
|
||||
|
||||
// Example public key (in a real app, this would be your actual public key)
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")
|
||||
.expect("Invalid public key hex");
|
||||
|
||||
println!("🔑 Public key: {}", hex::encode(&public_key));
|
||||
|
||||
// Create the client
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
println!("📡 Created client for: {}", client.url());
|
||||
|
||||
// Set up the sign request handler
|
||||
client.set_sign_handler(ExampleSignHandler);
|
||||
println!("✅ Sign request handler configured");
|
||||
|
||||
// Connect to the server
|
||||
println!("🔌 Connecting to sigsocket server...");
|
||||
match client.connect().await {
|
||||
Ok(()) => {
|
||||
println!("✅ Connected successfully!");
|
||||
println!("📊 Connection state: {:?}", client.state());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Failed to connect: {}", e);
|
||||
println!("💡 Make sure the sigsocket server is running on localhost:8080");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the connection alive and handle requests
|
||||
println!("👂 Listening for signature requests...");
|
||||
println!(" (Press Ctrl+C to exit)");
|
||||
|
||||
// In a real application, you might want to:
|
||||
// - Handle reconnection
|
||||
// - Provide a UI for user interaction
|
||||
// - Manage multiple concurrent requests
|
||||
// - Store and manage signatures
|
||||
|
||||
// For this example, we'll just wait
|
||||
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl-c");
|
||||
|
||||
println!("\n🛑 Shutting down...");
|
||||
client.disconnect().await?;
|
||||
println!("✅ Disconnected cleanly");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Example of how you might manually send a response (if needed)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[allow(dead_code)]
|
||||
async fn send_manual_response(client: &SigSocketClient) -> Result<()> {
|
||||
let response = SignResponse::new(
|
||||
"example-request-id",
|
||||
"dGVzdCBtZXNzYWdl", // "test message" in base64
|
||||
"ZmFrZV9zaWduYXR1cmU=", // "fake_signature" in base64
|
||||
);
|
||||
|
||||
client.send_sign_response(&response).await?;
|
||||
println!("📤 Sent manual response: {}", response.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// WASM main function (does nothing since this example is native-only)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
// This example is designed for native use only
|
||||
}
|
||||
384
sigsocket_client/src/client.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
//! Main client interface for sigsocket communication
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::String, vec::Vec, boxed::Box, string::ToString};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::collections::BTreeMap as HashMap;
|
||||
|
||||
use crate::{SignRequest, SignResponse, Result, SigSocketError};
|
||||
use crate::protocol::ManagedSignRequest;
|
||||
|
||||
|
||||
|
||||
/// Connection state of the sigsocket client
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
/// Client is disconnected
|
||||
Disconnected,
|
||||
/// Client is connecting
|
||||
Connecting,
|
||||
/// Client is connected and ready
|
||||
Connected,
|
||||
/// Client connection failed
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Trait for handling sign requests from the sigsocket server
|
||||
///
|
||||
/// Applications should implement this trait to handle incoming signature requests.
|
||||
/// The implementation should:
|
||||
/// 1. Present the request to the user
|
||||
/// 2. Get user approval
|
||||
/// 3. Sign the message (using external signing logic)
|
||||
/// 4. Return the signature
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub trait SignRequestHandler: Send + Sync {
|
||||
/// Handle a sign request from the server
|
||||
///
|
||||
/// This method is called when the server sends a signature request.
|
||||
/// The implementation should:
|
||||
/// - Decode and validate the message
|
||||
/// - Present it to the user for approval
|
||||
/// - If approved, sign the message and return the signature
|
||||
/// - If rejected, return an error
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - The sign request from the server
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(signature_bytes)` - The signature as raw bytes
|
||||
/// * `Err(error)` - If the request was rejected or signing failed
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// WASM version of SignRequestHandler (no Send + Sync requirements)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub trait SignRequestHandler {
|
||||
/// Handle a sign request from the server
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
/// Main sigsocket client
|
||||
///
|
||||
/// This is the primary interface for connecting to sigsocket servers.
|
||||
/// It handles the WebSocket connection, protocol communication, and
|
||||
/// delegates signing requests to the application.
|
||||
pub struct SigSocketClient {
|
||||
/// WebSocket server URL
|
||||
url: String,
|
||||
/// Client's public key (hex-encoded)
|
||||
public_key: Vec<u8>,
|
||||
/// Current connection state
|
||||
state: ConnectionState,
|
||||
/// Sign request handler
|
||||
sign_handler: Option<Box<dyn SignRequestHandler>>,
|
||||
/// Pending sign requests managed by the client
|
||||
pending_requests: HashMap<String, ManagedSignRequest>,
|
||||
/// Connected public key (hex-encoded) - set when connection is established
|
||||
connected_public_key: Option<String>,
|
||||
/// Platform-specific implementation
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
inner: Option<crate::native::NativeClient>,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
inner: Option<crate::wasm::WasmClient>,
|
||||
}
|
||||
|
||||
impl SigSocketClient {
|
||||
/// Create a new sigsocket client
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
/// * `public_key` - Client's public key as bytes
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(client)` - New client instance
|
||||
/// * `Err(error)` - If the URL is invalid or public key is invalid
|
||||
pub fn new(url: impl Into<String>, public_key: Vec<u8>) -> Result<Self> {
|
||||
let url = url.into();
|
||||
|
||||
// Validate URL
|
||||
let _ = url::Url::parse(&url)?;
|
||||
|
||||
// Validate public key (should be 33 bytes for compressed secp256k1)
|
||||
if public_key.is_empty() {
|
||||
return Err(SigSocketError::InvalidPublicKey("Public key cannot be empty".into()));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
url,
|
||||
public_key,
|
||||
state: ConnectionState::Disconnected,
|
||||
sign_handler: None,
|
||||
pending_requests: HashMap::new(),
|
||||
connected_public_key: None,
|
||||
inner: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the sign request handler
|
||||
///
|
||||
/// This handler will be called whenever the server sends a signature request.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `handler` - Implementation of SignRequestHandler trait
|
||||
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||
where
|
||||
H: SignRequestHandler + 'static,
|
||||
{
|
||||
self.sign_handler = Some(Box::new(handler));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Get the current connection state
|
||||
pub fn state(&self) -> ConnectionState {
|
||||
self.state
|
||||
}
|
||||
|
||||
/// Check if the client is connected
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.state == ConnectionState::Connected
|
||||
}
|
||||
|
||||
/// Get the client's public key as hex string
|
||||
pub fn public_key_hex(&self) -> String {
|
||||
hex::encode(&self.public_key)
|
||||
}
|
||||
|
||||
/// Get the WebSocket server URL
|
||||
pub fn url(&self) -> &str {
|
||||
&self.url
|
||||
}
|
||||
|
||||
/// Get the connected public key (if connected)
|
||||
pub fn connected_public_key(&self) -> Option<&str> {
|
||||
self.connected_public_key.as_deref()
|
||||
}
|
||||
|
||||
// === Request Management Methods ===
|
||||
|
||||
/// Add a pending sign request
|
||||
///
|
||||
/// This is typically called when a sign request is received from the server.
|
||||
/// The request will be stored and can be retrieved later for processing.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - The sign request to add
|
||||
/// * `target_public_key` - The public key this request is intended for
|
||||
pub fn add_pending_request(&mut self, request: SignRequest, target_public_key: String) {
|
||||
let managed_request = ManagedSignRequest::new(request, target_public_key);
|
||||
self.pending_requests.insert(managed_request.id().to_string(), managed_request);
|
||||
}
|
||||
|
||||
/// Remove a pending request by ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request to remove
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Some(request)` - The removed request if it existed
|
||||
/// * `None` - If no request with that ID was found
|
||||
pub fn remove_pending_request(&mut self, request_id: &str) -> Option<ManagedSignRequest> {
|
||||
self.pending_requests.remove(request_id)
|
||||
}
|
||||
|
||||
/// Get a pending request by ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request to retrieve
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Some(request)` - The request if it exists
|
||||
/// * `None` - If no request with that ID was found
|
||||
pub fn get_pending_request(&self, request_id: &str) -> Option<&ManagedSignRequest> {
|
||||
self.pending_requests.get(request_id)
|
||||
}
|
||||
|
||||
/// Get all pending requests
|
||||
///
|
||||
/// # Returns
|
||||
/// * A reference to the HashMap containing all pending requests
|
||||
pub fn get_pending_requests(&self) -> &HashMap<String, ManagedSignRequest> {
|
||||
&self.pending_requests
|
||||
}
|
||||
|
||||
/// Get pending requests filtered by public key
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `public_key` - The public key to filter by (hex-encoded)
|
||||
///
|
||||
/// # Returns
|
||||
/// * A vector of references to requests for the specified public key
|
||||
pub fn get_requests_for_public_key(&self, public_key: &str) -> Vec<&ManagedSignRequest> {
|
||||
self.pending_requests
|
||||
.values()
|
||||
.filter(|req| req.is_for_public_key(public_key))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if a request can be handled for the given public key
|
||||
///
|
||||
/// This performs protocol-level validation without cryptographic operations.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request` - The sign request to validate
|
||||
/// * `public_key` - The public key to check against (hex-encoded)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `true` - If the request can be handled for this public key
|
||||
/// * `false` - If the request cannot be handled
|
||||
pub fn can_handle_request_for_key(&self, request: &SignRequest, public_key: &str) -> bool {
|
||||
// Basic protocol validation
|
||||
if request.id.is_empty() || request.message.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we can decode the message
|
||||
if request.message_bytes().is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For now, we assume any valid request can be handled for any public key
|
||||
// More sophisticated validation can be added here
|
||||
!public_key.is_empty()
|
||||
}
|
||||
|
||||
/// Clear all pending requests
|
||||
pub fn clear_pending_requests(&mut self) {
|
||||
self.pending_requests.clear();
|
||||
}
|
||||
|
||||
/// Get the count of pending requests
|
||||
pub fn pending_request_count(&self) -> usize {
|
||||
self.pending_requests.len()
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-specific implementations will be added in separate modules
|
||||
impl SigSocketClient {
|
||||
/// Connect to the sigsocket server
|
||||
///
|
||||
/// This establishes a WebSocket connection and sends the introduction message
|
||||
/// with the client's public key.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Successfully connected
|
||||
/// * `Err(error)` - Connection failed
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
if self.state == ConnectionState::Connected {
|
||||
return Err(SigSocketError::AlreadyConnected);
|
||||
}
|
||||
|
||||
self.state = ConnectionState::Connecting;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let mut client = crate::native::NativeClient::new(&self.url, &self.public_key)?;
|
||||
if let Some(handler) = self.sign_handler.take() {
|
||||
client.set_sign_handler_boxed(handler);
|
||||
}
|
||||
client.connect().await?;
|
||||
self.inner = Some(client);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let mut client = crate::wasm::WasmClient::new(&self.url, &self.public_key)?;
|
||||
if let Some(handler) = self.sign_handler.take() {
|
||||
client.set_sign_handler_boxed(handler);
|
||||
}
|
||||
client.connect().await?;
|
||||
self.inner = Some(client);
|
||||
}
|
||||
|
||||
self.state = ConnectionState::Connected;
|
||||
self.connected_public_key = Some(self.public_key_hex());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect from the sigsocket server
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Successfully disconnected
|
||||
/// * `Err(error)` - Disconnect failed
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
if let Some(inner) = &mut self.inner {
|
||||
inner.disconnect().await?;
|
||||
}
|
||||
self.inner = None;
|
||||
self.state = ConnectionState::Disconnected;
|
||||
self.connected_public_key = None;
|
||||
self.clear_pending_requests();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a sign response to the server
|
||||
///
|
||||
/// This is typically called after the user has approved a signature request
|
||||
/// and the application has generated the signature.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `response` - The sign response containing the signature
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Response sent successfully
|
||||
/// * `Err(error)` - Failed to send response
|
||||
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||
if !self.is_connected() {
|
||||
return Err(SigSocketError::NotConnected);
|
||||
}
|
||||
|
||||
if let Some(inner) = &self.inner {
|
||||
inner.send_sign_response(response).await
|
||||
} else {
|
||||
Err(SigSocketError::NotConnected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a response for a specific request ID with signature
|
||||
///
|
||||
/// This is a convenience method that creates a SignResponse and sends it.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request being responded to
|
||||
/// * `message` - The original message (base64-encoded)
|
||||
/// * `signature` - The signature (base64-encoded)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Response sent successfully
|
||||
/// * `Err(error)` - Failed to send response
|
||||
pub async fn send_response(&self, request_id: &str, message: &str, signature: &str) -> Result<()> {
|
||||
let response = SignResponse::new(request_id, message, signature);
|
||||
self.send_sign_response(&response).await
|
||||
}
|
||||
|
||||
/// Send a rejection for a specific request ID
|
||||
///
|
||||
/// This sends an error response to indicate the request was rejected.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request being rejected
|
||||
/// * `reason` - The reason for rejection
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Rejection sent successfully
|
||||
/// * `Err(error)` - Failed to send rejection
|
||||
pub async fn send_rejection(&self, request_id: &str, _reason: &str) -> Result<()> {
|
||||
// For now, we'll send an empty signature to indicate rejection
|
||||
// This can be improved with a proper rejection protocol
|
||||
let response = SignResponse::new(request_id, "", "");
|
||||
self.send_sign_response(&response).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SigSocketClient {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup will be handled by the platform-specific implementations
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
168
sigsocket_client/src/error.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! Error types for the sigsocket client
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::{String, ToString}, format};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use core::fmt;
|
||||
|
||||
/// Result type alias for sigsocket client operations
|
||||
pub type Result<T> = core::result::Result<T, SigSocketError>;
|
||||
|
||||
/// Error types that can occur when using the sigsocket client
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SigSocketError {
|
||||
/// WebSocket connection error
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
|
||||
/// WebSocket protocol error
|
||||
#[error("Protocol error: {0}")]
|
||||
Protocol(String),
|
||||
|
||||
/// Message serialization/deserialization error
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// Invalid public key format
|
||||
#[error("Invalid public key: {0}")]
|
||||
InvalidPublicKey(String),
|
||||
|
||||
/// Invalid URL format
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
/// Client is not connected
|
||||
#[error("Client is not connected")]
|
||||
NotConnected,
|
||||
|
||||
/// Client is already connected
|
||||
#[error("Client is already connected")]
|
||||
AlreadyConnected,
|
||||
|
||||
/// Timeout error
|
||||
#[error("Operation timed out")]
|
||||
Timeout,
|
||||
|
||||
/// Send error
|
||||
#[error("Failed to send message: {0}")]
|
||||
Send(String),
|
||||
|
||||
/// Receive error
|
||||
#[error("Failed to receive message: {0}")]
|
||||
Receive(String),
|
||||
|
||||
/// Base64 encoding/decoding error
|
||||
#[error("Base64 error: {0}")]
|
||||
Base64(String),
|
||||
|
||||
/// Hex encoding/decoding error
|
||||
#[error("Hex error: {0}")]
|
||||
Hex(String),
|
||||
|
||||
/// Generic error
|
||||
#[error("Error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// WASM version of error types (no thiserror dependency)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(Debug)]
|
||||
pub enum SigSocketError {
|
||||
/// WebSocket connection error
|
||||
Connection(String),
|
||||
/// WebSocket protocol error
|
||||
Protocol(String),
|
||||
/// Message serialization/deserialization error
|
||||
Serialization(String),
|
||||
/// Invalid public key format
|
||||
InvalidPublicKey(String),
|
||||
/// Invalid URL format
|
||||
InvalidUrl(String),
|
||||
/// Client is not connected
|
||||
NotConnected,
|
||||
/// Client is already connected
|
||||
AlreadyConnected,
|
||||
/// Timeout error
|
||||
Timeout,
|
||||
/// Send error
|
||||
Send(String),
|
||||
/// Receive error
|
||||
Receive(String),
|
||||
/// Base64 encoding/decoding error
|
||||
Base64(String),
|
||||
/// Hex encoding/decoding error
|
||||
Hex(String),
|
||||
/// Generic error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl fmt::Display for SigSocketError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SigSocketError::Connection(msg) => write!(f, "Connection error: {}", msg),
|
||||
SigSocketError::Protocol(msg) => write!(f, "Protocol error: {}", msg),
|
||||
SigSocketError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
|
||||
SigSocketError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {}", msg),
|
||||
SigSocketError::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
|
||||
SigSocketError::NotConnected => write!(f, "Client is not connected"),
|
||||
SigSocketError::AlreadyConnected => write!(f, "Client is already connected"),
|
||||
SigSocketError::Timeout => write!(f, "Operation timed out"),
|
||||
SigSocketError::Send(msg) => write!(f, "Failed to send message: {}", msg),
|
||||
SigSocketError::Receive(msg) => write!(f, "Failed to receive message: {}", msg),
|
||||
SigSocketError::Base64(msg) => write!(f, "Base64 error: {}", msg),
|
||||
SigSocketError::Hex(msg) => write!(f, "Hex error: {}", msg),
|
||||
SigSocketError::Other(msg) => write!(f, "Error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Implement From traits for common error types
|
||||
impl From<serde_json::Error> for SigSocketError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
SigSocketError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for SigSocketError {
|
||||
fn from(err: base64::DecodeError) -> Self {
|
||||
SigSocketError::Base64(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for SigSocketError {
|
||||
fn from(err: hex::FromHexError) -> Self {
|
||||
SigSocketError::Hex(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for SigSocketError {
|
||||
fn from(err: url::ParseError) -> Self {
|
||||
SigSocketError::InvalidUrl(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Native-specific error conversions
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native_errors {
|
||||
use super::SigSocketError;
|
||||
|
||||
impl From<tokio_tungstenite::tungstenite::Error> for SigSocketError {
|
||||
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
|
||||
SigSocketError::Connection(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WASM-specific error conversions
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl From<wasm_bindgen::JsValue> for SigSocketError {
|
||||
fn from(err: wasm_bindgen::JsValue) -> Self {
|
||||
SigSocketError::Other(format!("{:?}", err))
|
||||
}
|
||||
}
|
||||
72
sigsocket_client/src/lib.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
//! # SigSocket Client
|
||||
//!
|
||||
//! A WebSocket client library for connecting to sigsocket servers with WASM-first support.
|
||||
//!
|
||||
//! This library provides a unified interface for both native and WASM environments,
|
||||
//! allowing applications to connect to sigsocket servers using a public key and handle
|
||||
//! incoming signature requests.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **WASM-first design**: Optimized for browser environments
|
||||
//! - **Native support**: Works in native Rust applications
|
||||
//! - **No signing logic**: Delegates signing to the application
|
||||
//! - **User approval flow**: Notifies applications about incoming requests
|
||||
//! - **sigsocket compatible**: Fully compatible with sigsocket server protocol
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result};
|
||||
//!
|
||||
//! struct MyHandler;
|
||||
//! impl SignRequestHandler for MyHandler {
|
||||
//! fn handle_sign_request(&self, _request: &SignRequest) -> Result<Vec<u8>> {
|
||||
//! Ok(b"fake_signature".to_vec())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<()> {
|
||||
//! // Create client with public key
|
||||
//! let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||
//! let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
|
||||
//!
|
||||
//! // Set up request handler
|
||||
//! client.set_sign_handler(MyHandler);
|
||||
//!
|
||||
//! // Connect to server
|
||||
//! client.connect().await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(target_arch = "wasm32", no_std)]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
mod error;
|
||||
mod protocol;
|
||||
mod client;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
|
||||
pub use error::{SigSocketError, Result};
|
||||
pub use protocol::{SignRequest, SignResponse, ManagedSignRequest, RequestStatus};
|
||||
pub use client::{SigSocketClient, SignRequestHandler, ConnectionState};
|
||||
|
||||
// Re-export for convenience
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
SigSocketClient, SignRequest, SignResponse, ManagedSignRequest, RequestStatus,
|
||||
SignRequestHandler, ConnectionState, SigSocketError, Result
|
||||
};
|
||||
}
|
||||
232
sigsocket_client/src/native.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
//! Native (non-WASM) implementation of the sigsocket client
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use url::Url;
|
||||
|
||||
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// Native WebSocket client implementation
|
||||
pub struct NativeClient {
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Arc<dyn SignRequestHandler>>,
|
||||
sender: Option<mpsc::UnboundedSender<Message>>,
|
||||
connected: Arc<RwLock<bool>>,
|
||||
reconnect_attempts: u32,
|
||||
max_reconnect_attempts: u32,
|
||||
reconnect_delay_ms: u64,
|
||||
}
|
||||
|
||||
impl NativeClient {
|
||||
/// Create a new native client
|
||||
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
public_key: public_key.to_vec(),
|
||||
sign_handler: None,
|
||||
sender: None,
|
||||
connected: Arc::new(RwLock::new(false)),
|
||||
reconnect_attempts: 0,
|
||||
max_reconnect_attempts: 5,
|
||||
reconnect_delay_ms: 1000, // Start with 1 second
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the sign request handler
|
||||
pub fn set_sign_handler<H>(&mut self, handler: H)
|
||||
where
|
||||
H: SignRequestHandler + 'static,
|
||||
{
|
||||
self.sign_handler = Some(Arc::new(handler));
|
||||
}
|
||||
|
||||
/// Set the sign request handler from a boxed trait object
|
||||
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
|
||||
self.sign_handler = Some(Arc::from(handler));
|
||||
}
|
||||
|
||||
/// Connect to the WebSocket server with automatic reconnection
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
self.reconnect_attempts = 0;
|
||||
self.connect_with_retry().await
|
||||
}
|
||||
|
||||
/// Connect with retry logic
|
||||
async fn connect_with_retry(&mut self) -> Result<()> {
|
||||
loop {
|
||||
match self.try_connect().await {
|
||||
Ok(()) => {
|
||||
self.reconnect_attempts = 0; // Reset on successful connection
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
self.reconnect_attempts += 1;
|
||||
|
||||
if self.reconnect_attempts > self.max_reconnect_attempts {
|
||||
log::error!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let delay = self.reconnect_delay_ms * (2_u64.pow(self.reconnect_attempts - 1)); // Exponential backoff
|
||||
log::warn!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||
self.reconnect_attempts, self.max_reconnect_attempts, delay, e);
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Single connection attempt
|
||||
async fn try_connect(&mut self) -> Result<()> {
|
||||
let url = Url::parse(&self.url)?;
|
||||
|
||||
// Connect to WebSocket
|
||||
let (ws_stream, _) = connect_async(url).await
|
||||
.map_err(|e| SigSocketError::Connection(e.to_string()))?;
|
||||
let (mut write, mut read) = ws_stream.split();
|
||||
|
||||
// Send introduction message (hex-encoded public key)
|
||||
let intro_message = hex::encode(&self.public_key);
|
||||
write.send(Message::Text(intro_message)).await
|
||||
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||
|
||||
// Set up message sender channel
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
self.sender = Some(tx);
|
||||
|
||||
// Set connected state
|
||||
*self.connected.write().await = true;
|
||||
|
||||
// Spawn write task
|
||||
let write_task = tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
if let Err(e) = write.send(message).await {
|
||||
log::error!("Failed to send message: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn read task
|
||||
let connected = self.connected.clone();
|
||||
let sign_handler = self.sign_handler.clone();
|
||||
let sender = self.sender.as_ref().unwrap().clone();
|
||||
|
||||
let read_task = tokio::spawn(async move {
|
||||
while let Some(message) = read.next().await {
|
||||
match message {
|
||||
Ok(Message::Text(text)) => {
|
||||
if let Err(e) = Self::handle_text_message(&text, &sign_handler, &sender).await {
|
||||
log::error!("Failed to handle message: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
log::info!("WebSocket connection closed");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
// Ignore other message types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as disconnected
|
||||
*connected.write().await = false;
|
||||
});
|
||||
|
||||
// Store tasks (in a real implementation, you'd want to manage these properly)
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::try_join!(write_task, read_task);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle incoming text messages
|
||||
async fn handle_text_message(
|
||||
text: &str,
|
||||
sign_handler: &Option<Arc<dyn SignRequestHandler>>,
|
||||
sender: &mpsc::UnboundedSender<Message>,
|
||||
) -> Result<()> {
|
||||
log::debug!("Received message: {}", text);
|
||||
|
||||
// Handle simple acknowledgment messages
|
||||
if text == "Connected" {
|
||||
log::info!("Server acknowledged connection");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try to parse as sign request
|
||||
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
|
||||
if let Some(handler) = sign_handler {
|
||||
// Handle the sign request
|
||||
match handler.handle_sign_request(&sign_request) {
|
||||
Ok(signature) => {
|
||||
// Create and send response
|
||||
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
|
||||
let response_json = serde_json::to_string(&response)?;
|
||||
|
||||
sender.send(Message::Text(response_json))
|
||||
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||
|
||||
log::info!("Sent signature response for request {}", response.id);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Sign request rejected: {}", e);
|
||||
// Optionally send an error response to the server
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No sign request handler registered, ignoring request");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::warn!("Failed to parse message: {}", text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect from the WebSocket server
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
*self.connected.write().await = false;
|
||||
|
||||
if let Some(sender) = &self.sender {
|
||||
// Send close message
|
||||
let _ = sender.send(Message::Close(None));
|
||||
}
|
||||
|
||||
self.sender = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a sign response to the server
|
||||
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||
if let Some(sender) = &self.sender {
|
||||
let response_json = serde_json::to_string(response)?;
|
||||
sender.send(Message::Text(response_json))
|
||||
.map_err(|e| SigSocketError::Send(e.to_string()))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SigSocketError::NotConnected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if connected
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
*self.connected.read().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NativeClient {
|
||||
fn drop(&mut self) {
|
||||
// Cleanup will be handled by the async tasks
|
||||
}
|
||||
}
|
||||
256
sigsocket_client/src/protocol.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! Protocol definitions for sigsocket communication
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Sign request from the sigsocket server
|
||||
///
|
||||
/// This represents a request from the server for the client to sign a message.
|
||||
/// The client should present this to the user for approval before signing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SignRequest {
|
||||
/// Unique identifier for this request
|
||||
pub id: String,
|
||||
/// Message to be signed (base64-encoded)
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Sign response to send back to the sigsocket server
|
||||
///
|
||||
/// This represents the client's response after the user has approved and signed the message.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SignResponse {
|
||||
/// Request identifier (must match the original request)
|
||||
pub id: String,
|
||||
/// Original message that was signed (base64-encoded)
|
||||
pub message: String,
|
||||
/// Signature of the message (base64-encoded)
|
||||
pub signature: String,
|
||||
}
|
||||
|
||||
impl SignRequest {
|
||||
/// Create a new sign request
|
||||
pub fn new(id: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the message as bytes (decoded from base64)
|
||||
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.message)
|
||||
}
|
||||
|
||||
/// Get the message as a hex string (for display purposes)
|
||||
pub fn message_hex(&self) -> Result<String, base64::DecodeError> {
|
||||
self.message_bytes().map(|bytes| hex::encode(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl SignResponse {
|
||||
/// Create a new sign response
|
||||
pub fn new(
|
||||
id: impl Into<String>,
|
||||
message: impl Into<String>,
|
||||
signature: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
message: message.into(),
|
||||
signature: signature.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a sign response from a request and signature bytes
|
||||
pub fn from_request_and_signature(
|
||||
request: &SignRequest,
|
||||
signature: &[u8],
|
||||
) -> Self {
|
||||
Self {
|
||||
id: request.id.clone(),
|
||||
message: request.message.clone(),
|
||||
signature: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the signature as bytes (decoded from base64)
|
||||
pub fn signature_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.signature)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced sign request with additional metadata for request management
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ManagedSignRequest {
|
||||
/// The original sign request
|
||||
#[serde(flatten)]
|
||||
pub request: SignRequest,
|
||||
/// Timestamp when the request was received (Unix timestamp in milliseconds)
|
||||
pub timestamp: u64,
|
||||
/// Target public key for this request (hex-encoded)
|
||||
pub target_public_key: String,
|
||||
/// Current status of the request
|
||||
pub status: RequestStatus,
|
||||
}
|
||||
|
||||
/// Status of a sign request
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum RequestStatus {
|
||||
/// Request is pending user approval
|
||||
Pending,
|
||||
/// Request has been approved and signed
|
||||
Approved,
|
||||
/// Request has been rejected by user
|
||||
Rejected,
|
||||
/// Request has expired or been cancelled
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl ManagedSignRequest {
|
||||
/// Create a new managed sign request
|
||||
pub fn new(request: SignRequest, target_public_key: String) -> Self {
|
||||
Self {
|
||||
request,
|
||||
timestamp: current_timestamp_ms(),
|
||||
target_public_key,
|
||||
status: RequestStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the request ID
|
||||
pub fn id(&self) -> &str {
|
||||
&self.request.id
|
||||
}
|
||||
|
||||
/// Get the message as bytes (decoded from base64)
|
||||
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
self.request.message_bytes()
|
||||
}
|
||||
|
||||
/// Check if this request is for the given public key
|
||||
pub fn is_for_public_key(&self, public_key: &str) -> bool {
|
||||
self.target_public_key == public_key
|
||||
}
|
||||
|
||||
/// Mark the request as approved
|
||||
pub fn mark_approved(&mut self) {
|
||||
self.status = RequestStatus::Approved;
|
||||
}
|
||||
|
||||
/// Mark the request as rejected
|
||||
pub fn mark_rejected(&mut self) {
|
||||
self.status = RequestStatus::Rejected;
|
||||
}
|
||||
|
||||
/// Check if the request is still pending
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self.status, RequestStatus::Pending)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current timestamp in milliseconds
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn current_timestamp_ms() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
/// Get current timestamp in milliseconds (WASM version)
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn current_timestamp_ms() -> u64 {
|
||||
// In WASM, we'll use a simple counter or Date.now() via JS
|
||||
// For now, return 0 - this can be improved later
|
||||
0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_creation() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
assert_eq!(request.id, "test-id");
|
||||
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_message_bytes() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
let bytes = request.message_bytes().unwrap();
|
||||
assert_eq!(bytes, b"test message");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_message_hex() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
let hex = request.message_hex().unwrap();
|
||||
assert_eq!(hex, hex::encode(b"test message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_creation() {
|
||||
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl"); // "signature" in base64
|
||||
assert_eq!(response.id, "test-id");
|
||||
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_from_request() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||
let signature = b"signature";
|
||||
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(request, deserialized);
|
||||
|
||||
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(response, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managed_sign_request() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||
let managed = ManagedSignRequest::new(request.clone(), "test-public-key".to_string());
|
||||
|
||||
assert_eq!(managed.id(), "test-id");
|
||||
assert_eq!(managed.request, request);
|
||||
assert_eq!(managed.target_public_key, "test-public-key");
|
||||
assert!(managed.is_pending());
|
||||
assert!(managed.is_for_public_key("test-public-key"));
|
||||
assert!(!managed.is_for_public_key("other-key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managed_request_status_changes() {
|
||||
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
|
||||
let mut managed = ManagedSignRequest::new(request, "test-public-key".to_string());
|
||||
|
||||
assert!(managed.is_pending());
|
||||
|
||||
managed.mark_approved();
|
||||
assert_eq!(managed.status, RequestStatus::Approved);
|
||||
assert!(!managed.is_pending());
|
||||
|
||||
managed.mark_rejected();
|
||||
assert_eq!(managed.status, RequestStatus::Rejected);
|
||||
assert!(!managed.is_pending());
|
||||
}
|
||||
}
|
||||
549
sigsocket_client/src/wasm.rs
Normal file
@@ -0,0 +1,549 @@
|
||||
//! WASM implementation of the sigsocket client
|
||||
|
||||
use alloc::{string::{String, ToString}, vec::Vec, boxed::Box, rc::Rc, format};
|
||||
use core::cell::RefCell;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{WebSocket, MessageEvent, Event, BinaryType};
|
||||
|
||||
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// WASM WebSocket client implementation
|
||||
pub struct WasmClient {
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
websocket: Option<WebSocket>,
|
||||
connected: Rc<RefCell<bool>>,
|
||||
reconnect_attempts: Rc<RefCell<u32>>,
|
||||
max_reconnect_attempts: u32,
|
||||
reconnect_delay_ms: u64,
|
||||
auto_reconnect: bool,
|
||||
}
|
||||
|
||||
impl WasmClient {
|
||||
/// Create a new WASM client
|
||||
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
url: url.to_string(),
|
||||
public_key: public_key.to_vec(),
|
||||
sign_handler: None,
|
||||
websocket: None,
|
||||
connected: Rc::new(RefCell::new(false)),
|
||||
reconnect_attempts: Rc::new(RefCell::new(0)),
|
||||
max_reconnect_attempts: 5,
|
||||
reconnect_delay_ms: 1000, // Start with 1 second
|
||||
auto_reconnect: false, // Disable auto-reconnect to avoid multiple connections
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the sign request handler from a boxed trait object
|
||||
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
|
||||
self.sign_handler = Some(Rc::new(RefCell::new(handler)));
|
||||
}
|
||||
|
||||
/// Enable or disable automatic reconnection
|
||||
pub fn set_auto_reconnect(&mut self, enabled: bool) {
|
||||
self.auto_reconnect = enabled;
|
||||
}
|
||||
|
||||
/// Set reconnection parameters
|
||||
pub fn set_reconnect_config(&mut self, max_attempts: u32, initial_delay_ms: u64) {
|
||||
self.max_reconnect_attempts = max_attempts;
|
||||
self.reconnect_delay_ms = initial_delay_ms;
|
||||
}
|
||||
|
||||
/// Connect to the WebSocket server with automatic reconnection
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
*self.reconnect_attempts.borrow_mut() = 0;
|
||||
self.connect_with_retry().await
|
||||
}
|
||||
|
||||
/// Connect with retry logic
|
||||
async fn connect_with_retry(&mut self) -> Result<()> {
|
||||
loop {
|
||||
match self.try_connect().await {
|
||||
Ok(()) => {
|
||||
*self.reconnect_attempts.borrow_mut() = 0; // Reset on successful connection
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
let mut attempts = self.reconnect_attempts.borrow_mut();
|
||||
*attempts += 1;
|
||||
|
||||
if *attempts > self.max_reconnect_attempts {
|
||||
web_sys::console::error_1(&format!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts).into());
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let delay = self.reconnect_delay_ms * (2_u64.pow(*attempts - 1)); // Exponential backoff
|
||||
web_sys::console::warn_1(&format!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
|
||||
*attempts, self.max_reconnect_attempts, delay, e).into());
|
||||
|
||||
// Drop the borrow before the async sleep
|
||||
drop(attempts);
|
||||
|
||||
// Wait before retrying
|
||||
self.sleep_ms(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sleep for the specified number of milliseconds (WASM-compatible)
|
||||
async fn sleep_ms(&self, ms: u64) -> () {
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use js_sys::Promise;
|
||||
|
||||
let promise = Promise::new(&mut |resolve, _reject| {
|
||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
timeout_callback.as_ref().unchecked_ref(),
|
||||
ms as i32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
timeout_callback.forget();
|
||||
});
|
||||
|
||||
let _ = JsFuture::from(promise).await;
|
||||
}
|
||||
|
||||
/// Single connection attempt
|
||||
async fn try_connect(&mut self) -> Result<()> {
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use js_sys::Promise;
|
||||
|
||||
web_sys::console::log_1(&format!("try_connect: Creating WebSocket to {}", self.url).into());
|
||||
|
||||
// Create WebSocket
|
||||
let ws = WebSocket::new(&self.url)
|
||||
.map_err(|e| {
|
||||
web_sys::console::error_1(&format!("Failed to create WebSocket: {:?}", e).into());
|
||||
SigSocketError::Connection(format!("{:?}", e))
|
||||
})?;
|
||||
|
||||
web_sys::console::log_1(&"try_connect: WebSocket created successfully".into());
|
||||
|
||||
// Set binary type
|
||||
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||
|
||||
web_sys::console::log_1(&"try_connect: Binary type set, setting up event handlers".into());
|
||||
|
||||
let connected = self.connected.clone();
|
||||
let public_key = self.public_key.clone();
|
||||
|
||||
// Set up onopen handler
|
||||
{
|
||||
let ws_clone = ws.clone();
|
||||
let public_key_clone = public_key.clone();
|
||||
|
||||
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
web_sys::console::log_1(&"MAIN CONNECTION: WebSocket opened, sending public key introduction".into());
|
||||
|
||||
// Send introduction message (hex-encoded public key)
|
||||
let intro_message = hex::encode(&public_key_clone);
|
||||
web_sys::console::log_1(&format!("MAIN CONNECTION: Sending public key: {}", &intro_message[..16]).into());
|
||||
|
||||
if let Err(e) = ws_clone.send_with_str(&intro_message) {
|
||||
web_sys::console::error_1(&format!("MAIN CONNECTION: Failed to send introduction: {:?}", e).into());
|
||||
} else {
|
||||
web_sys::console::log_1(&"MAIN CONNECTION: Public key sent successfully".into());
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||
onopen_callback.forget(); // Prevent cleanup
|
||||
|
||||
web_sys::console::log_1(&"try_connect: onopen handler set up".into());
|
||||
}
|
||||
|
||||
// Set up onmessage handler
|
||||
{
|
||||
let ws_clone = ws.clone();
|
||||
let handler_clone = self.sign_handler.clone();
|
||||
let connected_clone = connected.clone();
|
||||
|
||||
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
|
||||
let message = text.as_string().unwrap_or_default();
|
||||
web_sys::console::log_1(&format!("MAIN CONNECTION: Received message: {}", message).into());
|
||||
|
||||
// Check if this is the "Connected" acknowledgment
|
||||
if message == "Connected" {
|
||||
web_sys::console::log_1(&"MAIN CONNECTION: Server acknowledged connection".into());
|
||||
*connected_clone.borrow_mut() = true;
|
||||
}
|
||||
|
||||
// Handle the message with proper sign request support
|
||||
Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone);
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||
onmessage_callback.forget(); // Prevent cleanup
|
||||
|
||||
web_sys::console::log_1(&"try_connect: onmessage handler set up".into());
|
||||
}
|
||||
|
||||
// Set up onerror handler
|
||||
{
|
||||
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
|
||||
web_sys::console::error_1(&format!("MAIN CONNECTION: WebSocket error: {:?}", event).into());
|
||||
});
|
||||
|
||||
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||
onerror_callback.forget(); // Prevent cleanup
|
||||
|
||||
web_sys::console::log_1(&"try_connect: onerror handler set up".into());
|
||||
}
|
||||
|
||||
// Set up onclose handler with auto-reconnection support
|
||||
{
|
||||
let connected = connected.clone();
|
||||
let auto_reconnect = self.auto_reconnect;
|
||||
let reconnect_attempts = self.reconnect_attempts.clone();
|
||||
let max_attempts = self.max_reconnect_attempts;
|
||||
let url = self.url.clone();
|
||||
let public_key = self.public_key.clone();
|
||||
let sign_handler = self.sign_handler.clone();
|
||||
let delay_ms = self.reconnect_delay_ms;
|
||||
|
||||
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
*connected.borrow_mut() = false;
|
||||
web_sys::console::log_1(&"WebSocket connection closed".into());
|
||||
|
||||
// Trigger auto-reconnection if enabled
|
||||
if auto_reconnect {
|
||||
let attempts = reconnect_attempts.clone();
|
||||
let current_attempts = *attempts.borrow();
|
||||
|
||||
if current_attempts < max_attempts {
|
||||
web_sys::console::log_1(&"Attempting automatic reconnection...".into());
|
||||
|
||||
// Schedule reconnection attempt
|
||||
Self::schedule_reconnection(
|
||||
url.clone(),
|
||||
public_key.clone(),
|
||||
sign_handler.clone(),
|
||||
attempts.clone(),
|
||||
max_attempts,
|
||||
delay_ms,
|
||||
connected.clone(),
|
||||
);
|
||||
} else {
|
||||
web_sys::console::error_1(&format!("Max reconnection attempts ({}) reached, giving up", max_attempts).into());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
|
||||
onclose_callback.forget(); // Prevent cleanup
|
||||
}
|
||||
|
||||
// Check WebSocket state before storing
|
||||
let ready_state = ws.ready_state();
|
||||
web_sys::console::log_1(&format!("try_connect: WebSocket ready state: {}", ready_state).into());
|
||||
|
||||
self.websocket = Some(ws);
|
||||
|
||||
web_sys::console::log_1(&"try_connect: WebSocket stored, waiting for connection to be established".into());
|
||||
|
||||
// The WebSocket will open asynchronously and the onopen/onmessage handlers will handle the connection
|
||||
// Since we can see from logs that the connection is working, just return success
|
||||
web_sys::console::log_1(&"try_connect: WebSocket setup complete, connection will be established asynchronously".into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for WebSocket connection to be established
|
||||
async fn wait_for_connection(&self) -> Result<()> {
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use js_sys::Promise;
|
||||
|
||||
web_sys::console::log_1(&"wait_for_connection: Starting to wait for connection".into());
|
||||
|
||||
// Simple approach: just wait a bit and check if we're connected
|
||||
// The onopen handler should have fired by now if the connection is working
|
||||
|
||||
let connected = self.connected.clone();
|
||||
|
||||
// Wait up to 30 seconds, checking every 500ms
|
||||
for attempt in 1..=60 {
|
||||
// Check if we're connected
|
||||
if *connected.borrow() {
|
||||
web_sys::console::log_1(&format!("wait_for_connection: Connected after {} attempts ({}ms)", attempt, attempt * 500).into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Wait 500ms before next check
|
||||
let promise = Promise::new(&mut |resolve, _reject| {
|
||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
timeout_callback.as_ref().unchecked_ref(),
|
||||
500,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
timeout_callback.forget();
|
||||
});
|
||||
|
||||
let _ = JsFuture::from(promise).await;
|
||||
|
||||
if attempt % 10 == 0 {
|
||||
web_sys::console::log_1(&format!("wait_for_connection: Still waiting... attempt {}/60", attempt).into());
|
||||
}
|
||||
}
|
||||
|
||||
web_sys::console::error_1(&"wait_for_connection: Timeout after 30 seconds".into());
|
||||
Err(SigSocketError::Connection("Connection timeout".to_string()))
|
||||
}
|
||||
|
||||
/// Schedule a reconnection attempt (called from onclose handler)
|
||||
fn schedule_reconnection(
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
reconnect_attempts: Rc<RefCell<u32>>,
|
||||
_max_attempts: u32,
|
||||
delay_ms: u64,
|
||||
connected: Rc<RefCell<bool>>,
|
||||
) {
|
||||
let mut attempts = reconnect_attempts.borrow_mut();
|
||||
*attempts += 1;
|
||||
let current_attempt = *attempts;
|
||||
drop(attempts); // Release the borrow
|
||||
|
||||
let delay = delay_ms * (2_u64.pow(current_attempt - 1)); // Exponential backoff
|
||||
|
||||
web_sys::console::log_1(&format!("Scheduling reconnection attempt {} in {}ms", current_attempt, delay).into());
|
||||
|
||||
// Schedule the reconnection attempt
|
||||
let timeout_callback = Closure::wrap(Box::new(move || {
|
||||
// Create a new client instance for reconnection
|
||||
match Self::attempt_reconnection(url.clone(), public_key.clone(), sign_handler.clone(), connected.clone()) {
|
||||
Ok(_) => {
|
||||
web_sys::console::log_1(&"Reconnection attempt initiated".into());
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to initiate reconnection: {:?}", e).into());
|
||||
}
|
||||
}
|
||||
}) as Box<dyn FnMut()>);
|
||||
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
timeout_callback.as_ref().unchecked_ref(),
|
||||
delay as i32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
timeout_callback.forget();
|
||||
}
|
||||
|
||||
/// Attempt to reconnect (helper method)
|
||||
fn attempt_reconnection(
|
||||
url: String,
|
||||
public_key: Vec<u8>,
|
||||
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
connected: Rc<RefCell<bool>>,
|
||||
) -> Result<()> {
|
||||
// Create WebSocket
|
||||
let ws = WebSocket::new(&url)
|
||||
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||
|
||||
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||
|
||||
// Send public key on open
|
||||
{
|
||||
let public_key_clone = public_key.clone();
|
||||
let connected_clone = connected.clone();
|
||||
let ws_clone = ws.clone();
|
||||
|
||||
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
web_sys::console::log_1(&"Reconnection WebSocket opened, sending public key introduction".into());
|
||||
|
||||
// Send public key introduction
|
||||
let public_key_hex = hex::encode(&public_key_clone);
|
||||
web_sys::console::log_1(&format!("Reconnection sending public key: {}", &public_key_hex[..16]).into());
|
||||
|
||||
if let Err(e) = ws_clone.send_with_str(&public_key_hex) {
|
||||
web_sys::console::error_1(&format!("Failed to send public key on reconnection: {:?}", e).into());
|
||||
} else {
|
||||
web_sys::console::log_1(&"Reconnection public key sent successfully, waiting for server acknowledgment".into());
|
||||
// Don't set connected=true here, wait for "Connected" message
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||
onopen_callback.forget();
|
||||
}
|
||||
|
||||
// Set up message handler for reconnected socket
|
||||
{
|
||||
let ws_clone = ws.clone();
|
||||
let handler_clone = sign_handler.clone();
|
||||
let connected_clone = connected.clone();
|
||||
|
||||
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
|
||||
let message = text.as_string().unwrap_or_default();
|
||||
Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone);
|
||||
}
|
||||
});
|
||||
|
||||
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||
onmessage_callback.forget();
|
||||
}
|
||||
|
||||
// Set up error handler
|
||||
{
|
||||
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
|
||||
web_sys::console::error_1(&format!("Reconnection WebSocket error: {:?}", event).into());
|
||||
});
|
||||
|
||||
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||
onerror_callback.forget();
|
||||
}
|
||||
|
||||
// Set up close handler (for potential future reconnections)
|
||||
{
|
||||
let connected_clone = connected.clone();
|
||||
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
|
||||
*connected_clone.borrow_mut() = false;
|
||||
web_sys::console::log_1(&"Reconnected WebSocket closed".into());
|
||||
});
|
||||
|
||||
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
|
||||
onclose_callback.forget();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle incoming messages with full sign request support
|
||||
fn handle_message(
|
||||
text: &str,
|
||||
ws: &WebSocket,
|
||||
sign_handler: &Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
|
||||
connected: &Rc<RefCell<bool>>
|
||||
) {
|
||||
web_sys::console::log_1(&format!("Received message: {}", text).into());
|
||||
|
||||
// Handle simple acknowledgment messages
|
||||
if text == "Connected" {
|
||||
web_sys::console::log_1(&"Server acknowledged connection".into());
|
||||
*connected.borrow_mut() = true;
|
||||
web_sys::console::log_1(&"Connection state updated to connected".into());
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse as sign request
|
||||
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
|
||||
web_sys::console::log_1(&format!("Received sign request: {}", sign_request.id).into());
|
||||
|
||||
// Handle the sign request if we have a handler
|
||||
if let Some(handler_rc) = sign_handler {
|
||||
match handler_rc.try_borrow() {
|
||||
Ok(handler) => {
|
||||
match handler.handle_sign_request(&sign_request) {
|
||||
Ok(signature) => {
|
||||
// Create and send response
|
||||
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
|
||||
match serde_json::to_string(&response) {
|
||||
Ok(response_json) => {
|
||||
if let Err(e) = ws.send_with_str(&response_json) {
|
||||
web_sys::console::error_1(&format!("Failed to send response: {:?}", e).into());
|
||||
} else {
|
||||
web_sys::console::log_1(&format!("Sent signature response for request {}", response.id).into());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to serialize response: {}", e).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::warn_1(&format!("Sign request rejected: {}", e).into());
|
||||
// Optionally send an error response to the server
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
web_sys::console::error_1(&"Failed to borrow sign handler".into());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
web_sys::console::warn_1(&"No sign request handler registered, ignoring request".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
web_sys::console::warn_1(&format!("Failed to parse message: {}", text).into());
|
||||
}
|
||||
|
||||
/// Disconnect from the WebSocket server
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
if let Some(ws) = &self.websocket {
|
||||
ws.close()
|
||||
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
*self.connected.borrow_mut() = false;
|
||||
self.websocket = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a sign response to the server
|
||||
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
|
||||
if let Some(ws) = &self.websocket {
|
||||
let response_json = serde_json::to_string(response)?;
|
||||
ws.send_with_str(&response_json)
|
||||
.map_err(|e| SigSocketError::Send(format!("{:?}", e)))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SigSocketError::NotConnected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if connected
|
||||
pub fn is_connected(&self) -> bool {
|
||||
*self.connected.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WasmClient {
|
||||
fn drop(&mut self) {
|
||||
// Close WebSocket connection if it exists
|
||||
if let Some(ws) = self.websocket.take() {
|
||||
ws.close().unwrap_or_else(|e| {
|
||||
web_sys::console::warn_1(&format!("Failed to close WebSocket: {:?}", e).into());
|
||||
});
|
||||
web_sys::console::log_1(&"🔌 WebSocket connection closed on drop".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WASM-specific utilities
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
// Helper macro for logging in WASM
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
162
sigsocket_client/tests/integration_test.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
//! Integration tests for sigsocket_client
|
||||
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// Test sign request handler
|
||||
struct TestSignHandler {
|
||||
should_approve: bool,
|
||||
}
|
||||
|
||||
impl TestSignHandler {
|
||||
fn new(should_approve: bool) -> Self {
|
||||
Self { should_approve }
|
||||
}
|
||||
}
|
||||
|
||||
impl SignRequestHandler for TestSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
if self.should_approve {
|
||||
// Create a test signature
|
||||
let signature = format!("test_signature_for_{}", request.id);
|
||||
Ok(signature.into_bytes())
|
||||
} else {
|
||||
Err(SigSocketError::Other("User rejected request".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_creation() {
|
||||
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(request.id, "test-123");
|
||||
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_request_message_decoding() {
|
||||
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
|
||||
let bytes = request.message_bytes().unwrap();
|
||||
assert_eq!(bytes, b"test message");
|
||||
|
||||
let hex = request.message_hex().unwrap();
|
||||
assert_eq!(hex, hex::encode(b"test message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_creation() {
|
||||
let response = SignResponse::new("test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||
assert_eq!(response.id, "test-123");
|
||||
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_response_from_request() {
|
||||
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
|
||||
let signature = b"test_signature";
|
||||
|
||||
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protocol_serialization() {
|
||||
// Test SignRequest serialization
|
||||
let request = SignRequest::new("req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(request, deserialized);
|
||||
|
||||
// Test SignResponse serialization
|
||||
let response = SignResponse::new("req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(response, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_creation() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
|
||||
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
|
||||
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
|
||||
assert!(!client.is_connected());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_invalid_url() {
|
||||
let public_key = vec![1, 2, 3];
|
||||
let result = SigSocketClient::new("invalid-url", public_key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_empty_public_key() {
|
||||
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
|
||||
assert!(result.is_err());
|
||||
if let Err(error) = result {
|
||||
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_handler_approval() {
|
||||
let handler = TestSignHandler::new(true);
|
||||
let request = SignRequest::new("test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let signature = result.unwrap();
|
||||
assert_eq!(signature, b"test_signature_for_test-789");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_handler_rejection() {
|
||||
let handler = TestSignHandler::new(false);
|
||||
let request = SignRequest::new("test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_display() {
|
||||
let error = SigSocketError::NotConnected;
|
||||
assert_eq!(error.to_string(), "Client is not connected");
|
||||
|
||||
let error = SigSocketError::Connection("test error".to_string());
|
||||
assert_eq!(error.to_string(), "Connection error: test error");
|
||||
}
|
||||
|
||||
// Test that demonstrates the expected usage pattern
|
||||
#[test]
|
||||
fn test_usage_pattern() {
|
||||
// 1. Create client
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// 2. Set handler
|
||||
client.set_sign_handler(TestSignHandler::new(true));
|
||||
|
||||
// 3. Verify state
|
||||
assert!(!client.is_connected());
|
||||
|
||||
// 4. Create a test request/response cycle
|
||||
let request = SignRequest::new("test-request", "dGVzdCBtZXNzYWdl");
|
||||
let handler = TestSignHandler::new(true);
|
||||
let signature = handler.handle_sign_request(&request).unwrap();
|
||||
let response = SignResponse::from_request_and_signature(&request, &signature);
|
||||
|
||||
// 5. Verify the response
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
92
sigsocket_client/tests/request_management_test.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! Tests for the enhanced request management functionality
|
||||
|
||||
use sigsocket_client::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_client_request_management() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// Initially no requests
|
||||
assert_eq!(client.pending_request_count(), 0);
|
||||
assert!(client.get_pending_requests().is_empty());
|
||||
|
||||
// Add a request
|
||||
let request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl");
|
||||
let public_key_hex = "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
|
||||
client.add_pending_request(request.clone(), public_key_hex.to_string());
|
||||
|
||||
// Check request was added
|
||||
assert_eq!(client.pending_request_count(), 1);
|
||||
assert!(client.get_pending_request("test-1").is_some());
|
||||
|
||||
// Check filtering by public key
|
||||
let filtered = client.get_requests_for_public_key(public_key_hex);
|
||||
assert_eq!(filtered.len(), 1);
|
||||
assert_eq!(filtered[0].id(), "test-1");
|
||||
|
||||
// Add another request for different public key
|
||||
let request2 = SignRequest::new("test-2", "dGVzdCBtZXNzYWdlMg==");
|
||||
let other_public_key = "03f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9";
|
||||
client.add_pending_request(request2, other_public_key.to_string());
|
||||
|
||||
// Check total count
|
||||
assert_eq!(client.pending_request_count(), 2);
|
||||
|
||||
// Check filtering still works
|
||||
let filtered = client.get_requests_for_public_key(public_key_hex);
|
||||
assert_eq!(filtered.len(), 1);
|
||||
|
||||
let filtered_other = client.get_requests_for_public_key(other_public_key);
|
||||
assert_eq!(filtered_other.len(), 1);
|
||||
|
||||
// Remove a request
|
||||
let removed = client.remove_pending_request("test-1");
|
||||
assert!(removed.is_some());
|
||||
assert_eq!(removed.unwrap().id(), "test-1");
|
||||
assert_eq!(client.pending_request_count(), 1);
|
||||
|
||||
// Clear all requests
|
||||
client.clear_pending_requests();
|
||||
assert_eq!(client.pending_request_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_request_validation() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// Valid request
|
||||
let valid_request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl");
|
||||
assert!(client.can_handle_request_for_key(&valid_request, "some-public-key"));
|
||||
|
||||
// Invalid request - empty ID
|
||||
let invalid_request = SignRequest::new("", "dGVzdCBtZXNzYWdl");
|
||||
assert!(!client.can_handle_request_for_key(&invalid_request, "some-public-key"));
|
||||
|
||||
// Invalid request - empty message
|
||||
let invalid_request2 = SignRequest::new("test-1", "");
|
||||
assert!(!client.can_handle_request_for_key(&invalid_request2, "some-public-key"));
|
||||
|
||||
// Invalid request - invalid base64
|
||||
let invalid_request3 = SignRequest::new("test-1", "invalid-base64!");
|
||||
assert!(!client.can_handle_request_for_key(&invalid_request3, "some-public-key"));
|
||||
|
||||
// Invalid public key
|
||||
assert!(!client.can_handle_request_for_key(&valid_request, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_connection_state() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
|
||||
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// Initially disconnected
|
||||
assert_eq!(client.state(), ConnectionState::Disconnected);
|
||||
assert!(!client.is_connected());
|
||||
assert!(client.connected_public_key().is_none());
|
||||
|
||||
// Public key should be available
|
||||
assert_eq!(client.public_key_hex(), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9");
|
||||
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||
}
|
||||
181
sigsocket_client/tests/wasm_tests.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
//! WASM/browser tests for sigsocket_client using wasm-bindgen-test
|
||||
|
||||
use wasm_bindgen_test::*;
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
/// Test sign request handler for WASM tests
|
||||
struct TestWasmSignHandler {
|
||||
should_approve: bool,
|
||||
}
|
||||
|
||||
impl TestWasmSignHandler {
|
||||
fn new(should_approve: bool) -> Self {
|
||||
Self { should_approve }
|
||||
}
|
||||
}
|
||||
|
||||
impl SignRequestHandler for TestWasmSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
if self.should_approve {
|
||||
// Create a test signature
|
||||
let signature = format!("wasm_test_signature_for_{}", request.id);
|
||||
Ok(signature.into_bytes())
|
||||
} else {
|
||||
Err(SigSocketError::Other("User rejected request in WASM test".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_request_creation_wasm() {
|
||||
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(request.id, "wasm-test-123");
|
||||
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_request_message_decoding_wasm() {
|
||||
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
|
||||
|
||||
let bytes = request.message_bytes().unwrap();
|
||||
assert_eq!(bytes, b"test message");
|
||||
|
||||
let hex = request.message_hex().unwrap();
|
||||
assert_eq!(hex, hex::encode(b"test message"));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_response_creation_wasm() {
|
||||
let response = SignResponse::new("wasm-test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
|
||||
assert_eq!(response.id, "wasm-test-123");
|
||||
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
|
||||
assert_eq!(response.signature, "c2lnbmF0dXJl");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_response_from_request_wasm() {
|
||||
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
|
||||
let signature = b"wasm_test_signature";
|
||||
|
||||
let response = SignResponse::from_request_and_signature(&request, signature);
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_protocol_serialization_wasm() {
|
||||
// Test SignRequest serialization
|
||||
let request = SignRequest::new("wasm-req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(request, deserialized);
|
||||
|
||||
// Test SignResponse serialization
|
||||
let response = SignResponse::new("wasm-req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
|
||||
let json = serde_json::to_string(&response).unwrap();
|
||||
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(response, deserialized);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_client_creation_wasm() {
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
|
||||
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
|
||||
assert_eq!(client.url(), "ws://localhost:8080/ws");
|
||||
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
|
||||
assert!(!client.is_connected());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_client_invalid_url_wasm() {
|
||||
let public_key = vec![1, 2, 3];
|
||||
let result = SigSocketClient::new("invalid-url", public_key);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_client_empty_public_key_wasm() {
|
||||
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
|
||||
assert!(result.is_err());
|
||||
if let Err(error) = result {
|
||||
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_handler_approval_wasm() {
|
||||
let handler = TestWasmSignHandler::new(true);
|
||||
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let signature = result.unwrap();
|
||||
assert_eq!(signature, b"wasm_test_signature_for_wasm-test-789");
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_sign_handler_rejection_wasm() {
|
||||
let handler = TestWasmSignHandler::new(false);
|
||||
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
|
||||
|
||||
let result = handler.handle_sign_request(&request);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_error_display_wasm() {
|
||||
let error = SigSocketError::NotConnected;
|
||||
assert_eq!(error.to_string(), "Client is not connected");
|
||||
|
||||
let error = SigSocketError::Connection("wasm test error".to_string());
|
||||
assert_eq!(error.to_string(), "Connection error: wasm test error");
|
||||
}
|
||||
|
||||
// Test that demonstrates the expected WASM usage pattern
|
||||
#[wasm_bindgen_test]
|
||||
fn test_wasm_usage_pattern() {
|
||||
// 1. Create client
|
||||
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
|
||||
.unwrap();
|
||||
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
|
||||
|
||||
// 2. Set handler
|
||||
client.set_sign_handler(TestWasmSignHandler::new(true));
|
||||
|
||||
// 3. Verify state
|
||||
assert!(!client.is_connected());
|
||||
|
||||
// 4. Create a test request/response cycle
|
||||
let request = SignRequest::new("wasm-test-request", "dGVzdCBtZXNzYWdl");
|
||||
let handler = TestWasmSignHandler::new(true);
|
||||
let signature = handler.handle_sign_request(&request).unwrap();
|
||||
let response = SignResponse::from_request_and_signature(&request, &signature);
|
||||
|
||||
// 5. Verify the response
|
||||
assert_eq!(response.id, request.id);
|
||||
assert_eq!(response.message, request.message);
|
||||
assert_eq!(response.signature_bytes().unwrap(), signature);
|
||||
}
|
||||
|
||||
// Test WASM-specific console logging (if needed)
|
||||
#[wasm_bindgen_test]
|
||||
fn test_wasm_console_logging() {
|
||||
// This test verifies that WASM console logging works
|
||||
web_sys::console::log_1(&"SigSocket WASM test logging works!".into());
|
||||
|
||||
// Test that we can create and log protocol messages
|
||||
let request = SignRequest::new("log-test", "dGVzdA==");
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
web_sys::console::log_1(&format!("Sign request JSON: {}", json).into());
|
||||
|
||||
// This test always passes - it's just for verification that logging works
|
||||
assert!(true);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
once_cell = "1.18"
|
||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||
kvstore = { path = "../kvstore" }
|
||||
@@ -34,6 +35,7 @@ rhai = "1.21.0"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] }
|
||||
wasm-bindgen-test = "0.3"
|
||||
# console_error_panic_hook = "0.1"
|
||||
|
||||
|
||||
@@ -11,8 +11,15 @@ use rand_core::{RngCore, OsRng as RandOsRng};
|
||||
pub mod kdf {
|
||||
use super::*;
|
||||
|
||||
|
||||
/// Standard parameters for keyspace key derivation
|
||||
pub const KEYSPACE_KEY_LENGTH: usize = 32;
|
||||
pub const KEYSPACE_KEY_ITERATIONS: u32 = 10_000;
|
||||
|
||||
/// Derive a symmetric key for keyspace operations using standard parameters
|
||||
/// Always uses PBKDF2 with SHA-256, 32 bytes output, and 10,000 iterations
|
||||
pub fn keyspace_key(password: &[u8], salt: &[u8]) -> Vec<u8> {
|
||||
derive_key_pbkdf2(password, salt, KEYSPACE_KEY_LENGTH, KEYSPACE_KEY_ITERATIONS)
|
||||
}
|
||||
|
||||
pub fn derive_key_pbkdf2(password: &[u8], salt: &[u8], key_len: usize, iterations: u32) -> Vec<u8> {
|
||||
let mut key = vec![0u8; key_len];
|
||||
|
||||
598
vault/src/lib.rs
@@ -1,34 +1,31 @@
|
||||
//! vault: Cryptographic keyspace and operations
|
||||
|
||||
|
||||
//! vault: Cryptographic keyspace and operations
|
||||
|
||||
pub mod data;
|
||||
pub use crate::data::{KeyEntry, KeyMetadata, KeyType};
|
||||
pub use crate::session::SessionManager;
|
||||
pub use crate::data::{KeyType, KeyMetadata, KeyEntry};
|
||||
mod error;
|
||||
mod crypto;
|
||||
mod error;
|
||||
pub mod rhai_bindings;
|
||||
mod rhai_sync_helpers;
|
||||
pub mod session;
|
||||
mod utils;
|
||||
mod rhai_sync_helpers;
|
||||
pub mod rhai_bindings;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod session_singleton;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm_helpers;
|
||||
|
||||
|
||||
pub use kvstore::traits::KVStore;
|
||||
use crate::crypto::kdf;
|
||||
use crate::crypto::random_salt;
|
||||
use data::*;
|
||||
use error::VaultError;
|
||||
use crate::crypto::random_salt;
|
||||
use crate::crypto::kdf;
|
||||
pub use kvstore::traits::KVStore;
|
||||
|
||||
use crate::crypto::cipher::{encrypt_chacha20, decrypt_chacha20};
|
||||
use signature::SignatureEncoding;
|
||||
use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
|
||||
// TEMP: File-based debug logger for crypto troubleshooting
|
||||
use log::{debug};
|
||||
use log::debug;
|
||||
|
||||
/// Vault: Cryptographic keyspace and operations
|
||||
pub struct Vault<S: KVStore> {
|
||||
@@ -43,8 +40,7 @@ fn encrypt_with_nonce_prepended(key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>,
|
||||
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))?;
|
||||
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();
|
||||
@@ -60,23 +56,34 @@ impl<S: KVStore> Vault<S> {
|
||||
|
||||
/// 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> {
|
||||
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() {
|
||||
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 crate::crypto::{kdf, random_salt};
|
||||
use crate::data::{KeyspaceData, KeyspaceMetadata};
|
||||
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);
|
||||
let key = kdf::keyspace_key(password, &salt);
|
||||
debug!("derived key: {} bytes", key.len());
|
||||
// 3. Prepare initial keyspace data
|
||||
let keyspace_data = KeyspaceData { keypairs: vec![] };
|
||||
@@ -99,7 +106,7 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio
|
||||
// 6. Compose metadata
|
||||
let metadata = KeyspaceMetadata {
|
||||
name: name.to_string(),
|
||||
salt: salt.try_into().unwrap_or([0u8; 16]),
|
||||
salt: salt.clone().try_into().unwrap_or([0u8; 16]),
|
||||
encrypted_blob,
|
||||
created_at: Some(crate::utils::now()),
|
||||
tags,
|
||||
@@ -112,8 +119,15 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio
|
||||
return Err(VaultError::Serialization(e.to_string()));
|
||||
}
|
||||
};
|
||||
self.storage.set(name, &meta_bytes).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||
self.storage
|
||||
.set(name, &meta_bytes)
|
||||
.await
|
||||
.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||
debug!("success");
|
||||
|
||||
// 8. Create default keypair, passing the salt we already have
|
||||
self.create_default_keypair(name, password, &salt).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -121,10 +135,19 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio
|
||||
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 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 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);
|
||||
}
|
||||
@@ -136,31 +159,42 @@ pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Optio
|
||||
/// 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> {
|
||||
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 = 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()))?;
|
||||
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()));
|
||||
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);
|
||||
let key = kdf::keyspace_key(password, &metadata.salt);
|
||||
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!("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) {
|
||||
@@ -182,10 +216,72 @@ let plaintext = decrypt_chacha20(&key, ct, nonce).map_err(VaultError::Crypto)?;
|
||||
|
||||
// --- Keypair Management APIs ---
|
||||
|
||||
/// Create a default Secp256k1 keypair for client identity
|
||||
/// This keypair is deterministically generated from the password and salt
|
||||
/// and will always be the first keypair in the keyspace
|
||||
async fn create_default_keypair(
|
||||
&mut self,
|
||||
keyspace: &str,
|
||||
password: &[u8],
|
||||
salt: &[u8],
|
||||
) -> Result<String, VaultError> {
|
||||
// 1. Derive a deterministic seed using standard PBKDF2
|
||||
let seed = kdf::keyspace_key(password, salt);
|
||||
|
||||
// 2. Generate Secp256k1 keypair from the seed
|
||||
use k256::ecdsa::{SigningKey, VerifyingKey};
|
||||
|
||||
// Use the seed as the private key directly (32 bytes)
|
||||
let mut secret_key_bytes = [0u8; 32];
|
||||
secret_key_bytes.copy_from_slice(&seed[..32]);
|
||||
|
||||
// Create signing key
|
||||
let signing_key = SigningKey::from_bytes(&secret_key_bytes.into())
|
||||
.map_err(|e| VaultError::Crypto(format!("Failed to create signing key: {}", e)))?;
|
||||
|
||||
// Get verifying key
|
||||
let verifying_key = VerifyingKey::from(&signing_key);
|
||||
|
||||
// Convert keys to bytes
|
||||
let priv_bytes = signing_key.to_bytes().to_vec();
|
||||
let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
|
||||
let id = hex::encode(&pub_bytes);
|
||||
|
||||
// 3. Unlock keyspace to add the keypair
|
||||
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
||||
|
||||
// 4. Create key entry
|
||||
let entry = KeyEntry {
|
||||
id: id.clone(),
|
||||
key_type: KeyType::Secp256k1,
|
||||
private_key: priv_bytes,
|
||||
public_key: pub_bytes,
|
||||
metadata: Some(KeyMetadata {
|
||||
name: Some("Default Identity".to_string()),
|
||||
created_at: Some(crate::utils::now()),
|
||||
tags: Some(vec!["default".to_string(), "identity".to_string()]),
|
||||
}),
|
||||
};
|
||||
|
||||
// Ensure it's the first keypair by inserting at index 0
|
||||
data.keypairs.insert(0, entry);
|
||||
|
||||
// 5. Re-encrypt and store
|
||||
self.save_keyspace(keyspace, password, &data).await?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
/// 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;
|
||||
@@ -194,7 +290,7 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
|
||||
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 {
|
||||
let (private_key, public_key, id) = match key_type {
|
||||
KeyType::Ed25519 => {
|
||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||
let mut bytes = [0u8; 32];
|
||||
@@ -205,7 +301,7 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
|
||||
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;
|
||||
|
||||
@@ -215,7 +311,7 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
|
||||
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 {
|
||||
@@ -232,190 +328,296 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
|
||||
}
|
||||
|
||||
/// Remove a keypair by id from a keyspace
|
||||
pub async fn remove_keypair(&mut self, keyspace: &str, password: &[u8], key_id: &str) -> Result<(), VaultError> {
|
||||
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> {
|
||||
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())
|
||||
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> {
|
||||
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()))?;
|
||||
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> {
|
||||
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()));
|
||||
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::keyspace_key(password, &metadata.salt);
|
||||
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(())
|
||||
}
|
||||
// 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())
|
||||
/// 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::{Signer, SigningKey};
|
||||
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::{signature::Signer, SigningKey, Signature};
|
||||
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: Signature = sk.sign(message);
|
||||
// Return compact signature (64 bytes) instead of DER format
|
||||
Ok(sig.to_bytes().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())
|
||||
/// 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::{Signature, Verifier, VerifyingKey};
|
||||
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::{signature::Verifier, Signature, VerifyingKey};
|
||||
let pk = VerifyingKey::from_sec1_bytes(&key.public_key)
|
||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||
// Use compact format (64 bytes) instead of DER
|
||||
let sig_array: &[u8; 64] = signature.try_into().map_err(|_| {
|
||||
VaultError::Crypto("Invalid secp256k1 signature length".to_string())
|
||||
})?;
|
||||
let sig = Signature::from_bytes(sig_array.into())
|
||||
.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::keyspace_key(password, &meta.salt);
|
||||
// 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::keyspace_key(password, &meta.salt);
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,11 @@ use crate::session::SessionManager;
|
||||
#[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>>>,
|
||||
_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("select_default_keypair", RhaiSessionManager::<S>::select_default_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.
|
||||
@@ -36,6 +37,11 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan
|
||||
// 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 select_default_keypair(&self) -> Result<(), String> {
|
||||
self.inner.lock().unwrap().select_default_keypair()
|
||||
.map_err(|e| format!("select_default_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
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
//! 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> {
|
||||
// ... existing fields
|
||||
|
||||
vault: Vault<S>,
|
||||
unlocked_keyspace: Option<(String, Vec<u8>, KeyspaceData)>, // (name, password, data)
|
||||
current_keypair: Option<String>,
|
||||
@@ -38,7 +39,12 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
|
||||
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
|
||||
}
|
||||
@@ -90,12 +96,58 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
|
||||
self.unlocked_keyspace.as_ref().map(|(_, _, data)| data)
|
||||
}
|
||||
|
||||
/// Returns the name of the currently unlocked keyspace, if any.
|
||||
pub fn current_keyspace_name(&self) -> Option<&str> {
|
||||
self.unlocked_keyspace
|
||||
.as_ref()
|
||||
.map(|(name, _, _)| name.as_str())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns the metadata of the current selected keypair, if any.
|
||||
pub fn current_keypair_metadata(&self) -> Option<crate::KeyMetadata> {
|
||||
self.current_keypair().and_then(|k| k.metadata.clone())
|
||||
}
|
||||
|
||||
/// Returns the public key of the current selected keypair, if any.
|
||||
pub fn current_keypair_public_key(&self) -> Option<Vec<u8>> {
|
||||
self.current_keypair().map(|k| k.public_key.clone())
|
||||
}
|
||||
|
||||
/// Returns true if a keyspace is currently unlocked.
|
||||
pub fn is_unlocked(&self) -> bool {
|
||||
self.unlocked_keyspace.is_some()
|
||||
}
|
||||
|
||||
/// Returns the default keypair (first keypair) for client identity, if any.
|
||||
pub fn default_keypair(&self) -> Option<&KeyEntry> {
|
||||
self.current_keyspace()
|
||||
.and_then(|ks| ks.keypairs.first())
|
||||
}
|
||||
|
||||
/// Selects the default keypair (first keypair) as the current keypair.
|
||||
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
|
||||
let default_id = self
|
||||
.default_keypair()
|
||||
.map(|k| k.id.clone())
|
||||
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
|
||||
|
||||
self.select_keypair(&default_id)
|
||||
}
|
||||
|
||||
/// Returns true if the current keypair is the default keypair (first keypair).
|
||||
pub fn is_default_keypair_selected(&self) -> bool {
|
||||
match (self.current_keypair(), self.default_keypair()) {
|
||||
(Some(current), Some(default)) => current.id == default.id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||
let (name, password, _) = self
|
||||
.unlocked_keyspace
|
||||
@@ -107,6 +159,38 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
|
||||
self.vault.sign(name, password, &keypair.id, message).await
|
||||
}
|
||||
|
||||
/// Verify a signature using the currently selected keypair
|
||||
pub async fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, 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.verify(name, password, &keypair.id, message, signature).await
|
||||
}
|
||||
|
||||
/// Encrypt data using the keyspace symmetric cipher
|
||||
/// Returns the encrypted data with the nonce prepended
|
||||
pub async fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||
let (name, password, _) = self
|
||||
.unlocked_keyspace
|
||||
.as_ref()
|
||||
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||
self.vault.encrypt(name, password, plaintext).await
|
||||
}
|
||||
|
||||
/// Decrypt data using the keyspace symmetric cipher
|
||||
/// Expects the nonce to be prepended to the ciphertext (as returned by encrypt)
|
||||
pub async fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||
let (name, password, _) = self
|
||||
.unlocked_keyspace
|
||||
.as_ref()
|
||||
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||
self.vault.decrypt(name, password, ciphertext).await
|
||||
}
|
||||
|
||||
pub fn get_vault(&self) -> &Vault<S> {
|
||||
&self.vault
|
||||
}
|
||||
@@ -137,7 +221,12 @@ impl<S: KVStore> SessionManager<S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_keyspace(&mut self, name: &str, password: &[u8], tags: Option<Vec<String>>) -> Result<(), VaultError> {
|
||||
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
|
||||
}
|
||||
@@ -189,12 +278,58 @@ impl<S: KVStore> SessionManager<S> {
|
||||
self.unlocked_keyspace.as_ref().map(|(_, _, data)| data)
|
||||
}
|
||||
|
||||
/// Returns the name of the currently unlocked keyspace, if any.
|
||||
pub fn current_keyspace_name(&self) -> Option<&str> {
|
||||
self.unlocked_keyspace
|
||||
.as_ref()
|
||||
.map(|(name, _, _)| name.as_str())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Returns the metadata of the current selected keypair, if any.
|
||||
pub fn current_keypair_metadata(&self) -> Option<crate::KeyMetadata> {
|
||||
self.current_keypair().and_then(|k| k.metadata.clone())
|
||||
}
|
||||
|
||||
/// Returns the public key of the current selected keypair, if any.
|
||||
pub fn current_keypair_public_key(&self) -> Option<Vec<u8>> {
|
||||
self.current_keypair().map(|k| k.public_key.clone())
|
||||
}
|
||||
|
||||
/// Returns true if a keyspace is currently unlocked.
|
||||
pub fn is_unlocked(&self) -> bool {
|
||||
self.unlocked_keyspace.is_some()
|
||||
}
|
||||
|
||||
/// Returns the default keypair (first keypair) for client identity, if any.
|
||||
pub fn default_keypair(&self) -> Option<&KeyEntry> {
|
||||
self.current_keyspace()
|
||||
.and_then(|ks| ks.keypairs.first())
|
||||
}
|
||||
|
||||
/// Selects the default keypair (first keypair) as the current keypair.
|
||||
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
|
||||
let default_id = self
|
||||
.default_keypair()
|
||||
.map(|k| k.id.clone())
|
||||
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
|
||||
|
||||
self.select_keypair(&default_id)
|
||||
}
|
||||
|
||||
/// Returns true if the current keypair is the default keypair (first keypair).
|
||||
pub fn is_default_keypair_selected(&self) -> bool {
|
||||
match (self.current_keypair(), self.default_keypair()) {
|
||||
(Some(current), Some(default)) => current.id == default.id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||
let (name, password, _) = self
|
||||
.unlocked_keyspace
|
||||
@@ -206,6 +341,38 @@ impl<S: KVStore> SessionManager<S> {
|
||||
self.vault.sign(name, password, &keypair.id, message).await
|
||||
}
|
||||
|
||||
/// Verify a signature using the currently selected keypair
|
||||
pub async fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, 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.verify(name, password, &keypair.id, message, signature).await
|
||||
}
|
||||
|
||||
/// Encrypt data using the keyspace symmetric cipher
|
||||
/// Returns the encrypted data with the nonce prepended
|
||||
pub async fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||
let (name, password, _) = self
|
||||
.unlocked_keyspace
|
||||
.as_ref()
|
||||
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||
self.vault.encrypt(name, password, plaintext).await
|
||||
}
|
||||
|
||||
/// Decrypt data using the keyspace symmetric cipher
|
||||
/// Expects the nonce to be prepended to the ciphertext (as returned by encrypt)
|
||||
pub async fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> {
|
||||
let (name, password, _) = self
|
||||
.unlocked_keyspace
|
||||
.as_ref()
|
||||
.ok_or(VaultError::Crypto("No keyspace unlocked".to_string()))?;
|
||||
self.vault.decrypt(name, password, ciphertext).await
|
||||
}
|
||||
|
||||
pub fn get_vault(&self) -> &Vault<S> {
|
||||
&self.vault
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ async fn test_keypair_management_and_crypto() {
|
||||
vault.create_keyspace(keyspace, password, None).await.unwrap();
|
||||
|
||||
debug!("after create_keyspace: keyspace={} password={}", keyspace, hex::encode(password));
|
||||
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||
assert_eq!(keys.len(), 1); // should be 1 because we added a default keypair on create_keyspace
|
||||
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 {
|
||||
@@ -38,7 +40,7 @@ async fn test_keypair_management_and_crypto() {
|
||||
|
||||
debug!("before list_keypairs");
|
||||
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert_eq!(keys.len(), 3);
|
||||
|
||||
debug!("before export Ed25519 keypair");
|
||||
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
|
||||
@@ -65,5 +67,5 @@ async fn test_keypair_management_and_crypto() {
|
||||
// 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);
|
||||
assert_eq!(keys.len(), 2);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 vault = Vault::new(store);
|
||||
let keyspace = "personal";
|
||||
let password = b"testpass";
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@ async fn test_session_manager_lock_unlock_keypairs_persistence() {
|
||||
|
||||
// 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_eq!(keypairs_before.len(), 3);
|
||||
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")));
|
||||
|
||||
|
||||
@@ -7,13 +7,16 @@ edition = "2021"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
web-sys = { version = "0.3", features = ["console"] }
|
||||
js-sys = "0.3"
|
||||
kvstore = { path = "../kvstore" }
|
||||
hex = "0.4"
|
||||
base64 = "0.22"
|
||||
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"] }
|
||||
@@ -21,6 +24,7 @@ wasm-bindgen-futures = "0.4"
|
||||
once_cell = "1.21"
|
||||
vault = { path = "../vault" }
|
||||
evm_client = { path = "../evm_client" }
|
||||
sigsocket_client = { path = "../sigsocket_client" }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
@@ -24,8 +24,13 @@ pub use vault::session_singleton::SESSION_MANAGER;
|
||||
|
||||
// Include the keypair bindings module
|
||||
mod vault_bindings;
|
||||
mod sigsocket_bindings;
|
||||
pub use vault_bindings::*;
|
||||
|
||||
// Include the sigsocket module
|
||||
mod sigsocket;
|
||||
pub use sigsocket::*;
|
||||
|
||||
/// Initialize the scripting environment (must be called before run_rhai)
|
||||
#[wasm_bindgen]
|
||||
pub fn init_rhai_env() {
|
||||
|
||||
168
wasm_app/src/sigsocket/connection.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! SigSocket connection wrapper for WASM
|
||||
//!
|
||||
//! This module provides a WASM-bindgen compatible wrapper around the
|
||||
//! SigSocket client that can be used from JavaScript in the browser extension.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use sigsocket_client::{SigSocketClient, SignResponse};
|
||||
use crate::sigsocket::handler::JavaScriptSignHandler;
|
||||
|
||||
/// WASM-bindgen wrapper for SigSocket client
|
||||
///
|
||||
/// This provides a clean JavaScript API for the browser extension to:
|
||||
/// - Connect to SigSocket servers
|
||||
/// - Send responses to sign requests
|
||||
/// - Manage connection state
|
||||
#[wasm_bindgen]
|
||||
pub struct SigSocketConnection {
|
||||
client: Option<SigSocketClient>,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SigSocketConnection {
|
||||
/// Create a new SigSocket connection
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client: None,
|
||||
connected: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to a SigSocket server
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
/// * `public_key_hex` - Client's public key as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Successfully connected
|
||||
/// * `Err(error)` - Connection failed
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect(&mut self, server_url: &str, public_key_hex: &str) -> Result<(), JsValue> {
|
||||
web_sys::console::log_1(&format!("SigSocketConnection::connect called with URL: {}", server_url).into());
|
||||
web_sys::console::log_1(&format!("Public key (first 16 chars): {}", &public_key_hex[..16]).into());
|
||||
|
||||
// Decode public key from hex
|
||||
let public_key = hex::decode(public_key_hex)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid public key hex: {}", e)))?;
|
||||
|
||||
web_sys::console::log_1(&"Creating SigSocketClient...".into());
|
||||
|
||||
// Create client
|
||||
let mut client = SigSocketClient::new(server_url, public_key)
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to create client: {}", e)))?;
|
||||
|
||||
web_sys::console::log_1(&"SigSocketClient created, attempting connection...".into());
|
||||
|
||||
// Set up JavaScript handler
|
||||
client.set_sign_handler(JavaScriptSignHandler);
|
||||
|
||||
// Connect to server
|
||||
web_sys::console::log_1(&"Calling client.connect()...".into());
|
||||
client.connect().await
|
||||
.map_err(|e| {
|
||||
web_sys::console::error_1(&format!("Client connection failed: {}", e).into());
|
||||
JsValue::from_str(&format!("Failed to connect: {}", e))
|
||||
})?;
|
||||
|
||||
web_sys::console::log_1(&"Client connection successful!".into());
|
||||
|
||||
self.client = Some(client);
|
||||
self.connected = true;
|
||||
|
||||
web_sys::console::log_1(&"SigSocketConnection state updated to connected".into());
|
||||
|
||||
// Notify JavaScript of connection state change
|
||||
super::handler::on_connection_state_changed(true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a response to a sign request
|
||||
///
|
||||
/// This should be called by the extension after the user has approved
|
||||
/// a sign request and the message has been signed.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - ID of the original request
|
||||
/// * `message_base64` - Original message (base64-encoded)
|
||||
/// * `signature_hex` - Signature as hex string
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Response sent successfully
|
||||
/// * `Err(error)` - Failed to send response
|
||||
#[wasm_bindgen]
|
||||
pub async fn send_response(&self, request_id: &str, message_base64: &str, signature_hex: &str) -> Result<(), JsValue> {
|
||||
let client = self.client.as_ref()
|
||||
.ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||
|
||||
// Decode signature from hex
|
||||
let signature = hex::decode(signature_hex)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid signature hex: {}", e)))?;
|
||||
|
||||
// Create response
|
||||
let response = SignResponse::new(request_id, message_base64,
|
||||
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature));
|
||||
|
||||
// Send response
|
||||
client.send_sign_response(&response).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to send response: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a rejection for a sign request
|
||||
///
|
||||
/// This should be called when the user rejects a sign request.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - ID of the request to reject
|
||||
/// * `reason` - Reason for rejection (optional)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Rejection sent successfully
|
||||
/// * `Err(error)` - Failed to send rejection
|
||||
#[wasm_bindgen]
|
||||
pub async fn send_rejection(&self, request_id: &str, reason: &str) -> Result<(), JsValue> {
|
||||
// For now, we'll just log the rejection
|
||||
// In a full implementation, the server might support rejection messages
|
||||
web_sys::console::log_1(&format!("Sign request {} rejected: {}", request_id, reason).into());
|
||||
|
||||
// TODO: If the server supports rejection messages, send them here
|
||||
// For now, we just ignore the request (timeout on server side)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disconnect from the SigSocket server
|
||||
#[wasm_bindgen]
|
||||
pub fn disconnect(&mut self) {
|
||||
if let Some(_client) = self.client.take() {
|
||||
// Note: We can't await in a non-async function, so we'll just drop the client
|
||||
// The Drop implementation should handle cleanup
|
||||
self.connected = false;
|
||||
|
||||
// Notify JavaScript of connection state change
|
||||
super::handler::on_connection_state_changed(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if connected to the server
|
||||
#[wasm_bindgen]
|
||||
pub fn is_connected(&self) -> bool {
|
||||
// Check if we have a client and if it reports as connected
|
||||
if let Some(ref client) = self.client {
|
||||
client.is_connected()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SigSocketConnection {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
51
wasm_app/src/sigsocket/handler.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! JavaScript bridge handler for SigSocket sign requests
|
||||
//!
|
||||
//! This module provides a sign request handler that delegates to JavaScript
|
||||
//! callbacks, allowing the browser extension to handle the actual signing
|
||||
//! and user approval flow.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use sigsocket_client::{SignRequest, SignRequestHandler, Result, SigSocketError};
|
||||
|
||||
/// JavaScript sign handler that delegates to extension
|
||||
///
|
||||
/// This handler receives sign requests from the SigSocket server and
|
||||
/// calls JavaScript callbacks to notify the extension. The extension
|
||||
/// handles the user approval flow and signing, then responds via
|
||||
/// the SigSocketConnection.send_response() method.
|
||||
pub struct JavaScriptSignHandler;
|
||||
|
||||
impl SignRequestHandler for JavaScriptSignHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
|
||||
// Call JavaScript callback to notify extension of incoming request
|
||||
on_sign_request_received(&request.id, &request.message);
|
||||
|
||||
// Return error - JavaScript handles response via send_response()
|
||||
// This is intentional as the signing happens asynchronously in the extension
|
||||
Err(SigSocketError::Other("Handled by JavaScript extension".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// External JavaScript functions that the extension must implement
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// Called when a sign request is received from the server
|
||||
///
|
||||
/// The extension should:
|
||||
/// 1. Store the request details
|
||||
/// 2. Show notification/badge to user
|
||||
/// 3. Handle user approval flow when popup is opened
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - Unique identifier for the request
|
||||
/// * `message_base64` - Message to be signed (base64-encoded)
|
||||
#[wasm_bindgen(js_name = "onSignRequestReceived")]
|
||||
pub fn on_sign_request_received(request_id: &str, message_base64: &str);
|
||||
|
||||
/// Called when connection state changes
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `connected` - True if connected, false if disconnected
|
||||
#[wasm_bindgen(js_name = "onConnectionStateChanged")]
|
||||
pub fn on_connection_state_changed(connected: bool);
|
||||
}
|
||||
11
wasm_app/src/sigsocket/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
//! SigSocket integration module for WASM app
|
||||
//!
|
||||
//! This module provides a clean transport API for SigSocket communication
|
||||
//! that can be used by the browser extension. It handles connection management
|
||||
//! and delegates signing to the extension through JavaScript callbacks.
|
||||
|
||||
pub mod connection;
|
||||
pub mod handler;
|
||||
|
||||
pub use connection::SigSocketConnection;
|
||||
pub use handler::JavaScriptSignHandler;
|
||||
528
wasm_app/src/sigsocket_bindings.rs
Normal file
@@ -0,0 +1,528 @@
|
||||
//! SigSocket bindings for WASM - integrates sigsocket_client with vault system
|
||||
|
||||
use std::cell::RefCell;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result as SigSocketResult, SigSocketError};
|
||||
use web_sys::console;
|
||||
use base64::prelude::*;
|
||||
|
||||
use crate::vault_bindings::{get_workspace_default_public_key, get_current_keyspace_name, is_unlocked, sign_with_default_keypair};
|
||||
|
||||
// Global SigSocket client instance
|
||||
thread_local! {
|
||||
static SIGSOCKET_CLIENT: RefCell<Option<SigSocketClient>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
// Helper macro for console logging
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (console::log_1(&format!($($t)*).into()))
|
||||
}
|
||||
|
||||
/// Extension notification handler that forwards requests to JavaScript
|
||||
pub struct ExtensionNotificationHandler {
|
||||
callback: js_sys::Function,
|
||||
}
|
||||
|
||||
impl ExtensionNotificationHandler {
|
||||
pub fn new(callback: js_sys::Function) -> Self {
|
||||
Self { callback }
|
||||
}
|
||||
}
|
||||
|
||||
impl SignRequestHandler for ExtensionNotificationHandler {
|
||||
fn handle_sign_request(&self, request: &SignRequest) -> SigSocketResult<Vec<u8>> {
|
||||
console_log!("📨 WASM: Handling sign request: {}", request.id);
|
||||
|
||||
// First, store the request in the WASM client
|
||||
let store_result = SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client_opt = c.borrow_mut();
|
||||
if let Some(client) = client_opt.as_mut() {
|
||||
// Get the connected public key as the target
|
||||
if let Some(target_public_key) = client.connected_public_key() {
|
||||
client.add_pending_request(request.clone(), target_public_key.to_string());
|
||||
console_log!("✅ WASM: Stored sign request: {}", request.id);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SigSocketError::Other("No connected public key".to_string()))
|
||||
}
|
||||
} else {
|
||||
Err(SigSocketError::Other("No SigSocket client available".to_string()))
|
||||
}
|
||||
});
|
||||
|
||||
// If storage failed, return error
|
||||
if let Err(e) = store_result {
|
||||
console_log!("❌ WASM: Failed to store request: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Create event object for JavaScript notification
|
||||
let event = js_sys::Object::new();
|
||||
js_sys::Reflect::set(&event, &"type".into(), &"sign_request".into())
|
||||
.map_err(|_| SigSocketError::Other("Failed to set event type".to_string()))?;
|
||||
js_sys::Reflect::set(&event, &"request_id".into(), &request.id.clone().into())
|
||||
.map_err(|_| SigSocketError::Other("Failed to set request_id".to_string()))?;
|
||||
js_sys::Reflect::set(&event, &"message".into(), &request.message.clone().into())
|
||||
.map_err(|_| SigSocketError::Other("Failed to set message".to_string()))?;
|
||||
|
||||
// Notify the extension
|
||||
match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) {
|
||||
Ok(_) => {
|
||||
console_log!("✅ WASM: Notified extension about sign request: {}", request.id);
|
||||
// Return an error to indicate this request should not be auto-signed
|
||||
// The extension will handle the approval flow
|
||||
Err(SigSocketError::Other("Request forwarded to extension for approval".to_string()))
|
||||
}
|
||||
Err(e) => {
|
||||
console_log!("❌ WASM: Failed to notify extension: {:?}", e);
|
||||
Err(SigSocketError::Other("Extension notification failed".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection information for SigSocket
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SigSocketConnectionInfo {
|
||||
pub workspace: String,
|
||||
pub public_key: String,
|
||||
pub is_connected: bool,
|
||||
pub server_url: String,
|
||||
}
|
||||
|
||||
/// SigSocket manager for high-level operations
|
||||
#[wasm_bindgen]
|
||||
pub struct SigSocketManager;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SigSocketManager {
|
||||
/// Connect to SigSocket server with smart connection management
|
||||
///
|
||||
/// This handles all connection logic:
|
||||
/// - Reuses existing connection if same workspace
|
||||
/// - Switches connection if different workspace
|
||||
/// - Creates new connection if none exists
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `workspace` - The workspace name to connect with
|
||||
/// * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
/// * `event_callback` - JavaScript function to call when events occur
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(connection_info)` - JSON string with connection details
|
||||
/// * `Err(error)` - If connection failed or workspace is invalid
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect_workspace_with_events(workspace: &str, server_url: &str, event_callback: &js_sys::Function) -> Result<String, JsValue> {
|
||||
// 1. Validate workspace exists and get default public key from vault
|
||||
let public_key_js = get_workspace_default_public_key(workspace).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get workspace public key: {:?}", e)))?;
|
||||
|
||||
let public_key_hex = public_key_js.as_string()
|
||||
.ok_or_else(|| JsValue::from_str("Public key is not a string"))?;
|
||||
|
||||
// 2. Decode public key
|
||||
let public_key_bytes = hex::decode(&public_key_hex)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid public key format: {}", e)))?;
|
||||
|
||||
// 3. Check if already connected to same workspace and handle disconnection
|
||||
let should_connect = SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client_opt = c.borrow_mut();
|
||||
|
||||
// Check if we already have a client for this workspace
|
||||
if let Some(existing_client) = client_opt.as_ref() {
|
||||
if let Some(existing_key) = existing_client.connected_public_key() {
|
||||
if existing_key == hex::encode(&public_key_bytes) && existing_client.is_connected() {
|
||||
console_log!("🔄 WASM: Already connected to workspace: {}", workspace);
|
||||
return false; // Reuse existing connection
|
||||
} else {
|
||||
console_log!("🔄 WASM: Switching workspace from {} to {}",
|
||||
existing_key, hex::encode(&public_key_bytes));
|
||||
|
||||
// Disconnect the old client
|
||||
*client_opt = None; // This will drop the old client and close WebSocket
|
||||
console_log!("🔌 WASM: Disconnected from old workspace");
|
||||
|
||||
return true; // Need new connection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true // Need new connection, no old one to disconnect
|
||||
});
|
||||
|
||||
// 4. Create and connect if needed
|
||||
if should_connect {
|
||||
console_log!("🔗 WASM: Creating new connection for workspace: {}", workspace);
|
||||
|
||||
// Create new client
|
||||
let mut client = SigSocketClient::new(server_url, public_key_bytes.clone())
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to create client: {:?}", e)))?;
|
||||
|
||||
// Set up extension notification handler
|
||||
let handler = ExtensionNotificationHandler::new(event_callback.clone());
|
||||
client.set_sign_handler(handler);
|
||||
|
||||
// Connect to the WebSocket server
|
||||
client.connect().await
|
||||
.map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?;
|
||||
|
||||
console_log!("✅ WASM: Connected to SigSocket server for workspace: {}", workspace);
|
||||
|
||||
// Store the connected client
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
*c.borrow_mut() = Some(client);
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Return connection info
|
||||
let connection_info = SigSocketConnectionInfo {
|
||||
workspace: workspace.to_string(),
|
||||
public_key: public_key_hex.clone(),
|
||||
is_connected: true,
|
||||
server_url: server_url.to_string(),
|
||||
};
|
||||
|
||||
// 7. Serialize and return connection info
|
||||
serde_json::to_string(&connection_info)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||
}
|
||||
|
||||
/// Connect to SigSocket server with a specific workspace (backward compatibility)
|
||||
///
|
||||
/// This is a simpler version that doesn't set up event callbacks.
|
||||
/// Use connect_workspace_with_events for full functionality.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `workspace` - The workspace name to connect with
|
||||
/// * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws")
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(connection_info)` - JSON string with connection details
|
||||
/// * `Err(error)` - If connection failed or workspace is invalid
|
||||
#[wasm_bindgen]
|
||||
pub async fn connect_workspace(workspace: &str, server_url: &str) -> Result<String, JsValue> {
|
||||
// Create a dummy callback that just logs
|
||||
let dummy_callback = js_sys::Function::new_no_args("console.log('SigSocket event:', arguments[0]);");
|
||||
Self::connect_workspace_with_events(workspace, server_url, &dummy_callback).await
|
||||
}
|
||||
|
||||
/// Disconnect from SigSocket server
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Successfully disconnected
|
||||
/// * `Err(error)` - If disconnect failed
|
||||
#[wasm_bindgen]
|
||||
pub async fn disconnect() -> Result<(), JsValue> {
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client_opt = c.borrow_mut();
|
||||
if let Some(client) = client_opt.take() {
|
||||
let workspace_info = client.connected_public_key()
|
||||
.map(|key| key[..16].to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Dropping the client will close the WebSocket connection
|
||||
drop(client);
|
||||
console_log!("🔌 WASM: Disconnected SigSocket client (was: {}...)", workspace_info);
|
||||
} else {
|
||||
console_log!("🔌 WASM: No SigSocket client to disconnect");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if we can approve a specific sign request
|
||||
///
|
||||
/// This validates that:
|
||||
/// 1. The request exists
|
||||
/// 2. The vault session is unlocked
|
||||
/// 3. The current workspace matches the request's target
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request to validate
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(true)` - Request can be approved
|
||||
/// * `Ok(false)` - Request cannot be approved
|
||||
/// * `Err(error)` - Validation error
|
||||
#[wasm_bindgen]
|
||||
pub async fn can_approve_request(request_id: &str) -> Result<bool, JsValue> {
|
||||
// 1. Check if vault session is unlocked
|
||||
if !is_unlocked() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// 2. Get current workspace and its public key
|
||||
let current_workspace = get_current_keyspace_name()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?;
|
||||
|
||||
let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?;
|
||||
|
||||
let current_public_key = current_public_key_js.as_string()
|
||||
.ok_or_else(|| JsValue::from_str("Current public key is not a string"))?;
|
||||
|
||||
// 3. Check the request
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||
|
||||
// Get the request
|
||||
let request = client.get_pending_request(request_id)
|
||||
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||
|
||||
// Check if request matches current session
|
||||
let can_approve = request.target_public_key == current_public_key;
|
||||
|
||||
console_log!("Can approve request {}: {} (current: {}, target: {})",
|
||||
request_id, can_approve, current_public_key, request.target_public_key);
|
||||
|
||||
Ok(can_approve)
|
||||
})
|
||||
}
|
||||
|
||||
/// Approve a sign request and send the signature to the server
|
||||
///
|
||||
/// This performs the complete approval flow:
|
||||
/// 1. Validates the request can be approved
|
||||
/// 2. Signs the message using the vault
|
||||
/// 3. Sends the signature to the SigSocket server
|
||||
/// 4. Removes the request from pending list
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request to approve
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(signature)` - Base64-encoded signature that was sent
|
||||
/// * `Err(error)` - If approval failed
|
||||
#[wasm_bindgen]
|
||||
pub async fn approve_request(request_id: &str) -> Result<String, JsValue> {
|
||||
// 1. Validate we can approve this request
|
||||
if !Self::can_approve_request(request_id).await? {
|
||||
return Err(JsValue::from_str("Cannot approve this request"));
|
||||
}
|
||||
|
||||
// 2. Get request details and sign the message
|
||||
let (message_bytes, original_request) = SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||
|
||||
let request = client.get_pending_request(request_id)
|
||||
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||
|
||||
// Decode the message
|
||||
let message_bytes = request.message_bytes()
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid message format: {}", e)))?;
|
||||
|
||||
Ok::<(Vec<u8>, SignRequest), JsValue>((message_bytes, request.request.clone()))
|
||||
})?;
|
||||
|
||||
// 3. Sign with vault
|
||||
let signature_result = sign_with_default_keypair(&message_bytes).await?;
|
||||
let signature_hex = signature_result.as_string()
|
||||
.ok_or_else(|| JsValue::from_str("Signature result is not a string"))?;
|
||||
|
||||
// Convert hex signature to base64 for SigSocket protocol
|
||||
let signature_bytes = hex::decode(&signature_hex)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid hex signature: {}", e)))?;
|
||||
let signature_base64 = base64::prelude::BASE64_STANDARD.encode(&signature_bytes);
|
||||
|
||||
// 4. Get original message for response
|
||||
let original_message = SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||
|
||||
let request = client.get_pending_request(request_id)
|
||||
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||
|
||||
Ok::<String, JsValue>(request.request.message.clone())
|
||||
})?;
|
||||
|
||||
// 5. Send response to server (create a new scope to avoid borrowing issues)
|
||||
{
|
||||
let client_ref = SIGSOCKET_CLIENT.with(|c| {
|
||||
c.borrow().as_ref().map(|client| client as *const SigSocketClient)
|
||||
}).ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||
|
||||
// SAFETY: We know the client exists and we're using it synchronously
|
||||
let client = unsafe { &*client_ref };
|
||||
|
||||
client.send_response(request_id, &original_message, &signature_base64).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to send response: {:?}", e)))?;
|
||||
|
||||
console_log!("✅ WASM: Sent signature response to server for request: {}", request_id);
|
||||
}
|
||||
|
||||
// 6. Remove the request after successful send
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client = c.borrow_mut();
|
||||
if let Some(client) = client.as_mut() {
|
||||
client.remove_pending_request(request_id);
|
||||
console_log!("✅ WASM: Removed request from pending list: {}", request_id);
|
||||
}
|
||||
});
|
||||
|
||||
console_log!("🎉 WASM: Successfully approved and sent signature for request: {}", request_id);
|
||||
Ok(signature_base64)
|
||||
}
|
||||
|
||||
/// Reject a sign request
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request to reject
|
||||
/// * `reason` - The reason for rejection
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Request rejected successfully
|
||||
/// * `Err(error)` - If rejection failed
|
||||
#[wasm_bindgen]
|
||||
pub async fn reject_request(request_id: &str, reason: &str) -> Result<(), JsValue> {
|
||||
// Send rejection to server first
|
||||
{
|
||||
let client_ref = SIGSOCKET_CLIENT.with(|c| {
|
||||
c.borrow().as_ref().map(|client| client as *const SigSocketClient)
|
||||
}).ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||
|
||||
// SAFETY: We know the client exists and we're using it synchronously
|
||||
let client = unsafe { &*client_ref };
|
||||
|
||||
client.send_rejection(request_id, reason).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to send rejection: {:?}", e)))?;
|
||||
|
||||
console_log!("✅ WASM: Sent rejection to server for request: {}", request_id);
|
||||
}
|
||||
|
||||
// Remove the request after successful send
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client = c.borrow_mut();
|
||||
if let Some(client) = client.as_mut() {
|
||||
client.remove_pending_request(request_id);
|
||||
console_log!("✅ WASM: Removed rejected request from pending list: {}", request_id);
|
||||
}
|
||||
});
|
||||
|
||||
console_log!("🚫 WASM: Successfully rejected request: {} (reason: {})", request_id, reason);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get pending requests filtered by current workspace
|
||||
///
|
||||
/// This returns only the requests that the current vault session can handle,
|
||||
/// based on the unlocked workspace and its public key.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(requests_json)` - JSON array of filtered requests
|
||||
/// * `Err(error)` - If filtering failed
|
||||
#[wasm_bindgen]
|
||||
pub async fn get_filtered_requests() -> Result<String, JsValue> {
|
||||
// If vault is locked, return empty array
|
||||
if !is_unlocked() {
|
||||
return Ok("[]".to_string());
|
||||
}
|
||||
|
||||
// Get current workspace public key
|
||||
let current_workspace = get_current_keyspace_name()
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?;
|
||||
|
||||
let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?;
|
||||
|
||||
let current_public_key = current_public_key_js.as_string()
|
||||
.ok_or_else(|| JsValue::from_str("Current public key is not a string"))?;
|
||||
|
||||
// Filter requests for current workspace
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||
|
||||
let filtered_requests: Vec<_> = client.get_requests_for_public_key(¤t_public_key);
|
||||
|
||||
console_log!("Filtered requests: {} total, {} for current workspace",
|
||||
client.pending_request_count(), filtered_requests.len());
|
||||
|
||||
// Serialize and return
|
||||
serde_json::to_string(&filtered_requests)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a pending sign request (called when request arrives from server)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_json` - JSON string containing the sign request
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Request added successfully
|
||||
/// * `Err(error)` - If adding failed
|
||||
#[wasm_bindgen]
|
||||
pub fn add_pending_request(request_json: &str) -> Result<(), JsValue> {
|
||||
// Parse the request
|
||||
let request: SignRequest = serde_json::from_str(request_json)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid request JSON: {}", e)))?;
|
||||
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client = c.borrow_mut();
|
||||
let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||
|
||||
// Get the connected public key as the target
|
||||
let target_public_key = client.connected_public_key()
|
||||
.ok_or_else(|| JsValue::from_str("No connected public key"))?
|
||||
.to_string();
|
||||
|
||||
// Add the request
|
||||
client.add_pending_request(request, target_public_key);
|
||||
|
||||
console_log!("Added pending request: {}", request_json);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Get connection status
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(status_json)` - JSON object with connection status
|
||||
/// * `Err(error)` - If getting status failed
|
||||
#[wasm_bindgen]
|
||||
pub fn get_connection_status() -> Result<String, JsValue> {
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
|
||||
if let Some(client) = client.as_ref() {
|
||||
let status = serde_json::json!({
|
||||
"is_connected": client.is_connected(),
|
||||
"connected_public_key": client.connected_public_key(),
|
||||
"pending_request_count": client.pending_request_count(),
|
||||
"server_url": client.url()
|
||||
});
|
||||
|
||||
Ok(status.to_string())
|
||||
} else {
|
||||
let status = serde_json::json!({
|
||||
"is_connected": false,
|
||||
"connected_public_key": null,
|
||||
"pending_request_count": 0,
|
||||
"server_url": null
|
||||
});
|
||||
|
||||
Ok(status.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear all pending requests
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Requests cleared successfully
|
||||
#[wasm_bindgen]
|
||||
pub fn clear_pending_requests() -> Result<(), JsValue> {
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client = c.borrow_mut();
|
||||
if let Some(client) = client.as_mut() {
|
||||
client.clear_pending_requests();
|
||||
console_log!("Cleared all pending requests");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use vault::rhai_bindings as vault_rhai_bindings;
|
||||
use vault::session::SessionManager;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsValue;
|
||||
use js_sys::Uint8Array;
|
||||
|
||||
thread_local! {
|
||||
static ENGINE: Lazy<RefCell<Engine>> = Lazy::new(|| RefCell::new(Engine::new()));
|
||||
@@ -21,12 +22,40 @@ pub use vault::session_singleton::SESSION_MANAGER;
|
||||
// Session Lifecycle
|
||||
// =====================
|
||||
|
||||
/// Create and unlock a new keyspace with the given name and password
|
||||
#[wasm_bindgen]
|
||||
pub async fn create_keyspace(keyspace: &str, password: &str) -> Result<(), JsValue> {
|
||||
let keyspace = keyspace.to_string();
|
||||
let password_vec = password.as_bytes().to_vec();
|
||||
match WasmStore::open("vault").await {
|
||||
Ok(store) => {
|
||||
let vault = vault::Vault::new(store);
|
||||
let mut manager = SessionManager::new(vault);
|
||||
match manager.create_keyspace(&keyspace, &password_vec, None).await {
|
||||
Ok(_) => {
|
||||
SESSION_MANAGER.with(|cell| cell.replace(Some(manager)));
|
||||
}
|
||||
Err(e) => {
|
||||
web_sys::console::error_1(&format!("Failed to create keyspace: {e}").into());
|
||||
return Err(JsValue::from_str(&format!("Failed to create 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(())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
match WasmStore::open("vault").await {
|
||||
Ok(store) => {
|
||||
let vault = vault::Vault::new(store);
|
||||
let mut manager = SessionManager::new(vault);
|
||||
@@ -61,6 +90,71 @@ pub fn lock_session() {
|
||||
// Keypair Management
|
||||
// =====================
|
||||
|
||||
/// Get metadata of the currently selected keypair
|
||||
#[wasm_bindgen]
|
||||
pub fn current_keypair_metadata() -> Result<JsValue, JsValue> {
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
cell.borrow().as_ref()
|
||||
.and_then(|session| session.current_keypair_metadata())
|
||||
.map(|meta| wasm_bindgen::JsValue::from_serde(&meta).unwrap())
|
||||
.ok_or_else(|| JsValue::from_str("No keypair selected or no keyspace unlocked"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get public key of the currently selected keypair as Uint8Array
|
||||
#[wasm_bindgen]
|
||||
pub fn current_keypair_public_key() -> Result<JsValue, JsValue> {
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
cell.borrow().as_ref()
|
||||
.and_then(|session| session.current_keypair_public_key())
|
||||
.map(|pk| js_sys::Uint8Array::from(pk.as_slice()).into())
|
||||
.ok_or_else(|| JsValue::from_str("No keypair selected or no keyspace unlocked"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if a keyspace is currently unlocked
|
||||
#[wasm_bindgen]
|
||||
pub fn is_unlocked() -> bool {
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
cell.borrow().as_ref().map(|session| session.is_unlocked()).unwrap_or(false)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the default public key for a workspace (keyspace)
|
||||
/// This returns the public key of the first keypair in the keyspace
|
||||
#[wasm_bindgen]
|
||||
pub async fn get_workspace_default_public_key(workspace_id: &str) -> Result<JsValue, JsValue> {
|
||||
// For now, workspace_id is the same as keyspace name
|
||||
// In a full implementation, you might have a mapping from workspace to keyspace
|
||||
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
if let Some(session) = cell.borrow().as_ref() {
|
||||
if let Some(keyspace_name) = session.current_keyspace_name() {
|
||||
if keyspace_name == workspace_id {
|
||||
// Use the default_keypair method to get the first keypair
|
||||
if let Some(default_keypair) = session.default_keypair() {
|
||||
// Return the actual public key as hex
|
||||
let public_key_hex = hex::encode(&default_keypair.public_key);
|
||||
return Ok(JsValue::from_str(&public_key_hex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(JsValue::from_str("Workspace not found or no keypairs available"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current unlocked public key as hex string
|
||||
#[wasm_bindgen]
|
||||
pub fn get_current_unlocked_public_key() -> Result<String, JsValue> {
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
cell.borrow().as_ref()
|
||||
.and_then(|session| session.current_keypair_public_key())
|
||||
.map(|pk| hex::encode(pk.as_slice()))
|
||||
.ok_or_else(|| JsValue::from_str("No keypair selected or no keyspace unlocked"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all keypairs from the current session
|
||||
/// Returns an array of keypair objects with id, type, and metadata
|
||||
// #[wasm_bindgen]
|
||||
@@ -118,18 +212,12 @@ pub async fn add_keypair(
|
||||
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 = SESSION_MANAGER.with(|cell| {
|
||||
cell.borrow().as_ref().and_then(|session| {
|
||||
session.current_keyspace_name().map(|name| name.to_string())
|
||||
})
|
||||
});
|
||||
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 {
|
||||
@@ -153,27 +241,25 @@ pub async fn add_keypair(
|
||||
.add_keypair(&keyspace_name, &password, Some(key_type), metadata)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&format!("add_keypair error: {e}")))?;
|
||||
// Refresh in-memory keyspace data so list_keypairs reflects the new keypair immediately
|
||||
session.unlock_keyspace(&keyspace_name, &password).await
|
||||
.map_err(|e| JsValue::from_str(&format!("refresh keyspace after 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
|
||||
/// Sign message with current session (requires selected keypair)
|
||||
#[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);
|
||||
@@ -183,3 +269,145 @@ pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current keyspace name
|
||||
#[wasm_bindgen]
|
||||
pub fn get_current_keyspace_name() -> Result<String, JsValue> {
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
if let Some(session) = cell.borrow().as_ref() {
|
||||
if let Some(keyspace_name) = session.current_keyspace_name() {
|
||||
Ok(keyspace_name.to_string())
|
||||
} else {
|
||||
Err(JsValue::from_str("No keyspace unlocked"))
|
||||
}
|
||||
} else {
|
||||
Err(JsValue::from_str("Session not initialized"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Sign message with default keypair (first keypair in keyspace) without changing session state
|
||||
#[wasm_bindgen]
|
||||
pub async fn sign_with_default_keypair(message: &[u8]) -> Result<JsValue, JsValue> {
|
||||
// Temporarily select the default keypair, sign, then restore the original selection
|
||||
let original_keypair = SESSION_MANAGER.with(|cell| {
|
||||
cell.borrow().as_ref()
|
||||
.and_then(|session| session.current_keypair())
|
||||
.map(|kp| kp.id.clone())
|
||||
});
|
||||
|
||||
// Select default keypair
|
||||
let select_result = SESSION_MANAGER.with(|cell| {
|
||||
let mut session_opt = cell.borrow_mut().take();
|
||||
if let Some(ref mut session) = session_opt {
|
||||
let result = session.select_default_keypair();
|
||||
*cell.borrow_mut() = Some(session_opt.take().unwrap());
|
||||
result.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Err("Session not initialized".to_string())
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = select_result {
|
||||
return Err(JsValue::from_str(&format!("Failed to select default keypair: {e}")));
|
||||
}
|
||||
|
||||
// Sign with the default keypair
|
||||
let sign_result = {
|
||||
let session_ptr = SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _));
|
||||
let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr {
|
||||
Some(ptr) => unsafe { &*ptr },
|
||||
None => return Err(JsValue::from_str("Session not initialized")),
|
||||
};
|
||||
session.sign(message).await
|
||||
};
|
||||
|
||||
// Restore original keypair selection if there was one
|
||||
if let Some(original_id) = original_keypair {
|
||||
SESSION_MANAGER.with(|cell| {
|
||||
let mut session_opt = cell.borrow_mut().take();
|
||||
if let Some(ref mut session) = session_opt {
|
||||
let _ = session.select_keypair(&original_id); // Ignore errors here
|
||||
*cell.borrow_mut() = Some(session_opt.take().unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return the signature result
|
||||
match sign_result {
|
||||
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}"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a signature with the current session's selected keypair
|
||||
#[wasm_bindgen]
|
||||
pub async fn verify(message: &[u8], signature: &str) -> 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 session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr {
|
||||
Some(ptr) => unsafe { &*ptr },
|
||||
None => return Err(JsValue::from_str("Session not initialized")),
|
||||
};
|
||||
|
||||
// Convert hex signature to bytes
|
||||
let sig_bytes = match hex::decode(signature) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(JsValue::from_str(&format!("Invalid signature format: {e}"))),
|
||||
};
|
||||
|
||||
match session.verify(message, &sig_bytes).await {
|
||||
Ok(is_valid) => Ok(JsValue::from_bool(is_valid)),
|
||||
Err(e) => Err(JsValue::from_str(&format!("Verify error: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt data using the current session's keyspace symmetric cipher
|
||||
#[wasm_bindgen]
|
||||
pub async fn encrypt_data(data: &[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 session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr {
|
||||
Some(ptr) => unsafe { &*ptr },
|
||||
None => return Err(JsValue::from_str("Session not initialized")),
|
||||
};
|
||||
|
||||
match session.encrypt(data).await {
|
||||
Ok(encrypted) => {
|
||||
// Return as Uint8Array for JavaScript
|
||||
Ok(Uint8Array::from(&encrypted[..]).into())
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Encryption error: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt data using the current session's keyspace symmetric cipher
|
||||
#[wasm_bindgen]
|
||||
pub async fn decrypt_data(encrypted: &[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 session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr {
|
||||
Some(ptr) => unsafe { &*ptr },
|
||||
None => return Err(JsValue::from_str("Session not initialized")),
|
||||
};
|
||||
|
||||
match session.decrypt(encrypted).await {
|
||||
Ok(decrypted) => {
|
||||
// Return as Uint8Array for JavaScript
|
||||
Ok(Uint8Array::from(&decrypted[..]).into())
|
||||
}
|
||||
Err(e) => Err(JsValue::from_str(&format!("Decryption error: {e}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
wasm_console_demo/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WASM App Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WASM App Demo</h1>
|
||||
<script type="module">
|
||||
import init, * as wasm from './wasm_app.js';
|
||||
window.wasm = wasm;
|
||||
init().then(() => {
|
||||
console.log("WASM module loaded! Try window.wasm in the console.");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
13
wasm_console_demo/main.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Minimal loader for the vault WASM module for console interaction
|
||||
// Adjust the module path if needed (this assumes the default wasm-pack output in the parent dir)
|
||||
import init, * as vault from './wasm_app.js';
|
||||
|
||||
window.vault = null;
|
||||
|
||||
init().then(() => {
|
||||
window.vault = vault;
|
||||
console.log('Vault WASM module loaded. Use window.vault.<function>() in the console.');
|
||||
});
|
||||
|
||||
// Optional: Helper to convert Uint8Array to hex
|
||||
window.toHex = arr => Array.from(new Uint8Array(arr)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
15
wasm_console_demo/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "wasm_app",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"files": [
|
||||
"wasm_app_bg.wasm",
|
||||
"wasm_app.js",
|
||||
"wasm_app.d.ts"
|
||||
],
|
||||
"main": "wasm_app.js",
|
||||
"types": "wasm_app.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
||||
103
wasm_console_demo/wasm_app.d.ts
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Initialize the scripting environment (must be called before run_rhai)
|
||||
*/
|
||||
export function init_rhai_env(): void;
|
||||
/**
|
||||
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||
*/
|
||||
export function run_rhai(script: string): any;
|
||||
/**
|
||||
* Create and unlock a new keyspace with the given name and password
|
||||
*/
|
||||
export function create_keyspace(keyspace: string, password: string): Promise<void>;
|
||||
/**
|
||||
* Initialize session with keyspace and password
|
||||
*/
|
||||
export function init_session(keyspace: string, password: string): Promise<void>;
|
||||
/**
|
||||
* Lock the session (zeroize password and session)
|
||||
*/
|
||||
export function lock_session(): void;
|
||||
/**
|
||||
* Get metadata of the currently selected keypair
|
||||
*/
|
||||
export function current_keypair_metadata(): any;
|
||||
/**
|
||||
* Get public key of the currently selected keypair as Uint8Array
|
||||
*/
|
||||
export function current_keypair_public_key(): any;
|
||||
/**
|
||||
* Returns true if a keyspace is currently unlocked
|
||||
*/
|
||||
export function is_unlocked(): boolean;
|
||||
/**
|
||||
* Get all keypairs from the current session
|
||||
* Returns an array of keypair objects with id, type, and metadata
|
||||
* Select keypair for the session
|
||||
*/
|
||||
export function select_keypair(key_id: string): void;
|
||||
/**
|
||||
* List keypairs in the current session's keyspace
|
||||
*/
|
||||
export function list_keypairs(): Promise<any>;
|
||||
/**
|
||||
* Add a keypair to the current keyspace
|
||||
*/
|
||||
export function add_keypair(key_type?: string | null, metadata?: string | null): Promise<any>;
|
||||
/**
|
||||
* Sign message with current session
|
||||
*/
|
||||
export function sign(message: Uint8Array): Promise<any>;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly init_rhai_env: () => void;
|
||||
readonly run_rhai: (a: number, b: number) => [number, number, number];
|
||||
readonly create_keyspace: (a: number, b: number, c: number, d: number) => any;
|
||||
readonly init_session: (a: number, b: number, c: number, d: number) => any;
|
||||
readonly lock_session: () => void;
|
||||
readonly current_keypair_metadata: () => [number, number, number];
|
||||
readonly current_keypair_public_key: () => [number, number, number];
|
||||
readonly is_unlocked: () => number;
|
||||
readonly select_keypair: (a: number, b: number) => [number, number];
|
||||
readonly list_keypairs: () => any;
|
||||
readonly add_keypair: (a: number, b: number, c: number, d: number) => any;
|
||||
readonly sign: (a: number, b: number) => any;
|
||||
readonly __wbindgen_exn_store: (a: number) => void;
|
||||
readonly __externref_table_alloc: () => number;
|
||||
readonly __wbindgen_export_2: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_export_5: WebAssembly.Table;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly closure89_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly closure133_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly closure188_externref_shim: (a: number, b: number, c: any) => void;
|
||||
readonly closure1847_externref_shim: (a: number, b: number, c: any, d: any) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
@@ -1,5 +1,3 @@
|
||||
import * as __wbg_star0 from 'env';
|
||||
|
||||
let wasm;
|
||||
|
||||
function addToExternrefTable0(obj) {
|
||||
@@ -231,6 +229,21 @@ export function run_rhai(script) {
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and unlock a new keyspace with the given name and password
|
||||
* @param {string} keyspace
|
||||
* @param {string} password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function create_keyspace(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.create_keyspace(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize session with keyspace and password
|
||||
* @param {string} keyspace
|
||||
@@ -253,6 +266,39 @@ export function lock_session() {
|
||||
wasm.lock_session();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata of the currently selected keypair
|
||||
* @returns {any}
|
||||
*/
|
||||
export function current_keypair_metadata() {
|
||||
const ret = wasm.current_keypair_metadata();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key of the currently selected keypair as Uint8Array
|
||||
* @returns {any}
|
||||
*/
|
||||
export function current_keypair_public_key() {
|
||||
const ret = wasm.current_keypair_public_key();
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a keyspace is currently unlocked
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function is_unlocked() {
|
||||
const ret = wasm.is_unlocked();
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keypairs from the current session
|
||||
* Returns an array of keypair objects with id, type, and metadata
|
||||
@@ -311,19 +357,19 @@ export function sign(message) {
|
||||
}
|
||||
|
||||
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||
wasm.closure77_externref_shim(arg0, arg1, arg2);
|
||||
wasm.closure89_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||
wasm.closure126_externref_shim(arg0, arg1, arg2);
|
||||
wasm.closure133_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);
|
||||
function __wbg_adapter_135(arg0, arg1, arg2, arg3) {
|
||||
wasm.closure1847_externref_shim(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||
@@ -395,6 +441,10 @@ function __wbg_get_imports() {
|
||||
imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.getRandomValues(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getTime_46267b1c24877e30 = function(arg0) {
|
||||
const ret = arg0.getTime();
|
||||
return ret;
|
||||
};
|
||||
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);
|
||||
@@ -458,6 +508,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.msCrypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new0_f788a2397c7ca929 = function() {
|
||||
const ret = new Date();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
|
||||
try {
|
||||
var state0 = {a: arg0, b: arg1};
|
||||
@@ -465,7 +519,7 @@ function __wbg_get_imports() {
|
||||
const a = state0.a;
|
||||
state0.a = 0;
|
||||
try {
|
||||
return __wbg_adapter_123(a, state0.b, arg0, arg1);
|
||||
return __wbg_adapter_135(a, state0.b, arg0, arg1);
|
||||
} finally {
|
||||
state0.a = a;
|
||||
}
|
||||
@@ -504,6 +558,10 @@ function __wbg_get_imports() {
|
||||
const ret = arg0.node;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_now_d18023d54d4e5500 = function(arg0) {
|
||||
const ret = arg0.now();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) {
|
||||
const ret = arg0.objectStoreNames;
|
||||
return ret;
|
||||
@@ -615,15 +673,15 @@ function __wbg_get_imports() {
|
||||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32);
|
||||
imports.wbg.__wbindgen_closure_wrapper288 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 90, __wbg_adapter_32);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35);
|
||||
imports.wbg.__wbindgen_closure_wrapper518 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 134, __wbg_adapter_35);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) {
|
||||
imports.wbg.__wbindgen_closure_wrapper776 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
||||
return ret;
|
||||
};
|
||||
@@ -688,7 +746,6 @@ function __wbg_get_imports() {
|
||||
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports['env'] = __wbg_star0;
|
||||
|
||||
return imports;
|
||||
}
|
||||