7 Commits

Author SHA1 Message Date
6573a01d75 ... 2025-04-21 13:17:56 +02:00
c47f67b901 ... 2025-04-21 13:02:32 +02:00
2cf31905b0 ... 2025-04-21 11:57:49 +02:00
8569bb4bd8 ... 2025-04-21 11:57:11 +02:00
67cbb35156 ... 2025-04-21 11:54:18 +02:00
d8a314df41 ... 2025-04-19 19:43:16 +02:00
bf2f7b57bb ... 2025-04-19 19:24:07 +02:00
18 changed files with 2616 additions and 126 deletions

4
.gitignore vendored
View File

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

View File

@@ -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
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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