Compare commits
7 Commits
66555fcb0d
...
main
Author | SHA1 | Date | |
---|---|---|---|
6573a01d75 | |||
c47f67b901 | |||
2cf31905b0 | |||
8569bb4bd8 | |||
67cbb35156 | |||
d8a314df41 | |||
bf2f7b57bb |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -34,4 +34,6 @@ yarn-error.log
|
|||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
tmp/
|
@@ -22,6 +22,8 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
ethers = { version = "2.0", features = ["abigen", "legacy"] }
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
104
README.md
104
README.md
@@ -4,14 +4,22 @@ This project provides a WebAssembly module written in Rust that offers cryptogra
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- **Key Space Management**
|
||||||
|
- Password-protected encrypted spaces
|
||||||
|
- Multiple spaces with different passwords
|
||||||
|
- Persistent storage in browser's localStorage
|
||||||
|
- Auto-logout after 15 minutes of inactivity
|
||||||
|
|
||||||
- **Asymmetric Cryptography**
|
- **Asymmetric Cryptography**
|
||||||
- ECDSA keypair generation
|
- Multiple named ECDSA keypairs
|
||||||
|
- Keypair selection for operations
|
||||||
- Message signing
|
- Message signing
|
||||||
- Signature verification
|
- Signature verification
|
||||||
|
|
||||||
- **Symmetric Cryptography**
|
- **Symmetric Cryptography**
|
||||||
- ChaCha20Poly1305 encryption/decryption
|
- ChaCha20Poly1305 encryption/decryption
|
||||||
- Secure key generation
|
- Secure key generation
|
||||||
|
- Password-based encryption
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -22,35 +30,6 @@ Before you begin, ensure you have the following installed:
|
|||||||
- [Node.js](https://nodejs.org/) (14.0.0 or later)
|
- [Node.js](https://nodejs.org/) (14.0.0 or later)
|
||||||
- A modern web browser that supports WebAssembly
|
- A modern web browser that supports WebAssembly
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
webassembly/
|
|
||||||
├── src/
|
|
||||||
│ ├── api/ # Public API modules
|
|
||||||
│ │ ├── keypair.rs # Public keypair API
|
|
||||||
│ │ ├── mod.rs # API module exports
|
|
||||||
│ │ └── symmetric.rs # Public symmetric encryption API
|
|
||||||
│ ├── core/ # Internal implementation modules
|
|
||||||
│ │ ├── error.rs # Error types and conversions
|
|
||||||
│ │ ├── keypair.rs # Core keypair implementation
|
|
||||||
│ │ ├── mod.rs # Core module exports
|
|
||||||
│ │ └── symmetric.rs # Core symmetric encryption implementation
|
|
||||||
│ ├── tests/ # Test modules
|
|
||||||
│ │ ├── keypair_tests.rs # Tests for keypair functionality
|
|
||||||
│ │ ├── mod.rs # Test module exports
|
|
||||||
│ │ └── symmetric_tests.rs # Tests for symmetric encryption
|
|
||||||
│ └── lib.rs # Main entry point, exports WASM functions
|
|
||||||
├── www/
|
|
||||||
│ ├── index.html # Example HTML page
|
|
||||||
│ ├── server.js # Simple HTTP server for testing
|
|
||||||
│ └── js/
|
|
||||||
│ └── index.js # JavaScript code to load and use the WebAssembly module
|
|
||||||
├── Cargo.toml # Rust package configuration
|
|
||||||
├── start.sh # Script to build and run the example
|
|
||||||
└── README.md # This file
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Example
|
## Running the Example
|
||||||
|
|
||||||
The easiest way to run the example is to use the provided start script:
|
The easiest way to run the example is to use the provided start script:
|
||||||
@@ -76,7 +55,7 @@ wasm-pack build --target web
|
|||||||
|
|
||||||
2. Start the local server:
|
2. Start the local server:
|
||||||
```bash
|
```bash
|
||||||
node www/server.js
|
cd www && npm install && node server.js
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Open your browser and navigate to http://localhost:8080.
|
3. Open your browser and navigate to http://localhost:8080.
|
||||||
@@ -91,23 +70,57 @@ cargo test
|
|||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
|
### Key Space Management
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create a new key space
|
||||||
|
const result = await wasm.create_key_space("my_space");
|
||||||
|
if (result === 0) {
|
||||||
|
console.log("Space created successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the current space with a password
|
||||||
|
const encryptedSpace = await wasm.encrypt_key_space("my_password");
|
||||||
|
localStorage.setItem("crypto_space_my_space", encryptedSpace);
|
||||||
|
|
||||||
|
// Decrypt and load a space
|
||||||
|
const storedSpace = localStorage.getItem("crypto_space_my_space");
|
||||||
|
const decryptResult = await wasm.decrypt_key_space(storedSpace, "my_password");
|
||||||
|
if (decryptResult === 0) {
|
||||||
|
console.log("Space loaded successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout (clear current session)
|
||||||
|
wasm.logout();
|
||||||
|
```
|
||||||
|
|
||||||
### Keypair Operations
|
### Keypair Operations
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Initialize a new keypair
|
// Create a new keypair in the current space
|
||||||
const result = await wasm.keypair_new();
|
const result = await wasm.create_keypair("my_keypair");
|
||||||
if (result === 0) {
|
if (result === 0) {
|
||||||
console.log("Keypair initialized successfully");
|
console.log("Keypair created successfully");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the public key
|
// Select a keypair for use
|
||||||
|
const selectResult = await wasm.select_keypair("my_keypair");
|
||||||
|
if (selectResult === 0) {
|
||||||
|
console.log("Keypair selected successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all keypairs in the current space
|
||||||
|
const keypairs = await wasm.list_keypairs();
|
||||||
|
console.log("Available keypairs:", keypairs);
|
||||||
|
|
||||||
|
// Get the public key of the selected keypair
|
||||||
const pubKey = await wasm.keypair_pub_key();
|
const pubKey = await wasm.keypair_pub_key();
|
||||||
|
|
||||||
// Sign a message
|
// Sign a message with the selected keypair
|
||||||
const message = new TextEncoder().encode("Hello, world!");
|
const message = new TextEncoder().encode("Hello, world!");
|
||||||
const signature = await wasm.keypair_sign(message);
|
const signature = await wasm.keypair_sign(message);
|
||||||
|
|
||||||
// Verify a signature
|
// Verify a signature with the selected keypair
|
||||||
const isValid = await wasm.keypair_verify(message, signature);
|
const isValid = await wasm.keypair_verify(message, signature);
|
||||||
console.log("Signature valid:", isValid);
|
console.log("Signature valid:", isValid);
|
||||||
```
|
```
|
||||||
@@ -126,13 +139,28 @@ const ciphertext = await wasm.encrypt_symmetric(key, message);
|
|||||||
const decrypted = await wasm.decrypt_symmetric(key, ciphertext);
|
const decrypted = await wasm.decrypt_symmetric(key, ciphertext);
|
||||||
const decryptedText = new TextDecoder().decode(decrypted);
|
const decryptedText = new TextDecoder().decode(decrypted);
|
||||||
console.log("Decrypted:", decryptedText);
|
console.log("Decrypted:", decryptedText);
|
||||||
|
|
||||||
|
// Derive a key from a password
|
||||||
|
const derivedKey = wasm.derive_key_from_password("my_password");
|
||||||
|
|
||||||
|
// Encrypt with a password
|
||||||
|
const passwordMessage = new TextEncoder().encode("Password protected message");
|
||||||
|
const passwordCiphertext = await wasm.encrypt_with_password("my_password", passwordMessage);
|
||||||
|
|
||||||
|
// Decrypt with a password
|
||||||
|
const passwordDecrypted = await wasm.decrypt_with_password("my_password", passwordCiphertext);
|
||||||
|
const passwordDecryptedText = new TextDecoder().decode(passwordDecrypted);
|
||||||
|
console.log("Password decrypted:", passwordDecryptedText);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
- The keypair is stored in memory and is not persisted between page reloads.
|
- Key spaces are encrypted using ChaCha20Poly1305 with a key derived from the user's password.
|
||||||
|
- Keypairs are stored in encrypted spaces and persisted in localStorage when the space is saved.
|
||||||
|
- The system implements auto-logout after 15 minutes of inactivity for additional security.
|
||||||
- The symmetric encryption uses ChaCha20Poly1305, which provides authenticated encryption.
|
- The symmetric encryption uses ChaCha20Poly1305, which provides authenticated encryption.
|
||||||
- The nonce for symmetric encryption is generated randomly and appended to the ciphertext.
|
- The nonce for symmetric encryption is generated randomly and appended to the ciphertext.
|
||||||
|
- Password-based key derivation uses SHA-256 (consider using a more secure KDF like Argon2 for production).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
507
implementation_plan_indexeddb.md
Normal file
507
implementation_plan_indexeddb.md
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
# Implementation Plan: Migrating from LocalStorage to IndexedDB
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the plan for migrating the WebAssembly crypto example application from using `localStorage` to `IndexedDB` for persisting encrypted key spaces. The primary motivations for this migration are:
|
||||||
|
|
||||||
|
1. Transaction capabilities for better data integrity
|
||||||
|
2. Improved performance for larger data operations
|
||||||
|
3. More structured approach to data storage
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
The current implementation uses localStorage with the following key functions:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// LocalStorage functions for key spaces
|
||||||
|
const STORAGE_PREFIX = 'crypto_space_';
|
||||||
|
|
||||||
|
// Save encrypted space to localStorage
|
||||||
|
function saveSpaceToStorage(spaceName, encryptedData) {
|
||||||
|
localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get encrypted space from localStorage
|
||||||
|
function getSpaceFromStorage(spaceName) {
|
||||||
|
return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all spaces in localStorage
|
||||||
|
function listSpacesFromStorage() {
|
||||||
|
const spaces = [];
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key.startsWith(STORAGE_PREFIX)) {
|
||||||
|
spaces.push(key.substring(STORAGE_PREFIX.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove space from localStorage
|
||||||
|
function removeSpaceFromStorage(spaceName) {
|
||||||
|
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### 1. Database Structure
|
||||||
|
|
||||||
|
- Create a database named 'CryptoSpaceDB'
|
||||||
|
- Create an object store named 'keySpaces' with 'name' as the key path
|
||||||
|
- Add indexes for efficient querying: 'name' (unique) and 'lastAccessed'
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
KeySpaces {
|
||||||
|
string name PK
|
||||||
|
string encryptedData
|
||||||
|
date created
|
||||||
|
date lastAccessed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Database Initialization
|
||||||
|
|
||||||
|
Create a module for initializing and managing the IndexedDB database:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Database constants
|
||||||
|
const DB_NAME = 'CryptoSpaceDB';
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
const STORE_NAME = 'keySpaces';
|
||||||
|
|
||||||
|
// Initialize the database
|
||||||
|
function initDatabase() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject('Error opening database: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
resolve(db);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
// Create object store for key spaces if it doesn't exist
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' });
|
||||||
|
store.createIndex('name', 'name', { unique: true });
|
||||||
|
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get database connection
|
||||||
|
function getDB() {
|
||||||
|
return initDatabase();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Replace Storage Functions
|
||||||
|
|
||||||
|
Replace the localStorage functions with IndexedDB equivalents:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Save encrypted space to IndexedDB
|
||||||
|
async function saveSpaceToStorage(spaceName, encryptedData) {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
|
||||||
|
const space = {
|
||||||
|
name: spaceName,
|
||||||
|
encryptedData: encryptedData,
|
||||||
|
created: new Date(),
|
||||||
|
lastAccessed: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = store.put(space);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject('Error saving space: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get encrypted space from IndexedDB
|
||||||
|
async function getSpaceFromStorage(spaceName) {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.get(spaceName);
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const space = event.target.result;
|
||||||
|
if (space) {
|
||||||
|
// Update last accessed timestamp
|
||||||
|
updateLastAccessed(spaceName).catch(console.error);
|
||||||
|
resolve(space.encryptedData);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject('Error retrieving space: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last accessed timestamp
|
||||||
|
async function updateLastAccessed(spaceName) {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.get(spaceName);
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const space = event.target.result;
|
||||||
|
if (space) {
|
||||||
|
space.lastAccessed = new Date();
|
||||||
|
store.put(space);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all spaces in IndexedDB
|
||||||
|
async function listSpacesFromStorage() {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.openCursor();
|
||||||
|
|
||||||
|
const spaces = [];
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (cursor) {
|
||||||
|
spaces.push(cursor.value.name);
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve(spaces);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject('Error listing spaces: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove space from IndexedDB
|
||||||
|
async function removeSpaceFromStorage(spaceName) {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.delete(spaceName);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
reject('Error removing space: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Update Application Flow
|
||||||
|
|
||||||
|
Update the login, logout, and other functions to handle the asynchronous nature of IndexedDB:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Login to a space
|
||||||
|
async function performLogin() {
|
||||||
|
const spaceName = document.getElementById('space-name').value.trim();
|
||||||
|
const password = document.getElementById('space-password').value;
|
||||||
|
|
||||||
|
if (!spaceName || !password) {
|
||||||
|
document.getElementById('space-result').textContent = 'Please enter both space name and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get encrypted space from IndexedDB
|
||||||
|
const encryptedSpace = await getSpaceFromStorage(spaceName);
|
||||||
|
if (!encryptedSpace) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the space
|
||||||
|
const result = decrypt_key_space(encryptedSpace, password);
|
||||||
|
if (result === 0) {
|
||||||
|
isLoggedIn = true;
|
||||||
|
currentSpace = spaceName;
|
||||||
|
updateLoginUI();
|
||||||
|
updateKeypairsList();
|
||||||
|
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
|
||||||
|
|
||||||
|
// Setup auto-logout
|
||||||
|
updateActivity();
|
||||||
|
setupAutoLogout();
|
||||||
|
|
||||||
|
// Add activity listeners
|
||||||
|
document.addEventListener('click', updateActivity);
|
||||||
|
document.addEventListener('keypress', updateActivity);
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new space
|
||||||
|
async function performCreateSpace() {
|
||||||
|
const spaceName = document.getElementById('space-name').value.trim();
|
||||||
|
const password = document.getElementById('space-password').value;
|
||||||
|
|
||||||
|
if (!spaceName || !password) {
|
||||||
|
document.getElementById('space-result').textContent = 'Please enter both space name and password';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if space already exists
|
||||||
|
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||||
|
if (existingSpace) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new space
|
||||||
|
const result = create_key_space(spaceName);
|
||||||
|
if (result === 0) {
|
||||||
|
// Encrypt and save the space
|
||||||
|
const encryptedSpace = encrypt_key_space(password);
|
||||||
|
await saveSpaceToStorage(spaceName, encryptedSpace);
|
||||||
|
|
||||||
|
isLoggedIn = true;
|
||||||
|
currentSpace = spaceName;
|
||||||
|
updateLoginUI();
|
||||||
|
updateKeypairsList();
|
||||||
|
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
|
||||||
|
|
||||||
|
// Setup auto-logout
|
||||||
|
updateActivity();
|
||||||
|
setupAutoLogout();
|
||||||
|
|
||||||
|
// Add activity listeners
|
||||||
|
document.addEventListener('click', updateActivity);
|
||||||
|
document.addEventListener('keypress', updateActivity);
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a space from storage
|
||||||
|
async function deleteSpace(spaceName) {
|
||||||
|
if (!spaceName) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if space exists
|
||||||
|
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||||
|
if (!existingSpace) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from IndexedDB
|
||||||
|
await removeSpaceFromStorage(spaceName);
|
||||||
|
|
||||||
|
// If this was the current space, logout
|
||||||
|
if (isLoggedIn && currentSpace === spaceName) {
|
||||||
|
performLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting space:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the spaces dropdown list
|
||||||
|
async function updateSpacesList() {
|
||||||
|
const spacesList = document.getElementById('space-list');
|
||||||
|
|
||||||
|
// Clear existing options
|
||||||
|
while (spacesList.options.length > 1) {
|
||||||
|
spacesList.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get spaces list
|
||||||
|
const spaces = await listSpacesFromStorage();
|
||||||
|
|
||||||
|
// Add options for each space
|
||||||
|
spaces.forEach(spaceName => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = spaceName;
|
||||||
|
option.textContent = spaceName;
|
||||||
|
spacesList.appendChild(option);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating spaces list:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the current space to storage
|
||||||
|
async function saveCurrentSpace() {
|
||||||
|
if (!isLoggedIn || !currentSpace) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store the password in a session variable when logging in
|
||||||
|
// and use it here to avoid issues when the password field is cleared
|
||||||
|
const password = document.getElementById('space-password').value;
|
||||||
|
if (!password) {
|
||||||
|
console.error('Password not available for saving space');
|
||||||
|
alert('Please re-enter your password to save changes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedSpace = encrypt_key_space(password);
|
||||||
|
await saveSpaceToStorage(currentSpace, encryptedSpace);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error saving space:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Update Event Handlers
|
||||||
|
|
||||||
|
Update the event handlers in the `run()` function to handle asynchronous operations:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.getElementById('delete-space-button').addEventListener('click', async () => {
|
||||||
|
if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) {
|
||||||
|
try {
|
||||||
|
if (await deleteSpace(currentSpace)) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('delete-selected-space-button').addEventListener('click', async () => {
|
||||||
|
const selectedSpace = document.getElementById('space-list').value;
|
||||||
|
if (!selectedSpace) {
|
||||||
|
document.getElementById('space-result').textContent = 'Please select a space to delete';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) {
|
||||||
|
try {
|
||||||
|
if (await deleteSpace(selectedSpace)) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`;
|
||||||
|
await updateSpacesList();
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
1. **Unit Tests**:
|
||||||
|
- Test individual IndexedDB functions
|
||||||
|
- Verify CRUD operations work correctly
|
||||||
|
|
||||||
|
2. **Integration Tests**:
|
||||||
|
- Test full application flow with IndexedDB
|
||||||
|
- Verify UI updates correctly
|
||||||
|
|
||||||
|
3. **Error Handling Tests**:
|
||||||
|
- Test database connection errors
|
||||||
|
- Test transaction rollbacks
|
||||||
|
|
||||||
|
4. **Performance Tests**:
|
||||||
|
- Compare performance with localStorage
|
||||||
|
- Verify improved performance for larger data sets
|
||||||
|
|
||||||
|
## Potential Challenges and Solutions
|
||||||
|
|
||||||
|
1. **Browser Compatibility**:
|
||||||
|
- IndexedDB is supported in all modern browsers, but older browsers might have compatibility issues
|
||||||
|
- Consider using a feature detection approach before initializing IndexedDB
|
||||||
|
- Provide a fallback mechanism for browsers that don't support IndexedDB
|
||||||
|
|
||||||
|
2. **Transaction Management**:
|
||||||
|
- Properly manage transactions to maintain data integrity
|
||||||
|
- Ensure all operations within a transaction are completed or rolled back
|
||||||
|
- Use appropriate transaction modes ('readonly' or 'readwrite')
|
||||||
|
|
||||||
|
3. **Error Handling**:
|
||||||
|
- Implement comprehensive error handling for all IndexedDB operations
|
||||||
|
- Provide user-friendly error messages
|
||||||
|
- Log detailed error information for debugging
|
||||||
|
|
||||||
|
4. **Asynchronous Operations**:
|
||||||
|
- Handle Promise rejections with try/catch blocks
|
||||||
|
- Provide loading indicators for operations that might take time
|
||||||
|
- Consider using async/await for cleaner code and better error handling
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. Create the database initialization module
|
||||||
|
2. Implement the IndexedDB storage functions
|
||||||
|
3. Update the UI functions to handle asynchronous operations
|
||||||
|
4. Add comprehensive error handling
|
||||||
|
5. Test all functionality
|
||||||
|
6. Deploy the updated application
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Migrating from localStorage to IndexedDB will provide better performance, transaction capabilities, and a more structured approach to data storage. The asynchronous nature of IndexedDB requires updates to the application flow, but the benefits outweigh the implementation effort.
|
173
src/api/ethereum.rs
Normal file
173
src/api/ethereum.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
//! Public API for Ethereum operations.
|
||||||
|
|
||||||
|
use crate::core::ethereum;
|
||||||
|
use crate::core::error::CryptoError;
|
||||||
|
use ethers::prelude::*;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from the currently selected keypair.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(())` if the wallet was created successfully.
|
||||||
|
/// * `Err(CryptoError::NoActiveSpace)` if no space is active.
|
||||||
|
/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected.
|
||||||
|
/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found.
|
||||||
|
/// * `Err(CryptoError::InvalidKeyLength)` if the keypair's private key is invalid for Ethereum.
|
||||||
|
pub fn create_ethereum_wallet() -> Result<(), CryptoError> {
|
||||||
|
ethereum::create_ethereum_wallet()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from a name and the currently selected keypair.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `name` - The name to use for deterministic derivation.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(())` if the wallet was created successfully.
|
||||||
|
/// * `Err(CryptoError)` if an error occurred.
|
||||||
|
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<(), CryptoError> {
|
||||||
|
ethereum::create_ethereum_wallet_from_name(name)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from a private key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `private_key` - The private key as a hex string (with or without 0x prefix).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(())` if the wallet was created successfully.
|
||||||
|
/// * `Err(CryptoError)` if an error occurred.
|
||||||
|
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<(), CryptoError> {
|
||||||
|
ethereum::create_ethereum_wallet_from_private_key(private_key)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the Ethereum address of the current wallet.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(String)` containing the Ethereum address.
|
||||||
|
/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available.
|
||||||
|
pub fn get_ethereum_address() -> Result<String, CryptoError> {
|
||||||
|
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||||
|
Ok(wallet.address_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the Ethereum private key as a hex string.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(String)` containing the Ethereum private key as a hex string.
|
||||||
|
/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available.
|
||||||
|
pub fn get_ethereum_private_key() -> Result<String, CryptoError> {
|
||||||
|
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||||
|
Ok(wallet.private_key_hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs a message with the Ethereum wallet.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `message` - The message to sign.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(String)` containing the signature.
|
||||||
|
/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available.
|
||||||
|
/// * `Err(CryptoError::SignatureFormatError)` if signing fails.
|
||||||
|
pub async fn sign_ethereum_message(message: &[u8]) -> Result<String, CryptoError> {
|
||||||
|
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||||
|
wallet.sign_message(message).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats an Ethereum balance for display.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `balance_hex` - The balance as a hex string.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `String` containing the formatted balance.
|
||||||
|
pub fn format_eth_balance(balance_hex: &str) -> String {
|
||||||
|
let balance = U256::from_str_radix(balance_hex.trim_start_matches("0x"), 16)
|
||||||
|
.unwrap_or_default();
|
||||||
|
ethereum::format_eth_balance(balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the balance of an Ethereum address.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `address_str` - The Ethereum address as a string.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(String)` containing the balance as a hex string.
|
||||||
|
/// * `Err(CryptoError)` if getting the balance fails.
|
||||||
|
pub async fn get_ethereum_balance(address_str: &str) -> Result<String, CryptoError> {
|
||||||
|
// Create a provider
|
||||||
|
let provider = ethereum::create_gnosis_provider()?;
|
||||||
|
|
||||||
|
// Parse the address
|
||||||
|
let address = address_str.parse::<Address>()
|
||||||
|
.map_err(|_| CryptoError::InvalidEthereumAddress)?;
|
||||||
|
|
||||||
|
// Get the balance
|
||||||
|
let balance = ethereum::get_balance(&provider, address).await?;
|
||||||
|
|
||||||
|
// Return the balance as a hex string
|
||||||
|
Ok(format!("0x{:x}", balance))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends Ethereum from the current wallet to another address.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `to_address` - The recipient's Ethereum address as a string.
|
||||||
|
/// * `amount_eth` - The amount to send in ETH (as a string).
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(String)` containing the transaction hash.
|
||||||
|
/// * `Err(CryptoError)` if sending fails.
|
||||||
|
pub async fn send_ethereum(
|
||||||
|
to_address: &str,
|
||||||
|
amount_eth: &str,
|
||||||
|
) -> Result<String, CryptoError> {
|
||||||
|
// Create a provider
|
||||||
|
let provider = ethereum::create_gnosis_provider()?;
|
||||||
|
|
||||||
|
// Get the current wallet
|
||||||
|
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||||
|
|
||||||
|
// Parse the recipient address
|
||||||
|
let to = to_address.parse::<Address>()
|
||||||
|
.map_err(|_| CryptoError::InvalidEthereumAddress)?;
|
||||||
|
|
||||||
|
// Parse the amount
|
||||||
|
let amount_eth_float = amount_eth.parse::<f64>()
|
||||||
|
.map_err(|_| CryptoError::Other("Invalid amount".to_string()))?;
|
||||||
|
|
||||||
|
// Convert ETH to Wei
|
||||||
|
let amount_wei = (amount_eth_float * 1_000_000_000_000_000_000.0) as u128;
|
||||||
|
let amount = U256::from(amount_wei);
|
||||||
|
|
||||||
|
// Send the transaction
|
||||||
|
let tx_hash = ethereum::send_eth(&wallet, &provider, to, amount).await?;
|
||||||
|
|
||||||
|
// Return the transaction hash
|
||||||
|
Ok(format!("0x{:x}", tx_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all Ethereum wallets.
|
||||||
|
pub fn clear_ethereum_wallets() {
|
||||||
|
ethereum::clear_ethereum_wallets();
|
||||||
|
}
|
@@ -70,6 +70,20 @@ pub fn pub_key() -> Result<Vec<u8>, CryptoError> {
|
|||||||
keypair::keypair_pub_key()
|
keypair::keypair_pub_key()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derives a public key from a private key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `private_key` - The private key bytes.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<u8>)` containing the public key bytes.
|
||||||
|
/// * `Err(CryptoError::InvalidKeyLength)` if the private key is invalid.
|
||||||
|
pub fn derive_public_key(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
keypair::derive_public_key(private_key)
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs a message using the selected keypair.
|
/// Signs a message using the selected keypair.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -105,6 +119,60 @@ pub fn verify(message: &[u8], signature: &[u8]) -> Result<bool, CryptoError> {
|
|||||||
keypair::keypair_verify(message, signature)
|
keypair::keypair_verify(message, signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies a signature using only a public key.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `public_key` - The public key bytes.
|
||||||
|
/// * `message` - The message that was signed.
|
||||||
|
/// * `signature` - The signature to verify.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(true)` if the signature is valid.
|
||||||
|
/// * `Ok(false)` if the signature is invalid.
|
||||||
|
/// * `Err(CryptoError::InvalidKeyLength)` if the public key is invalid.
|
||||||
|
/// * `Err(CryptoError::SignatureFormatError)` if the signature format is invalid.
|
||||||
|
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<bool, CryptoError> {
|
||||||
|
keypair::verify_with_public_key(public_key, message, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts a message using asymmetric encryption.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `recipient_public_key` - The public key of the recipient.
|
||||||
|
/// * `message` - The message to encrypt.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<u8>)` containing the encrypted message.
|
||||||
|
/// * `Err(CryptoError::NoActiveSpace)` if no space is active.
|
||||||
|
/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected.
|
||||||
|
/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found.
|
||||||
|
/// * `Err(CryptoError::InvalidKeyLength)` if the recipient's public key is invalid.
|
||||||
|
/// * `Err(CryptoError::EncryptionFailed)` if encryption fails.
|
||||||
|
pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
keypair::encrypt_asymmetric(recipient_public_key, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a message using asymmetric encryption.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `ciphertext` - The encrypted message.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<u8>)` containing the decrypted message.
|
||||||
|
/// * `Err(CryptoError::NoActiveSpace)` if no space is active.
|
||||||
|
/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected.
|
||||||
|
/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found.
|
||||||
|
/// * `Err(CryptoError::DecryptionFailed)` if decryption fails.
|
||||||
|
pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
keypair::decrypt_asymmetric(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
/// Encrypts a key space with a password.
|
/// Encrypts a key space with a password.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pub mod keypair;
|
pub mod keypair;
|
||||||
pub mod symmetric;
|
pub mod symmetric;
|
||||||
|
pub mod ethereum;
|
||||||
|
|
||||||
// Re-export commonly used items for external users
|
// Re-export commonly used items for external users
|
||||||
// (Keeping this even though it's currently unused, as it's good practice for public APIs)
|
// (Keeping this even though it's currently unused, as it's good practice for public APIs)
|
||||||
|
@@ -34,6 +34,12 @@ pub enum CryptoError {
|
|||||||
InvalidPassword,
|
InvalidPassword,
|
||||||
/// Error during serialization or deserialization.
|
/// Error during serialization or deserialization.
|
||||||
SerializationError,
|
SerializationError,
|
||||||
|
/// No Ethereum wallet is available.
|
||||||
|
NoEthereumWallet,
|
||||||
|
/// Ethereum transaction failed.
|
||||||
|
EthereumTransactionFailed,
|
||||||
|
/// Invalid Ethereum address.
|
||||||
|
InvalidEthereumAddress,
|
||||||
/// Other error with description.
|
/// Other error with description.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
Other(String),
|
Other(String),
|
||||||
@@ -57,6 +63,9 @@ impl std::fmt::Display for CryptoError {
|
|||||||
CryptoError::SpaceAlreadyExists => write!(f, "Space already exists"),
|
CryptoError::SpaceAlreadyExists => write!(f, "Space already exists"),
|
||||||
CryptoError::InvalidPassword => write!(f, "Invalid password"),
|
CryptoError::InvalidPassword => write!(f, "Invalid password"),
|
||||||
CryptoError::SerializationError => write!(f, "Serialization error"),
|
CryptoError::SerializationError => write!(f, "Serialization error"),
|
||||||
|
CryptoError::NoEthereumWallet => write!(f, "No Ethereum wallet available"),
|
||||||
|
CryptoError::EthereumTransactionFailed => write!(f, "Ethereum transaction failed"),
|
||||||
|
CryptoError::InvalidEthereumAddress => write!(f, "Invalid Ethereum address"),
|
||||||
CryptoError::Other(s) => write!(f, "Crypto error: {}", s),
|
CryptoError::Other(s) => write!(f, "Crypto error: {}", s),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +91,9 @@ pub fn error_to_status_code(err: CryptoError) -> i32 {
|
|||||||
CryptoError::SpaceAlreadyExists => -13,
|
CryptoError::SpaceAlreadyExists => -13,
|
||||||
CryptoError::InvalidPassword => -14,
|
CryptoError::InvalidPassword => -14,
|
||||||
CryptoError::SerializationError => -15,
|
CryptoError::SerializationError => -15,
|
||||||
|
CryptoError::NoEthereumWallet => -16,
|
||||||
|
CryptoError::EthereumTransactionFailed => -17,
|
||||||
|
CryptoError::InvalidEthereumAddress => -18,
|
||||||
CryptoError::Other(_) => -99,
|
CryptoError::Other(_) => -99,
|
||||||
}
|
}
|
||||||
}
|
}
|
228
src/core/ethereum.rs
Normal file
228
src/core/ethereum.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
//! Core implementation of Ethereum functionality.
|
||||||
|
|
||||||
|
use ethers::prelude::*;
|
||||||
|
use ethers::signers::{LocalWallet, Signer, Wallet};
|
||||||
|
use ethers::utils::hex;
|
||||||
|
use k256::ecdsa::SigningKey;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
|
use super::error::CryptoError;
|
||||||
|
use super::keypair::KeyPair;
|
||||||
|
|
||||||
|
// Gnosis Chain configuration
|
||||||
|
pub const GNOSIS_CHAIN_ID: u64 = 100;
|
||||||
|
pub const GNOSIS_RPC_URL: &str = "https://rpc.gnosis.gateway.fm";
|
||||||
|
pub const GNOSIS_EXPLORER: &str = "https://gnosisscan.io";
|
||||||
|
|
||||||
|
/// An Ethereum wallet derived from a keypair.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct EthereumWallet {
|
||||||
|
pub address: Address,
|
||||||
|
pub wallet: Wallet<SigningKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EthereumWallet {
|
||||||
|
/// Creates a new Ethereum wallet from a keypair.
|
||||||
|
pub fn from_keypair(keypair: &KeyPair) -> Result<Self, CryptoError> {
|
||||||
|
// Get the private key bytes from the keypair
|
||||||
|
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||||
|
|
||||||
|
// Convert to a hex string (without 0x prefix)
|
||||||
|
let private_key_hex = hex::encode(private_key_bytes);
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the private key
|
||||||
|
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?
|
||||||
|
.with_chain_id(GNOSIS_CHAIN_ID);
|
||||||
|
|
||||||
|
// Get the Ethereum address
|
||||||
|
let address = wallet.address();
|
||||||
|
|
||||||
|
Ok(EthereumWallet {
|
||||||
|
address,
|
||||||
|
wallet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation).
|
||||||
|
pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result<Self, CryptoError> {
|
||||||
|
// Get the private key bytes from the keypair
|
||||||
|
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||||
|
|
||||||
|
// Create a deterministic seed by combining name and private key
|
||||||
|
let mut hasher = Sha256::default();
|
||||||
|
hasher.update(name.as_bytes());
|
||||||
|
hasher.update(&private_key_bytes);
|
||||||
|
let seed = hasher.finalize();
|
||||||
|
|
||||||
|
// Use the seed as a private key
|
||||||
|
let private_key_hex = hex::encode(seed);
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the derived private key
|
||||||
|
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?
|
||||||
|
.with_chain_id(GNOSIS_CHAIN_ID);
|
||||||
|
|
||||||
|
// Get the Ethereum address
|
||||||
|
let address = wallet.address();
|
||||||
|
|
||||||
|
Ok(EthereumWallet {
|
||||||
|
address,
|
||||||
|
wallet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Ethereum wallet from a private key.
|
||||||
|
pub fn from_private_key(private_key: &str) -> Result<Self, CryptoError> {
|
||||||
|
// Remove 0x prefix if present
|
||||||
|
let private_key_clean = private_key.trim_start_matches("0x");
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the private key
|
||||||
|
let wallet = LocalWallet::from_str(private_key_clean)
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?
|
||||||
|
.with_chain_id(GNOSIS_CHAIN_ID);
|
||||||
|
|
||||||
|
// Get the Ethereum address
|
||||||
|
let address = wallet.address();
|
||||||
|
|
||||||
|
Ok(EthereumWallet {
|
||||||
|
address,
|
||||||
|
wallet,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the Ethereum address as a string.
|
||||||
|
pub fn address_string(&self) -> String {
|
||||||
|
format!("{:?}", self.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs a message with the Ethereum wallet.
|
||||||
|
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
|
||||||
|
let signature = self.wallet.sign_message(message)
|
||||||
|
.await
|
||||||
|
.map_err(|_| CryptoError::SignatureFormatError)?;
|
||||||
|
|
||||||
|
Ok(signature.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the private key as a hex string.
|
||||||
|
pub fn private_key_hex(&self) -> String {
|
||||||
|
let bytes = self.wallet.signer().to_bytes();
|
||||||
|
hex::encode(bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global storage for Ethereum wallets.
|
||||||
|
static ETH_WALLETS: Lazy<Mutex<Vec<EthereumWallet>>> = Lazy::new(|| {
|
||||||
|
Mutex::new(Vec::new())
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from the currently selected keypair.
|
||||||
|
pub fn create_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||||
|
// Get the currently selected keypair
|
||||||
|
let keypair = super::keypair::get_selected_keypair()?;
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the keypair
|
||||||
|
let wallet = EthereumWallet::from_keypair(&keypair)?;
|
||||||
|
|
||||||
|
// Store the wallet
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
wallets.push(wallet.clone());
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the current Ethereum wallet.
|
||||||
|
pub fn get_current_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||||
|
let wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
|
||||||
|
if wallets.is_empty() {
|
||||||
|
return Err(CryptoError::NoKeypairSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wallets.last().unwrap().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all Ethereum wallets.
|
||||||
|
pub fn clear_ethereum_wallets() {
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
wallets.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats an Ethereum balance for display.
|
||||||
|
pub fn format_eth_balance(balance: U256) -> String {
|
||||||
|
let wei = balance.as_u128();
|
||||||
|
let eth = wei as f64 / 1_000_000_000_000_000_000.0;
|
||||||
|
format!("{:.6} ETH", eth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the balance of an Ethereum address.
|
||||||
|
pub async fn get_balance(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
|
||||||
|
provider.get_balance(address, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| CryptoError::Other("Failed to get balance".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends Ethereum from one address to another.
|
||||||
|
pub async fn send_eth(
|
||||||
|
wallet: &EthereumWallet,
|
||||||
|
provider: &Provider<Http>,
|
||||||
|
to: Address,
|
||||||
|
amount: U256,
|
||||||
|
) -> Result<H256, CryptoError> {
|
||||||
|
// Create a client with the wallet
|
||||||
|
let client = SignerMiddleware::new(
|
||||||
|
provider.clone(),
|
||||||
|
wallet.wallet.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the transaction
|
||||||
|
let tx = TransactionRequest::new()
|
||||||
|
.to(to)
|
||||||
|
.value(amount)
|
||||||
|
.gas(21000);
|
||||||
|
|
||||||
|
// Send the transaction
|
||||||
|
let pending_tx = client.send_transaction(tx, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| CryptoError::Other("Failed to send transaction".to_string()))?;
|
||||||
|
|
||||||
|
// Return the transaction hash instead of waiting for the receipt
|
||||||
|
Ok(pending_tx.tx_hash())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a provider for the Gnosis Chain.
|
||||||
|
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
|
||||||
|
Provider::<Http>::try_from(GNOSIS_RPC_URL)
|
||||||
|
.map_err(|_| CryptoError::Other("Failed to create Gnosis provider".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from a name and the currently selected keypair.
|
||||||
|
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
|
||||||
|
// Get the currently selected keypair
|
||||||
|
let keypair = super::keypair::get_selected_keypair()?;
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the name and keypair
|
||||||
|
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair)?;
|
||||||
|
|
||||||
|
// Store the wallet
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
wallets.push(wallet.clone());
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an Ethereum wallet from a private key.
|
||||||
|
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
|
||||||
|
// Create an Ethereum wallet from the private key
|
||||||
|
let wallet = EthereumWallet::from_private_key(private_key)?;
|
||||||
|
|
||||||
|
// Store the wallet
|
||||||
|
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||||
|
wallets.push(wallet.clone());
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
|
}
|
@@ -6,6 +6,7 @@ use serde::{Serialize, Deserialize};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
use super::error::CryptoError;
|
use super::error::CryptoError;
|
||||||
|
|
||||||
@@ -116,6 +117,14 @@ impl KeyPair {
|
|||||||
pub fn pub_key(&self) -> Vec<u8> {
|
pub fn pub_key(&self) -> Vec<u8> {
|
||||||
self.verifying_key.to_sec1_bytes().to_vec()
|
self.verifying_key.to_sec1_bytes().to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derives a public key from a private key.
|
||||||
|
pub fn pub_key_from_private(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let signing_key = SigningKey::from_bytes(private_key.into())
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
let verifying_key = VerifyingKey::from(&signing_key);
|
||||||
|
Ok(verifying_key.to_sec1_bytes().to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs a message.
|
/// Signs a message.
|
||||||
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
||||||
@@ -133,6 +142,88 @@ impl KeyPair {
|
|||||||
Err(_) => Ok(false), // Verification failed, but operation was successful
|
Err(_) => Ok(false), // Verification failed, but operation was successful
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies a message signature using only a public key.
|
||||||
|
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||||
|
let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
|
let signature = Signature::from_bytes(signature_bytes.into())
|
||||||
|
.map_err(|_| CryptoError::SignatureFormatError)?;
|
||||||
|
|
||||||
|
match verifying_key.verify(message, &signature) {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(_) => Ok(false), // Verification failed, but operation was successful
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts a message using the recipient's public key.
|
||||||
|
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme):
|
||||||
|
/// 1. Generate an ephemeral keypair
|
||||||
|
/// 2. Derive a shared secret using ECDH
|
||||||
|
/// 3. Derive encryption key from the shared secret
|
||||||
|
/// 4. Encrypt the message using symmetric encryption
|
||||||
|
/// 5. Return the ephemeral public key and the ciphertext
|
||||||
|
pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
// Parse recipient's public key
|
||||||
|
let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key)
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
|
// Generate ephemeral keypair
|
||||||
|
let ephemeral_signing_key = SigningKey::random(&mut OsRng);
|
||||||
|
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
|
||||||
|
|
||||||
|
// Derive shared secret (this is a simplified ECDH)
|
||||||
|
// In a real implementation, we would use proper ECDH, but for this example:
|
||||||
|
let shared_point = recipient_key.to_encoded_point(false);
|
||||||
|
let shared_secret = {
|
||||||
|
let mut hasher = Sha256::default();
|
||||||
|
hasher.update(ephemeral_signing_key.to_bytes());
|
||||||
|
hasher.update(shared_point.as_bytes());
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encrypt the message using the derived key
|
||||||
|
let ciphertext = super::symmetric::encrypt_with_key(&shared_secret, message)
|
||||||
|
.map_err(|_| CryptoError::EncryptionFailed)?;
|
||||||
|
|
||||||
|
// Format: ephemeral_public_key || ciphertext
|
||||||
|
let mut result = ephemeral_public_key.to_sec1_bytes().to_vec();
|
||||||
|
result.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a message using the recipient's private key.
|
||||||
|
/// This is the counterpart to encrypt_asymmetric.
|
||||||
|
pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
// The first 33 or 65 bytes (depending on compression) are the ephemeral public key
|
||||||
|
// For simplicity, we'll assume uncompressed keys (65 bytes)
|
||||||
|
if ciphertext.len() <= 65 {
|
||||||
|
return Err(CryptoError::DecryptionFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract ephemeral public key and actual ciphertext
|
||||||
|
let ephemeral_public_key = &ciphertext[..65];
|
||||||
|
let actual_ciphertext = &ciphertext[65..];
|
||||||
|
|
||||||
|
// Parse ephemeral public key
|
||||||
|
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key)
|
||||||
|
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
|
|
||||||
|
// Derive shared secret (simplified ECDH)
|
||||||
|
let shared_point = sender_key.to_encoded_point(false);
|
||||||
|
let shared_secret = {
|
||||||
|
let mut hasher = Sha256::default();
|
||||||
|
hasher.update(self.signing_key.to_bytes());
|
||||||
|
hasher.update(shared_point.as_bytes());
|
||||||
|
hasher.finalize().to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decrypt the message using the derived key
|
||||||
|
super::symmetric::decrypt_with_key(&shared_secret, actual_ciphertext)
|
||||||
|
.map_err(|_| CryptoError::DecryptionFailed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collection of keypairs.
|
/// A collection of keypairs.
|
||||||
@@ -299,6 +390,11 @@ pub fn keypair_pub_key() -> Result<Vec<u8>, CryptoError> {
|
|||||||
Ok(keypair.pub_key())
|
Ok(keypair.pub_key())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derives a public key from a private key.
|
||||||
|
pub fn derive_public_key(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
KeyPair::pub_key_from_private(private_key)
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs a message with the selected keypair.
|
/// Signs a message with the selected keypair.
|
||||||
pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
let keypair = get_selected_keypair()?;
|
let keypair = get_selected_keypair()?;
|
||||||
@@ -309,4 +405,21 @@ pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
|||||||
pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||||
let keypair = get_selected_keypair()?;
|
let keypair = get_selected_keypair()?;
|
||||||
keypair.verify(message, signature_bytes)
|
keypair.verify(message, signature_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies a message signature with a public key.
|
||||||
|
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||||
|
KeyPair::verify_with_public_key(public_key, message, signature_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts a message for a recipient using their public key.
|
||||||
|
pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let keypair = get_selected_keypair()?;
|
||||||
|
keypair.encrypt_asymmetric(recipient_public_key, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts a message that was encrypted with the current keypair's public key.
|
||||||
|
pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
let keypair = get_selected_keypair()?;
|
||||||
|
keypair.decrypt_asymmetric(ciphertext)
|
||||||
}
|
}
|
@@ -3,6 +3,7 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod keypair;
|
pub mod keypair;
|
||||||
pub mod symmetric;
|
pub mod symmetric;
|
||||||
|
pub mod ethereum;
|
||||||
|
|
||||||
// Re-export commonly used items for internal use
|
// Re-export commonly used items for internal use
|
||||||
// (Keeping this even though it's currently unused, as it's good practice for internal modules)
|
// (Keeping this even though it's currently unused, as it's good practice for internal modules)
|
||||||
|
@@ -33,7 +33,7 @@ pub fn generate_symmetric_key() -> [u8; 32] {
|
|||||||
///
|
///
|
||||||
/// A 32-byte array containing the derived key.
|
/// A 32-byte array containing the derived key.
|
||||||
pub fn derive_key_from_password(password: &str) -> [u8; 32] {
|
pub fn derive_key_from_password(password: &str) -> [u8; 32] {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::default();
|
||||||
hasher.update(password.as_bytes());
|
hasher.update(password.as_bytes());
|
||||||
let result = hasher.finalize();
|
let result = hasher.finalize();
|
||||||
|
|
||||||
@@ -111,6 +111,36 @@ pub fn decrypt_symmetric(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec
|
|||||||
.map_err(|_| CryptoError::DecryptionFailed)
|
.map_err(|_| CryptoError::DecryptionFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encrypts data using a key directly (for internal use).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - The encryption key.
|
||||||
|
/// * `message` - The message to encrypt.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<u8>)` containing the ciphertext with the nonce appended.
|
||||||
|
/// * `Err(CryptoError)` if encryption fails.
|
||||||
|
pub fn encrypt_with_key(key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
encrypt_symmetric(key, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts data using a key directly (for internal use).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `key` - The decryption key.
|
||||||
|
/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(Vec<u8>)` containing the decrypted message.
|
||||||
|
/// * `Err(CryptoError)` if decryption fails.
|
||||||
|
pub fn decrypt_with_key(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||||
|
decrypt_symmetric(key, ciphertext_with_nonce)
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadata for an encrypted key space.
|
/// Metadata for an encrypted key space.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EncryptedKeySpaceMetadata {
|
pub struct EncryptedKeySpaceMetadata {
|
||||||
|
73
src/lib.rs
73
src/lib.rs
@@ -11,6 +11,7 @@ mod tests;
|
|||||||
// Re-export for internal use
|
// Re-export for internal use
|
||||||
use api::keypair;
|
use api::keypair;
|
||||||
use api::symmetric;
|
use api::symmetric;
|
||||||
|
use api::ethereum;
|
||||||
use core::error::error_to_status_code;
|
use core::error::error_to_status_code;
|
||||||
|
|
||||||
// This is like the `main` function, except for JavaScript.
|
// This is like the `main` function, except for JavaScript.
|
||||||
@@ -98,6 +99,30 @@ pub fn keypair_verify(message: &[u8], signature: &[u8]) -> Result<bool, JsValue>
|
|||||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn derive_public_key(private_key: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
keypair::derive_public_key(private_key)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<bool, JsValue> {
|
||||||
|
keypair::verify_with_public_key(public_key, message, signature)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
keypair::encrypt_asymmetric(recipient_public_key, message)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||||
|
keypair::decrypt_asymmetric(ciphertext)
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
// --- WebAssembly Exports for Symmetric Encryption ---
|
// --- WebAssembly Exports for Symmetric Encryption ---
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -133,3 +158,51 @@ pub fn decrypt_with_password(password: &str, ciphertext: &[u8]) -> Result<Vec<u8
|
|||||||
symmetric::decrypt_with_password(password, ciphertext)
|
symmetric::decrypt_with_password(password, ciphertext)
|
||||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- WebAssembly Exports for Ethereum ---
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn create_ethereum_wallet() -> i32 {
|
||||||
|
match ethereum::create_ethereum_wallet() {
|
||||||
|
Ok(_) => 0, // Success
|
||||||
|
Err(e) => error_to_status_code(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn create_ethereum_wallet_from_name(name: &str) -> i32 {
|
||||||
|
match ethereum::create_ethereum_wallet_from_name(name) {
|
||||||
|
Ok(_) => 0, // Success
|
||||||
|
Err(e) => error_to_status_code(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> i32 {
|
||||||
|
match ethereum::create_ethereum_wallet_from_private_key(private_key) {
|
||||||
|
Ok(_) => 0, // Success
|
||||||
|
Err(e) => error_to_status_code(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_ethereum_address() -> Result<String, JsValue> {
|
||||||
|
ethereum::get_ethereum_address()
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn get_ethereum_private_key() -> Result<String, JsValue> {
|
||||||
|
ethereum::get_ethereum_private_key()
|
||||||
|
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn format_eth_balance(balance_hex: &str) -> String {
|
||||||
|
ethereum::format_eth_balance(balance_hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn clear_ethereum_wallets() {
|
||||||
|
ethereum::clear_ethereum_wallets();
|
||||||
|
}
|
||||||
|
@@ -6,15 +6,16 @@ mod tests {
|
|||||||
|
|
||||||
// Helper to ensure keypair is initialized for tests that need it.
|
// Helper to ensure keypair is initialized for tests that need it.
|
||||||
fn ensure_keypair_initialized() {
|
fn ensure_keypair_initialized() {
|
||||||
// Use try_init which doesn't panic if already initialized
|
// Create a space and keypair for testing
|
||||||
let _ = keypair::keypair_new();
|
let _ = keypair::create_space("test_space");
|
||||||
assert!(keypair::KEYPAIR.get().is_some(), "KEYPAIR should be initialized");
|
let _ = keypair::create_keypair("test_keypair");
|
||||||
|
let _ = keypair::select_keypair("test_keypair");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keypair_generation_and_retrieval() {
|
fn test_keypair_generation_and_retrieval() {
|
||||||
let _ = keypair::keypair_new(); // Ignore error if already initialized by another test
|
ensure_keypair_initialized();
|
||||||
let pub_key = keypair::keypair_pub_key().expect("Should be able to get pub key after init");
|
let pub_key = keypair::pub_key().expect("Should be able to get pub key after init");
|
||||||
assert!(!pub_key.is_empty(), "Public key should not be empty");
|
assert!(!pub_key.is_empty(), "Public key should not be empty");
|
||||||
// Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix)
|
// Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix)
|
||||||
assert!(pub_key.len() == 33 || pub_key.len() == 65, "Public key length is incorrect");
|
assert!(pub_key.len() == 33 || pub_key.len() == 65, "Public key length is incorrect");
|
||||||
@@ -25,10 +26,10 @@ mod tests {
|
|||||||
fn test_sign_verify_valid() {
|
fn test_sign_verify_valid() {
|
||||||
ensure_keypair_initialized();
|
ensure_keypair_initialized();
|
||||||
let message = b"this is a test message";
|
let message = b"this is a test message";
|
||||||
let signature = keypair::keypair_sign(message).expect("Signing failed");
|
let signature = keypair::sign(message).expect("Signing failed");
|
||||||
assert!(!signature.is_empty(), "Signature should not be empty");
|
assert!(!signature.is_empty(), "Signature should not be empty");
|
||||||
|
|
||||||
let is_valid = keypair::keypair_verify(message, &signature).expect("Verification failed");
|
let is_valid = keypair::verify(message, &signature).expect("Verification failed");
|
||||||
assert!(is_valid, "Signature should be valid");
|
assert!(is_valid, "Signature should be valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,11 +37,11 @@ mod tests {
|
|||||||
fn test_verify_invalid_signature() {
|
fn test_verify_invalid_signature() {
|
||||||
ensure_keypair_initialized();
|
ensure_keypair_initialized();
|
||||||
let message = b"another test message";
|
let message = b"another test message";
|
||||||
let mut invalid_signature = keypair::keypair_sign(message).expect("Signing failed");
|
let mut invalid_signature = keypair::sign(message).expect("Signing failed");
|
||||||
// Tamper with the signature
|
// Tamper with the signature
|
||||||
invalid_signature[0] = invalid_signature[0].wrapping_add(1);
|
invalid_signature[0] = invalid_signature[0].wrapping_add(1);
|
||||||
|
|
||||||
let is_valid = keypair::keypair_verify(message, &invalid_signature).expect("Verification process failed");
|
let is_valid = keypair::verify(message, &invalid_signature).expect("Verification process failed");
|
||||||
assert!(!is_valid, "Tampered signature should be invalid");
|
assert!(!is_valid, "Tampered signature should be invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +50,14 @@ mod tests {
|
|||||||
ensure_keypair_initialized();
|
ensure_keypair_initialized();
|
||||||
let message = b"original message";
|
let message = b"original message";
|
||||||
let wrong_message = b"different message";
|
let wrong_message = b"different message";
|
||||||
let signature = keypair::keypair_sign(message).expect("Signing failed");
|
let signature = keypair::sign(message).expect("Signing failed");
|
||||||
|
|
||||||
let is_valid = keypair::keypair_verify(wrong_message, &signature).expect("Verification process failed");
|
let is_valid = keypair::verify(wrong_message, &signature).expect("Verification process failed");
|
||||||
assert!(!is_valid, "Signature should be invalid for a different message");
|
assert!(!is_valid, "Signature should be invalid for a different message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up after tests
|
||||||
|
fn cleanup() {
|
||||||
|
keypair::logout();
|
||||||
|
}
|
||||||
}
|
}
|
201
www/ethereum.html
Normal file
201
www/ethereum.html
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Ethereum WebAssembly Demo</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button.secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
button.danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
input, textarea, select {
|
||||||
|
padding: 8px;
|
||||||
|
margin: 5px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.key-display {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.status.logged-in {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.status.logged-out {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.address-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.address-label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.address-value {
|
||||||
|
font-family: monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
margin-right: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.nav-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Ethereum WebAssembly Demo</h1>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.html">Main Crypto Demo</a>
|
||||||
|
<a href="ethereum.html">Ethereum Demo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note">Note: You must first login and create a keypair in the <a href="index.html">Main Crypto Demo</a> page.</div>
|
||||||
|
|
||||||
|
<!-- Keypair Selection Section -->
|
||||||
|
<div class="container" id="keypair-selection-container">
|
||||||
|
<h2>Select Keypair</h2>
|
||||||
|
|
||||||
|
<div id="login-status" class="status logged-out">
|
||||||
|
Status: Not logged in
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="select-keypair">Select Keypair:</label>
|
||||||
|
<select id="select-keypair">
|
||||||
|
<option value="">-- Select a keypair --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result" id="keypair-management-result">Result will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ethereum Wallet Section -->
|
||||||
|
<div class="container" id="ethereum-wallet-container">
|
||||||
|
<h2>Ethereum Wallet</h2>
|
||||||
|
|
||||||
|
<div class="note">Note: All operations use the Gnosis Chain (xDAI)</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button id="create-ethereum-wallet-button">Create Ethereum Wallet from Selected Keypair</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="wallet-name">Create from Name and Keypair:</label>
|
||||||
|
<input type="text" id="wallet-name" placeholder="Enter name for deterministic derivation" />
|
||||||
|
<button id="create-from-name-button">Create from Name</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="private-key">Import Private Key:</label>
|
||||||
|
<input type="text" id="private-key" placeholder="Enter private key (with or without 0x prefix)" />
|
||||||
|
<button id="import-private-key-button">Import Private Key</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ethereum-wallet-info" class="hidden">
|
||||||
|
<div class="address-container">
|
||||||
|
<div class="address-label">Ethereum Address:</div>
|
||||||
|
<div class="address-value" id="ethereum-address-value"></div>
|
||||||
|
<button id="copy-address-button" class="secondary">Copy Address</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="address-container">
|
||||||
|
<div class="address-label">Private Key (hex):</div>
|
||||||
|
<div class="address-value" id="ethereum-private-key-value"></div>
|
||||||
|
<button id="copy-private-key-button" class="secondary">Copy Private Key</button>
|
||||||
|
<div class="note">Warning: Never share your private key with anyone!</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result" id="ethereum-wallet-result">Result will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ethereum Balance Section -->
|
||||||
|
<div class="container" id="ethereum-balance-container">
|
||||||
|
<h2>Check Ethereum Balance</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button id="check-balance-button">Check Current Wallet Balance</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result" id="balance-result">Balance will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./js/ethereum.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
101
www/index.html
101
www/index.html
@@ -84,11 +84,47 @@
|
|||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.nav-links {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
margin-right: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.nav-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.pubkey-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.pubkey-label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.pubkey-value {
|
||||||
|
font-family: monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Rust WebAssembly Crypto Example</h1>
|
<h1>Rust WebAssembly Crypto Example</h1>
|
||||||
|
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="index.html">Main Crypto Demo</a>
|
||||||
|
<a href="ethereum.html">Ethereum Demo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Login/Space Management Section -->
|
<!-- Login/Space Management Section -->
|
||||||
<div class="container" id="login-container">
|
<div class="container" id="login-container">
|
||||||
<h2>Key Space Management</h2>
|
<h2>Key Space Management</h2>
|
||||||
@@ -118,7 +154,21 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Current Space: <span id="current-space-name"></span></label>
|
<label>Current Space: <span id="current-space-name"></span></label>
|
||||||
</div>
|
</div>
|
||||||
<button id="logout-button" class="danger">Logout</button>
|
<div class="form-group">
|
||||||
|
<button id="logout-button" class="danger">Logout</button>
|
||||||
|
<button id="delete-space-button" class="danger">Delete Space</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="manage-spaces-form">
|
||||||
|
<h3>Manage Spaces</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="space-list">Available Spaces:</label>
|
||||||
|
<select id="space-list">
|
||||||
|
<option value="">-- Select a space --</option>
|
||||||
|
</select>
|
||||||
|
<button id="delete-selected-space-button" class="danger">Delete Selected Space</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result" id="space-result">Result will appear here</div>
|
<div class="result" id="space-result">Result will appear here</div>
|
||||||
@@ -166,6 +216,55 @@
|
|||||||
<div class="result" id="verify-result">Verification result will appear here</div>
|
<div class="result" id="verify-result">Verification result will appear here</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Verify with Public Key Only</h2>
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pubkey-verify-pubkey">Public Key (hex):</label>
|
||||||
|
<input type="text" id="pubkey-verify-pubkey" placeholder="Enter public key in hex format" />
|
||||||
|
</div>
|
||||||
|
<textarea id="pubkey-verify-message" placeholder="Enter message to verify" rows="3"></textarea>
|
||||||
|
<textarea id="pubkey-verify-signature" placeholder="Enter signature to verify" rows="3"></textarea>
|
||||||
|
<button id="pubkey-verify-button">Verify with Public Key</button>
|
||||||
|
</div>
|
||||||
|
<div class="result" id="pubkey-verify-result">Verification result will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Derive Public Key from Private Key</h2>
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="derive-pubkey-privkey">Private Key (hex):</label>
|
||||||
|
<input type="text" id="derive-pubkey-privkey" placeholder="Enter private key in hex format" />
|
||||||
|
</div>
|
||||||
|
<button id="derive-pubkey-button">Derive Public Key</button>
|
||||||
|
</div>
|
||||||
|
<div class="result" id="derive-pubkey-result">Public key will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Asymmetric Encryption</h2>
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="asymmetric-encrypt-pubkey">Recipient's Public Key (hex):</label>
|
||||||
|
<input type="text" id="asymmetric-encrypt-pubkey" placeholder="Enter recipient's public key in hex format" />
|
||||||
|
</div>
|
||||||
|
<textarea id="asymmetric-encrypt-message" placeholder="Enter message to encrypt" rows="3">This is a secret message that will be encrypted with asymmetric encryption</textarea>
|
||||||
|
<button id="asymmetric-encrypt-button">Encrypt with Public Key</button>
|
||||||
|
</div>
|
||||||
|
<div class="result" id="asymmetric-encrypt-result">Encrypted data will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h2>Asymmetric Decryption</h2>
|
||||||
|
<div>
|
||||||
|
<div class="note">Note: Uses the currently selected keypair for decryption</div>
|
||||||
|
<textarea id="asymmetric-decrypt-ciphertext" placeholder="Enter ciphertext (hex)" rows="3"></textarea>
|
||||||
|
<button id="asymmetric-decrypt-button">Decrypt with Private Key</button>
|
||||||
|
</div>
|
||||||
|
<div class="result" id="asymmetric-decrypt-result">Decrypted data will appear here</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Symmetric Encryption</h2>
|
<h2>Symmetric Encryption</h2>
|
||||||
<div>
|
<div>
|
||||||
|
483
www/js/ethereum.js
Normal file
483
www/js/ethereum.js
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
// Import our WebAssembly module
|
||||||
|
import init, {
|
||||||
|
create_key_space,
|
||||||
|
encrypt_key_space,
|
||||||
|
decrypt_key_space,
|
||||||
|
logout,
|
||||||
|
create_keypair,
|
||||||
|
select_keypair,
|
||||||
|
list_keypairs,
|
||||||
|
keypair_pub_key,
|
||||||
|
create_ethereum_wallet,
|
||||||
|
create_ethereum_wallet_from_name,
|
||||||
|
create_ethereum_wallet_from_private_key,
|
||||||
|
get_ethereum_address,
|
||||||
|
get_ethereum_private_key,
|
||||||
|
format_eth_balance,
|
||||||
|
clear_ethereum_wallets
|
||||||
|
} from '../../pkg/webassembly.js';
|
||||||
|
|
||||||
|
// Helper function to convert ArrayBuffer to hex string
|
||||||
|
function bufferToHex(buffer) {
|
||||||
|
return Array.from(new Uint8Array(buffer))
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to convert hex string to Uint8Array
|
||||||
|
function hexToBuffer(hex) {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexedDB setup for Ethereum wallets
|
||||||
|
const DB_NAME = 'EthWalletDB';
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
const STORE_NAME = 'ethWallets';
|
||||||
|
|
||||||
|
// Initialize the database
|
||||||
|
function initDatabase() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error opening Ethereum wallet database:', event.target.error);
|
||||||
|
reject('Error opening database: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
resolve(db);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
// Create object store for Ethereum wallets if it doesn't exist
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(STORE_NAME, { keyPath: 'address' });
|
||||||
|
store.createIndex('address', 'address', { unique: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get database connection
|
||||||
|
function getDB() {
|
||||||
|
return initDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Ethereum wallet to IndexedDB
|
||||||
|
async function saveEthWalletToStorage(address, privateKey) {
|
||||||
|
try {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
|
||||||
|
const wallet = {
|
||||||
|
address: address,
|
||||||
|
privateKey: privateKey,
|
||||||
|
created: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = store.put(wallet);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error saving Ethereum wallet:', event.target.error);
|
||||||
|
reject('Error saving wallet: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database error in saveEthWalletToStorage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Ethereum wallet from IndexedDB
|
||||||
|
async function getEthWalletFromStorage(address) {
|
||||||
|
try {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.get(address);
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const wallet = event.target.result;
|
||||||
|
if (wallet) {
|
||||||
|
resolve(wallet.privateKey);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error retrieving Ethereum wallet:', event.target.error);
|
||||||
|
reject('Error retrieving wallet: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database error in getEthWalletFromStorage:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session state
|
||||||
|
let selectedKeypair = null;
|
||||||
|
let hasEthereumWallet = false;
|
||||||
|
|
||||||
|
// Update UI based on login state
|
||||||
|
function updateLoginUI() {
|
||||||
|
const loginStatus = document.getElementById('login-status');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to list keypairs to check if logged in
|
||||||
|
const keypairs = list_keypairs();
|
||||||
|
|
||||||
|
if (keypairs && keypairs.length > 0) {
|
||||||
|
loginStatus.textContent = 'Status: Logged in';
|
||||||
|
loginStatus.className = 'status logged-in';
|
||||||
|
|
||||||
|
// Update keypairs list
|
||||||
|
updateKeypairsList();
|
||||||
|
} else {
|
||||||
|
loginStatus.textContent = 'Status: Not logged in. Please login in the Main Crypto Demo page first.';
|
||||||
|
loginStatus.className = 'status logged-out';
|
||||||
|
|
||||||
|
// Hide Ethereum wallet info when logged out
|
||||||
|
document.getElementById('ethereum-wallet-info').classList.add('hidden');
|
||||||
|
hasEthereumWallet = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loginStatus.textContent = 'Status: Not logged in. Please login in the Main Crypto Demo page first.';
|
||||||
|
loginStatus.className = 'status logged-out';
|
||||||
|
|
||||||
|
// Hide Ethereum wallet info when logged out
|
||||||
|
document.getElementById('ethereum-wallet-info').classList.add('hidden');
|
||||||
|
hasEthereumWallet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the keypairs dropdown list
|
||||||
|
function updateKeypairsList() {
|
||||||
|
const selectKeypair = document.getElementById('select-keypair');
|
||||||
|
|
||||||
|
// Clear existing options
|
||||||
|
while (selectKeypair.options.length > 1) {
|
||||||
|
selectKeypair.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get keypairs list
|
||||||
|
const keypairs = list_keypairs();
|
||||||
|
|
||||||
|
// Add options for each keypair
|
||||||
|
keypairs.forEach(keypairName => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = keypairName;
|
||||||
|
option.textContent = keypairName;
|
||||||
|
selectKeypair.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's a selected keypair, select it in the dropdown
|
||||||
|
if (selectedKeypair) {
|
||||||
|
selectKeypair.value = selectedKeypair;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating keypairs list:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select a keypair
|
||||||
|
async function performSelectKeypair() {
|
||||||
|
const keypairName = document.getElementById('select-keypair').value;
|
||||||
|
|
||||||
|
if (!keypairName) {
|
||||||
|
document.getElementById('keypair-management-result').textContent = 'Please select a keypair';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Select keypair
|
||||||
|
const result = select_keypair(keypairName);
|
||||||
|
if (result === 0) {
|
||||||
|
selectedKeypair = keypairName;
|
||||||
|
document.getElementById('keypair-management-result').textContent = `Selected keypair "${keypairName}"`;
|
||||||
|
|
||||||
|
// Hide Ethereum wallet info when changing keypairs
|
||||||
|
document.getElementById('ethereum-wallet-info').classList.add('hidden');
|
||||||
|
hasEthereumWallet = false;
|
||||||
|
} else {
|
||||||
|
document.getElementById('keypair-management-result').textContent = `Error selecting keypair: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('keypair-management-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from the selected keypair
|
||||||
|
async function performCreateEthereumWallet() {
|
||||||
|
if (!selectedKeypair) {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Please select a keypair first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Creating wallet...';
|
||||||
|
|
||||||
|
// Create Ethereum wallet
|
||||||
|
console.log('Creating Ethereum wallet from keypair:', selectedKeypair);
|
||||||
|
const result = create_ethereum_wallet();
|
||||||
|
console.log('Create Ethereum wallet result:', result);
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
hasEthereumWallet = true;
|
||||||
|
|
||||||
|
// Get and display Ethereum address
|
||||||
|
const address = get_ethereum_address();
|
||||||
|
console.log('Generated Ethereum address:', address);
|
||||||
|
document.getElementById('ethereum-address-value').textContent = address;
|
||||||
|
|
||||||
|
// Get and display private key
|
||||||
|
const privateKey = get_ethereum_private_key();
|
||||||
|
document.getElementById('ethereum-private-key-value').textContent = privateKey;
|
||||||
|
|
||||||
|
// Show the wallet info
|
||||||
|
document.getElementById('ethereum-wallet-info').classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save the wallet to IndexedDB
|
||||||
|
console.log('Saving wallet to IndexedDB:', address);
|
||||||
|
await saveEthWalletToStorage(address, privateKey);
|
||||||
|
console.log('Wallet saved successfully');
|
||||||
|
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Successfully created Ethereum wallet';
|
||||||
|
} catch (saveError) {
|
||||||
|
console.error('Error saving wallet to IndexedDB:', saveError);
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Wallet created but failed to save to storage';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Error creating Ethereum wallet: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in performCreateEthereumWallet:', e);
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from a name and the selected keypair
|
||||||
|
async function performCreateEthereumWalletFromName() {
|
||||||
|
if (!selectedKeypair) {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Please select a keypair first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = document.getElementById('wallet-name').value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Please enter a name for derivation';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Creating wallet...';
|
||||||
|
|
||||||
|
// Create Ethereum wallet from name
|
||||||
|
console.log('Creating Ethereum wallet from name:', name);
|
||||||
|
const result = create_ethereum_wallet_from_name(name);
|
||||||
|
console.log('Create Ethereum wallet from name result:', result);
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
hasEthereumWallet = true;
|
||||||
|
|
||||||
|
// Get and display Ethereum address
|
||||||
|
const address = get_ethereum_address();
|
||||||
|
console.log('Generated Ethereum address:', address);
|
||||||
|
document.getElementById('ethereum-address-value').textContent = address;
|
||||||
|
|
||||||
|
// Get and display private key
|
||||||
|
const privateKey = get_ethereum_private_key();
|
||||||
|
document.getElementById('ethereum-private-key-value').textContent = privateKey;
|
||||||
|
|
||||||
|
// Show the wallet info
|
||||||
|
document.getElementById('ethereum-wallet-info').classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save the wallet to IndexedDB
|
||||||
|
console.log('Saving wallet to IndexedDB:', address);
|
||||||
|
await saveEthWalletToStorage(address, privateKey);
|
||||||
|
console.log('Wallet saved successfully');
|
||||||
|
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Successfully created Ethereum wallet from name "${name}"`;
|
||||||
|
} catch (saveError) {
|
||||||
|
console.error('Error saving wallet to IndexedDB:', saveError);
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Wallet created but failed to save to storage';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Error creating Ethereum wallet: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in performCreateEthereumWalletFromName:', e);
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Ethereum wallet from a private key
|
||||||
|
async function performCreateEthereumWalletFromPrivateKey() {
|
||||||
|
const privateKey = document.getElementById('private-key').value.trim();
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Please enter a private key';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Creating wallet...';
|
||||||
|
|
||||||
|
// Create Ethereum wallet from private key
|
||||||
|
console.log('Creating Ethereum wallet from private key');
|
||||||
|
const result = create_ethereum_wallet_from_private_key(privateKey);
|
||||||
|
console.log('Create Ethereum wallet from private key result:', result);
|
||||||
|
|
||||||
|
if (result === 0) {
|
||||||
|
hasEthereumWallet = true;
|
||||||
|
|
||||||
|
// Get and display Ethereum address
|
||||||
|
const address = get_ethereum_address();
|
||||||
|
console.log('Generated Ethereum address:', address);
|
||||||
|
document.getElementById('ethereum-address-value').textContent = address;
|
||||||
|
|
||||||
|
// Get and display private key
|
||||||
|
const displayPrivateKey = get_ethereum_private_key();
|
||||||
|
document.getElementById('ethereum-private-key-value').textContent = displayPrivateKey;
|
||||||
|
|
||||||
|
// Show the wallet info
|
||||||
|
document.getElementById('ethereum-wallet-info').classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save the wallet to IndexedDB
|
||||||
|
console.log('Saving wallet to IndexedDB:', address);
|
||||||
|
await saveEthWalletToStorage(address, displayPrivateKey);
|
||||||
|
console.log('Wallet saved successfully');
|
||||||
|
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Successfully imported Ethereum wallet from private key';
|
||||||
|
} catch (saveError) {
|
||||||
|
console.error('Error saving wallet to IndexedDB:', saveError);
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = 'Wallet imported but failed to save to storage';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Error importing Ethereum wallet: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error in performCreateEthereumWalletFromPrivateKey:', e);
|
||||||
|
document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the balance of an Ethereum address
|
||||||
|
async function checkBalance() {
|
||||||
|
if (!hasEthereumWallet) {
|
||||||
|
document.getElementById('balance-result').textContent = 'Please create an Ethereum wallet first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const address = get_ethereum_address();
|
||||||
|
|
||||||
|
document.getElementById('balance-result').textContent = 'Checking balance...';
|
||||||
|
|
||||||
|
// Use the Ethereum Web3 API directly from JavaScript
|
||||||
|
const response = await fetch(GNOSIS_RPC_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'eth_getBalance',
|
||||||
|
params: [address, 'latest'],
|
||||||
|
id: 1,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
document.getElementById('balance-result').textContent = `Error: ${data.error.message}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const balanceHex = data.result;
|
||||||
|
const formattedBalance = format_eth_balance(balanceHex);
|
||||||
|
|
||||||
|
document.getElementById('balance-result').textContent = `Balance: ${formattedBalance}`;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('balance-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy text to clipboard
|
||||||
|
function copyToClipboard(text, successMessage) {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.then(() => {
|
||||||
|
alert(successMessage);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const GNOSIS_RPC_URL = "https://rpc.gnosis.gateway.fm";
|
||||||
|
const GNOSIS_EXPLORER = "https://gnosisscan.io";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
// Initialize the WebAssembly module
|
||||||
|
await init();
|
||||||
|
|
||||||
|
console.log('WebAssembly crypto module initialized!');
|
||||||
|
|
||||||
|
// Set up the keypair selection
|
||||||
|
document.getElementById('select-keypair').addEventListener('change', performSelectKeypair);
|
||||||
|
|
||||||
|
// Set up the Ethereum wallet management
|
||||||
|
document.getElementById('create-ethereum-wallet-button').addEventListener('click', performCreateEthereumWallet);
|
||||||
|
document.getElementById('create-from-name-button').addEventListener('click', performCreateEthereumWalletFromName);
|
||||||
|
document.getElementById('import-private-key-button').addEventListener('click', performCreateEthereumWalletFromPrivateKey);
|
||||||
|
|
||||||
|
// Set up the copy buttons
|
||||||
|
document.getElementById('copy-address-button').addEventListener('click', () => {
|
||||||
|
const address = document.getElementById('ethereum-address-value').textContent;
|
||||||
|
copyToClipboard(address, 'Ethereum address copied to clipboard!');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('copy-private-key-button').addEventListener('click', () => {
|
||||||
|
const privateKey = document.getElementById('ethereum-private-key-value').textContent;
|
||||||
|
copyToClipboard(privateKey, 'Private key copied to clipboard!');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the balance check
|
||||||
|
document.getElementById('check-balance-button').addEventListener('click', checkBalance);
|
||||||
|
|
||||||
|
// Initialize UI
|
||||||
|
updateLoginUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(console.error);
|
609
www/js/index.js
609
www/js/index.js
@@ -1,5 +1,5 @@
|
|||||||
// Import our WebAssembly module
|
// Import our WebAssembly module
|
||||||
import init, {
|
import init, {
|
||||||
create_key_space,
|
create_key_space,
|
||||||
encrypt_key_space,
|
encrypt_key_space,
|
||||||
decrypt_key_space,
|
decrypt_key_space,
|
||||||
@@ -10,6 +10,10 @@ import init, {
|
|||||||
keypair_pub_key,
|
keypair_pub_key,
|
||||||
keypair_sign,
|
keypair_sign,
|
||||||
keypair_verify,
|
keypair_verify,
|
||||||
|
derive_public_key,
|
||||||
|
verify_with_public_key,
|
||||||
|
encrypt_asymmetric,
|
||||||
|
decrypt_asymmetric,
|
||||||
generate_symmetric_key,
|
generate_symmetric_key,
|
||||||
derive_key_from_password,
|
derive_key_from_password,
|
||||||
encrypt_symmetric,
|
encrypt_symmetric,
|
||||||
@@ -66,34 +70,186 @@ function clearAutoLogout() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalStorage functions for key spaces
|
// IndexedDB setup and functions
|
||||||
const STORAGE_PREFIX = 'crypto_space_';
|
const DB_NAME = 'CryptoSpaceDB';
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
const STORE_NAME = 'keySpaces';
|
||||||
|
|
||||||
// Save encrypted space to localStorage
|
// Initialize the database
|
||||||
function saveSpaceToStorage(spaceName, encryptedData) {
|
function initDatabase() {
|
||||||
localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData);
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error opening database:', event.target.error);
|
||||||
|
reject('Error opening database: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
resolve(db);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
// Create object store for key spaces if it doesn't exist
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' });
|
||||||
|
store.createIndex('name', 'name', { unique: true });
|
||||||
|
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get encrypted space from localStorage
|
// Get database connection
|
||||||
function getSpaceFromStorage(spaceName) {
|
function getDB() {
|
||||||
return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`);
|
return initDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all spaces in localStorage
|
// Save encrypted space to IndexedDB
|
||||||
function listSpacesFromStorage() {
|
async function saveSpaceToStorage(spaceName, encryptedData) {
|
||||||
const spaces = [];
|
const db = await getDB();
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
return new Promise((resolve, reject) => {
|
||||||
const key = localStorage.key(i);
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
if (key.startsWith(STORAGE_PREFIX)) {
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
spaces.push(key.substring(STORAGE_PREFIX.length));
|
|
||||||
}
|
const space = {
|
||||||
|
name: spaceName,
|
||||||
|
encryptedData: encryptedData,
|
||||||
|
created: new Date(),
|
||||||
|
lastAccessed: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = store.put(space);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error saving space:', event.target.error);
|
||||||
|
reject('Error saving space: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get encrypted space from IndexedDB
|
||||||
|
async function getSpaceFromStorage(spaceName) {
|
||||||
|
try {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.get(spaceName);
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const space = event.target.result;
|
||||||
|
if (space) {
|
||||||
|
// Update last accessed timestamp
|
||||||
|
updateLastAccessed(spaceName).catch(console.error);
|
||||||
|
resolve(space.encryptedData);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error retrieving space:', event.target.error);
|
||||||
|
reject('Error retrieving space: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database error in getSpaceFromStorage:', error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return spaces;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove space from localStorage
|
// Update last accessed timestamp
|
||||||
function removeSpaceFromStorage(spaceName) {
|
async function updateLastAccessed(spaceName) {
|
||||||
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.get(spaceName);
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const space = event.target.result;
|
||||||
|
if (space) {
|
||||||
|
space.lastAccessed = new Date();
|
||||||
|
store.put(space);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all spaces in IndexedDB
|
||||||
|
async function listSpacesFromStorage() {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.openCursor();
|
||||||
|
|
||||||
|
const spaces = [];
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (cursor) {
|
||||||
|
spaces.push(cursor.value.name);
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve(spaces);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error listing spaces:', event.target.error);
|
||||||
|
reject('Error listing spaces: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove space from IndexedDB
|
||||||
|
async function removeSpaceFromStorage(spaceName) {
|
||||||
|
const db = await getDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||||
|
const store = transaction.objectStore(STORE_NAME);
|
||||||
|
const request = store.delete(spaceName);
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = (event) => {
|
||||||
|
console.error('Error removing space:', event.target.error);
|
||||||
|
reject('Error removing space: ' + event.target.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
transaction.oncomplete = () => {
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session state
|
// Session state
|
||||||
@@ -102,7 +258,7 @@ let currentSpace = null;
|
|||||||
let selectedKeypair = null;
|
let selectedKeypair = null;
|
||||||
|
|
||||||
// Update UI based on login state
|
// Update UI based on login state
|
||||||
function updateLoginUI() {
|
async function updateLoginUI() {
|
||||||
const loginForm = document.getElementById('login-form');
|
const loginForm = document.getElementById('login-form');
|
||||||
const logoutForm = document.getElementById('logout-form');
|
const logoutForm = document.getElementById('logout-form');
|
||||||
const loginStatus = document.getElementById('login-status');
|
const loginStatus = document.getElementById('login-status');
|
||||||
@@ -121,6 +277,38 @@ function updateLoginUI() {
|
|||||||
loginStatus.className = 'status logged-out';
|
loginStatus.className = 'status logged-out';
|
||||||
currentSpaceName.textContent = '';
|
currentSpaceName.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the spaces list
|
||||||
|
try {
|
||||||
|
await updateSpacesList();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating spaces list in UI:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the spaces dropdown list
|
||||||
|
async function updateSpacesList() {
|
||||||
|
const spacesList = document.getElementById('space-list');
|
||||||
|
|
||||||
|
// Clear existing options
|
||||||
|
while (spacesList.options.length > 1) {
|
||||||
|
spacesList.remove(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get spaces list
|
||||||
|
const spaces = await listSpacesFromStorage();
|
||||||
|
|
||||||
|
// Add options for each space
|
||||||
|
spaces.forEach(spaceName => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = spaceName;
|
||||||
|
option.textContent = spaceName;
|
||||||
|
spacesList.appendChild(option);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error updating spaces list:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login to a space
|
// Login to a space
|
||||||
@@ -134,33 +322,46 @@ async function performLogin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get encrypted space from localStorage
|
// Show loading state
|
||||||
const encryptedSpace = getSpaceFromStorage(spaceName);
|
document.getElementById('space-result').textContent = 'Loading...';
|
||||||
|
|
||||||
|
// Get encrypted space from IndexedDB
|
||||||
|
const encryptedSpace = await getSpaceFromStorage(spaceName);
|
||||||
if (!encryptedSpace) {
|
if (!encryptedSpace) {
|
||||||
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
|
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the space
|
console.log('Retrieved space from IndexedDB:', { spaceName, encryptedDataLength: encryptedSpace.length });
|
||||||
const result = decrypt_key_space(encryptedSpace, password);
|
|
||||||
if (result === 0) {
|
try {
|
||||||
isLoggedIn = true;
|
// Decrypt the space - this is a synchronous WebAssembly function
|
||||||
currentSpace = spaceName;
|
const result = decrypt_key_space(encryptedSpace, password);
|
||||||
updateLoginUI();
|
console.log('Decrypt result:', result);
|
||||||
updateKeypairsList();
|
|
||||||
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
|
|
||||||
|
|
||||||
// Setup auto-logout
|
if (result === 0) {
|
||||||
updateActivity();
|
isLoggedIn = true;
|
||||||
setupAutoLogout();
|
currentSpace = spaceName;
|
||||||
|
await updateLoginUI();
|
||||||
// Add activity listeners
|
updateKeypairsList();
|
||||||
document.addEventListener('click', updateActivity);
|
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
|
||||||
document.addEventListener('keypress', updateActivity);
|
|
||||||
} else {
|
// Setup auto-logout
|
||||||
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
|
updateActivity();
|
||||||
|
setupAutoLogout();
|
||||||
|
|
||||||
|
// Add activity listeners
|
||||||
|
document.addEventListener('click', updateActivity);
|
||||||
|
document.addEventListener('keypress', updateActivity);
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (decryptErr) {
|
||||||
|
console.error('Decryption error:', decryptErr);
|
||||||
|
document.getElementById('space-result').textContent = `Decryption error: ${decryptErr}`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('Login error:', e);
|
||||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,37 +376,61 @@ async function performCreateSpace() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if space already exists
|
|
||||||
if (getSpaceFromStorage(spaceName)) {
|
|
||||||
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create new space
|
// Show loading state
|
||||||
const result = create_key_space(spaceName);
|
document.getElementById('space-result').textContent = 'Loading...';
|
||||||
if (result === 0) {
|
|
||||||
// Encrypt and save the space
|
// Check if space already exists
|
||||||
const encryptedSpace = encrypt_key_space(password);
|
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||||
saveSpaceToStorage(spaceName, encryptedSpace);
|
if (existingSpace) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create new space
|
||||||
|
console.log('Creating new space:', spaceName);
|
||||||
|
const result = create_key_space(spaceName);
|
||||||
|
console.log('Create space result:', result);
|
||||||
|
|
||||||
isLoggedIn = true;
|
if (result === 0) {
|
||||||
currentSpace = spaceName;
|
try {
|
||||||
updateLoginUI();
|
// Encrypt and save the space
|
||||||
updateKeypairsList();
|
console.log('Encrypting space with password');
|
||||||
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
|
const encryptedSpace = encrypt_key_space(password);
|
||||||
|
console.log('Encrypted space length:', encryptedSpace.length);
|
||||||
// Setup auto-logout
|
|
||||||
updateActivity();
|
// Save to IndexedDB
|
||||||
setupAutoLogout();
|
console.log('Saving to IndexedDB');
|
||||||
|
await saveSpaceToStorage(spaceName, encryptedSpace);
|
||||||
// Add activity listeners
|
console.log('Save completed');
|
||||||
document.addEventListener('click', updateActivity);
|
|
||||||
document.addEventListener('keypress', updateActivity);
|
isLoggedIn = true;
|
||||||
} else {
|
currentSpace = spaceName;
|
||||||
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
|
await updateLoginUI();
|
||||||
|
updateKeypairsList();
|
||||||
|
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
|
||||||
|
|
||||||
|
// Setup auto-logout
|
||||||
|
updateActivity();
|
||||||
|
setupAutoLogout();
|
||||||
|
|
||||||
|
// Add activity listeners
|
||||||
|
document.addEventListener('click', updateActivity);
|
||||||
|
document.addEventListener('keypress', updateActivity);
|
||||||
|
} catch (encryptError) {
|
||||||
|
console.error('Error encrypting or saving space:', encryptError);
|
||||||
|
document.getElementById('space-result').textContent = `Error saving space: ${encryptError}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
|
||||||
|
}
|
||||||
|
} catch (createError) {
|
||||||
|
console.error('Error in WebAssembly create_key_space:', createError);
|
||||||
|
document.getElementById('space-result').textContent = `Error creating key space: ${createError}`;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('Error checking existing space:', e);
|
||||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +526,7 @@ async function performCreateKeypair() {
|
|||||||
// Display public key
|
// Display public key
|
||||||
displaySelectedKeypairPublicKey();
|
displaySelectedKeypairPublicKey();
|
||||||
|
|
||||||
// Save the updated space to localStorage
|
// Save the updated space to IndexedDB
|
||||||
saveCurrentSpace();
|
saveCurrentSpace();
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('keypair-management-result').textContent = `Error creating keypair: ${result}`;
|
document.getElementById('keypair-management-result').textContent = `Error creating keypair: ${result}`;
|
||||||
@@ -346,22 +571,86 @@ async function performSelectKeypair() {
|
|||||||
function displaySelectedKeypairPublicKey() {
|
function displaySelectedKeypairPublicKey() {
|
||||||
try {
|
try {
|
||||||
const pubKey = keypair_pub_key();
|
const pubKey = keypair_pub_key();
|
||||||
document.getElementById('selected-pubkey-display').textContent = `Public Key: ${bufferToHex(pubKey)}`;
|
const pubKeyHex = bufferToHex(pubKey);
|
||||||
|
|
||||||
|
// Create a more user-friendly display with copy button
|
||||||
|
const pubKeyDisplay = document.getElementById('selected-pubkey-display');
|
||||||
|
pubKeyDisplay.innerHTML = `
|
||||||
|
<div class="pubkey-container">
|
||||||
|
<div class="pubkey-label">Public Key (hex):</div>
|
||||||
|
<div class="pubkey-value" id="pubkey-hex-value">${pubKeyHex}</div>
|
||||||
|
<button id="copy-pubkey-button" class="secondary">Copy Public Key</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add event listener for the copy button
|
||||||
|
document.getElementById('copy-pubkey-button').addEventListener('click', () => {
|
||||||
|
const pubKeyText = document.getElementById('pubkey-hex-value').textContent;
|
||||||
|
navigator.clipboard.writeText(pubKeyText)
|
||||||
|
.then(() => {
|
||||||
|
alert('Public key copied to clipboard!');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also populate the public key field in the verify with public key section
|
||||||
|
document.getElementById('pubkey-verify-pubkey').value = pubKeyHex;
|
||||||
|
|
||||||
|
// And in the asymmetric encryption section
|
||||||
|
document.getElementById('asymmetric-encrypt-pubkey').value = pubKeyHex;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('selected-pubkey-display').textContent = `Error getting public key: ${e}`;
|
document.getElementById('selected-pubkey-display').textContent = `Error getting public key: ${e}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the current space to localStorage
|
// Save the current space to IndexedDB
|
||||||
function saveCurrentSpace() {
|
async function saveCurrentSpace() {
|
||||||
if (!isLoggedIn || !currentSpace) return;
|
if (!isLoggedIn || !currentSpace) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Store the password in a session variable when logging in
|
||||||
|
// and use it here to avoid issues when the password field is cleared
|
||||||
const password = document.getElementById('space-password').value;
|
const password = document.getElementById('space-password').value;
|
||||||
|
if (!password) {
|
||||||
|
console.error('Password not available for saving space');
|
||||||
|
alert('Please re-enter your password to save changes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const encryptedSpace = encrypt_key_space(password);
|
const encryptedSpace = encrypt_key_space(password);
|
||||||
saveSpaceToStorage(currentSpace, encryptedSpace);
|
await saveSpaceToStorage(currentSpace, encryptedSpace);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error saving space:', e);
|
console.error('Error saving space:', e);
|
||||||
|
alert('Error saving space: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a space from IndexedDB
|
||||||
|
async function deleteSpace(spaceName) {
|
||||||
|
if (!spaceName) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if space exists
|
||||||
|
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||||
|
if (!existingSpace) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from IndexedDB
|
||||||
|
await removeSpaceFromStorage(spaceName);
|
||||||
|
|
||||||
|
// If this was the current space, logout
|
||||||
|
if (isLoggedIn && currentSpace === spaceName) {
|
||||||
|
performLogout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error deleting space:', e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +664,46 @@ async function run() {
|
|||||||
document.getElementById('login-button').addEventListener('click', performLogin);
|
document.getElementById('login-button').addEventListener('click', performLogin);
|
||||||
document.getElementById('create-space-button').addEventListener('click', performCreateSpace);
|
document.getElementById('create-space-button').addEventListener('click', performCreateSpace);
|
||||||
document.getElementById('logout-button').addEventListener('click', performLogout);
|
document.getElementById('logout-button').addEventListener('click', performLogout);
|
||||||
|
document.getElementById('delete-space-button').addEventListener('click', async () => {
|
||||||
|
if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) {
|
||||||
|
document.getElementById('space-result').textContent = 'Deleting...';
|
||||||
|
try {
|
||||||
|
const result = await deleteSpace(currentSpace);
|
||||||
|
if (result) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error during space deletion:', e);
|
||||||
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('delete-selected-space-button').addEventListener('click', async () => {
|
||||||
|
const selectedSpace = document.getElementById('space-list').value;
|
||||||
|
if (!selectedSpace) {
|
||||||
|
document.getElementById('space-result').textContent = 'Please select a space to delete';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) {
|
||||||
|
document.getElementById('space-result').textContent = 'Deleting...';
|
||||||
|
try {
|
||||||
|
const result = await deleteSpace(selectedSpace);
|
||||||
|
if (result) {
|
||||||
|
document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`;
|
||||||
|
await updateSpacesList();
|
||||||
|
} else {
|
||||||
|
document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error during space deletion:', e);
|
||||||
|
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set up the keypair management
|
// Set up the keypair management
|
||||||
document.getElementById('create-keypair-button').addEventListener('click', performCreateKeypair);
|
document.getElementById('create-keypair-button').addEventListener('click', performCreateKeypair);
|
||||||
@@ -530,6 +859,140 @@ async function run() {
|
|||||||
document.getElementById('password-decrypt-result').textContent = `Error: ${e}`;
|
document.getElementById('password-decrypt-result').textContent = `Error: ${e}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up the public key verification example
|
||||||
|
document.getElementById('pubkey-verify-button').addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
const publicKeyHex = document.getElementById('pubkey-verify-pubkey').value.trim();
|
||||||
|
if (!publicKeyHex) {
|
||||||
|
document.getElementById('pubkey-verify-result').textContent = 'Please enter a public key';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = document.getElementById('pubkey-verify-message').value;
|
||||||
|
const messageBytes = new TextEncoder().encode(message);
|
||||||
|
const signatureHex = document.getElementById('pubkey-verify-signature').value;
|
||||||
|
const signatureBytes = hexToBuffer(signatureHex);
|
||||||
|
const publicKeyBytes = hexToBuffer(publicKeyHex);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isValid = verify_with_public_key(publicKeyBytes, messageBytes, signatureBytes);
|
||||||
|
document.getElementById('pubkey-verify-result').textContent =
|
||||||
|
isValid ? 'Signature is valid!' : 'Signature is NOT valid!';
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('pubkey-verify-result').textContent = `Error verifying: ${e}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('pubkey-verify-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the derive public key example
|
||||||
|
document.getElementById('derive-pubkey-button').addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
const privateKeyHex = document.getElementById('derive-pubkey-privkey').value.trim();
|
||||||
|
if (!privateKeyHex) {
|
||||||
|
document.getElementById('derive-pubkey-result').textContent = 'Please enter a private key';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKeyBytes = hexToBuffer(privateKeyHex);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const publicKey = derive_public_key(privateKeyBytes);
|
||||||
|
const publicKeyHex = bufferToHex(publicKey);
|
||||||
|
|
||||||
|
// Create a more user-friendly display with copy button
|
||||||
|
const pubKeyDisplay = document.getElementById('derive-pubkey-result');
|
||||||
|
pubKeyDisplay.innerHTML = `
|
||||||
|
<div class="pubkey-container">
|
||||||
|
<div class="pubkey-label">Derived Public Key (hex):</div>
|
||||||
|
<div class="pubkey-value" id="derived-pubkey-hex-value">${publicKeyHex}</div>
|
||||||
|
<button id="copy-derived-pubkey-button" class="secondary">Copy Public Key</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add event listener for the copy button
|
||||||
|
document.getElementById('copy-derived-pubkey-button').addEventListener('click', () => {
|
||||||
|
const pubKeyText = document.getElementById('derived-pubkey-hex-value').textContent;
|
||||||
|
navigator.clipboard.writeText(pubKeyText)
|
||||||
|
.then(() => {
|
||||||
|
alert('Public key copied to clipboard!');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also populate the public key field in the verify with public key section
|
||||||
|
document.getElementById('pubkey-verify-pubkey').value = publicKeyHex;
|
||||||
|
|
||||||
|
// And in the asymmetric encryption section
|
||||||
|
document.getElementById('asymmetric-encrypt-pubkey').value = publicKeyHex;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('derive-pubkey-result').textContent = `Error deriving public key: ${e}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('derive-pubkey-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the asymmetric encryption example
|
||||||
|
document.getElementById('asymmetric-encrypt-button').addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
const publicKeyHex = document.getElementById('asymmetric-encrypt-pubkey').value.trim();
|
||||||
|
if (!publicKeyHex) {
|
||||||
|
document.getElementById('asymmetric-encrypt-result').textContent = 'Please enter a recipient public key';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = document.getElementById('asymmetric-encrypt-message').value;
|
||||||
|
const messageBytes = new TextEncoder().encode(message);
|
||||||
|
const publicKeyBytes = hexToBuffer(publicKeyHex);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ciphertext = encrypt_asymmetric(publicKeyBytes, messageBytes);
|
||||||
|
const ciphertextHex = bufferToHex(ciphertext);
|
||||||
|
document.getElementById('asymmetric-encrypt-result').textContent = `Ciphertext: ${ciphertextHex}`;
|
||||||
|
|
||||||
|
// Store for decryption
|
||||||
|
document.getElementById('asymmetric-decrypt-ciphertext').value = ciphertextHex;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('asymmetric-encrypt-result').textContent = `Error encrypting: ${e}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('asymmetric-encrypt-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up the asymmetric decryption example
|
||||||
|
document.getElementById('asymmetric-decrypt-button').addEventListener('click', () => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
document.getElementById('asymmetric-decrypt-result').textContent = 'Please login first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedKeypair) {
|
||||||
|
document.getElementById('asymmetric-decrypt-result').textContent = 'Please select a keypair first';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ciphertextHex = document.getElementById('asymmetric-decrypt-ciphertext').value;
|
||||||
|
const ciphertext = hexToBuffer(ciphertextHex);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const plaintext = decrypt_asymmetric(ciphertext);
|
||||||
|
const decodedText = new TextDecoder().decode(plaintext);
|
||||||
|
document.getElementById('asymmetric-decrypt-result').textContent = `Decrypted: ${decodedText}`;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('asymmetric-decrypt-result').textContent = `Error decrypting: ${e}`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById('asymmetric-decrypt-result').textContent = `Error: ${e}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize UI
|
// Initialize UI
|
||||||
updateLoginUI();
|
updateLoginUI();
|
||||||
|
Reference in New Issue
Block a user