refactor: migrate extension to TypeScript and add Material-UI components
21
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,18 +25,7 @@ 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
|
||||
cd extension && npm run build
|
||||
|
||||
# Build everything: wasm, copy, then extension
|
||||
build-vault-browser-ext:
|
||||
cd wasm_app && wasm-pack build --target web --out-dir ../vault_browser_ext/wasm_app/pkg
|
||||
cp vault_browser_ext/wasm_app/pkg/wasm_app.js vault_browser_ext/public/wasm/
|
||||
cp vault_browser_ext/wasm_app/pkg/wasm_app_bg.wasm vault_browser_ext/public/wasm/
|
||||
cd vault_browser_ext && npm install && npm run build
|
||||
cp vault_browser_ext/manifest.json vault_browser_ext/dist/
|
||||
cp vault_browser_ext/*.png vault_browser_ext/dist/
|
||||
mkdir -p vault_browser_ext/dist/src
|
||||
cp vault_browser_ext/sandbox.html vault_browser_ext/dist/
|
||||
cp vault_browser_ext/sandbox.js vault_browser_ext/dist/
|
||||
# Build Hero Vault extension: wasm, copy, then extension
|
||||
build-hero-vault-extension:
|
||||
cd wasm_app && wasm-pack build --target web
|
||||
cd hero_vault_extension && npm run build
|
48
build.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/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: Build the frontend extension
|
||||
echo -e "${BLUE}Building frontend extension...${RESET}"
|
||||
cd ../hero_vault_extension || exit 1
|
||||
|
||||
# Copy WASM files to the extension's public directory
|
||||
echo "Copying WASM files..."
|
||||
mkdir -p public/wasm
|
||||
cp ../wasm_app/pkg/wasm_app* public/wasm/
|
||||
cp ../wasm_app/pkg/*.d.ts public/wasm/
|
||||
cp ../wasm_app/pkg/package.json public/wasm/
|
||||
|
||||
# Build the extension without TypeScript checking
|
||||
echo "Building extension..."
|
||||
export NO_TYPECHECK=true
|
||||
npm run build
|
||||
|
||||
# Ensure the background script is properly built
|
||||
echo "Building background script..."
|
||||
node scripts/build-background.js
|
||||
echo -e "${GREEN}✓ Frontend build successful!${RESET}"
|
||||
|
||||
echo -e "${GREEN}=== Build Complete ===${RESET}"
|
||||
echo "Extension is ready in: $(pwd)/dist"
|
||||
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 'dist' directory: $(pwd)/dist"
|
@ -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)
|
@ -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.');
|
70
extension/dist/assets/popup.js
vendored
BIN
extension/dist/assets/wasm_app_bg.wasm
vendored
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>"]
|
||||
}
|
||||
]
|
||||
}
|
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;
|
||||
}
|
765
extension/dist/wasm/wasm_app.js
vendored
@ -1,765 +0,0 @@
|
||||
import * as __wbg_star0 from 'env';
|
||||
|
||||
let wasm;
|
||||
|
||||
function addToExternrefTable0(obj) {
|
||||
const idx = wasm.__externref_table_alloc();
|
||||
wasm.__wbindgen_export_2.set(idx, obj);
|
||||
return idx;
|
||||
}
|
||||
|
||||
function handleError(f, args) {
|
||||
try {
|
||||
return f.apply(this, args);
|
||||
} catch (e) {
|
||||
const idx = addToExternrefTable0(e);
|
||||
wasm.__wbindgen_exn_store(idx);
|
||||
}
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
? function (arg, view) {
|
||||
return cachedTextEncoder.encodeInto(arg, view);
|
||||
}
|
||||
: function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
});
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(state => {
|
||||
wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b)
|
||||
});
|
||||
|
||||
function makeMutClosure(arg0, arg1, dtor, f) {
|
||||
const state = { a: arg0, b: arg1, cnt: 1, dtor };
|
||||
const real = (...args) => {
|
||||
// First up with a closure we increment the internal reference
|
||||
// count. This ensures that the Rust closure environment won't
|
||||
// be deallocated while we're invoking it.
|
||||
state.cnt++;
|
||||
const a = state.a;
|
||||
state.a = 0;
|
||||
try {
|
||||
return f(a, state.b, ...args);
|
||||
} finally {
|
||||
if (--state.cnt === 0) {
|
||||
wasm.__wbindgen_export_5.get(state.dtor)(a, state.b);
|
||||
CLOSURE_DTORS.unregister(state);
|
||||
} else {
|
||||
state.a = a;
|
||||
}
|
||||
}
|
||||
};
|
||||
real.original = state;
|
||||
CLOSURE_DTORS.register(real, state, state);
|
||||
return real;
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
// primitive types
|
||||
const type = typeof val;
|
||||
if (type == 'number' || type == 'boolean' || val == null) {
|
||||
return `${val}`;
|
||||
}
|
||||
if (type == 'string') {
|
||||
return `"${val}"`;
|
||||
}
|
||||
if (type == 'symbol') {
|
||||
const description = val.description;
|
||||
if (description == null) {
|
||||
return 'Symbol';
|
||||
} else {
|
||||
return `Symbol(${description})`;
|
||||
}
|
||||
}
|
||||
if (type == 'function') {
|
||||
const name = val.name;
|
||||
if (typeof name == 'string' && name.length > 0) {
|
||||
return `Function(${name})`;
|
||||
} else {
|
||||
return 'Function';
|
||||
}
|
||||
}
|
||||
// objects
|
||||
if (Array.isArray(val)) {
|
||||
const length = val.length;
|
||||
let debug = '[';
|
||||
if (length > 0) {
|
||||
debug += debugString(val[0]);
|
||||
}
|
||||
for(let i = 1; i < length; i++) {
|
||||
debug += ', ' + debugString(val[i]);
|
||||
}
|
||||
debug += ']';
|
||||
return debug;
|
||||
}
|
||||
// Test for built-in
|
||||
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||
let className;
|
||||
if (builtInMatches && builtInMatches.length > 1) {
|
||||
className = builtInMatches[1];
|
||||
} else {
|
||||
// Failed to match the standard '[object ClassName]'
|
||||
return toString.call(val);
|
||||
}
|
||||
if (className == 'Object') {
|
||||
// we're a user defined class or Object
|
||||
// JSON.stringify avoids problems with cycles, and is generally much
|
||||
// easier than looping through ownProperties of `val`.
|
||||
try {
|
||||
return 'Object(' + JSON.stringify(val) + ')';
|
||||
} catch (_) {
|
||||
return 'Object';
|
||||
}
|
||||
}
|
||||
// errors
|
||||
if (val instanceof Error) {
|
||||
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||
}
|
||||
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||
return className;
|
||||
}
|
||||
/**
|
||||
* Initialize the scripting environment (must be called before run_rhai)
|
||||
*/
|
||||
export function init_rhai_env() {
|
||||
wasm.init_rhai_env();
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_export_2.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||
* @param {string} script
|
||||
* @returns {any}
|
||||
*/
|
||||
export function run_rhai(script) {
|
||||
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.run_rhai(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize session with keyspace and password
|
||||
* @param {string} keyspace
|
||||
* @param {string} password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function init_session(keyspace, password) {
|
||||
const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.init_session(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the session (zeroize password and session)
|
||||
*/
|
||||
export function lock_session() {
|
||||
wasm.lock_session();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keypairs from the current session
|
||||
* Returns an array of keypair objects with id, type, and metadata
|
||||
* Select keypair for the session
|
||||
* @param {string} key_id
|
||||
*/
|
||||
export function select_keypair(key_id) {
|
||||
const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.select_keypair(ptr0, len0);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List keypairs in the current session's keyspace
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function list_keypairs() {
|
||||
const ret = wasm.list_keypairs();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a keypair to the current keyspace
|
||||
* @param {string | null} [key_type]
|
||||
* @param {string | null} [metadata]
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function add_keypair(key_type, metadata) {
|
||||
var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.add_keypair(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
/**
|
||||
* Sign message with current session
|
||||
* @param {Uint8Array} message
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function sign(message) {
|
||||
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sign(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||
wasm.closure77_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||
wasm.closure126_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||
wasm.closure188_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_123(arg0, arg1, arg2, arg3) {
|
||||
wasm.closure213_externref_shim(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
|
||||
} catch (e) {
|
||||
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) {
|
||||
const ret = arg0.buffer;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.call(arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.call(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) {
|
||||
const ret = arg0.crypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||
console.error(arg0);
|
||||
};
|
||||
imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) {
|
||||
const ret = arg0.error;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) {
|
||||
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.getRandomValues(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) {
|
||||
const ret = arg1[arg2 >>> 0];
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = Reflect.get(arg0, arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.get(arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBDatabase;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBFactory;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBOpenDBRequest;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBRequest;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||
const ret = arg0.msCrypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
|
||||
try {
|
||||
var state0 = {a: arg0, b: arg1};
|
||||
var cb0 = (arg0, arg1) => {
|
||||
const a = state0.a;
|
||||
state0.a = 0;
|
||||
try {
|
||||
return __wbg_adapter_123(a, state0.b, arg0, arg1);
|
||||
} finally {
|
||||
state0.a = a;
|
||||
}
|
||||
};
|
||||
const ret = new Promise(cb0);
|
||||
return ret;
|
||||
} finally {
|
||||
state0.a = state0.b = 0;
|
||||
}
|
||||
};
|
||||
imports.wbg.__wbg_new_405e22f390576ce2 = function() {
|
||||
const ret = new Object();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_78feb108b6472713 = function() {
|
||||
const ret = new Array();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||
const ret = new Uint8Array(arg0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
|
||||
const ret = new Function(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) {
|
||||
const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) {
|
||||
const ret = new Uint8Array(arg0 >>> 0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) {
|
||||
const ret = arg0.node;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) {
|
||||
const ret = arg0.objectStoreNames;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) {
|
||||
const ret = arg0.process;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) {
|
||||
const ret = arg0.push(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.put(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.put(arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
|
||||
queueMicrotask(arg0);
|
||||
};
|
||||
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
|
||||
const ret = arg0.queueMicrotask;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.randomFillSync(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||
const ret = module.require;
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
|
||||
const ret = Promise.resolve(arg0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) {
|
||||
const ret = arg0.result;
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||
arg0.set(arg1, arg2 >>> 0);
|
||||
};
|
||||
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||
arg0.onerror = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||
arg0.onsuccess = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) {
|
||||
arg0.onupgradeneeded = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
|
||||
const ret = typeof global === 'undefined' ? null : global;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
|
||||
const ret = typeof globalThis === 'undefined' ? null : globalThis;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
|
||||
const ret = typeof self === 'undefined' ? null : self;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
|
||||
const ret = typeof window === 'undefined' ? null : window;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) {
|
||||
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) {
|
||||
const ret = arg0.target;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
|
||||
const ret = arg0.then(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) {
|
||||
const ret = arg0.versions;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||
const obj = arg0.original;
|
||||
if (obj.cnt-- == 1) {
|
||||
obj.a = 0;
|
||||
return true;
|
||||
}
|
||||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||
const ret = debugString(arg1);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||
const table = wasm.__wbindgen_export_2;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_function = function(arg0) {
|
||||
const ret = typeof(arg0) === 'function';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_null = function(arg0) {
|
||||
const ret = arg0 === null;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_object = function(arg0) {
|
||||
const val = arg0;
|
||||
const ret = typeof(val) === 'object' && val !== null;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_string = function(arg0) {
|
||||
const ret = typeof(arg0) === 'string';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_undefined = function(arg0) {
|
||||
const ret = arg0 === undefined;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_json_parse = function(arg0, arg1) {
|
||||
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
|
||||
const obj = arg1;
|
||||
const ret = JSON.stringify(obj === undefined ? null : obj);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbindgen_memory = function() {
|
||||
const ret = wasm.memory;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports['env'] = __wbg_star0;
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
function __wbg_init_memory(imports, memory) {
|
||||
|
||||
}
|
||||
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasm = instance.exports;
|
||||
__wbg_init.__wbindgen_wasm_module = module;
|
||||
cachedDataViewMemory0 = null;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
|
||||
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module_or_path !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module_or_path === 'undefined') {
|
||||
module_or_path = new URL('wasm_app_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync };
|
||||
export default __wbg_init;
|
BIN
extension/dist/wasm/wasm_app_bg.wasm
vendored
@ -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,623 +0,0 @@
|
||||
import init, * as wasmModuleImport from '@wasm/wasm_app.js';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
await init();
|
||||
window.wasm_app = wasmModuleImport;
|
||||
|
||||
// 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,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,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,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,765 +0,0 @@
|
||||
import * as __wbg_star0 from 'env';
|
||||
|
||||
let wasm;
|
||||
|
||||
function addToExternrefTable0(obj) {
|
||||
const idx = wasm.__externref_table_alloc();
|
||||
wasm.__wbindgen_export_2.set(idx, obj);
|
||||
return idx;
|
||||
}
|
||||
|
||||
function handleError(f, args) {
|
||||
try {
|
||||
return f.apply(this, args);
|
||||
} catch (e) {
|
||||
const idx = addToExternrefTable0(e);
|
||||
wasm.__wbindgen_exn_store(idx);
|
||||
}
|
||||
}
|
||||
|
||||
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
|
||||
|
||||
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
function isLikeNone(x) {
|
||||
return x === undefined || x === null;
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
|
||||
|
||||
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
|
||||
? function (arg, view) {
|
||||
return cachedTextEncoder.encodeInto(arg, view);
|
||||
}
|
||||
: function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
});
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = encodeString(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let cachedDataViewMemory0 = null;
|
||||
|
||||
function getDataViewMemory0() {
|
||||
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||
}
|
||||
return cachedDataViewMemory0;
|
||||
}
|
||||
|
||||
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(state => {
|
||||
wasm.__wbindgen_export_5.get(state.dtor)(state.a, state.b)
|
||||
});
|
||||
|
||||
function makeMutClosure(arg0, arg1, dtor, f) {
|
||||
const state = { a: arg0, b: arg1, cnt: 1, dtor };
|
||||
const real = (...args) => {
|
||||
// First up with a closure we increment the internal reference
|
||||
// count. This ensures that the Rust closure environment won't
|
||||
// be deallocated while we're invoking it.
|
||||
state.cnt++;
|
||||
const a = state.a;
|
||||
state.a = 0;
|
||||
try {
|
||||
return f(a, state.b, ...args);
|
||||
} finally {
|
||||
if (--state.cnt === 0) {
|
||||
wasm.__wbindgen_export_5.get(state.dtor)(a, state.b);
|
||||
CLOSURE_DTORS.unregister(state);
|
||||
} else {
|
||||
state.a = a;
|
||||
}
|
||||
}
|
||||
};
|
||||
real.original = state;
|
||||
CLOSURE_DTORS.register(real, state, state);
|
||||
return real;
|
||||
}
|
||||
|
||||
function debugString(val) {
|
||||
// primitive types
|
||||
const type = typeof val;
|
||||
if (type == 'number' || type == 'boolean' || val == null) {
|
||||
return `${val}`;
|
||||
}
|
||||
if (type == 'string') {
|
||||
return `"${val}"`;
|
||||
}
|
||||
if (type == 'symbol') {
|
||||
const description = val.description;
|
||||
if (description == null) {
|
||||
return 'Symbol';
|
||||
} else {
|
||||
return `Symbol(${description})`;
|
||||
}
|
||||
}
|
||||
if (type == 'function') {
|
||||
const name = val.name;
|
||||
if (typeof name == 'string' && name.length > 0) {
|
||||
return `Function(${name})`;
|
||||
} else {
|
||||
return 'Function';
|
||||
}
|
||||
}
|
||||
// objects
|
||||
if (Array.isArray(val)) {
|
||||
const length = val.length;
|
||||
let debug = '[';
|
||||
if (length > 0) {
|
||||
debug += debugString(val[0]);
|
||||
}
|
||||
for(let i = 1; i < length; i++) {
|
||||
debug += ', ' + debugString(val[i]);
|
||||
}
|
||||
debug += ']';
|
||||
return debug;
|
||||
}
|
||||
// Test for built-in
|
||||
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||
let className;
|
||||
if (builtInMatches && builtInMatches.length > 1) {
|
||||
className = builtInMatches[1];
|
||||
} else {
|
||||
// Failed to match the standard '[object ClassName]'
|
||||
return toString.call(val);
|
||||
}
|
||||
if (className == 'Object') {
|
||||
// we're a user defined class or Object
|
||||
// JSON.stringify avoids problems with cycles, and is generally much
|
||||
// easier than looping through ownProperties of `val`.
|
||||
try {
|
||||
return 'Object(' + JSON.stringify(val) + ')';
|
||||
} catch (_) {
|
||||
return 'Object';
|
||||
}
|
||||
}
|
||||
// errors
|
||||
if (val instanceof Error) {
|
||||
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||
}
|
||||
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||
return className;
|
||||
}
|
||||
/**
|
||||
* Initialize the scripting environment (must be called before run_rhai)
|
||||
*/
|
||||
export function init_rhai_env() {
|
||||
wasm.init_rhai_env();
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_export_2.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||
* @param {string} script
|
||||
* @returns {any}
|
||||
*/
|
||||
export function run_rhai(script) {
|
||||
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.run_rhai(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
return takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize session with keyspace and password
|
||||
* @param {string} keyspace
|
||||
* @param {string} password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function init_session(keyspace, password) {
|
||||
const ptr0 = passStringToWasm0(keyspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.init_session(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the session (zeroize password and session)
|
||||
*/
|
||||
export function lock_session() {
|
||||
wasm.lock_session();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keypairs from the current session
|
||||
* Returns an array of keypair objects with id, type, and metadata
|
||||
* Select keypair for the session
|
||||
* @param {string} key_id
|
||||
*/
|
||||
export function select_keypair(key_id) {
|
||||
const ptr0 = passStringToWasm0(key_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.select_keypair(ptr0, len0);
|
||||
if (ret[1]) {
|
||||
throw takeFromExternrefTable0(ret[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List keypairs in the current session's keyspace
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function list_keypairs() {
|
||||
const ret = wasm.list_keypairs();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a keypair to the current keyspace
|
||||
* @param {string | null} [key_type]
|
||||
* @param {string | null} [metadata]
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function add_keypair(key_type, metadata) {
|
||||
var ptr0 = isLikeNone(key_type) ? 0 : passStringToWasm0(key_type, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len0 = WASM_VECTOR_LEN;
|
||||
var ptr1 = isLikeNone(metadata) ? 0 : passStringToWasm0(metadata, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.add_keypair(ptr0, len0, ptr1, len1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
/**
|
||||
* Sign message with current session
|
||||
* @param {Uint8Array} message
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function sign(message) {
|
||||
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.sign(ptr0, len0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||
wasm.closure77_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||
wasm.closure126_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||
wasm.closure188_externref_shim(arg0, arg1, arg2);
|
||||
}
|
||||
|
||||
function __wbg_adapter_123(arg0, arg1, arg2, arg3) {
|
||||
wasm.closure213_externref_shim(arg0, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
|
||||
} catch (e) {
|
||||
if (module.headers.get('Content-Type') != 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_buffer_609cc3eee51ed158 = function(arg0) {
|
||||
const ret = arg0.buffer;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.call(arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.call(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_crypto_574e78ad8b13b65f = function(arg0) {
|
||||
const ret = arg0.crypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||
console.error(arg0);
|
||||
};
|
||||
imports.wbg.__wbg_error_ff4ddaabdfc5dbb3 = function() { return handleError(function (arg0) {
|
||||
const ret = arg0.error;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getRandomValues_3c9c0d586e575a16 = function() { return handleError(function (arg0, arg1) {
|
||||
globalThis.crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.getRandomValues(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_4f73335ab78445db = function(arg0, arg1, arg2) {
|
||||
const ret = arg1[arg2 >>> 0];
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbg_get_67b2ba62fc30de12 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = Reflect.get(arg0, arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_8da03f81f6a1111e = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.get(arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_instanceof_IdbDatabase_a3ef009ca00059f9 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBDatabase;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_IdbFactory_12eaba3366f4302f = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBFactory;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_IdbOpenDbRequest_a3416e156c9db893 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBOpenDBRequest;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_IdbRequest_4813c3f207666aa4 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = arg0 instanceof IDBRequest;
|
||||
} catch (_) {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||
const ret = arg0.length;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||
const ret = arg0.msCrypto;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
|
||||
try {
|
||||
var state0 = {a: arg0, b: arg1};
|
||||
var cb0 = (arg0, arg1) => {
|
||||
const a = state0.a;
|
||||
state0.a = 0;
|
||||
try {
|
||||
return __wbg_adapter_123(a, state0.b, arg0, arg1);
|
||||
} finally {
|
||||
state0.a = a;
|
||||
}
|
||||
};
|
||||
const ret = new Promise(cb0);
|
||||
return ret;
|
||||
} finally {
|
||||
state0.a = state0.b = 0;
|
||||
}
|
||||
};
|
||||
imports.wbg.__wbg_new_405e22f390576ce2 = function() {
|
||||
const ret = new Object();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_78feb108b6472713 = function() {
|
||||
const ret = new Array();
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||
const ret = new Uint8Array(arg0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
|
||||
const ret = new Function(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function(arg0, arg1, arg2) {
|
||||
const ret = new Uint8Array(arg0, arg1 >>> 0, arg2 >>> 0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_newwithlength_a381634e90c276d4 = function(arg0) {
|
||||
const ret = new Uint8Array(arg0 >>> 0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_node_905d3e251edff8a2 = function(arg0) {
|
||||
const ret = arg0.node;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_objectStoreNames_9bb1ab04a7012aaf = function(arg0) {
|
||||
const ret = arg0.objectStoreNames;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_objectStore_21878d46d25b64b6 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_open_e0c0b2993eb596e1 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2), arg3 >>> 0);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_process_dc0fbacc7c1c06f7 = function(arg0) {
|
||||
const ret = arg0.process;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_push_737cfc8c1432c2c6 = function(arg0, arg1) {
|
||||
const ret = arg0.push(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_put_066faa31a6a88f5b = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.put(arg1, arg2);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_put_9ef5363941008835 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = arg0.put(arg1);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
|
||||
queueMicrotask(arg0);
|
||||
};
|
||||
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
|
||||
const ret = arg0.queueMicrotask;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||
arg0.randomFillSync(arg1);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||
const ret = module.require;
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
|
||||
const ret = Promise.resolve(arg0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_result_f29afabdf2c05826 = function() { return handleError(function (arg0) {
|
||||
const ret = arg0.result;
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||
arg0.set(arg1, arg2 >>> 0);
|
||||
};
|
||||
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||
arg0.onerror = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||
arg0.onsuccess = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_setonupgradeneeded_fcf7ce4f2eb0cb5f = function(arg0, arg1) {
|
||||
arg0.onupgradeneeded = arg1;
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
|
||||
const ret = typeof global === 'undefined' ? null : global;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
|
||||
const ret = typeof globalThis === 'undefined' ? null : globalThis;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
|
||||
const ret = typeof self === 'undefined' ? null : self;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
|
||||
const ret = typeof window === 'undefined' ? null : window;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_subarray_aa9065fa9dc5df96 = function(arg0, arg1, arg2) {
|
||||
const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_target_0a62d9d79a2a1ede = function(arg0) {
|
||||
const ret = arg0.target;
|
||||
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
||||
};
|
||||
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
|
||||
const ret = arg0.then(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
||||
return ret;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_versions_c01dfd4722a88165 = function(arg0) {
|
||||
const ret = arg0.versions;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||
const obj = arg0.original;
|
||||
if (obj.cnt-- == 1) {
|
||||
obj.a = 0;
|
||||
return true;
|
||||
}
|
||||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper284 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 78, __wbg_adapter_32);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper493 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 127, __wbg_adapter_35);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper762 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||
const ret = debugString(arg1);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbindgen_init_externref_table = function() {
|
||||
const table = wasm.__wbindgen_export_2;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_function = function(arg0) {
|
||||
const ret = typeof(arg0) === 'function';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_null = function(arg0) {
|
||||
const ret = arg0 === null;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_object = function(arg0) {
|
||||
const val = arg0;
|
||||
const ret = typeof(val) === 'object' && val !== null;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_string = function(arg0) {
|
||||
const ret = typeof(arg0) === 'string';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_is_undefined = function(arg0) {
|
||||
const ret = arg0 === undefined;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_json_parse = function(arg0, arg1) {
|
||||
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
|
||||
const obj = arg1;
|
||||
const ret = JSON.stringify(obj === undefined ? null : obj);
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||
};
|
||||
imports.wbg.__wbindgen_memory = function() {
|
||||
const ret = wasm.memory;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||
const ret = getStringFromWasm0(arg0, arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports['env'] = __wbg_star0;
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
function __wbg_init_memory(imports, memory) {
|
||||
|
||||
}
|
||||
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasm = instance.exports;
|
||||
__wbg_init.__wbindgen_wasm_module = module;
|
||||
cachedDataViewMemory0 = null;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
|
||||
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
async function __wbg_init(module_or_path) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (typeof module_or_path !== 'undefined') {
|
||||
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
||||
({module_or_path} = module_or_path)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module_or_path === 'undefined') {
|
||||
module_or_path = new URL('wasm_app_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
||||
module_or_path = fetch(module_or_path);
|
||||
}
|
||||
|
||||
__wbg_init_memory(imports);
|
||||
|
||||
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
||||
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync };
|
||||
export default __wbg_init;
|
@ -1,122 +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 from the pkg output
|
||||
const wasmBinSource = resolve(__dirname, '../wasm_app/pkg/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 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');
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@wasm': path.resolve(__dirname, '../wasm_app/pkg')
|
||||
}
|
||||
},
|
||||
|
||||
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: ['../']
|
||||
}
|
||||
}
|
||||
});
|
88
hero_vault_extension/README.md
Normal file
@ -0,0 +1,88 @@
|
||||
# SAL Modular Cryptographic Browser Extension
|
||||
|
||||
A modern, secure browser extension for interacting with the SAL modular Rust cryptographic stack, enabling key management, cryptographic operations, and secure Rhai script execution.
|
||||
|
||||
## Features
|
||||
|
||||
### Session & Key Management
|
||||
- Create and unlock encrypted keyspaces with password protection
|
||||
- Create, select, and manage multiple keypairs (Ed25519, Secp256k1)
|
||||
- Clear session state visualization and management
|
||||
|
||||
### Cryptographic Operations
|
||||
- Sign and verify messages using selected keypair
|
||||
- Encrypt and decrypt messages using asymmetric cryptography
|
||||
- Support for symmetric encryption using password-derived keys
|
||||
|
||||
### Scripting (Rhai)
|
||||
- Execute Rhai scripts securely within the extension
|
||||
- Explicit user approval for all script executions
|
||||
- Script history and audit trail
|
||||
|
||||
### WebSocket Integration
|
||||
- Connect to WebSocket servers using keypair's public key
|
||||
- Receive, review, and approve/reject incoming scripts
|
||||
- Support for both local and remote script execution
|
||||
|
||||
### Security
|
||||
- Dark mode UI with modern, responsive design
|
||||
- Session auto-lock after configurable inactivity period
|
||||
- Explicit user approval for all sensitive operations
|
||||
- No persistent storage of passwords or private keys in plaintext
|
||||
|
||||
## Architecture
|
||||
|
||||
The extension is built with a modern tech stack:
|
||||
|
||||
- **Frontend**: React with TypeScript, Material-UI
|
||||
- **State Management**: Zustand
|
||||
- **Backend**: WebAssembly (WASM) modules compiled from Rust
|
||||
- **Storage**: Chrome extension storage API with encryption
|
||||
- **Networking**: WebSocket for server communication
|
||||
|
||||
## Development Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```
|
||||
cd sal_extension
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Build the extension:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. Load the extension in Chrome/Edge:
|
||||
- Navigate to `chrome://extensions/`
|
||||
- Enable "Developer mode"
|
||||
- Click "Load unpacked" and select the `dist` directory
|
||||
|
||||
4. For development with hot-reload:
|
||||
```
|
||||
npm run watch
|
||||
```
|
||||
|
||||
## Integration with WASM
|
||||
|
||||
The extension uses WebAssembly modules compiled from Rust to perform cryptographic operations securely. The WASM modules are loaded in the extension's background script and provide a secure API for the frontend.
|
||||
|
||||
Key WASM functions exposed:
|
||||
- `init_session` - Unlock a keyspace with password
|
||||
- `create_keyspace` - Create a new keyspace
|
||||
- `add_keypair` - Create a new keypair
|
||||
- `select_keypair` - Select a keypair for use
|
||||
- `sign` - Sign a message with the selected keypair
|
||||
- `run_rhai` - Execute a Rhai script securely
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- The extension follows the principle of least privilege
|
||||
- All sensitive operations require explicit user approval
|
||||
- Passwords are never stored persistently, only kept in memory during an active session
|
||||
- Session state is automatically cleared when the extension is locked
|
||||
- WebSocket connections are authenticated using the user's public key
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE)
|
1
hero_vault_extension/dist/assets/index-11057528.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
:root{font-family:Roboto,system-ui,sans-serif;line-height:1.5;font-weight:400;color-scheme:dark}body{margin:0;min-width:360px;min-height:520px;overflow-x:hidden}#root{width:100%;height:100%}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:rgba(255,255,255,.05);border-radius:3px}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.3)}
|
205
hero_vault_extension/dist/assets/index-b58c7e43.js
vendored
Normal file
1
hero_vault_extension/dist/assets/simple-background.ts-e63275e1.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
console.log("Background script initialized");let i=!1,e=null;chrome.runtime.onMessage.addListener((o,l,r)=>{if(console.log("Background received message:",o.type),o.type==="SESSION_STATUS")return r({active:i}),!0;if(o.type==="SESSION_UNLOCK")return i=!0,r({success:!0}),!0;if(o.type==="SESSION_LOCK")return i=!1,e&&(e.close(),e=null),r({success:!0}),!0;if(o.type==="CONNECT_WEBSOCKET"&&o.serverUrl&&o.publicKey){try{e&&e.close(),e=new WebSocket(o.serverUrl),e.onopen=()=>{console.log("WebSocket connection established"),e&&e.send(JSON.stringify({type:"IDENTIFY",publicKey:o.publicKey}))},e.onmessage=c=>{try{const t=JSON.parse(c.data);console.log("WebSocket message received:",t),chrome.runtime.sendMessage({type:"WEBSOCKET_MESSAGE",data:t}).catch(n=>{console.error("Failed to forward WebSocket message:",n)})}catch(t){console.error("Failed to parse WebSocket message:",t)}},e.onerror=c=>{console.error("WebSocket error:",c)},e.onclose=()=>{console.log("WebSocket connection closed"),e=null},r({success:!0})}catch(c){console.error("Failed to connect to WebSocket:",c),r({success:!1,error:c.message})}return!0}return o.type==="DISCONNECT_WEBSOCKET"?(e?(e.close(),e=null,r({success:!0})):r({success:!1,error:"No active WebSocket connection"}),!0):!1});chrome.notifications&&chrome.notifications.onClicked&&chrome.notifications.onClicked.addListener(o=>{chrome.action.openPopup()});
|
2
hero_vault_extension/dist/assets/wasm_app-bd9134aa.js
vendored
Normal file
61
hero_vault_extension/dist/background.js
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
// Background Service Worker for SAL Modular Cryptographic Extension
|
||||
// This is a simplified version that only handles messaging
|
||||
|
||||
console.log('Background script initialized');
|
||||
|
||||
// Store active WebSocket connection
|
||||
let activeWebSocket = null;
|
||||
let sessionActive = false;
|
||||
|
||||
// Listen for messages from popup or content scripts
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
console.log('Background received message:', message.type);
|
||||
|
||||
if (message.type === 'SESSION_STATUS') {
|
||||
sendResponse({ active: sessionActive });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_UNLOCK') {
|
||||
sessionActive = true;
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_LOCK') {
|
||||
sessionActive = false;
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'CONNECT_WEBSOCKET') {
|
||||
// Simplified WebSocket handling
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Initialize notification setup
|
||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
||||
// Open the extension popup when a notification is clicked
|
||||
chrome.action.openPopup();
|
||||
});
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@ -3,12 +3,12 @@
|
||||
<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">
|
||||
<title>Hero Vault</title>
|
||||
<script type="module" crossorigin src="/assets/index-b58c7e43.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-11057528.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="./main.jsx"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
26
hero_vault_extension/dist/manifest.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hero Vault",
|
||||
"version": "1.0.0",
|
||||
"description": "A secure browser extension for cryptographic operations and Rhai script execution",
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_title": "Hero Vault"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "service-worker-loader.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
}
|
||||
}
|
1
hero_vault_extension/dist/service-worker-loader.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import './assets/simple-background.ts-e63275e1.js';
|
@ -3,13 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Modular Vault Extension</title>
|
||||
|
||||
<script type="module" crossorigin src="/assets/popup.js"></script>
|
||||
<title>Hero Vault</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
4862
hero_vault_extension/package-lock.json
generated
Normal file
42
hero_vault_extension/package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "hero-vault-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Hero Vault - A secure browser extension for cryptographic operations",
|
||||
"scripts": {
|
||||
"dev": "node scripts/copy-wasm.js && vite",
|
||||
"build": "node scripts/copy-wasm.js && ([ \"$NO_TYPECHECK\" = \"true\" ] || tsc) && vite build",
|
||||
"watch": "node scripts/copy-wasm.js && tsc && vite build --watch",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,css,scss}\"",
|
||||
"copy-wasm": "node scripts/copy-wasm.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.3",
|
||||
"@mui/material": "^5.14.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.14.2",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.18",
|
||||
"@types/chrome": "^0.0.243",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"esbuild": "^0.25.4",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.3",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.64.1",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 454 B |
Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
26
hero_vault_extension/public/manifest.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Hero Vault",
|
||||
"version": "1.0.0",
|
||||
"description": "A secure browser extension for cryptographic operations and Rhai script execution",
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_title": "Hero Vault"
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "src/background/simple-background.ts",
|
||||
"type": "module"
|
||||
},
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
}
|
||||
}
|
85
hero_vault_extension/scripts/build-background.js
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Script to build the background script for the extension
|
||||
*/
|
||||
const { build } = require('esbuild');
|
||||
const { resolve } = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
async function buildBackground() {
|
||||
try {
|
||||
console.log('Building background script...');
|
||||
|
||||
// First, create a simplified background script that doesn't import WASM
|
||||
const backgroundContent = `
|
||||
// Background Service Worker for SAL Modular Cryptographic Extension
|
||||
// This is a simplified version that only handles messaging
|
||||
|
||||
console.log('Background script initialized');
|
||||
|
||||
// Store active WebSocket connection
|
||||
let activeWebSocket = null;
|
||||
let sessionActive = false;
|
||||
|
||||
// Listen for messages from popup or content scripts
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
console.log('Background received message:', message.type);
|
||||
|
||||
if (message.type === 'SESSION_STATUS') {
|
||||
sendResponse({ active: sessionActive });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_UNLOCK') {
|
||||
sessionActive = true;
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_LOCK') {
|
||||
sessionActive = false;
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'CONNECT_WEBSOCKET') {
|
||||
// Simplified WebSocket handling
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// Initialize notification setup
|
||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
||||
// Open the extension popup when a notification is clicked
|
||||
chrome.action.openPopup();
|
||||
});
|
||||
`;
|
||||
|
||||
// Write the simplified background script to a temporary file
|
||||
fs.writeFileSync(resolve(__dirname, '../dist/background.js'), backgroundContent);
|
||||
|
||||
console.log('Background script built successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error building background script:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
buildBackground();
|
33
hero_vault_extension/scripts/copy-wasm.js
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Script to copy WASM files from wasm_app/pkg to the extension build directory
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Source and destination paths
|
||||
const sourceDir = path.resolve(__dirname, '../../wasm_app/pkg');
|
||||
const destDir = path.resolve(__dirname, '../public/wasm');
|
||||
|
||||
// Create destination directory if it doesn't exist
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
console.log(`Created directory: ${destDir}`);
|
||||
}
|
||||
|
||||
// Copy all files from source to destination
|
||||
try {
|
||||
const files = fs.readdirSync(sourceDir);
|
||||
|
||||
files.forEach(file => {
|
||||
const sourcePath = path.join(sourceDir, file);
|
||||
const destPath = path.join(destDir, file);
|
||||
|
||||
fs.copyFileSync(sourcePath, destPath);
|
||||
console.log(`Copied: ${file}`);
|
||||
});
|
||||
|
||||
console.log('WASM files copied successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error copying WASM files:', error);
|
||||
process.exit(1);
|
||||
}
|
127
hero_vault_extension/src/App.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Container, Paper } from '@mui/material';
|
||||
import { Routes, Route, HashRouter } from 'react-router-dom';
|
||||
|
||||
// Import pages
|
||||
import HomePage from './pages/HomePage';
|
||||
import SessionPage from './pages/SessionPage';
|
||||
import KeypairPage from './pages/KeypairPage';
|
||||
import ScriptPage from './pages/ScriptPage';
|
||||
import SettingsPage from './pages/SettingsPage';
|
||||
import WebSocketPage from './pages/WebSocketPage';
|
||||
import CryptoPage from './pages/CryptoPage';
|
||||
|
||||
// Import components
|
||||
import Header from './components/Header';
|
||||
import Navigation from './components/Navigation';
|
||||
|
||||
// Import session state management
|
||||
import { useSessionStore } from './store/sessionStore';
|
||||
|
||||
function App() {
|
||||
const { checkSessionStatus, initWasm } = useSessionStore();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [wasmError, setWasmError] = useState<string | null>(null);
|
||||
|
||||
// Initialize WASM and check session status on mount
|
||||
useEffect(() => {
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// First initialize WASM module
|
||||
const wasmInitialized = await initWasm();
|
||||
|
||||
if (!wasmInitialized) {
|
||||
throw new Error('Failed to initialize WASM module');
|
||||
}
|
||||
|
||||
// Then check session status
|
||||
await checkSessionStatus();
|
||||
} catch (error) {
|
||||
console.error('Initialization error:', error);
|
||||
setWasmError((error as Error).message || 'Failed to initialize the extension');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
}, [checkSessionStatus, initWasm]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (wasmError) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
p: 3,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Paper sx={{ p: 3, maxWidth: 400 }}>
|
||||
<h6 style={{ color: 'red', marginBottom: '8px' }}>
|
||||
WASM Module Failed to Initialize
|
||||
</h6>
|
||||
<p style={{ marginBottom: '16px' }}>
|
||||
The WASM module could not be loaded. Please try reloading the extension.
|
||||
</p>
|
||||
<p style={{ fontSize: '0.875rem', color: 'gray' }}>
|
||||
Error: {wasmError} Please contact support if the problem persists.
|
||||
</p>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||
<Header />
|
||||
|
||||
<Container component="main" sx={{ flexGrow: 1, overflow: 'auto', py: 2 }}>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 2,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/session" element={<SessionPage />} />
|
||||
<Route path="/keypair" element={<KeypairPage />} />
|
||||
<Route path="/crypto" element={<CryptoPage />} />
|
||||
<Route path="/script" element={<ScriptPage />} />
|
||||
<Route path="/websocket" element={<WebSocketPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</Paper>
|
||||
</Container>
|
||||
|
||||
<Navigation />
|
||||
</Box>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
145
hero_vault_extension/src/background/index.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Background Service Worker for Hero Vault Extension
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Maintain WebSocket connections
|
||||
* - Handle incoming script requests
|
||||
* - Manage session state when popup is closed
|
||||
* - Provide messaging interface for popup/content scripts
|
||||
* - Initialize WASM module when extension starts
|
||||
*/
|
||||
|
||||
// Import WASM helper functions
|
||||
import { initWasm } from '../wasm/wasmHelper';
|
||||
|
||||
// Initialize WASM module when service worker starts
|
||||
initWasm().catch(error => {
|
||||
console.error('Failed to initialize WASM module:', error);
|
||||
});
|
||||
|
||||
// Store active WebSocket connection
|
||||
let activeWebSocket: WebSocket | null = null;
|
||||
let sessionActive = false;
|
||||
|
||||
// Listen for messages from popup or content scripts
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
if (message.type === 'SESSION_STATUS') {
|
||||
sendResponse({ active: sessionActive });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_UNLOCK') {
|
||||
sessionActive = true;
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_LOCK') {
|
||||
sessionActive = false;
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) {
|
||||
connectToWebSocket(message.serverUrl, message.publicKey)
|
||||
.then(success => sendResponse({ success }))
|
||||
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||
return true; // Indicates we'll respond asynchronously
|
||||
}
|
||||
|
||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Connect to a WebSocket server with the user's public key
|
||||
*/
|
||||
async function connectToWebSocket(serverUrl: string, publicKey: string): Promise<boolean> {
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const ws = new WebSocket(serverUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
// Send authentication message with public key
|
||||
ws.send(JSON.stringify({
|
||||
type: 'AUTH',
|
||||
publicKey
|
||||
}));
|
||||
|
||||
activeWebSocket = ws;
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
reject(new Error('Failed to connect to WebSocket server'));
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
activeWebSocket = null;
|
||||
console.log('WebSocket connection closed');
|
||||
};
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Handle incoming script requests
|
||||
if (data.type === 'SCRIPT_REQUEST') {
|
||||
// Notify the user of the script request
|
||||
chrome.notifications.create({
|
||||
type: 'basic',
|
||||
iconUrl: 'icons/icon128.png',
|
||||
title: 'Script Request',
|
||||
message: `Received script request: ${data.title || 'Untitled Script'}`,
|
||||
priority: 2
|
||||
});
|
||||
|
||||
// Store the script request for the popup to handle
|
||||
await chrome.storage.local.set({
|
||||
pendingScripts: [
|
||||
...(await chrome.storage.local.get('pendingScripts')).pendingScripts || [],
|
||||
{
|
||||
id: data.id,
|
||||
title: data.title || 'Untitled Script',
|
||||
description: data.description || '',
|
||||
script: data.script,
|
||||
tags: data.tags || [],
|
||||
timestamp: Date.now()
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize notification setup
|
||||
chrome.notifications.onClicked.addListener((_notificationId) => {
|
||||
// Open the extension popup when a notification is clicked
|
||||
chrome.action.openPopup();
|
||||
});
|
||||
|
||||
console.log('Hero Vault Extension background service worker initialized');
|
115
hero_vault_extension/src/background/simple-background.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Simplified Background Service Worker for Hero Vault Extension
|
||||
*
|
||||
* This is a version that doesn't use WASM to avoid service worker limitations
|
||||
* with dynamic imports. It only handles basic messaging between components.
|
||||
*/
|
||||
|
||||
console.log('Background script initialized');
|
||||
|
||||
// Store session state
|
||||
let sessionActive = false;
|
||||
let activeWebSocket: WebSocket | null = null;
|
||||
|
||||
// Listen for messages from popup or content scripts
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
console.log('Background received message:', message.type);
|
||||
|
||||
if (message.type === 'SESSION_STATUS') {
|
||||
sendResponse({ active: sessionActive });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_UNLOCK') {
|
||||
sessionActive = true;
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'SESSION_LOCK') {
|
||||
sessionActive = false;
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'CONNECT_WEBSOCKET' && message.serverUrl && message.publicKey) {
|
||||
// Simplified WebSocket handling
|
||||
try {
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
}
|
||||
|
||||
activeWebSocket = new WebSocket(message.serverUrl);
|
||||
|
||||
activeWebSocket.onopen = () => {
|
||||
console.log('WebSocket connection established');
|
||||
// Send public key to identify this client
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.send(JSON.stringify({
|
||||
type: 'IDENTIFY',
|
||||
publicKey: message.publicKey
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
activeWebSocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('WebSocket message received:', data);
|
||||
|
||||
// Forward message to popup
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'WEBSOCKET_MESSAGE',
|
||||
data
|
||||
}).catch(error => {
|
||||
console.error('Failed to forward WebSocket message:', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
activeWebSocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
activeWebSocket.onclose = () => {
|
||||
console.log('WebSocket connection closed');
|
||||
activeWebSocket = null;
|
||||
};
|
||||
|
||||
sendResponse({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to WebSocket:', error);
|
||||
sendResponse({ success: false, error: error.message });
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === 'DISCONNECT_WEBSOCKET') {
|
||||
if (activeWebSocket) {
|
||||
activeWebSocket.close();
|
||||
activeWebSocket = null;
|
||||
sendResponse({ success: true });
|
||||
} else {
|
||||
sendResponse({ success: false, error: 'No active WebSocket connection' });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we don't handle the message, return false
|
||||
return false;
|
||||
});
|
||||
|
||||
// Handle notifications if available
|
||||
if (chrome.notifications && chrome.notifications.onClicked) {
|
||||
chrome.notifications.onClicked.addListener((notificationId) => {
|
||||
// Open the extension popup when a notification is clicked
|
||||
chrome.action.openPopup();
|
||||
});
|
||||
}
|
97
hero_vault_extension/src/components/Header.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { AppBar, Toolbar, Typography, IconButton, Box, Chip } from '@mui/material';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
||||
import SignalWifiStatusbar4BarIcon from '@mui/icons-material/SignalWifiStatusbar4Bar';
|
||||
import SignalWifiOffIcon from '@mui/icons-material/SignalWifiOff';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
|
||||
const Header = () => {
|
||||
const {
|
||||
isSessionUnlocked,
|
||||
currentKeyspace,
|
||||
currentKeypair,
|
||||
isWebSocketConnected,
|
||||
lockSession
|
||||
} = useSessionStore();
|
||||
|
||||
const handleLockClick = async () => {
|
||||
if (isSessionUnlocked) {
|
||||
await lockSession();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="static" color="primary" elevation={0}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
Hero Vault
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{/* WebSocket connection status */}
|
||||
{isWebSocketConnected ? (
|
||||
<Chip
|
||||
icon={<SignalWifiStatusbar4BarIcon fontSize="small" />}
|
||||
label="Connected"
|
||||
size="small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
/>
|
||||
) : (
|
||||
<Chip
|
||||
icon={<SignalWifiOffIcon fontSize="small" />}
|
||||
label="Offline"
|
||||
size="small"
|
||||
color="default"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Session status */}
|
||||
{isSessionUnlocked ? (
|
||||
<Chip
|
||||
icon={<LockOpenIcon fontSize="small" />}
|
||||
label={currentKeyspace || 'Unlocked'}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
) : (
|
||||
<Chip
|
||||
icon={<LockIcon fontSize="small" />}
|
||||
label="Locked"
|
||||
size="small"
|
||||
color="error"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Current keypair */}
|
||||
{isSessionUnlocked && currentKeypair && (
|
||||
<Chip
|
||||
label={currentKeypair.name || currentKeypair.id}
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Lock button */}
|
||||
{isSessionUnlocked && (
|
||||
<IconButton
|
||||
edge="end"
|
||||
color="inherit"
|
||||
onClick={handleLockClick}
|
||||
size="small"
|
||||
aria-label="lock session"
|
||||
>
|
||||
<LockIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
130
hero_vault_extension/src/components/Navigation.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { useState } from 'react';
|
||||
import { BottomNavigation, BottomNavigationAction, Paper, Box, IconButton, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import VpnKeyIcon from '@mui/icons-material/VpnKey';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import WifiIcon from '@mui/icons-material/Wifi';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
|
||||
const Navigation = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { isSessionUnlocked } = useSessionStore();
|
||||
|
||||
// Get current path without leading slash
|
||||
const currentPath = location.pathname.substring(1) || 'home';
|
||||
|
||||
// State for the more menu
|
||||
const [moreAnchorEl, setMoreAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const isMoreMenuOpen = Boolean(moreAnchorEl);
|
||||
|
||||
const handleMoreClick = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
setMoreAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMoreClose = () => {
|
||||
setMoreAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleNavigation = (path: string) => {
|
||||
navigate(`/${path === 'home' ? '' : path}`);
|
||||
handleMoreClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{ position: 'static', bottom: 0, left: 0, right: 0 }}
|
||||
elevation={3}
|
||||
>
|
||||
<Box sx={{ display: 'flex', width: '100%' }}>
|
||||
<BottomNavigation
|
||||
showLabels
|
||||
value={currentPath}
|
||||
onChange={(_, newValue) => {
|
||||
navigate(`/${newValue === 'home' ? '' : newValue}`);
|
||||
}}
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
<BottomNavigationAction
|
||||
label="Home"
|
||||
value="home"
|
||||
icon={<HomeIcon />}
|
||||
/>
|
||||
|
||||
<BottomNavigationAction
|
||||
label="Keys"
|
||||
value="keypair"
|
||||
icon={<VpnKeyIcon />}
|
||||
disabled={!isSessionUnlocked}
|
||||
/>
|
||||
|
||||
<BottomNavigationAction
|
||||
label="Crypto"
|
||||
value="crypto"
|
||||
icon={<LockIcon />}
|
||||
disabled={!isSessionUnlocked}
|
||||
/>
|
||||
|
||||
<BottomNavigationAction
|
||||
label="More"
|
||||
value="more"
|
||||
icon={<MoreVertIcon />}
|
||||
onClick={handleMoreClick}
|
||||
/>
|
||||
</BottomNavigation>
|
||||
|
||||
<Menu
|
||||
anchorEl={moreAnchorEl}
|
||||
open={isMoreMenuOpen}
|
||||
onClose={handleMoreClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => handleNavigation('script')}
|
||||
disabled={!isSessionUnlocked}
|
||||
selected={currentPath === 'script'}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<CodeIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Scripts</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => handleNavigation('websocket')}
|
||||
disabled={!isSessionUnlocked}
|
||||
selected={currentPath === 'websocket'}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<WifiIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>WebSocket</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => handleNavigation('settings')}
|
||||
selected={currentPath === 'settings'}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Settings</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navigation;
|
38
hero_vault_extension/src/index.css
Normal file
@ -0,0 +1,38 @@
|
||||
:root {
|
||||
font-family: 'Roboto', system-ui, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 360px;
|
||||
min-height: 520px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
64
hero_vault_extension/src/main.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
// Create a dark theme for the extension
|
||||
const darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#6200ee',
|
||||
},
|
||||
secondary: {
|
||||
main: '#03dac6',
|
||||
},
|
||||
background: {
|
||||
default: '#121212',
|
||||
paper: '#1e1e1e',
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
|
||||
h1: {
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: '1.125rem',
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
textTransform: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
392
hero_vault_extension/src/pages/CryptoPage.tsx
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Cryptographic Operations Page
|
||||
*
|
||||
* This page provides a UI for:
|
||||
* - Encrypting/decrypting data using the keyspace's symmetric cipher
|
||||
* - Signing/verifying messages using the selected keypair
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { SyntheticEvent } from '../types';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Paper,
|
||||
Tabs,
|
||||
Tab,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
import { useCryptoStore } from '../store/cryptoStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const CryptoPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isSessionUnlocked, currentKeypair } = useSessionStore();
|
||||
const {
|
||||
encryptData,
|
||||
decryptData,
|
||||
signMessage,
|
||||
verifySignature,
|
||||
isEncrypting,
|
||||
isDecrypting,
|
||||
isSigning,
|
||||
isVerifying,
|
||||
error,
|
||||
clearError
|
||||
} = useCryptoStore();
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [copySuccess, setCopySuccess] = useState<string | null>(null);
|
||||
|
||||
// Encryption state
|
||||
const [plaintext, setPlaintext] = useState('');
|
||||
const [encryptedData, setEncryptedData] = useState('');
|
||||
|
||||
// Decryption state
|
||||
const [ciphertext, setCiphertext] = useState('');
|
||||
const [decryptedData, setDecryptedData] = useState('');
|
||||
|
||||
// Signing state
|
||||
const [messageToSign, setMessageToSign] = useState('');
|
||||
const [signature, setSignature] = useState('');
|
||||
|
||||
// Verification state
|
||||
const [messageToVerify, setMessageToVerify] = useState('');
|
||||
const [signatureToVerify, setSignatureToVerify] = useState('');
|
||||
const [isVerified, setIsVerified] = useState<boolean | null>(null);
|
||||
|
||||
// Redirect if not unlocked
|
||||
useEffect(() => {
|
||||
if (!isSessionUnlocked) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isSessionUnlocked, navigate]);
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent<Element, Event>, newValue: number) => {
|
||||
setActiveTab(newValue);
|
||||
clearError();
|
||||
setCopySuccess(null);
|
||||
};
|
||||
|
||||
const handleEncrypt = async () => {
|
||||
try {
|
||||
const result = await encryptData(plaintext);
|
||||
setEncryptedData(result);
|
||||
} catch (err) {
|
||||
// Error is already handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
const handleDecrypt = async () => {
|
||||
try {
|
||||
const result = await decryptData(ciphertext);
|
||||
setDecryptedData(result);
|
||||
} catch (err) {
|
||||
// Error is already handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
const handleSign = async () => {
|
||||
try {
|
||||
const result = await signMessage(messageToSign);
|
||||
setSignature(result);
|
||||
} catch (err) {
|
||||
// Error is already handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerify = async () => {
|
||||
try {
|
||||
const result = await verifySignature(messageToVerify, signatureToVerify);
|
||||
setIsVerified(result);
|
||||
} catch (err) {
|
||||
setIsVerified(false);
|
||||
// Error is already handled in the store
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => {
|
||||
setCopySuccess(`${label} copied to clipboard!`);
|
||||
setTimeout(() => setCopySuccess(null), 2000);
|
||||
},
|
||||
() => {
|
||||
setCopySuccess('Failed to copy!');
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!isSessionUnlocked) {
|
||||
return null; // Will redirect via useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>Cryptographic Operations</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{copySuccess && (
|
||||
<Alert severity="success" sx={{ mb: 2 }}>
|
||||
{copySuccess}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{/* Tabs with smaller width and scrollable */}
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{ minHeight: '48px' }}
|
||||
>
|
||||
<Tab label="Encrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
||||
<Tab label="Decrypt" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
||||
<Tab label="Sign" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
||||
<Tab label="Verify" sx={{ minWidth: '80px', minHeight: '48px', py: 0 }} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Content area with proper scrolling */}
|
||||
<Box sx={{ p: 2, flexGrow: 1, overflow: 'auto', height: 'calc(100% - 48px)' }}>
|
||||
{/* Encryption Tab */}
|
||||
{activeTab === 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>Encrypt Data</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Data will be encrypted using ChaCha20-Poly1305 with a key derived from your keyspace password.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Data to Encrypt"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={plaintext}
|
||||
onChange={(e) => setPlaintext(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleEncrypt}
|
||||
disabled={!plaintext || isEncrypting}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{isEncrypting ? <CircularProgress size={24} /> : 'Encrypt'}
|
||||
</Button>
|
||||
|
||||
{encryptedData && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle1">Encrypted Result</Typography>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<TextField
|
||||
label="Encrypted Data (Base64)"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={encryptedData}
|
||||
InputProps={{ readOnly: true }}
|
||||
margin="normal"
|
||||
/>
|
||||
<Tooltip title="Copy to clipboard">
|
||||
<IconButton
|
||||
sx={{ position: 'absolute', top: 8, right: 8 }}
|
||||
onClick={() => copyToClipboard(encryptedData, 'Encrypted data')}
|
||||
>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Decryption Tab */}
|
||||
{activeTab === 1 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>Decrypt Data</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Paste encrypted data (in Base64 format) to decrypt it using your keyspace password.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Encrypted Data (Base64)"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={ciphertext}
|
||||
onChange={(e) => setCiphertext(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleDecrypt}
|
||||
disabled={!ciphertext || isDecrypting}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{isDecrypting ? <CircularProgress size={24} /> : 'Decrypt'}
|
||||
</Button>
|
||||
|
||||
{decryptedData && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle1">Decrypted Result</Typography>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<TextField
|
||||
label="Decrypted Data"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={decryptedData}
|
||||
InputProps={{ readOnly: true }}
|
||||
margin="normal"
|
||||
/>
|
||||
<Tooltip title="Copy to clipboard">
|
||||
<IconButton
|
||||
sx={{ position: 'absolute', top: 8, right: 8 }}
|
||||
onClick={() => copyToClipboard(decryptedData, 'Decrypted data')}
|
||||
>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Signing Tab */}
|
||||
{activeTab === 2 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>Sign Message</Typography>
|
||||
|
||||
{!currentKeypair ? (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
Please select a keypair from the Keypair page before signing messages.
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
Signing with keypair: {currentKeypair.name || currentKeypair.id.substring(0, 8)}...
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Message to Sign"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={messageToSign}
|
||||
onChange={(e) => setMessageToSign(e.target.value)}
|
||||
margin="normal"
|
||||
disabled={!currentKeypair}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSign}
|
||||
disabled={!messageToSign || !currentKeypair || isSigning}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{isSigning ? <CircularProgress size={24} /> : 'Sign Message'}
|
||||
</Button>
|
||||
|
||||
{signature && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle1">Signature</Typography>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<TextField
|
||||
label="Signature (Hex)"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={signature}
|
||||
InputProps={{ readOnly: true }}
|
||||
margin="normal"
|
||||
/>
|
||||
<Tooltip title="Copy to clipboard">
|
||||
<IconButton
|
||||
sx={{ position: 'absolute', top: 8, right: 8 }}
|
||||
onClick={() => copyToClipboard(signature, 'Signature')}
|
||||
>
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Verification Tab */}
|
||||
{activeTab === 3 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>Verify Signature</Typography>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Verify that a message was signed by the currently selected keypair.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Message"
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={messageToVerify}
|
||||
onChange={(e) => setMessageToVerify(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextField
|
||||
label="Signature (Hex)"
|
||||
multiline
|
||||
rows={2}
|
||||
fullWidth
|
||||
value={signatureToVerify}
|
||||
onChange={(e) => setSignatureToVerify(e.target.value)}
|
||||
margin="normal"
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleVerify}
|
||||
disabled={!messageToVerify || !signatureToVerify || isVerifying}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{isVerifying ? <CircularProgress size={24} /> : 'Verify Signature'}
|
||||
</Button>
|
||||
|
||||
{isVerified !== null && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Alert severity={isVerified ? "success" : "error"}>
|
||||
{isVerified
|
||||
? "Signature is valid! The message was signed by the expected keypair."
|
||||
: "Invalid signature. The message may have been tampered with or signed by a different keypair."}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CryptoPage;
|
155
hero_vault_extension/src/pages/HomePage.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Card,
|
||||
CardContent,
|
||||
Stack,
|
||||
Alert,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
|
||||
const HomePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isSessionUnlocked, unlockSession, createKeyspace } = useSessionStore();
|
||||
|
||||
const [keyspace, setKeyspace] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState<'unlock' | 'create'>('unlock');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
let success = false;
|
||||
|
||||
if (mode === 'unlock') {
|
||||
success = await unlockSession(keyspace, password);
|
||||
} else {
|
||||
success = await createKeyspace(keyspace, password);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Navigate to keypair page on success
|
||||
navigate('/keypair');
|
||||
} else {
|
||||
setError(mode === 'unlock'
|
||||
? 'Failed to unlock keyspace. Check your password and try again.'
|
||||
: 'Failed to create keyspace. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'An unexpected error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionUnlocked) {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Welcome to Hero Vault
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" paragraph>
|
||||
Your session is unlocked. You can now use the extension features.
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} justifyContent="center" mt={3}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate('/keypair')}
|
||||
>
|
||||
Manage Keys
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => navigate('/script')}
|
||||
>
|
||||
Run Scripts
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 400, mx: 'auto', py: 2 }}>
|
||||
<Typography variant="h5" align="center" gutterBottom>
|
||||
Hero Vault
|
||||
</Typography>
|
||||
|
||||
<Card variant="outlined" sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{mode === 'unlock' ? 'Unlock Keyspace' : 'Create New Keyspace'}
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
label="Keyspace Name"
|
||||
value={keyspace}
|
||||
onChange={(e) => setKeyspace(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => setMode(mode === 'unlock' ? 'create' : 'unlock')}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{mode === 'unlock' ? 'Create New Keyspace' : 'Unlock Existing'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isLoading || !keyspace || !password}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
) : mode === 'unlock' ? (
|
||||
'Unlock'
|
||||
) : (
|
||||
'Create'
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
242
hero_vault_extension/src/pages/KeypairPage.tsx
Normal file
@ -0,0 +1,242 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
Divider,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
CircularProgress,
|
||||
Paper,
|
||||
Alert,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const KeypairPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
isSessionUnlocked,
|
||||
availableKeypairs,
|
||||
currentKeypair,
|
||||
listKeypairs,
|
||||
selectKeypair,
|
||||
createKeypair
|
||||
} = useSessionStore();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [newKeypairName, setNewKeypairName] = useState('');
|
||||
const [newKeypairType, setNewKeypairType] = useState('Secp256k1');
|
||||
const [newKeypairDescription, setNewKeypairDescription] = useState('');
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
// Redirect if not unlocked
|
||||
useEffect(() => {
|
||||
if (!isSessionUnlocked) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isSessionUnlocked, navigate]);
|
||||
|
||||
// Load keypairs on mount
|
||||
useEffect(() => {
|
||||
const loadKeypairs = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await listKeypairs();
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Failed to load keypairs');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionUnlocked) {
|
||||
loadKeypairs();
|
||||
}
|
||||
}, [isSessionUnlocked, listKeypairs]);
|
||||
|
||||
const handleSelectKeypair = async (keypairId: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await selectKeypair(keypairId);
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Failed to select keypair');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateKeypair = async () => {
|
||||
try {
|
||||
setIsCreating(true);
|
||||
setError(null);
|
||||
|
||||
await createKeypair(newKeypairType, {
|
||||
name: newKeypairName,
|
||||
description: newKeypairDescription
|
||||
});
|
||||
|
||||
setCreateDialogOpen(false);
|
||||
setNewKeypairName('');
|
||||
setNewKeypairDescription('');
|
||||
|
||||
// Refresh the list
|
||||
await listKeypairs();
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Failed to create keypair');
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isSessionUnlocked) {
|
||||
return null; // Will redirect via useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">Keypair Management</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Create New
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : availableKeypairs.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No keypairs found. Create your first keypair to get started.
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<List disablePadding>
|
||||
{availableKeypairs.map((keypair: any, index: number) => (
|
||||
<Box key={keypair.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem
|
||||
button
|
||||
selected={currentKeypair?.id === keypair.id}
|
||||
onClick={() => handleSelectKeypair(keypair.id)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{keypair.name || keypair.id}
|
||||
<Chip
|
||||
label={keypair.type}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{keypair.description || 'No description'}
|
||||
<br />
|
||||
Created: {new Date(keypair.createdAt).toLocaleString()}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{currentKeypair?.id === keypair.id && (
|
||||
<IconButton edge="end" disabled>
|
||||
<CheckIcon color="success" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Create Keypair Dialog */}
|
||||
<Dialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Create New Keypair</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
label="Name"
|
||||
value={newKeypairName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairName(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
disabled={isCreating}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Type</InputLabel>
|
||||
<Select
|
||||
value={newKeypairType}
|
||||
onChange={(e) => setNewKeypairType(e.target.value)}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<MenuItem value="Ed25519">Ed25519</MenuItem>
|
||||
<MenuItem value="Secp256k1">Secp256k1 (Ethereum)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Description"
|
||||
value={newKeypairDescription}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setNewKeypairDescription(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={2}
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateDialogOpen(false)} disabled={isCreating}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateKeypair}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={isCreating || !newKeypairName}
|
||||
>
|
||||
{isCreating ? <CircularProgress size={24} /> : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeypairPage;
|
557
hero_vault_extension/src/pages/ScriptPage.tsx
Normal file
@ -0,0 +1,557 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getChromeApi } from '../utils/chromeApi';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Paper,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
Divider,
|
||||
Tabs,
|
||||
Tab,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
// DeleteIcon removed as it's not used
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
|
||||
interface ScriptResult {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
script: string;
|
||||
result: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface PendingScript {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
script: string;
|
||||
tags: string[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const ScriptPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isSessionUnlocked, currentKeypair } = useSessionStore();
|
||||
|
||||
const [tabValue, setTabValue] = useState<number>(0);
|
||||
const [scriptInput, setScriptInput] = useState<string>('');
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
const [executionResult, setExecutionResult] = useState<string | null>(null);
|
||||
const [executionSuccess, setExecutionSuccess] = useState<boolean | null>(null);
|
||||
const [scriptResults, setScriptResults] = useState<ScriptResult[]>([]);
|
||||
const [pendingScripts, setPendingScripts] = useState<PendingScript[]>([]);
|
||||
const [selectedPendingScript, setSelectedPendingScript] = useState<PendingScript | null>(null);
|
||||
const [scriptDialogOpen, setScriptDialogOpen] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Redirect if not unlocked
|
||||
useEffect(() => {
|
||||
if (!isSessionUnlocked) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isSessionUnlocked, navigate]);
|
||||
|
||||
// Load pending scripts from storage
|
||||
useEffect(() => {
|
||||
const loadPendingScripts = async () => {
|
||||
try {
|
||||
const chromeApi = getChromeApi();
|
||||
const data = await chromeApi.storage.local.get('pendingScripts');
|
||||
if (data.pendingScripts) {
|
||||
setPendingScripts(data.pendingScripts);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load pending scripts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionUnlocked) {
|
||||
loadPendingScripts();
|
||||
}
|
||||
}, [isSessionUnlocked]);
|
||||
|
||||
// Load script history from storage
|
||||
useEffect(() => {
|
||||
const loadScriptResults = async () => {
|
||||
try {
|
||||
const chromeApi = getChromeApi();
|
||||
const data = await chromeApi.storage.local.get('scriptResults');
|
||||
if (data.scriptResults) {
|
||||
setScriptResults(data.scriptResults);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load script results:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionUnlocked) {
|
||||
loadScriptResults();
|
||||
}
|
||||
}, [isSessionUnlocked]);
|
||||
|
||||
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleExecuteScript = async () => {
|
||||
if (!scriptInput.trim()) return;
|
||||
|
||||
setIsExecuting(true);
|
||||
setError(null);
|
||||
setExecutionResult(null);
|
||||
setExecutionSuccess(null);
|
||||
|
||||
try {
|
||||
// Call the WASM run_rhai function via our store
|
||||
const result = await useSessionStore.getState().executeScript(scriptInput);
|
||||
|
||||
setExecutionResult(result);
|
||||
setExecutionSuccess(true);
|
||||
|
||||
// Save to history
|
||||
const newResult: ScriptResult = {
|
||||
id: `script-${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
script: scriptInput,
|
||||
result,
|
||||
success: true
|
||||
};
|
||||
|
||||
const updatedResults = [newResult, ...scriptResults].slice(0, 20); // Keep last 20
|
||||
setScriptResults(updatedResults);
|
||||
|
||||
// Save to storage
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.storage.local.set({ scriptResults: updatedResults });
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Failed to execute script');
|
||||
setExecutionSuccess(false);
|
||||
setExecutionResult('Execution failed');
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewPendingScript = (script: PendingScript) => {
|
||||
setSelectedPendingScript(script);
|
||||
setScriptDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleApprovePendingScript = async () => {
|
||||
if (!selectedPendingScript) return;
|
||||
|
||||
setScriptDialogOpen(false);
|
||||
setScriptInput(selectedPendingScript.script);
|
||||
setTabValue(0); // Switch to execute tab
|
||||
|
||||
// Remove from pending list
|
||||
const updatedPendingScripts = pendingScripts.filter(
|
||||
script => script.id !== selectedPendingScript.id
|
||||
);
|
||||
|
||||
setPendingScripts(updatedPendingScripts);
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts });
|
||||
setSelectedPendingScript(null);
|
||||
};
|
||||
|
||||
const handleRejectPendingScript = async () => {
|
||||
if (!selectedPendingScript) return;
|
||||
|
||||
// Remove from pending list
|
||||
const updatedPendingScripts = pendingScripts.filter(
|
||||
script => script.id !== selectedPendingScript.id
|
||||
);
|
||||
|
||||
setPendingScripts(updatedPendingScripts);
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.storage.local.set({ pendingScripts: updatedPendingScripts });
|
||||
|
||||
setScriptDialogOpen(false);
|
||||
setSelectedPendingScript(null);
|
||||
};
|
||||
|
||||
const handleClearHistory = async () => {
|
||||
setScriptResults([]);
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.storage.local.set({ scriptResults: [] });
|
||||
};
|
||||
|
||||
if (!isSessionUnlocked) {
|
||||
return null; // Will redirect via useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={handleTabChange}
|
||||
aria-label="script tabs"
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{ minHeight: '48px' }}
|
||||
>
|
||||
<Tab label="Execute" sx={{ minHeight: '48px', py: 0 }} />
|
||||
<Tab
|
||||
label={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
Pending
|
||||
{pendingScripts.length > 0 && (
|
||||
<Chip
|
||||
label={pendingScripts.length}
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{ ml: 1 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
sx={{ minHeight: '48px', py: 0 }}
|
||||
/>
|
||||
<Tab label="History" sx={{ minHeight: '48px', py: 0 }} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Execute Tab */}
|
||||
{tabValue === 0 && (
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
height: 'calc(100% - 48px)' // Subtract tab height
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
height: '100%',
|
||||
pb: 2 // Add padding at bottom for scrolling
|
||||
}}>
|
||||
{!currentKeypair && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
No keypair selected. Select a keypair to enable script execution with signing capabilities.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Rhai Script"
|
||||
multiline
|
||||
rows={6} // Reduced from 8 to leave more space for results
|
||||
value={scriptInput}
|
||||
onChange={(e) => setScriptInput(e.target.value)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
placeholder="Enter your Rhai script here..."
|
||||
sx={{ mb: 2 }}
|
||||
disabled={isExecuting}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PlayArrowIcon />}
|
||||
onClick={handleExecuteScript}
|
||||
disabled={isExecuting || !scriptInput.trim()}
|
||||
>
|
||||
{isExecuting ? <CircularProgress size={24} /> : 'Execute'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{executionResult && (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: executionSuccess ? 'success.dark' : 'error.dark',
|
||||
color: 'white',
|
||||
overflowY: 'auto',
|
||||
mb: 2, // Add margin at bottom
|
||||
minHeight: '100px', // Ensure minimum height for visibility
|
||||
maxHeight: '200px' // Limit maximum height
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Execution Result:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="pre"
|
||||
sx={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
>
|
||||
{executionResult}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Pending Scripts Tab */}
|
||||
{tabValue === 1 && (
|
||||
<Box sx={{ p: 2, flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
{pendingScripts.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No pending scripts. Incoming scripts from connected WebSocket servers will appear here.
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<List disablePadding>
|
||||
{pendingScripts.map((script, index) => (
|
||||
<Box key={script.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={script.title}
|
||||
secondary={
|
||||
<>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{script.description || 'No description'}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{script.tags.map(tag => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
size="small"
|
||||
color={tag === 'remote' ? 'secondary' : 'primary'}
|
||||
variant="outlined"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() => handleViewPendingScript(script)}
|
||||
aria-label="view script"
|
||||
>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* History Tab */}
|
||||
{tabValue === 2 && (
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
height: 'calc(100% - 48px)' // Subtract tab height
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
height: '100%',
|
||||
pb: 2 // Add padding at bottom for scrolling
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 2 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={handleClearHistory}
|
||||
disabled={scriptResults.length === 0}
|
||||
>
|
||||
Clear History
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{scriptResults.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No script execution history yet.
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<List disablePadding>
|
||||
{scriptResults.map((result, index) => (
|
||||
<Box key={result.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="subtitle2">
|
||||
{new Date(result.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={result.success ? 'Success' : 'Failed'}
|
||||
size="small"
|
||||
color={result.success ? 'success' : 'error'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '280px'
|
||||
}}
|
||||
>
|
||||
{result.script}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={() => {
|
||||
setScriptInput(result.script);
|
||||
setTabValue(0);
|
||||
}}
|
||||
aria-label="reuse script"
|
||||
>
|
||||
<PlayArrowIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Pending Script Dialog */}
|
||||
<Dialog
|
||||
open={scriptDialogOpen}
|
||||
onClose={() => setScriptDialogOpen(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
{selectedPendingScript?.title || 'Script Details'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{selectedPendingScript && (
|
||||
<>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Description:
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
{selectedPendingScript.description || 'No description provided'}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
{selectedPendingScript.tags.map(tag => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
size="small"
|
||||
color={tag === 'remote' ? 'secondary' : 'primary'}
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Script Content:
|
||||
</Typography>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2,
|
||||
bgcolor: 'background.paper',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="pre"
|
||||
sx={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
>
|
||||
{selectedPendingScript.script}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
<Typography variant="body2">
|
||||
{selectedPendingScript.tags.includes('remote')
|
||||
? 'This is a remote script. If approved, your signature will be sent to the server and the script may execute remotely.'
|
||||
: 'This script will execute locally in your browser extension if approved.'}
|
||||
</Typography>
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={handleRejectPendingScript}
|
||||
color="error"
|
||||
variant="outlined"
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleApprovePendingScript}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
>
|
||||
Approve
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptPage;
|
191
hero_vault_extension/src/pages/SessionPage.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Paper,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import SecurityIcon from '@mui/icons-material/Security';
|
||||
// HistoryIcon removed as it's not used
|
||||
|
||||
interface SessionActivity {
|
||||
id: string;
|
||||
action: string;
|
||||
timestamp: number;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
const SessionPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
isSessionUnlocked,
|
||||
currentKeyspace,
|
||||
currentKeypair,
|
||||
lockSession
|
||||
} = useSessionStore();
|
||||
|
||||
const [sessionActivities, setSessionActivities] = useState<SessionActivity[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Redirect if not unlocked
|
||||
useEffect(() => {
|
||||
if (!isSessionUnlocked) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isSessionUnlocked, navigate]);
|
||||
|
||||
// Load session activities from storage
|
||||
useEffect(() => {
|
||||
const loadSessionActivities = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await chrome.storage.local.get('sessionActivities');
|
||||
if (data.sessionActivities) {
|
||||
setSessionActivities(data.sessionActivities);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load session activities:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionUnlocked) {
|
||||
loadSessionActivities();
|
||||
}
|
||||
}, [isSessionUnlocked]);
|
||||
|
||||
const handleLockSession = async () => {
|
||||
try {
|
||||
await lockSession();
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
console.error('Failed to lock session:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isSessionUnlocked) {
|
||||
return null; // Will redirect via useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Session Management
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
Current Keyspace
|
||||
</Typography>
|
||||
<Typography variant="h5" component="div">
|
||||
{currentKeyspace || 'None'}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Typography color="text.secondary" gutterBottom>
|
||||
Selected Keypair
|
||||
</Typography>
|
||||
<Typography variant="h5" component="div">
|
||||
{currentKeypair?.name || currentKeypair?.id || 'None'}
|
||||
</Typography>
|
||||
{currentKeypair && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Type: {currentKeypair.type}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="subtitle1">
|
||||
Session Activity
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<LockIcon />}
|
||||
onClick={handleLockSession}
|
||||
>
|
||||
Lock Session
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : sessionActivities.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No session activity recorded yet.
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<List disablePadding>
|
||||
{sessionActivities.map((activity, index) => (
|
||||
<Box key={activity.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="subtitle2">
|
||||
{activity.action}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{new Date(activity.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
{activity.details && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{activity.details}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Alert severity="info" icon={<SecurityIcon />}>
|
||||
Your session is active. All cryptographic operations and script executions require explicit approval.
|
||||
</Alert>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionPage;
|
246
hero_vault_extension/src/pages/SettingsPage.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Switch,
|
||||
// FormControlLabel removed as it's not used
|
||||
Divider,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Alert,
|
||||
Snackbar
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
|
||||
interface Settings {
|
||||
darkMode: boolean;
|
||||
autoLockTimeout: number; // minutes
|
||||
confirmCryptoOperations: boolean;
|
||||
showScriptNotifications: boolean;
|
||||
}
|
||||
|
||||
const SettingsPage = () => {
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
darkMode: true,
|
||||
autoLockTimeout: 15,
|
||||
confirmCryptoOperations: true,
|
||||
showScriptNotifications: true
|
||||
});
|
||||
|
||||
const [clearDataDialogOpen, setClearDataDialogOpen] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState('');
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
|
||||
// Load settings from storage
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const data = await chrome.storage.local.get('settings');
|
||||
if (data.settings) {
|
||||
setSettings(data.settings);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load settings:', err);
|
||||
}
|
||||
};
|
||||
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
// Save settings when changed
|
||||
const handleSettingChange = (key: keyof Settings, value: boolean | number) => {
|
||||
const updatedSettings = { ...settings, [key]: value };
|
||||
setSettings(updatedSettings);
|
||||
|
||||
// Save to storage
|
||||
chrome.storage.local.set({ settings: updatedSettings })
|
||||
.then(() => {
|
||||
setSnackbarMessage('Settings saved');
|
||||
setSnackbarOpen(true);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to save settings:', err);
|
||||
setSnackbarMessage('Failed to save settings');
|
||||
setSnackbarOpen(true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleClearAllData = () => {
|
||||
if (confirmText !== 'CLEAR ALL DATA') {
|
||||
setSnackbarMessage('Please type the confirmation text exactly');
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all extension data
|
||||
chrome.storage.local.clear()
|
||||
.then(() => {
|
||||
setSnackbarMessage('All data cleared successfully');
|
||||
setSnackbarOpen(true);
|
||||
setClearDataDialogOpen(false);
|
||||
setConfirmText('');
|
||||
|
||||
// Reset settings to defaults
|
||||
setSettings({
|
||||
darkMode: true,
|
||||
autoLockTimeout: 15,
|
||||
confirmCryptoOperations: true,
|
||||
showScriptNotifications: true
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to clear data:', err);
|
||||
setSnackbarMessage('Failed to clear data');
|
||||
setSnackbarOpen(true);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Settings
|
||||
</Typography>
|
||||
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<List disablePadding>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Dark Mode"
|
||||
secondary="Use dark theme for the extension"
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={settings.darkMode}
|
||||
onChange={(e) => handleSettingChange('darkMode', e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Auto-Lock Timeout"
|
||||
secondary={`Automatically lock session after ${settings.autoLockTimeout} minutes of inactivity`}
|
||||
/>
|
||||
<Box sx={{ width: 120 }}>
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
value={settings.autoLockTimeout}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value);
|
||||
if (!isNaN(value) && value >= 1) {
|
||||
handleSettingChange('autoLockTimeout', value);
|
||||
}
|
||||
}}
|
||||
InputProps={{ inputProps: { min: 1, max: 60 } }}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Confirm Cryptographic Operations"
|
||||
secondary="Always ask for confirmation before signing or encrypting"
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={settings.confirmCryptoOperations}
|
||||
onChange={(e) => handleSettingChange('confirmCryptoOperations', e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Script Notifications"
|
||||
secondary="Show notifications when new scripts are received"
|
||||
/>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={settings.showScriptNotifications}
|
||||
onChange={(e) => handleSettingChange('showScriptNotifications', e.target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<InfoIcon />}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
The extension stores all cryptographic keys in encrypted form. Your password is never stored and is only kept in memory while the session is unlocked.
|
||||
</Typography>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={() => setClearDataDialogOpen(true)}
|
||||
fullWidth
|
||||
>
|
||||
Clear All Data
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Clear Data Confirmation Dialog */}
|
||||
<Dialog open={clearDataDialogOpen} onClose={() => setClearDataDialogOpen(false)}>
|
||||
<DialogTitle>Clear All Extension Data</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" paragraph>
|
||||
This will permanently delete all your keyspaces, keypairs, and settings. This action cannot be undone.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="error" paragraph>
|
||||
Type "CLEAR ALL DATA" to confirm:
|
||||
</Typography>
|
||||
<TextField
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
placeholder="CLEAR ALL DATA"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setClearDataDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClearAllData}
|
||||
color="error"
|
||||
disabled={confirmText !== 'CLEAR ALL DATA'}
|
||||
>
|
||||
Clear All Data
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Snackbar for notifications */}
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
message={snackbarMessage}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
248
hero_vault_extension/src/pages/WebSocketPage.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Paper,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSessionStore } from '../store/sessionStore';
|
||||
|
||||
interface ConnectionHistory {
|
||||
id: string;
|
||||
url: string;
|
||||
timestamp: number;
|
||||
status: 'connected' | 'disconnected';
|
||||
}
|
||||
|
||||
const WebSocketPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
isSessionUnlocked,
|
||||
currentKeypair,
|
||||
isWebSocketConnected,
|
||||
webSocketUrl,
|
||||
connectWebSocket,
|
||||
disconnectWebSocket
|
||||
} = useSessionStore();
|
||||
|
||||
const [serverUrl, setServerUrl] = useState('');
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [connectionHistory, setConnectionHistory] = useState<ConnectionHistory[]>([]);
|
||||
|
||||
// Redirect if not unlocked
|
||||
useEffect(() => {
|
||||
if (!isSessionUnlocked) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [isSessionUnlocked, navigate]);
|
||||
|
||||
// Load connection history from storage
|
||||
useEffect(() => {
|
||||
const loadConnectionHistory = async () => {
|
||||
try {
|
||||
const data = await chrome.storage.local.get('connectionHistory');
|
||||
if (data.connectionHistory) {
|
||||
setConnectionHistory(data.connectionHistory);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load connection history:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (isSessionUnlocked) {
|
||||
loadConnectionHistory();
|
||||
}
|
||||
}, [isSessionUnlocked]);
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!serverUrl.trim() || !currentKeypair) return;
|
||||
|
||||
setIsConnecting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const success = await connectWebSocket(serverUrl);
|
||||
|
||||
if (success) {
|
||||
// Add to connection history
|
||||
const newConnection: ConnectionHistory = {
|
||||
id: `conn-${Date.now()}`,
|
||||
url: serverUrl,
|
||||
timestamp: Date.now(),
|
||||
status: 'connected'
|
||||
};
|
||||
|
||||
const updatedHistory = [newConnection, ...connectionHistory].slice(0, 10); // Keep last 10
|
||||
setConnectionHistory(updatedHistory);
|
||||
|
||||
// Save to storage
|
||||
await chrome.storage.local.set({ connectionHistory: updatedHistory });
|
||||
} else {
|
||||
throw new Error('Failed to connect to WebSocket server');
|
||||
}
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Failed to connect to WebSocket server');
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
const success = await disconnectWebSocket();
|
||||
|
||||
if (success && webSocketUrl) {
|
||||
// Update connection history
|
||||
const updatedHistory = connectionHistory.map(conn =>
|
||||
conn.url === webSocketUrl && conn.status === 'connected'
|
||||
? { ...conn, status: 'disconnected' }
|
||||
: conn
|
||||
);
|
||||
|
||||
setConnectionHistory(updatedHistory);
|
||||
|
||||
// Save to storage
|
||||
await chrome.storage.local.set({ connectionHistory: updatedHistory });
|
||||
}
|
||||
} catch (err) {
|
||||
setError((err as Error).message || 'Failed to disconnect from WebSocket server');
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickConnect = (url: string) => {
|
||||
setServerUrl(url);
|
||||
// Don't auto-connect to avoid unexpected connections
|
||||
};
|
||||
|
||||
if (!isSessionUnlocked) {
|
||||
return null; // Will redirect via useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
WebSocket Connection
|
||||
</Typography>
|
||||
|
||||
{!currentKeypair && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
No keypair selected. Select a keypair before connecting to a WebSocket server.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2, mb: 2 }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Connection Status:
|
||||
</Typography>
|
||||
<Chip
|
||||
label={isWebSocketConnected ? 'Connected' : 'Disconnected'}
|
||||
color={isWebSocketConnected ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
{isWebSocketConnected && webSocketUrl && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
Connected to: {webSocketUrl}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
label="WebSocket Server URL"
|
||||
placeholder="wss://example.com/ws"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
fullWidth
|
||||
disabled={isConnecting || isWebSocketConnected || !currentKeypair}
|
||||
/>
|
||||
|
||||
{isWebSocketConnected ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleDisconnect}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleConnect}
|
||||
disabled={isConnecting || !serverUrl.trim() || !currentKeypair}
|
||||
>
|
||||
{isConnecting ? <CircularProgress size={24} /> : 'Connect'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
Connection History
|
||||
</Typography>
|
||||
|
||||
{connectionHistory.length === 0 ? (
|
||||
<Paper sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
No connection history yet.
|
||||
</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper variant="outlined" sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<List disablePadding>
|
||||
{connectionHistory.map((conn, index) => (
|
||||
<Box key={conn.id}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem
|
||||
button
|
||||
onClick={() => handleQuickConnect(conn.url)}
|
||||
disabled={isWebSocketConnected}
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="subtitle2">
|
||||
{conn.url}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={conn.status}
|
||||
size="small"
|
||||
color={conn.status === 'connected' ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{new Date(conn.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketPage;
|
144
hero_vault_extension/src/store/cryptoStore.ts
Normal file
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Crypto Store for Hero Vault Extension
|
||||
*
|
||||
* This store manages cryptographic operations such as:
|
||||
* - Encryption/decryption using the keyspace's symmetric cipher
|
||||
* - Signing/verification using the selected keypair
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { getWasmModule, stringToUint8Array, uint8ArrayToString } from '../wasm/wasmHelper';
|
||||
|
||||
// Helper functions for Unicode-safe base64 encoding/decoding
|
||||
function base64Encode(data: Uint8Array): string {
|
||||
// Convert binary data to a string that only uses the low 8 bits of each character
|
||||
const binaryString = Array.from(data)
|
||||
.map(byte => String.fromCharCode(byte))
|
||||
.join('');
|
||||
|
||||
// Use btoa on the binary string
|
||||
return btoa(binaryString);
|
||||
}
|
||||
|
||||
function base64Decode(base64: string): Uint8Array {
|
||||
// Decode base64 to binary string
|
||||
const binaryString = atob(base64);
|
||||
|
||||
// Convert binary string to Uint8Array
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
interface CryptoState {
|
||||
// State
|
||||
isEncrypting: boolean;
|
||||
isDecrypting: boolean;
|
||||
isSigning: boolean;
|
||||
isVerifying: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
encryptData: (data: string) => Promise<string>;
|
||||
decryptData: (encrypted: string) => Promise<string>;
|
||||
signMessage: (message: string) => Promise<string>;
|
||||
verifySignature: (message: string, signature: string) => Promise<boolean>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useCryptoStore = create<CryptoState>()((set, get) => ({
|
||||
isEncrypting: false,
|
||||
isDecrypting: false,
|
||||
isSigning: false,
|
||||
isVerifying: false,
|
||||
error: null,
|
||||
|
||||
encryptData: async (data: string) => {
|
||||
try {
|
||||
set({ isEncrypting: true, error: null });
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Convert input to Uint8Array
|
||||
const dataBytes = stringToUint8Array(data);
|
||||
|
||||
// Encrypt the data
|
||||
const encrypted = await wasmModule.encrypt_data(dataBytes);
|
||||
|
||||
// Convert result to base64 for storage/display using our Unicode-safe function
|
||||
const encryptedBase64 = base64Encode(encrypted);
|
||||
|
||||
return encryptedBase64;
|
||||
} catch (error) {
|
||||
set({ error: (error as Error).message || 'Failed to encrypt data' });
|
||||
throw error;
|
||||
} finally {
|
||||
set({ isEncrypting: false });
|
||||
}
|
||||
},
|
||||
|
||||
decryptData: async (encrypted: string) => {
|
||||
try {
|
||||
set({ isDecrypting: true, error: null });
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Convert input from base64 using our Unicode-safe function
|
||||
const encryptedBytes = base64Decode(encrypted);
|
||||
|
||||
// Decrypt the data
|
||||
const decrypted = await wasmModule.decrypt_data(encryptedBytes);
|
||||
|
||||
// Convert result to string
|
||||
return uint8ArrayToString(decrypted);
|
||||
} catch (error) {
|
||||
set({ error: (error as Error).message || 'Failed to decrypt data' });
|
||||
throw error;
|
||||
} finally {
|
||||
set({ isDecrypting: false });
|
||||
}
|
||||
},
|
||||
|
||||
signMessage: async (message: string) => {
|
||||
try {
|
||||
set({ isSigning: true, error: null });
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Convert message to Uint8Array
|
||||
const messageBytes = stringToUint8Array(message);
|
||||
|
||||
// Sign the message
|
||||
const signature = await wasmModule.sign(messageBytes);
|
||||
|
||||
return signature;
|
||||
} catch (error) {
|
||||
set({ error: (error as Error).message || 'Failed to sign message' });
|
||||
throw error;
|
||||
} finally {
|
||||
set({ isSigning: false });
|
||||
}
|
||||
},
|
||||
|
||||
verifySignature: async (message: string, signature: string) => {
|
||||
try {
|
||||
set({ isVerifying: true, error: null });
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Convert inputs
|
||||
const messageBytes = stringToUint8Array(message);
|
||||
|
||||
// Verify the signature
|
||||
const isValid = await wasmModule.verify(messageBytes, signature);
|
||||
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
set({ error: (error as Error).message || 'Failed to verify signature' });
|
||||
throw error;
|
||||
} finally {
|
||||
set({ isVerifying: false });
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null })
|
||||
}));
|
416
hero_vault_extension/src/store/sessionStore.ts
Normal file
@ -0,0 +1,416 @@
|
||||
import { create } from 'zustand';
|
||||
import { getWasmModule, stringToUint8Array } from '../wasm/wasmHelper';
|
||||
import { getChromeApi } from '../utils/chromeApi';
|
||||
|
||||
// Import Chrome types
|
||||
/// <reference types="chrome" />
|
||||
|
||||
interface KeypairMetadata {
|
||||
id: string;
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface SessionState {
|
||||
isSessionUnlocked: boolean;
|
||||
currentKeyspace: string | null;
|
||||
currentKeypair: KeypairMetadata | null;
|
||||
availableKeypairs: KeypairMetadata[];
|
||||
isWebSocketConnected: boolean;
|
||||
webSocketUrl: string | null;
|
||||
isWasmLoaded: boolean;
|
||||
|
||||
// Actions
|
||||
initWasm: () => Promise<boolean>;
|
||||
checkSessionStatus: () => Promise<boolean>;
|
||||
unlockSession: (keyspace: string, password: string) => Promise<boolean>;
|
||||
lockSession: () => Promise<boolean>;
|
||||
createKeyspace: (keyspace: string, password: string) => Promise<boolean>;
|
||||
listKeypairs: () => Promise<KeypairMetadata[]>;
|
||||
selectKeypair: (keypairId: string) => Promise<boolean>;
|
||||
createKeypair: (type: string, metadata?: Record<string, any>) => Promise<string>;
|
||||
connectWebSocket: (url: string) => Promise<boolean>;
|
||||
disconnectWebSocket: () => Promise<boolean>;
|
||||
executeScript: (script: string) => Promise<string>;
|
||||
signMessage: (message: string) => Promise<string>;
|
||||
}
|
||||
|
||||
// Create the store
|
||||
export const useSessionStore = create<SessionState>((set: any, get: any) => ({
|
||||
isSessionUnlocked: false,
|
||||
currentKeyspace: null,
|
||||
currentKeypair: null,
|
||||
availableKeypairs: [],
|
||||
isWebSocketConnected: false,
|
||||
webSocketUrl: null,
|
||||
isWasmLoaded: false,
|
||||
|
||||
// Initialize WASM module
|
||||
initWasm: async () => {
|
||||
try {
|
||||
set({ isWasmLoaded: true });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize WASM module:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Check if a session is currently active
|
||||
checkSessionStatus: async () => {
|
||||
try {
|
||||
// First check with the background service worker
|
||||
const chromeApi = getChromeApi();
|
||||
const response = await chromeApi.runtime.sendMessage({ type: 'SESSION_STATUS' });
|
||||
|
||||
if (response && response.active) {
|
||||
// If session is active in the background, check with WASM
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
const isUnlocked = wasmModule.is_unlocked();
|
||||
|
||||
if (isUnlocked) {
|
||||
// Get current keypair metadata if available
|
||||
try {
|
||||
const keypairMetadata = await wasmModule.current_keypair_metadata();
|
||||
const parsedMetadata = JSON.parse(keypairMetadata);
|
||||
|
||||
set({
|
||||
isSessionUnlocked: true,
|
||||
currentKeypair: parsedMetadata
|
||||
});
|
||||
|
||||
// Load keypairs
|
||||
await get().listKeypairs();
|
||||
} catch (e) {
|
||||
// No keypair selected, but session is unlocked
|
||||
set({ isSessionUnlocked: true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (wasmError) {
|
||||
console.error('WASM error checking session status:', wasmError);
|
||||
}
|
||||
}
|
||||
|
||||
set({ isSessionUnlocked: false });
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to check session status:', error);
|
||||
set({ isSessionUnlocked: false });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Unlock a session with keyspace and password
|
||||
unlockSession: async (keyspace: string, password: string) => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Call the WASM init_session function
|
||||
await wasmModule.init_session(keyspace, password);
|
||||
|
||||
// Initialize Rhai environment
|
||||
wasmModule.init_rhai_env();
|
||||
|
||||
// Notify background service worker
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' });
|
||||
|
||||
set({
|
||||
isSessionUnlocked: true,
|
||||
currentKeyspace: keyspace,
|
||||
currentKeypair: null
|
||||
});
|
||||
|
||||
// Load keypairs after unlocking
|
||||
const keypairs = await get().listKeypairs();
|
||||
set({ availableKeypairs: keypairs });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to unlock session:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Lock the current session
|
||||
lockSession: async () => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Call the WASM lock_session function
|
||||
wasmModule.lock_session();
|
||||
|
||||
// Notify background service worker
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.runtime.sendMessage({ type: 'SESSION_LOCK' });
|
||||
|
||||
set({
|
||||
isSessionUnlocked: false,
|
||||
currentKeyspace: null,
|
||||
currentKeypair: null,
|
||||
availableKeypairs: [],
|
||||
isWebSocketConnected: false,
|
||||
webSocketUrl: null
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to lock session:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Create a new keyspace
|
||||
createKeyspace: async (keyspace: string, password: string) => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Call the WASM create_keyspace function
|
||||
await wasmModule.create_keyspace(keyspace, password);
|
||||
|
||||
// Initialize Rhai environment
|
||||
wasmModule.init_rhai_env();
|
||||
|
||||
// Notify background service worker
|
||||
const chromeApi = getChromeApi();
|
||||
await chromeApi.runtime.sendMessage({ type: 'SESSION_UNLOCK' });
|
||||
|
||||
set({
|
||||
isSessionUnlocked: true,
|
||||
currentKeyspace: keyspace,
|
||||
currentKeypair: null,
|
||||
availableKeypairs: []
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to create keyspace:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// List all keypairs in the current keyspace
|
||||
listKeypairs: async () => {
|
||||
try {
|
||||
console.log('Listing keypairs from WASM module');
|
||||
const wasmModule = await getWasmModule();
|
||||
console.log('WASM module loaded, calling list_keypairs');
|
||||
|
||||
// Call the WASM list_keypairs function
|
||||
let keypairsJson;
|
||||
try {
|
||||
keypairsJson = await wasmModule.list_keypairs();
|
||||
console.log('Raw keypairs JSON from WASM:', keypairsJson);
|
||||
} catch (listError) {
|
||||
console.error('Error calling list_keypairs:', listError);
|
||||
throw new Error(`Failed to list keypairs: ${listError.message || listError}`);
|
||||
}
|
||||
|
||||
let keypairs;
|
||||
try {
|
||||
keypairs = JSON.parse(keypairsJson);
|
||||
console.log('Parsed keypairs object:', keypairs);
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing keypairs JSON:', parseError);
|
||||
throw new Error(`Failed to parse keypairs JSON: ${parseError.message}`);
|
||||
}
|
||||
|
||||
// Transform the keypairs to our expected format
|
||||
const formattedKeypairs: KeypairMetadata[] = keypairs.map((keypair: any, index: number) => {
|
||||
console.log(`Processing keypair at index ${index}:`, keypair);
|
||||
return {
|
||||
id: keypair.id, // Use the actual keypair ID from the WASM module
|
||||
type: keypair.key_type || 'Unknown',
|
||||
name: keypair.metadata?.name,
|
||||
description: keypair.metadata?.description,
|
||||
createdAt: keypair.metadata?.created_at || Date.now()
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Formatted keypairs for UI:', formattedKeypairs);
|
||||
set({ availableKeypairs: formattedKeypairs });
|
||||
return formattedKeypairs;
|
||||
} catch (error) {
|
||||
console.error('Failed to list keypairs:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Select a keypair for use
|
||||
selectKeypair: async (keypairId: string) => {
|
||||
try {
|
||||
console.log('Selecting keypair with ID:', keypairId);
|
||||
|
||||
// First, let's log the available keypairs to see what we have
|
||||
const { availableKeypairs } = get();
|
||||
console.log('Available keypairs:', JSON.stringify(availableKeypairs));
|
||||
|
||||
const wasmModule = await getWasmModule();
|
||||
console.log('WASM module loaded, attempting to select keypair');
|
||||
|
||||
try {
|
||||
// Call the WASM select_keypair function
|
||||
await wasmModule.select_keypair(keypairId);
|
||||
console.log('Successfully selected keypair in WASM');
|
||||
} catch (selectError) {
|
||||
console.error('Error in WASM select_keypair:', selectError);
|
||||
throw new Error(`select_keypair error: ${selectError.message || selectError}`);
|
||||
}
|
||||
|
||||
// Find the keypair in our availableKeypairs list
|
||||
const selectedKeypair = availableKeypairs.find((kp: KeypairMetadata) => kp.id === keypairId);
|
||||
|
||||
if (selectedKeypair) {
|
||||
console.log('Found keypair in available list, setting as current');
|
||||
set({ currentKeypair: selectedKeypair });
|
||||
} else {
|
||||
console.log('Keypair not found in available list, creating new entry from available data');
|
||||
// If not found in our list (rare case), create a new entry with what we know
|
||||
// Since we can't get metadata from WASM, use what we have from the keypair list
|
||||
const matchingKeypair = availableKeypairs.find(k => k.id === keypairId);
|
||||
|
||||
if (matchingKeypair) {
|
||||
set({ currentKeypair: matchingKeypair });
|
||||
} else {
|
||||
// Last resort: create a minimal keypair entry
|
||||
const newKeypair: KeypairMetadata = {
|
||||
id: keypairId,
|
||||
type: 'Unknown',
|
||||
name: `Keypair ${keypairId.substring(0, 8)}...`,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
set({ currentKeypair: newKeypair });
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to select keypair:', error);
|
||||
throw error; // Re-throw to show error in UI
|
||||
}
|
||||
},
|
||||
|
||||
// Create a new keypair
|
||||
createKeypair: async (type: string, metadata?: Record<string, any>) => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Format metadata for WASM
|
||||
const metadataJson = metadata ? JSON.stringify({
|
||||
name: metadata.name,
|
||||
description: metadata.description,
|
||||
created_at: Date.now()
|
||||
}) : undefined;
|
||||
|
||||
// Call the WASM add_keypair function
|
||||
const keypairId = await wasmModule.add_keypair(type, metadataJson);
|
||||
|
||||
// Refresh the keypair list
|
||||
await get().listKeypairs();
|
||||
|
||||
return keypairId;
|
||||
} catch (error) {
|
||||
console.error('Failed to create keypair:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Connect to a WebSocket server
|
||||
connectWebSocket: async (url: string) => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
const { currentKeypair } = get();
|
||||
|
||||
if (!currentKeypair) {
|
||||
throw new Error('No keypair selected');
|
||||
}
|
||||
|
||||
// Get the public key from WASM
|
||||
const publicKeyArray = await wasmModule.current_keypair_public_key();
|
||||
const publicKeyHex = Array.from(publicKeyArray)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
|
||||
// Connect to WebSocket via background service worker
|
||||
const chromeApi = getChromeApi();
|
||||
const response = await chromeApi.runtime.sendMessage({
|
||||
type: 'CONNECT_WEBSOCKET',
|
||||
serverUrl: url,
|
||||
publicKey: publicKeyHex
|
||||
});
|
||||
|
||||
if (response && response.success) {
|
||||
set({
|
||||
isWebSocketConnected: true,
|
||||
webSocketUrl: url
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response?.error || 'Failed to connect to WebSocket server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to WebSocket:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Disconnect from WebSocket server
|
||||
disconnectWebSocket: async () => {
|
||||
try {
|
||||
// Disconnect via background service worker
|
||||
const chromeApi = getChromeApi();
|
||||
const response = await chromeApi.runtime.sendMessage({
|
||||
type: 'DISCONNECT_WEBSOCKET'
|
||||
});
|
||||
|
||||
if (response && response.success) {
|
||||
set({
|
||||
isWebSocketConnected: false,
|
||||
webSocketUrl: null
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response?.error || 'Failed to disconnect from WebSocket server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect from WebSocket:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Execute a Rhai script
|
||||
executeScript: async (script: string) => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Call the WASM run_rhai function
|
||||
const result = await wasmModule.run_rhai(script);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to execute script:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Sign a message with the current keypair
|
||||
signMessage: async (message: string) => {
|
||||
try {
|
||||
const wasmModule = await getWasmModule();
|
||||
|
||||
// Convert message to Uint8Array
|
||||
const messageBytes = stringToUint8Array(message);
|
||||
|
||||
// Call the WASM sign function
|
||||
const signature = await wasmModule.sign(messageBytes);
|
||||
return signature;
|
||||
} catch (error) {
|
||||
console.error('Failed to sign message:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}));
|
45
hero_vault_extension/src/types.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Common TypeScript types for the Hero Vault Extension
|
||||
*/
|
||||
|
||||
// React types
|
||||
export type SyntheticEvent<T = Element, E = Event> = React.BaseSyntheticEvent<E, EventTarget & T, EventTarget>;
|
||||
|
||||
// Session types
|
||||
export interface SessionActivity {
|
||||
timestamp: number;
|
||||
action: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
// Script types
|
||||
export interface ScriptResult {
|
||||
id: string;
|
||||
script: string;
|
||||
result: string;
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface PendingScript {
|
||||
id: string;
|
||||
name: string;
|
||||
script: string;
|
||||
}
|
||||
|
||||
// WebSocket types
|
||||
export interface ConnectionHistory {
|
||||
id: string;
|
||||
url: string;
|
||||
timestamp: number;
|
||||
status: 'connected' | 'disconnected' | 'error';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Settings types
|
||||
export interface Settings {
|
||||
darkMode: boolean;
|
||||
autoLockTimeout: number;
|
||||
defaultKeyType: string;
|
||||
showScriptNotifications: boolean;
|
||||
}
|
5
hero_vault_extension/src/types/chrome.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="chrome" />
|
||||
|
||||
// This file provides type declarations for Chrome extension APIs
|
||||
// It's needed because we're using the Chrome extension API in a TypeScript project
|
||||
// The actual implementation is provided by the browser at runtime
|
14
hero_vault_extension/src/types/declarations.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
// Type declarations for modules without type definitions
|
||||
|
||||
// React and Material UI
|
||||
declare module 'react';
|
||||
declare module 'react-dom';
|
||||
declare module 'react-router-dom';
|
||||
declare module '@mui/material';
|
||||
declare module '@mui/material/*';
|
||||
declare module '@mui/icons-material/*';
|
||||
|
||||
// Project modules
|
||||
declare module './pages/*';
|
||||
declare module './components/*';
|
||||
declare module './store/*';
|
16
hero_vault_extension/src/types/wasm.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
declare module '*/wasm_app.js' {
|
||||
export default function init(): Promise<void>;
|
||||
export function init_session(keyspace: string, password: string): Promise<void>;
|
||||
export function create_keyspace(keyspace: string, password: string): Promise<void>;
|
||||
export function lock_session(): void;
|
||||
export function is_unlocked(): boolean;
|
||||
export function add_keypair(key_type: string | undefined, metadata: string | undefined): Promise<string>;
|
||||
export function list_keypairs(): Promise<string>;
|
||||
export function select_keypair(key_id: string): Promise<void>;
|
||||
export function current_keypair_metadata(): Promise<any>;
|
||||
export function current_keypair_public_key(): Promise<Uint8Array>;
|
||||
export function sign(message: Uint8Array): Promise<string>;
|
||||
export function verify(signature: string, message: Uint8Array): Promise<boolean>;
|
||||
export function init_rhai_env(): void;
|
||||
export function run_rhai(script: string): Promise<string>;
|
||||
}
|
103
hero_vault_extension/src/utils/chromeApi.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Chrome API utilities for Hero Vault Extension
|
||||
*
|
||||
* This module provides Chrome API detection and mocks for development mode
|
||||
*/
|
||||
|
||||
// Check if we're running in a Chrome extension environment
|
||||
export const isExtensionEnvironment = (): boolean => {
|
||||
return typeof chrome !== 'undefined' && !!chrome.runtime && !!chrome.runtime.id;
|
||||
};
|
||||
|
||||
// Mock storage for development mode
|
||||
const mockStorage: Record<string, any> = {
|
||||
// Initialize with some default values for script storage
|
||||
pendingScripts: [],
|
||||
scriptResults: []
|
||||
};
|
||||
|
||||
// Mock Chrome API for development mode
|
||||
export const getChromeApi = () => {
|
||||
// If we're in a Chrome extension environment, return the real Chrome API
|
||||
if (isExtensionEnvironment()) {
|
||||
return chrome;
|
||||
}
|
||||
|
||||
// Otherwise, return a mock implementation
|
||||
return {
|
||||
runtime: {
|
||||
sendMessage: (message: any): Promise<any> => {
|
||||
console.log('Mock sendMessage called with:', message);
|
||||
|
||||
// Mock responses based on message type
|
||||
if (message.type === 'SESSION_STATUS') {
|
||||
return Promise.resolve({ active: false });
|
||||
}
|
||||
|
||||
if (message.type === 'CREATE_KEYSPACE') {
|
||||
mockStorage['currentKeyspace'] = message.keyspace;
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
|
||||
if (message.type === 'UNLOCK_SESSION') {
|
||||
mockStorage['currentKeyspace'] = message.keyspace;
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
|
||||
if (message.type === 'LOCK_SESSION') {
|
||||
delete mockStorage['currentKeyspace'];
|
||||
return Promise.resolve({ success: true });
|
||||
}
|
||||
|
||||
return Promise.resolve({ success: false });
|
||||
},
|
||||
getURL: (path: string): string => {
|
||||
return path;
|
||||
}
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
get: (keys: string | string[] | object): Promise<Record<string, any>> => {
|
||||
console.log('Mock storage.local.get called with:', keys);
|
||||
|
||||
if (typeof keys === 'string') {
|
||||
// Handle specific script storage keys
|
||||
if (keys === 'pendingScripts' && !mockStorage[keys]) {
|
||||
mockStorage[keys] = [];
|
||||
}
|
||||
if (keys === 'scriptResults' && !mockStorage[keys]) {
|
||||
mockStorage[keys] = [];
|
||||
}
|
||||
return Promise.resolve({ [keys]: mockStorage[keys] });
|
||||
}
|
||||
|
||||
if (Array.isArray(keys)) {
|
||||
const result: Record<string, any> = {};
|
||||
keys.forEach(key => {
|
||||
// Handle specific script storage keys
|
||||
if (key === 'pendingScripts' && !mockStorage[key]) {
|
||||
mockStorage[key] = [];
|
||||
}
|
||||
if (key === 'scriptResults' && !mockStorage[key]) {
|
||||
mockStorage[key] = [];
|
||||
}
|
||||
result[key] = mockStorage[key];
|
||||
});
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
return Promise.resolve(mockStorage);
|
||||
},
|
||||
set: (items: Record<string, any>): Promise<void> => {
|
||||
console.log('Mock storage.local.set called with:', items);
|
||||
|
||||
Object.keys(items).forEach(key => {
|
||||
mockStorage[key] = items[key];
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
} as typeof chrome;
|
||||
};
|
139
hero_vault_extension/src/wasm/wasmHelper.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* WASM Helper for Hero Vault Extension
|
||||
*
|
||||
* This module handles loading and initializing the WASM module,
|
||||
* and provides a typed interface to the WASM functions.
|
||||
*/
|
||||
|
||||
// Import types for TypeScript
|
||||
interface WasmModule {
|
||||
// Session management
|
||||
init_session: (keyspace: string, password: string) => Promise<void>;
|
||||
create_keyspace: (keyspace: string, password: string) => Promise<void>;
|
||||
lock_session: () => void;
|
||||
is_unlocked: () => boolean;
|
||||
|
||||
// Keypair management
|
||||
add_keypair: (key_type: string | undefined, metadata: string | undefined) => Promise<string>;
|
||||
list_keypairs: () => Promise<string>;
|
||||
select_keypair: (key_id: string) => Promise<void>;
|
||||
current_keypair_metadata: () => Promise<any>;
|
||||
current_keypair_public_key: () => Promise<Uint8Array>;
|
||||
|
||||
// Cryptographic operations
|
||||
sign: (message: Uint8Array) => Promise<string>;
|
||||
verify: (message: Uint8Array, signature: string) => Promise<boolean>;
|
||||
encrypt_data: (data: Uint8Array) => Promise<Uint8Array>;
|
||||
decrypt_data: (encrypted: Uint8Array) => Promise<Uint8Array>;
|
||||
|
||||
// Rhai scripting
|
||||
init_rhai_env: () => void;
|
||||
run_rhai: (script: string) => Promise<string>;
|
||||
}
|
||||
|
||||
// Global reference to the WASM module
|
||||
let wasmModule: WasmModule | null = null;
|
||||
let isInitializing = false;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Initialize the WASM module
|
||||
* This should be called before any other WASM functions
|
||||
*/
|
||||
export const initWasm = async (): Promise<void> => {
|
||||
if (wasmModule) {
|
||||
return Promise.resolve(); // Already initialized
|
||||
}
|
||||
|
||||
if (isInitializing && initPromise) {
|
||||
return initPromise; // Already initializing
|
||||
}
|
||||
|
||||
isInitializing = true;
|
||||
|
||||
initPromise = new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
try {
|
||||
// Import the WASM module
|
||||
// Use a relative path that will be resolved by Vite during build
|
||||
const wasmImport = await import('../../public/wasm/wasm_app.js');
|
||||
|
||||
// Initialize the WASM module
|
||||
await wasmImport.default();
|
||||
|
||||
// Store the WASM module globally
|
||||
wasmModule = wasmImport as unknown as WasmModule;
|
||||
|
||||
console.log('WASM module initialized successfully');
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize WASM module:', error);
|
||||
reject(error);
|
||||
}
|
||||
|
||||
} finally {
|
||||
isInitializing = false;
|
||||
}
|
||||
});
|
||||
|
||||
return initPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the WASM module
|
||||
* This will initialize the module if it hasn't been initialized yet
|
||||
*/
|
||||
export const getWasmModule = async (): Promise<WasmModule> => {
|
||||
if (!wasmModule) {
|
||||
await initWasm();
|
||||
}
|
||||
|
||||
if (!wasmModule) {
|
||||
throw new Error('WASM module failed to initialize');
|
||||
}
|
||||
|
||||
return wasmModule;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the WASM module is initialized
|
||||
*/
|
||||
export const isWasmInitialized = (): boolean => {
|
||||
return wasmModule !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert string to Uint8Array
|
||||
*/
|
||||
export const stringToUint8Array = (str: string): Uint8Array => {
|
||||
const encoder = new TextEncoder();
|
||||
return encoder.encode(str);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert Uint8Array to string
|
||||
*/
|
||||
export const uint8ArrayToString = (array: Uint8Array): string => {
|
||||
const decoder = new TextDecoder();
|
||||
return decoder.decode(array);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert hex string to Uint8Array
|
||||
*/
|
||||
export const hexToUint8Array = (hex: string): Uint8Array => {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert Uint8Array to hex string
|
||||
*/
|
||||
export const uint8ArrayToHex = (array: Uint8Array): string => {
|
||||
return Array.from(array)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
};
|
30
hero_vault_extension/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["./node_modules/@types", "./src/types"],
|
||||
"jsxImportSource": "react"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
10
hero_vault_extension/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
33
hero_vault_extension/vite.config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { crx } from '@crxjs/vite-plugin';
|
||||
import { resolve } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
import fs from 'fs';
|
||||
|
||||
const manifest = JSON.parse(
|
||||
readFileSync('public/manifest.json', 'utf-8')
|
||||
);
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
crx({ manifest }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, 'index.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
// Copy WASM files to the dist directory
|
||||
publicDir: 'public',
|
||||
});
|
526
vault/src/lib.rs
@ -1,34 +1,32 @@
|
||||
//! 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 crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
|
||||
use signature::SignatureEncoding;
|
||||
// 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 +41,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,17 +57,28 @@ 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);
|
||||
@ -112,7 +120,10 @@ 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");
|
||||
Ok(())
|
||||
}
|
||||
@ -121,10 +132,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 +156,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);
|
||||
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) {
|
||||
@ -184,8 +215,14 @@ let plaintext = decrypt_chacha20(&key, ct, nonce).map_err(VaultError::Crypto)?;
|
||||
|
||||
/// 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 +231,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 +242,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 +252,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 +269,291 @@ 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::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(())
|
||||
}
|
||||
// 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};
|
||||
let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| {
|
||||
VaultError::Crypto("Invalid secp256k1 private key length".to_string())
|
||||
})?;
|
||||
let sk = SigningKey::from_bytes(arr.into())
|
||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||
let sig: k256::ecdsa::DerSignature = sk.sign(message);
|
||||
Ok(sig.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a signature with a stored keypair in a keyspace
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `keyspace` - Keyspace name
|
||||
/// * `password` - Keyspace password
|
||||
/// * `key_id` - Keypair ID
|
||||
/// * `message` - Message that was signed
|
||||
/// * `signature` - Signature to verify
|
||||
pub async fn verify(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8], signature: &[u8]) -> Result<bool, VaultError> {
|
||||
let data = self.unlock_keyspace(keyspace, password).await?;
|
||||
let key = data.keypairs.iter().find(|k| k.id == key_id).ok_or(VaultError::KeyNotFound(key_id.to_string()))?;
|
||||
match key.key_type {
|
||||
KeyType::Ed25519 => {
|
||||
use ed25519_dalek::{VerifyingKey, Signature, Verifier};
|
||||
let verifying = VerifyingKey::from_bytes(&key.public_key.clone().try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 public key length".to_string()))?)
|
||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||
let sig = Signature::from_bytes(&signature.try_into().map_err(|_| VaultError::Crypto("Invalid Ed25519 signature length".to_string()))?);
|
||||
Ok(verifying.verify(message, &sig).is_ok())
|
||||
}
|
||||
KeyType::Secp256k1 => {
|
||||
use k256::ecdsa::{VerifyingKey, Signature, signature::Verifier};
|
||||
let pk = VerifyingKey::from_sec1_bytes(&key.public_key).map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||
let sig = Signature::from_der(signature).map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||
Ok(pk.verify(message, &sig).is_ok())
|
||||
/// 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()))?;
|
||||
let sig = Signature::from_der(signature)
|
||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||
Ok(pk.verify(message, &sig).is_ok())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt a message using the keyspace symmetric cipher
|
||||
/// (for simplicity, uses keyspace password-derived key)
|
||||
pub async fn encrypt(
|
||||
&self,
|
||||
keyspace: &str,
|
||||
password: &[u8],
|
||||
plaintext: &[u8],
|
||||
) -> Result<Vec<u8>, VaultError> {
|
||||
debug!("encrypt");
|
||||
|
||||
// 1. Load keyspace metadata
|
||||
let meta_bytes = self
|
||||
.storage
|
||||
.get(keyspace)
|
||||
.await
|
||||
.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||
let meta_bytes = match meta_bytes {
|
||||
Some(val) => val,
|
||||
None => {
|
||||
debug!("keyspace not found");
|
||||
return Err(VaultError::Other("Keyspace not found".to_string()));
|
||||
}
|
||||
};
|
||||
let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
debug!("serialization error: {}", e);
|
||||
return Err(VaultError::Serialization(e.to_string()));
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"salt={:?} (hex salt: {})",
|
||||
meta.salt,
|
||||
hex::encode(&meta.salt)
|
||||
);
|
||||
// 2. Derive key
|
||||
let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000);
|
||||
// 3. Generate nonce
|
||||
let nonce = random_salt(12);
|
||||
debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(&nonce));
|
||||
// 4. Encrypt
|
||||
let ciphertext = encrypt_chacha20(&key, plaintext, &nonce).map_err(VaultError::Crypto)?;
|
||||
let mut out = nonce;
|
||||
out.extend_from_slice(&ciphertext);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Decrypt a message using the keyspace symmetric cipher
|
||||
/// (for simplicity, uses keyspace password-derived key)
|
||||
pub async fn decrypt(
|
||||
&self,
|
||||
keyspace: &str,
|
||||
password: &[u8],
|
||||
ciphertext: &[u8],
|
||||
) -> Result<Vec<u8>, VaultError> {
|
||||
debug!("decrypt");
|
||||
|
||||
// 1. Load keyspace metadata
|
||||
let meta_bytes = self
|
||||
.storage
|
||||
.get(keyspace)
|
||||
.await
|
||||
.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
|
||||
let meta_bytes = match meta_bytes {
|
||||
Some(val) => val,
|
||||
None => {
|
||||
debug!("keyspace not found");
|
||||
return Err(VaultError::Other("Keyspace not found".to_string()));
|
||||
}
|
||||
};
|
||||
let meta: KeyspaceMetadata = match serde_json::from_slice(&meta_bytes) {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
debug!("serialization error: {}", e);
|
||||
return Err(VaultError::Serialization(e.to_string()));
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"salt={:?} (hex salt: {})",
|
||||
meta.salt,
|
||||
hex::encode(&meta.salt)
|
||||
);
|
||||
// 2. Derive key
|
||||
let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000);
|
||||
// 3. Extract nonce
|
||||
let nonce = &ciphertext[..12];
|
||||
debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce));
|
||||
// 4. Decrypt
|
||||
let plaintext =
|
||||
decrypt_chacha20(&key, &ciphertext[12..], nonce).map_err(VaultError::Crypto)?;
|
||||
Ok(plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
@ -136,6 +136,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
|
||||
}
|
||||
@ -262,6 +294,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
|
||||
}
|
||||
|
@ -221,15 +221,10 @@ 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);
|
||||
@ -239,3 +234,72 @@ pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|