refactor: migrate extension to TypeScript and add Material-UI components

This commit is contained in:
Sameh Abouel-saad 2025-05-26 23:01:47 +03:00
parent 0224755ba3
commit beba294054
82 changed files with 9659 additions and 5594 deletions

View File

@ -2,7 +2,7 @@
BROWSER ?= firefox 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 test-browser-all: test-browser-kvstore test-browser-vault test-browser-evm-client
@ -25,18 +25,7 @@ test-browser-evm-client:
build-wasm-app: build-wasm-app:
cd wasm_app && wasm-pack build --target web cd wasm_app && wasm-pack build --target web
# Build everything: wasm, copy, then extension # Build Hero Vault extension: wasm, copy, then extension
build-extension-all: build-wasm-app build-hero-vault-extension:
cd extension && npm run build cd wasm_app && wasm-pack build --target web
cd hero_vault_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/

48
build.sh Executable file
View 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"

View File

@ -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)

View File

@ -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
}
});

View File

@ -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.');

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -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
}
});

View File

@ -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>"]
}
]
}

View File

@ -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;
}

View File

@ -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;

Binary file not shown.

View File

@ -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>"]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 });
});
});
}

View File

@ -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>
);
}

View File

@ -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;
}

View File

@ -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 />);

View File

@ -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 />);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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');
}
});
}

View File

@ -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;
}
});

View File

@ -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;

View File

@ -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: ['../']
}
}
});

View 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)

View 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)}

File diff suppressed because one or more lines are too long

View 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()});

File diff suppressed because one or more lines are too long

61
hero_vault_extension/dist/background.js vendored Normal file
View 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();
});

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 454 B

View File

Before

Width:  |  Height:  |  Size: 712 B

After

Width:  |  Height:  |  Size: 712 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -3,12 +3,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modular Vault Extension</title> <title>Hero Vault</title>
<link rel="stylesheet" href="popup.css"> <script type="module" crossorigin src="/assets/index-b58c7e43.js"></script>
<link rel="stylesheet" href="/assets/index-11057528.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="./main.jsx"></script>
</body> </body>
</html> </html>

26
hero_vault_extension/dist/manifest.json vendored Normal file
View 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'"
}
}

View File

@ -0,0 +1 @@
import './assets/simple-background.ts-e63275e1.js';

View File

@ -3,13 +3,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modular Vault Extension</title> <title>Hero Vault</title>
<script type="module" crossorigin src="/assets/popup.js"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

4862
hero_vault_extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 454 B

View File

Before

Width:  |  Height:  |  Size: 712 B

After

Width:  |  Height:  |  Size: 712 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View 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'"
}
}

View 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();

View 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);
}

View 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;

View 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');

View 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();
});
}

View 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;

View 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;

View 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);
}

View 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>
);

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 })
}));

View 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;
}
}
}));

View 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;
}

View 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

View 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/*';

View 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>;
}

View 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;
};

View 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('');
};

View 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" }]
}

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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',
});

View File

@ -1,34 +1,32 @@
//! vault: Cryptographic keyspace and operations //! vault: Cryptographic keyspace and operations
//! vault: Cryptographic keyspace and operations //! vault: Cryptographic keyspace and operations
pub mod data; pub mod data;
pub use crate::data::{KeyEntry, KeyMetadata, KeyType};
pub use crate::session::SessionManager; pub use crate::session::SessionManager;
pub use crate::data::{KeyType, KeyMetadata, KeyEntry};
mod error;
mod crypto; mod crypto;
mod error;
pub mod rhai_bindings;
mod rhai_sync_helpers;
pub mod session; pub mod session;
mod utils; mod utils;
mod rhai_sync_helpers;
pub mod rhai_bindings;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod session_singleton; pub mod session_singleton;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod wasm_helpers; pub mod wasm_helpers;
use crate::crypto::kdf;
pub use kvstore::traits::KVStore; use crate::crypto::random_salt;
use data::*; use data::*;
use error::VaultError; use error::VaultError;
use crate::crypto::random_salt; pub use kvstore::traits::KVStore;
use crate::crypto::kdf;
use crate::crypto::cipher::{encrypt_chacha20, decrypt_chacha20}; use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
use signature::SignatureEncoding; use signature::SignatureEncoding;
// TEMP: File-based debug logger for crypto troubleshooting // TEMP: File-based debug logger for crypto troubleshooting
use log::{debug}; use log::debug;
/// Vault: Cryptographic keyspace and operations /// Vault: Cryptographic keyspace and operations
pub struct Vault<S: KVStore> { 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); let nonce = random_salt(12);
debug!("nonce: {}", hex::encode(&nonce)); debug!("nonce: {}", hex::encode(&nonce));
// Always use ChaCha20Poly1305 for encryption // Always use ChaCha20Poly1305 for encryption
let ct = encrypt_chacha20(key, plaintext, &nonce) let ct = encrypt_chacha20(key, plaintext, &nonce).map_err(|e| VaultError::Crypto(e))?;
.map_err(|e| VaultError::Crypto(e))?;
debug!("ct: {}", hex::encode(&ct)); debug!("ct: {}", hex::encode(&ct));
debug!("key: {}", hex::encode(key)); debug!("key: {}", hex::encode(key));
let mut blob = nonce.clone(); let mut blob = nonce.clone();
@ -60,15 +57,26 @@ impl<S: KVStore> Vault<S> {
/// Create a new keyspace with the given name, password, and options. /// 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. /// 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 // 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); debug!("keyspace '{}' already exists", name);
return Err(VaultError::Crypto("Keyspace already exists".to_string())); return Err(VaultError::Crypto("Keyspace already exists".to_string()));
} }
debug!("entry: name={}", name); debug!("entry: name={}", name);
use crate::crypto::{random_salt, kdf}; use crate::crypto::{kdf, random_salt};
use crate::data::{KeyspaceMetadata, KeyspaceData}; use crate::data::{KeyspaceData, KeyspaceMetadata};
use serde_json; use serde_json;
// 1. Generate salt // 1. Generate 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())); 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"); debug!("success");
Ok(()) 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> { pub async fn list_keyspaces(&self) -> Result<Vec<KeyspaceMetadata>, VaultError> {
use serde_json; use serde_json;
// 1. List all keys in kvstore // 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(); let mut keyspaces = Vec::new();
for key in keys { 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) { if let Ok(meta) = serde_json::from_slice::<KeyspaceMetadata>(&bytes) {
keyspaces.push(meta); keyspaces.push(meta);
} }
@ -136,17 +156,28 @@ 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
/// 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. /// 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); debug!("unlock_keyspace entry: name={}", name);
// use crate::crypto::kdf; // removed if not needed // use crate::crypto::kdf; // removed if not needed
use serde_json; use serde_json;
// 1. Fetch keyspace metadata // 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 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 { if metadata.salt.len() != 16 {
debug!("salt length {} != 16", metadata.salt.len()); 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 // 2. Derive key
let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000);
@ -185,7 +216,13 @@ 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)
/// 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. /// 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> { 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 crate::data::KeyEntry;
use rand_core::OsRng; use rand_core::OsRng;
use rand_core::RngCore; use rand_core::RngCore;
@ -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 pub_bytes = verifying.to_bytes().to_vec();
let id = hex::encode(&pub_bytes); let id = hex::encode(&pub_bytes);
(priv_bytes, pub_bytes, id) (priv_bytes, pub_bytes, id)
}, }
KeyType::Secp256k1 => { KeyType::Secp256k1 => {
use k256::ecdsa::SigningKey; 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 pub_bytes = pk.to_encoded_point(false).as_bytes().to_vec();
let id = hex::encode(&pub_bytes); let id = hex::encode(&pub_bytes);
(priv_bytes, pub_bytes, id) (priv_bytes, pub_bytes, id)
}, }
}; };
// 3. Add to keypairs // 3. Add to keypairs
let entry = KeyEntry { let entry = KeyEntry {
@ -232,39 +269,76 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
} }
/// Remove a keypair by id from a keyspace /// 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?; let mut data = self.unlock_keyspace(keyspace, password).await?;
data.keypairs.retain(|k| k.id != key_id); data.keypairs.retain(|k| k.id != key_id);
self.save_keyspace(keyspace, password, &data).await self.save_keyspace(keyspace, password, &data).await
} }
/// List all keypairs in a keyspace (public info only) /// 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?; 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 /// 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 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())) Ok((key.private_key.clone(), key.public_key.clone()))
} }
/// Save the updated keyspace data (helper) /// 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); debug!("save_keyspace entry: keyspace={}", keyspace);
use crate::crypto::kdf; use crate::crypto::kdf;
use serde_json; use serde_json;
let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; let meta_bytes = self
debug!("got meta_bytes: {}", meta_bytes.as_ref().map(|v| v.len()).unwrap_or(0)); .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 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()))?; let mut metadata: KeyspaceMetadata = serde_json::from_slice(&meta_bytes)
.map_err(|e| VaultError::Serialization(e.to_string()))?;
debug!("metadata: salt={:?}", metadata.salt); debug!("metadata: salt={:?}", metadata.salt);
if metadata.salt.len() != 16 { if metadata.salt.len() != 16 {
debug!("salt length {} != 16", metadata.salt.len()); 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 // 2. Derive key
let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000); let key = kdf::derive_key_pbkdf2(password, &metadata.salt, 32, 10_000);
@ -293,7 +367,10 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
return Err(VaultError::Serialization(e.to_string())); return Err(VaultError::Serialization(e.to_string()));
} }
}; };
self.storage.set(keyspace, &meta_bytes).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; self.storage
.set(keyspace, &meta_bytes)
.await
.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
debug!("success"); debug!("success");
Ok(()) Ok(())
} }
@ -305,20 +382,36 @@ pub async fn add_keypair(&mut self, keyspace: &str, password: &[u8], key_type: O
/// * `password` - Keyspace password /// * `password` - Keyspace password
/// * `key_id` - Keypair ID /// * `key_id` - Keypair ID
/// * `message` - Message to sign /// * `message` - Message to sign
pub async fn sign(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8]) -> Result<Vec<u8>, VaultError> { 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 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()))?;
match key.key_type { match key.key_type {
KeyType::Ed25519 => { KeyType::Ed25519 => {
use ed25519_dalek::{SigningKey, Signer}; 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 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); let sig = signing.sign(message);
Ok(sig.to_bytes().to_vec()) Ok(sig.to_bytes().to_vec())
} }
KeyType::Secp256k1 => { KeyType::Secp256k1 => {
use k256::ecdsa::{SigningKey, signature::Signer}; 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 arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| {
let sk = SigningKey::from_bytes(arr.into()).map_err(|e| VaultError::Crypto(e.to_string()))?; 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); let sig: k256::ecdsa::DerSignature = sk.sign(message);
Ok(sig.to_vec()) Ok(sig.to_vec())
} }
@ -333,21 +426,39 @@ pub async fn sign(&self, keyspace: &str, password: &[u8], key_id: &str, message:
/// * `key_id` - Keypair ID /// * `key_id` - Keypair ID
/// * `message` - Message that was signed /// * `message` - Message that was signed
/// * `signature` - Signature to verify /// * `signature` - Signature to verify
pub async fn verify(&self, keyspace: &str, password: &[u8], key_id: &str, message: &[u8], signature: &[u8]) -> Result<bool, VaultError> { 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 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()))?;
match key.key_type { match key.key_type {
KeyType::Ed25519 => { KeyType::Ed25519 => {
use ed25519_dalek::{VerifyingKey, Signature, Verifier}; 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()))?) 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()))?; .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()))?); 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()) Ok(verifying.verify(message, &sig).is_ok())
} }
KeyType::Secp256k1 => { KeyType::Secp256k1 => {
use k256::ecdsa::{VerifyingKey, Signature, signature::Verifier}; 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 pk = VerifyingKey::from_sec1_bytes(&key.public_key)
let sig = Signature::from_der(signature).map_err(|e| VaultError::Crypto(e.to_string()))?; .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()) Ok(pk.verify(message, &sig).is_ok())
} }
} }
@ -355,11 +466,20 @@ pub async fn verify(&self, keyspace: &str, password: &[u8], key_id: &str, messag
/// Encrypt a message using the keyspace symmetric cipher /// Encrypt a message using the keyspace symmetric cipher
/// (for simplicity, uses keyspace password-derived key) /// (for simplicity, uses keyspace password-derived key)
pub async fn encrypt(&self, keyspace: &str, password: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, VaultError> { pub async fn encrypt(
&self,
keyspace: &str,
password: &[u8],
plaintext: &[u8],
) -> Result<Vec<u8>, VaultError> {
debug!("encrypt"); debug!("encrypt");
// 1. Load keyspace metadata // 1. Load keyspace metadata
let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; let meta_bytes = self
.storage
.get(keyspace)
.await
.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
let meta_bytes = match meta_bytes { let meta_bytes = match meta_bytes {
Some(val) => val, Some(val) => val,
None => { None => {
@ -374,7 +494,11 @@ pub async fn encrypt(&self, keyspace: &str, password: &[u8], plaintext: &[u8]) -
return Err(VaultError::Serialization(e.to_string())); return Err(VaultError::Serialization(e.to_string()));
} }
}; };
debug!("salt={:?} (hex salt: {})", meta.salt, hex::encode(&meta.salt)); debug!(
"salt={:?} (hex salt: {})",
meta.salt,
hex::encode(&meta.salt)
);
// 2. Derive key // 2. Derive key
let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000);
// 3. Generate nonce // 3. Generate nonce
@ -389,11 +513,20 @@ pub async fn encrypt(&self, keyspace: &str, password: &[u8], plaintext: &[u8]) -
/// Decrypt a message using the keyspace symmetric cipher /// Decrypt a message using the keyspace symmetric cipher
/// (for simplicity, uses keyspace password-derived key) /// (for simplicity, uses keyspace password-derived key)
pub async fn decrypt(&self, keyspace: &str, password: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, VaultError> { pub async fn decrypt(
&self,
keyspace: &str,
password: &[u8],
ciphertext: &[u8],
) -> Result<Vec<u8>, VaultError> {
debug!("decrypt"); debug!("decrypt");
// 1. Load keyspace metadata // 1. Load keyspace metadata
let meta_bytes = self.storage.get(keyspace).await.map_err(|e| VaultError::Storage(format!("{e:?}")))?; let meta_bytes = self
.storage
.get(keyspace)
.await
.map_err(|e| VaultError::Storage(format!("{e:?}")))?;
let meta_bytes = match meta_bytes { let meta_bytes = match meta_bytes {
Some(val) => val, Some(val) => val,
None => { None => {
@ -408,14 +541,19 @@ pub async fn decrypt(&self, keyspace: &str, password: &[u8], ciphertext: &[u8])
return Err(VaultError::Serialization(e.to_string())); return Err(VaultError::Serialization(e.to_string()));
} }
}; };
debug!("salt={:?} (hex salt: {})", meta.salt, hex::encode(&meta.salt)); debug!(
"salt={:?} (hex salt: {})",
meta.salt,
hex::encode(&meta.salt)
);
// 2. Derive key // 2. Derive key
let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000); let key = kdf::derive_key_pbkdf2(password, &meta.salt, 32, 10_000);
// 3. Extract nonce // 3. Extract nonce
let nonce = &ciphertext[..12]; let nonce = &ciphertext[..12];
debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce)); debug!("nonce={:?} (hex nonce: {})", nonce, hex::encode(nonce));
// 4. Decrypt // 4. Decrypt
let plaintext = decrypt_chacha20(&key, &ciphertext[12..], nonce).map_err(VaultError::Crypto)?; let plaintext =
decrypt_chacha20(&key, &ciphertext[12..], nonce).map_err(VaultError::Crypto)?;
Ok(plaintext) Ok(plaintext)
} }
} }

View File

@ -136,6 +136,38 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
self.vault.sign(name, password, &keypair.id, message).await 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> { pub fn get_vault(&self) -> &Vault<S> {
&self.vault &self.vault
} }
@ -262,6 +294,38 @@ impl<S: KVStore> SessionManager<S> {
self.vault.sign(name, password, &keypair.id, message).await 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> { pub fn get_vault(&self) -> &Vault<S> {
&self.vault &self.vault
} }

View File

@ -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. // SAFETY: We only use this pointer synchronously within this function, and SESSION_MANAGER outlives this scope.
let session_ptr = let session_ptr =
SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); 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 { let session: &vault::session::SessionManager<kvstore::wasm::WasmStore> = match session_ptr {
Some(ptr) => unsafe { &*ptr }, Some(ptr) => unsafe { &*ptr },
None => return Err(JsValue::from_str("Session not initialized")), 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 { match session.sign(message).await {
Ok(sig_bytes) => { Ok(sig_bytes) => {
let hex_sig = hex::encode(&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}"))),
}
}
}