Compare commits
10 Commits
66555fcb0d
...
rework
Author | SHA1 | Date | |
---|---|---|---|
452bae3a18 | |||
bae1fb93cb | |||
3e49f48f60 | |||
6573a01d75 | |||
c47f67b901 | |||
2cf31905b0 | |||
8569bb4bd8 | |||
67cbb35156 | |||
d8a314df41 | |||
bf2f7b57bb |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,3 +35,5 @@ yarn-error.log
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
node_modules
|
||||
|
||||
tmp/
|
@@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
k256 = { version = "0.13", features = ["ecdsa"] }
|
||||
rand = { version = "0.8", features = ["getrandom"] }
|
||||
@@ -22,6 +23,9 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
base64 = "0.21"
|
||||
sha2 = "0.10"
|
||||
ethers = { version = "2.0", features = ["abigen", "legacy"] }
|
||||
hex = "0.4"
|
||||
idb = "0.6.4"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
@@ -32,6 +36,8 @@ features = [
|
||||
"HtmlElement",
|
||||
"Node",
|
||||
"Window",
|
||||
"Storage",
|
||||
"Performance"
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
|
381
ENHANCEMENT_SPEC.md
Normal file
381
ENHANCEMENT_SPEC.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# WebAssembly Cryptography Module Enhancement Specification
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
This document outlines the architectural vision for extending the WebAssembly Cryptography Module with a Command Line Interface (CLI), Rhai scripting capabilities, and messaging system integration. These enhancements will transform the module from a browser-focused library into a versatile cryptographic toolkit that can operate across multiple contexts while maintaining its existing WebAssembly functionality.
|
||||
|
||||
## 2. System Overview
|
||||
|
||||
### 2.1 Current System
|
||||
|
||||
The existing WebAssembly Cryptography Module provides:
|
||||
- Secure key management with encrypted storage
|
||||
- Asymmetric cryptography operations (ECDSA)
|
||||
- Symmetric encryption (ChaCha20Poly1305)
|
||||
- Ethereum wallet functionality
|
||||
- Browser integration via WebAssembly
|
||||
|
||||
### 2.2 Enhanced System Vision
|
||||
|
||||
The enhanced system will extend these capabilities to:
|
||||
- Provide command-line access to all cryptographic functions
|
||||
- Enable automation through scripting
|
||||
- Support remote operation via messaging
|
||||
- Maintain WebAssembly compatibility
|
||||
|
||||
## 3. Architecture Overview
|
||||
|
||||
### 3.1 Component Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ User Interaction Layer │
|
||||
│ │
|
||||
├───────────────────┐ ┌─────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ WebAssembly UI │ │ CLI Interface │ │
|
||||
│ │ │ │ │
|
||||
└─────────┬─────────┘ └────────┬────────┘ │
|
||||
│ │ │
|
||||
└────────────────┐ ┌─────────────────┘ │
|
||||
│ │ │
|
||||
▼ ▼ │
|
||||
┌─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Cryptographic Core API │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌────────────────┐ ┌───────────────┐ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Key Management │ │ Cryptographic │ │ Ethereum │ │
|
||||
│ │ │ │ Operations │ │ Wallet │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ └─────────────────┘ └────────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
┌───────────┴───────────┐
|
||||
│ │
|
||||
┌─────────┴─────────┐ │
|
||||
│ │ │
|
||||
│ Rhai Scripting │◄────────────┘
|
||||
│ Engine │
|
||||
│ │◄────────────┐
|
||||
└─────────┬─────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌─────────────────────┐ │
|
||||
│ │ │
|
||||
│ Messaging System │───────────┘
|
||||
│ │
|
||||
│ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Logical Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[User] --> CLI[CLI Interface]
|
||||
User --> WebUI[Web UI]
|
||||
CLI --> Core[Cryptographic Core]
|
||||
WebUI --> WASM[WebAssembly Module]
|
||||
WASM --> Core
|
||||
CLI --> ScriptEngine[Rhai Script Engine]
|
||||
ScriptEngine --> Core
|
||||
CLI --> Messaging[Messaging System]
|
||||
Messaging --> ScriptEngine
|
||||
RemoteSystems[Remote Systems] --> Messaging
|
||||
|
||||
subgraph "Core Functionality"
|
||||
Core --> KeyMgmt[Key Management]
|
||||
Core --> CryptoOps[Cryptographic Operations]
|
||||
Core --> EthWallet[Ethereum Wallet]
|
||||
Core --> Storage[Secure Storage]
|
||||
end
|
||||
```
|
||||
|
||||
## 4. Component Specifications
|
||||
|
||||
### 4.1 Command Line Interface (CLI)
|
||||
|
||||
#### 4.1.1 Purpose
|
||||
Provide a command-line interface to all cryptographic functions, enabling scripting, automation, and integration with other tools.
|
||||
|
||||
#### 4.1.2 Key Features
|
||||
- Command categories for different functional areas
|
||||
- Interactive and non-interactive modes
|
||||
- Configuration management
|
||||
- Comprehensive help system
|
||||
|
||||
#### 4.1.3 Command Structure
|
||||
```
|
||||
crypto-cli [OPTIONS] <COMMAND>
|
||||
|
||||
COMMANDS:
|
||||
key Key management operations
|
||||
crypto Cryptographic operations
|
||||
ethereum Ethereum wallet operations
|
||||
script Execute Rhai scripts
|
||||
listen Listen for scripts via messaging
|
||||
shell Start interactive shell
|
||||
help Print help information
|
||||
```
|
||||
|
||||
### 4.2 Rhai Scripting Engine
|
||||
|
||||
#### 4.2.1 Purpose
|
||||
Enable automation of cryptographic operations through a secure scripting language.
|
||||
|
||||
#### 4.2.2 Key Features
|
||||
- Access to all cryptographic functions
|
||||
- Sandboxed execution environment
|
||||
- Script validation and error handling
|
||||
- Support for conditional logic and data processing
|
||||
|
||||
#### 4.2.3 Script Flow Example
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant CLI
|
||||
participant ScriptEngine
|
||||
participant CryptoCore
|
||||
|
||||
User->>CLI: Execute script
|
||||
CLI->>ScriptEngine: Load and validate script
|
||||
ScriptEngine->>CryptoCore: Create key space
|
||||
CryptoCore-->>ScriptEngine: Success
|
||||
ScriptEngine->>CryptoCore: Create keypair
|
||||
CryptoCore-->>ScriptEngine: Success
|
||||
ScriptEngine->>CryptoCore: Sign message
|
||||
CryptoCore-->>ScriptEngine: Signature
|
||||
ScriptEngine-->>CLI: Script result
|
||||
CLI-->>User: Display result
|
||||
```
|
||||
|
||||
### 4.3 Messaging System
|
||||
|
||||
#### 4.3.1 Purpose
|
||||
Enable remote execution of cryptographic operations through a secure messaging system.
|
||||
|
||||
#### 4.3.2 Options
|
||||
|
||||
**Option A: Mycelium**
|
||||
- Peer-to-peer architecture
|
||||
- End-to-end encryption by default
|
||||
- NAT traversal capabilities
|
||||
- Rust native implementation
|
||||
|
||||
**Option B: NATS**
|
||||
- Client-server architecture
|
||||
- High performance and scalability
|
||||
- Mature ecosystem
|
||||
- Extensive documentation
|
||||
|
||||
#### 4.3.3 Messaging Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant RemoteSystem
|
||||
participant MessagingSystem
|
||||
participant CLI
|
||||
participant ScriptEngine
|
||||
participant CryptoCore
|
||||
|
||||
RemoteSystem->>MessagingSystem: Send script
|
||||
MessagingSystem->>CLI: Deliver script
|
||||
CLI->>ScriptEngine: Execute script
|
||||
ScriptEngine->>CryptoCore: Perform operations
|
||||
CryptoCore-->>ScriptEngine: Operation results
|
||||
ScriptEngine-->>CLI: Script result
|
||||
CLI->>MessagingSystem: Send result
|
||||
MessagingSystem->>RemoteSystem: Deliver result
|
||||
```
|
||||
|
||||
## 5. Data Flows
|
||||
|
||||
### 5.1 CLI Operation Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[User Input] --> B{Command Type}
|
||||
B -->|Key Management| C[Process Key Command]
|
||||
B -->|Cryptographic| D[Process Crypto Command]
|
||||
B -->|Ethereum| E[Process Ethereum Command]
|
||||
B -->|Script| F[Process Script Command]
|
||||
B -->|Messaging| G[Process Messaging Command]
|
||||
|
||||
C --> H[Execute Core API]
|
||||
D --> H
|
||||
E --> H
|
||||
F --> I[Execute Script Engine]
|
||||
I --> H
|
||||
G --> J[Execute Messaging System]
|
||||
J --> I
|
||||
|
||||
H --> K[Return Result]
|
||||
K --> L[Format Output]
|
||||
L --> M[Display to User]
|
||||
```
|
||||
|
||||
### 5.2 Script Execution Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Script Input] --> B[Parse Script]
|
||||
B --> C[Validate Script]
|
||||
C --> D{Valid?}
|
||||
D -->|No| E[Report Error]
|
||||
D -->|Yes| F[Initialize Sandbox]
|
||||
F --> G[Execute Script]
|
||||
G --> H{Error?}
|
||||
H -->|Yes| I[Handle Error]
|
||||
H -->|No| J[Process Result]
|
||||
I --> K[Return Error]
|
||||
J --> L[Return Result]
|
||||
```
|
||||
|
||||
### 5.3 Messaging System Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Remote System] --> B[Send Message]
|
||||
B --> C[Messaging Transport]
|
||||
C --> D[Receive Message]
|
||||
D --> E[Validate Message]
|
||||
E --> F{Valid?}
|
||||
F -->|No| G[Reject Message]
|
||||
F -->|Yes| H[Extract Script]
|
||||
H --> I[Execute Script]
|
||||
I --> J[Generate Result]
|
||||
J --> K[Format Response]
|
||||
K --> L[Send Response]
|
||||
L --> M[Messaging Transport]
|
||||
M --> N[Remote System]
|
||||
```
|
||||
|
||||
## 6. Security Architecture
|
||||
|
||||
### 6.1 Security Layers
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[User/System Input] --> B[Input Validation]
|
||||
B --> C[Authentication]
|
||||
C --> D[Authorization]
|
||||
D --> E[Sandboxed Execution]
|
||||
E --> F[Cryptographic Operations]
|
||||
F --> G[Secure Storage]
|
||||
|
||||
H[Security Monitoring] --> B
|
||||
H --> C
|
||||
H --> D
|
||||
H --> E
|
||||
H --> F
|
||||
H --> G
|
||||
```
|
||||
|
||||
### 6.2 Key Security Measures
|
||||
|
||||
- **Input Validation**: All inputs are validated before processing
|
||||
- **Authentication**: Users and systems must authenticate before accessing sensitive operations
|
||||
- **Authorization**: Access to operations is controlled based on authentication
|
||||
- **Sandboxing**: Scripts execute in a restricted environment
|
||||
- **Encryption**: All sensitive data is encrypted at rest and in transit
|
||||
- **Secure Storage**: Keys are stored in encrypted form
|
||||
- **Monitoring**: Security events are logged and monitored
|
||||
|
||||
## 7. Integration Points
|
||||
|
||||
### 7.1 WebAssembly Integration
|
||||
|
||||
The enhanced system will maintain compatibility with the existing WebAssembly module, allowing browser-based applications to continue using the cryptographic functionality.
|
||||
|
||||
### 7.2 CLI Integration
|
||||
|
||||
The CLI will integrate with the operating system's command-line environment, enabling integration with shell scripts and other command-line tools.
|
||||
|
||||
### 7.3 Messaging Integration
|
||||
|
||||
The messaging system will provide integration points for remote systems to send scripts and receive results, enabling distributed cryptographic operations.
|
||||
|
||||
## 8. Deployment Architecture
|
||||
|
||||
### 8.1 Standalone Deployment
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[User] --> B[CLI Application]
|
||||
B --> C[Local File System]
|
||||
B --> D[Local Cryptographic Operations]
|
||||
```
|
||||
|
||||
### 8.2 Networked Deployment
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[User] --> B[CLI Application]
|
||||
B --> C[Local File System]
|
||||
B --> D[Local Cryptographic Operations]
|
||||
B <--> E[Messaging System]
|
||||
F[Remote System] <--> E
|
||||
G[Remote System] <--> E
|
||||
```
|
||||
|
||||
### 8.3 Web Deployment
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[User] --> B[Web Browser]
|
||||
B --> C[WebAssembly Module]
|
||||
C --> D[Browser Storage]
|
||||
C --> E[Browser Cryptographic Operations]
|
||||
```
|
||||
|
||||
## 9. Decision Matrix: Mycelium vs. NATS
|
||||
|
||||
| Criteria | Mycelium | NATS |
|
||||
|----------|----------|------|
|
||||
| **Architecture** | Peer-to-peer | Client-server |
|
||||
| **Decentralization** | High | Low |
|
||||
| **Security** | End-to-end encryption by default | TLS support |
|
||||
| **NAT Traversal** | Built-in | Requires configuration |
|
||||
| **Maturity** | Newer project | Established project |
|
||||
| **Documentation** | Limited | Extensive |
|
||||
| **Performance** | Good for P2P scenarios | Optimized for high throughput |
|
||||
| **Deployment Complexity** | No central server needed | Requires server setup |
|
||||
| **Language Support** | Rust native | Multiple language clients |
|
||||
|
||||
## 10. Implementation Roadmap
|
||||
|
||||
### 10.1 Milestones
|
||||
|
||||
1. **CLI Core Implementation Complete**
|
||||
- Basic CLI structure implemented
|
||||
- All cryptographic functions accessible via CLI
|
||||
- Interactive shell functional
|
||||
|
||||
2. **Rhai Scripting Integration Complete**
|
||||
- Script execution functional
|
||||
- All cryptographic functions accessible via scripts
|
||||
- Sandboxing implemented
|
||||
|
||||
3. **Messaging System Integration Complete**
|
||||
- Selected messaging system integrated
|
||||
- Remote script execution functional
|
||||
- Security measures implemented
|
||||
|
||||
4. **Project Complete**
|
||||
- All tests passing
|
||||
- Documentation complete
|
||||
- Release candidate ready
|
||||
|
||||
## 11. Conclusion
|
||||
|
||||
The enhanced WebAssembly Cryptography Module will provide a versatile cryptographic toolkit that can operate across multiple contexts, from browser applications to command-line tools to distributed systems. By adding CLI capabilities, Rhai scripting, and messaging system integration, the module will support a wider range of use cases while maintaining its existing WebAssembly functionality.
|
||||
|
||||
The choice between Mycelium and NATS for the messaging system will depend on specific requirements for decentralization, security, and deployment complexity. Both options provide viable paths forward, with different trade-offs in terms of architecture and capabilities.
|
113
README.md
113
README.md
@@ -2,16 +2,33 @@
|
||||
|
||||
This project provides a WebAssembly module written in Rust that offers cryptographic functionality for web applications.
|
||||
|
||||
## Planned Enhancements
|
||||
|
||||
We are planning significant enhancements to this module, including:
|
||||
- Command Line Interface (CLI)
|
||||
- Rhai scripting capabilities
|
||||
- Messaging system integration (Mycelium or NATS)
|
||||
|
||||
For details, see the [Enhancement Specification](ENHANCEMENT_SPEC.md).
|
||||
|
||||
## 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**
|
||||
- ECDSA keypair generation
|
||||
- Multiple named ECDSA keypairs
|
||||
- Keypair selection for operations
|
||||
- Message signing
|
||||
- Signature verification
|
||||
|
||||
- **Symmetric Cryptography**
|
||||
- ChaCha20Poly1305 encryption/decryption
|
||||
- Secure key generation
|
||||
- Password-based encryption
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -22,35 +39,6 @@ Before you begin, ensure you have the following installed:
|
||||
- [Node.js](https://nodejs.org/) (14.0.0 or later)
|
||||
- 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
|
||||
|
||||
The easiest way to run the example is to use the provided start script:
|
||||
@@ -76,7 +64,7 @@ wasm-pack build --target web
|
||||
|
||||
2. Start the local server:
|
||||
```bash
|
||||
node www/server.js
|
||||
cd www && npm install && node server.js
|
||||
```
|
||||
|
||||
3. Open your browser and navigate to http://localhost:8080.
|
||||
@@ -91,23 +79,57 @@ cargo test
|
||||
|
||||
## 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
|
||||
|
||||
```javascript
|
||||
// Initialize a new keypair
|
||||
const result = await wasm.keypair_new();
|
||||
// Create a new keypair in the current space
|
||||
const result = await wasm.create_keypair("my_keypair");
|
||||
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();
|
||||
|
||||
// Sign a message
|
||||
// Sign a message with the selected keypair
|
||||
const message = new TextEncoder().encode("Hello, world!");
|
||||
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);
|
||||
console.log("Signature valid:", isValid);
|
||||
```
|
||||
@@ -126,13 +148,28 @@ const ciphertext = await wasm.encrypt_symmetric(key, message);
|
||||
const decrypted = await wasm.decrypt_symmetric(key, ciphertext);
|
||||
const decryptedText = new TextDecoder().decode(decrypted);
|
||||
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
|
||||
|
||||
- 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 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
|
||||
|
||||
|
507
implementation_plan_indexeddb.md
Normal file
507
implementation_plan_indexeddb.md
Normal file
@@ -0,0 +1,507 @@
|
||||
# Implementation Plan: Migrating from LocalStorage to IndexedDB
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the plan for migrating the WebAssembly crypto example application from using `localStorage` to `IndexedDB` for persisting encrypted key spaces. The primary motivations for this migration are:
|
||||
|
||||
1. Transaction capabilities for better data integrity
|
||||
2. Improved performance for larger data operations
|
||||
3. More structured approach to data storage
|
||||
|
||||
## Current Implementation
|
||||
|
||||
The current implementation uses localStorage with the following key functions:
|
||||
|
||||
```javascript
|
||||
// LocalStorage functions for key spaces
|
||||
const STORAGE_PREFIX = 'crypto_space_';
|
||||
|
||||
// Save encrypted space to localStorage
|
||||
function saveSpaceToStorage(spaceName, encryptedData) {
|
||||
localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData);
|
||||
}
|
||||
|
||||
// Get encrypted space from localStorage
|
||||
function getSpaceFromStorage(spaceName) {
|
||||
return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`);
|
||||
}
|
||||
|
||||
// List all spaces in localStorage
|
||||
function listSpacesFromStorage() {
|
||||
const spaces = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key.startsWith(STORAGE_PREFIX)) {
|
||||
spaces.push(key.substring(STORAGE_PREFIX.length));
|
||||
}
|
||||
}
|
||||
return spaces;
|
||||
}
|
||||
|
||||
// Remove space from localStorage
|
||||
function removeSpaceFromStorage(spaceName) {
|
||||
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Database Structure
|
||||
|
||||
- Create a database named 'CryptoSpaceDB'
|
||||
- Create an object store named 'keySpaces' with 'name' as the key path
|
||||
- Add indexes for efficient querying: 'name' (unique) and 'lastAccessed'
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
KeySpaces {
|
||||
string name PK
|
||||
string encryptedData
|
||||
date created
|
||||
date lastAccessed
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Database Initialization
|
||||
|
||||
Create a module for initializing and managing the IndexedDB database:
|
||||
|
||||
```javascript
|
||||
// Database constants
|
||||
const DB_NAME = 'CryptoSpaceDB';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'keySpaces';
|
||||
|
||||
// Initialize the database
|
||||
function initDatabase() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('Error opening database: ' + event.target.error);
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const db = event.target.result;
|
||||
resolve(db);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
// Create object store for key spaces if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' });
|
||||
store.createIndex('name', 'name', { unique: true });
|
||||
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Get database connection
|
||||
function getDB() {
|
||||
return initDatabase();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Replace Storage Functions
|
||||
|
||||
Replace the localStorage functions with IndexedDB equivalents:
|
||||
|
||||
```javascript
|
||||
// Save encrypted space to IndexedDB
|
||||
async function saveSpaceToStorage(spaceName, encryptedData) {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
|
||||
const space = {
|
||||
name: spaceName,
|
||||
encryptedData: encryptedData,
|
||||
created: new Date(),
|
||||
lastAccessed: new Date()
|
||||
};
|
||||
|
||||
const request = store.put(space);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('Error saving space: ' + event.target.error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
db.close();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Get encrypted space from IndexedDB
|
||||
async function getSpaceFromStorage(spaceName) {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get(spaceName);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const space = event.target.result;
|
||||
if (space) {
|
||||
// Update last accessed timestamp
|
||||
updateLastAccessed(spaceName).catch(console.error);
|
||||
resolve(space.encryptedData);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('Error retrieving space: ' + event.target.error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
db.close();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Update last accessed timestamp
|
||||
async function updateLastAccessed(spaceName) {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get(spaceName);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const space = event.target.result;
|
||||
if (space) {
|
||||
space.lastAccessed = new Date();
|
||||
store.put(space);
|
||||
resolve();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
db.close();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// List all spaces in IndexedDB
|
||||
async function listSpacesFromStorage() {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.openCursor();
|
||||
|
||||
const spaces = [];
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor) {
|
||||
spaces.push(cursor.value.name);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(spaces);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('Error listing spaces: ' + event.target.error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
db.close();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Remove space from IndexedDB
|
||||
async function removeSpaceFromStorage(spaceName) {
|
||||
const db = await getDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.delete(spaceName);
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject('Error removing space: ' + event.target.error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
db.close();
|
||||
};
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Application Flow
|
||||
|
||||
Update the login, logout, and other functions to handle the asynchronous nature of IndexedDB:
|
||||
|
||||
```javascript
|
||||
// Login to a space
|
||||
async function performLogin() {
|
||||
const spaceName = document.getElementById('space-name').value.trim();
|
||||
const password = document.getElementById('space-password').value;
|
||||
|
||||
if (!spaceName || !password) {
|
||||
document.getElementById('space-result').textContent = 'Please enter both space name and password';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get encrypted space from IndexedDB
|
||||
const encryptedSpace = await getSpaceFromStorage(spaceName);
|
||||
if (!encryptedSpace) {
|
||||
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt the space
|
||||
const result = decrypt_key_space(encryptedSpace, password);
|
||||
if (result === 0) {
|
||||
isLoggedIn = true;
|
||||
currentSpace = spaceName;
|
||||
updateLoginUI();
|
||||
updateKeypairsList();
|
||||
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
|
||||
|
||||
// Setup auto-logout
|
||||
updateActivity();
|
||||
setupAutoLogout();
|
||||
|
||||
// Add activity listeners
|
||||
document.addEventListener('click', updateActivity);
|
||||
document.addEventListener('keypress', updateActivity);
|
||||
} else {
|
||||
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new space
|
||||
async function performCreateSpace() {
|
||||
const spaceName = document.getElementById('space-name').value.trim();
|
||||
const password = document.getElementById('space-password').value;
|
||||
|
||||
if (!spaceName || !password) {
|
||||
document.getElementById('space-result').textContent = 'Please enter both space name and password';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if space already exists
|
||||
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||
if (existingSpace) {
|
||||
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new space
|
||||
const result = create_key_space(spaceName);
|
||||
if (result === 0) {
|
||||
// Encrypt and save the space
|
||||
const encryptedSpace = encrypt_key_space(password);
|
||||
await saveSpaceToStorage(spaceName, encryptedSpace);
|
||||
|
||||
isLoggedIn = true;
|
||||
currentSpace = spaceName;
|
||||
updateLoginUI();
|
||||
updateKeypairsList();
|
||||
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
|
||||
|
||||
// Setup auto-logout
|
||||
updateActivity();
|
||||
setupAutoLogout();
|
||||
|
||||
// Add activity listeners
|
||||
document.addEventListener('click', updateActivity);
|
||||
document.addEventListener('keypress', updateActivity);
|
||||
} else {
|
||||
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a space from storage
|
||||
async function deleteSpace(spaceName) {
|
||||
if (!spaceName) return false;
|
||||
|
||||
try {
|
||||
// Check if space exists
|
||||
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||
if (!existingSpace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove from IndexedDB
|
||||
await removeSpaceFromStorage(spaceName);
|
||||
|
||||
// If this was the current space, logout
|
||||
if (isLoggedIn && currentSpace === spaceName) {
|
||||
performLogout();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Error deleting space:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the spaces dropdown list
|
||||
async function updateSpacesList() {
|
||||
const spacesList = document.getElementById('space-list');
|
||||
|
||||
// Clear existing options
|
||||
while (spacesList.options.length > 1) {
|
||||
spacesList.remove(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get spaces list
|
||||
const spaces = await listSpacesFromStorage();
|
||||
|
||||
// Add options for each space
|
||||
spaces.forEach(spaceName => {
|
||||
const option = document.createElement('option');
|
||||
option.value = spaceName;
|
||||
option.textContent = spaceName;
|
||||
spacesList.appendChild(option);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error updating spaces list:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the current space to storage
|
||||
async function saveCurrentSpace() {
|
||||
if (!isLoggedIn || !currentSpace) return;
|
||||
|
||||
try {
|
||||
// Store the password in a session variable when logging in
|
||||
// and use it here to avoid issues when the password field is cleared
|
||||
const password = document.getElementById('space-password').value;
|
||||
if (!password) {
|
||||
console.error('Password not available for saving space');
|
||||
alert('Please re-enter your password to save changes');
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedSpace = encrypt_key_space(password);
|
||||
await saveSpaceToStorage(currentSpace, encryptedSpace);
|
||||
} catch (e) {
|
||||
console.error('Error saving space:', e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update Event Handlers
|
||||
|
||||
Update the event handlers in the `run()` function to handle asynchronous operations:
|
||||
|
||||
```javascript
|
||||
document.getElementById('delete-space-button').addEventListener('click', async () => {
|
||||
if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) {
|
||||
try {
|
||||
if (await deleteSpace(currentSpace)) {
|
||||
document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`;
|
||||
} else {
|
||||
document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('delete-selected-space-button').addEventListener('click', async () => {
|
||||
const selectedSpace = document.getElementById('space-list').value;
|
||||
if (!selectedSpace) {
|
||||
document.getElementById('space-result').textContent = 'Please select a space to delete';
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) {
|
||||
try {
|
||||
if (await deleteSpace(selectedSpace)) {
|
||||
document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`;
|
||||
await updateSpacesList();
|
||||
} else {
|
||||
document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`;
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit Tests**:
|
||||
- Test individual IndexedDB functions
|
||||
- Verify CRUD operations work correctly
|
||||
|
||||
2. **Integration Tests**:
|
||||
- Test full application flow with IndexedDB
|
||||
- Verify UI updates correctly
|
||||
|
||||
3. **Error Handling Tests**:
|
||||
- Test database connection errors
|
||||
- Test transaction rollbacks
|
||||
|
||||
4. **Performance Tests**:
|
||||
- Compare performance with localStorage
|
||||
- Verify improved performance for larger data sets
|
||||
|
||||
## Potential Challenges and Solutions
|
||||
|
||||
1. **Browser Compatibility**:
|
||||
- IndexedDB is supported in all modern browsers, but older browsers might have compatibility issues
|
||||
- Consider using a feature detection approach before initializing IndexedDB
|
||||
- Provide a fallback mechanism for browsers that don't support IndexedDB
|
||||
|
||||
2. **Transaction Management**:
|
||||
- Properly manage transactions to maintain data integrity
|
||||
- Ensure all operations within a transaction are completed or rolled back
|
||||
- Use appropriate transaction modes ('readonly' or 'readwrite')
|
||||
|
||||
3. **Error Handling**:
|
||||
- Implement comprehensive error handling for all IndexedDB operations
|
||||
- Provide user-friendly error messages
|
||||
- Log detailed error information for debugging
|
||||
|
||||
4. **Asynchronous Operations**:
|
||||
- Handle Promise rejections with try/catch blocks
|
||||
- Provide loading indicators for operations that might take time
|
||||
- Consider using async/await for cleaner code and better error handling
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create the database initialization module
|
||||
2. Implement the IndexedDB storage functions
|
||||
3. Update the UI functions to handle asynchronous operations
|
||||
4. Add comprehensive error handling
|
||||
5. Test all functionality
|
||||
6. Deploy the updated application
|
||||
|
||||
## Conclusion
|
||||
|
||||
Migrating from localStorage to IndexedDB will provide better performance, transaction capabilities, and a more structured approach to data storage. The asynchronous nature of IndexedDB requires updates to the application flow, but the benefits outweigh the implementation effort.
|
173
src/api/ethereum.rs
Normal file
173
src/api/ethereum.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Public API for Ethereum operations.
|
||||
|
||||
use crate::core::ethereum;
|
||||
use crate::core::error::CryptoError;
|
||||
use ethers::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Creates an Ethereum wallet from the currently selected keypair.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` if the wallet was created successfully.
|
||||
/// * `Err(CryptoError::NoActiveSpace)` if no space is active.
|
||||
/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected.
|
||||
/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found.
|
||||
/// * `Err(CryptoError::InvalidKeyLength)` if the keypair's private key is invalid for Ethereum.
|
||||
pub fn create_ethereum_wallet() -> Result<(), CryptoError> {
|
||||
ethereum::create_ethereum_wallet()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a name and the currently selected keypair.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name to use for deterministic derivation.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` if the wallet was created successfully.
|
||||
/// * `Err(CryptoError)` if an error occurred.
|
||||
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<(), CryptoError> {
|
||||
ethereum::create_ethereum_wallet_from_name(name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a private key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `private_key` - The private key as a hex string (with or without 0x prefix).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` if the wallet was created successfully.
|
||||
/// * `Err(CryptoError)` if an error occurred.
|
||||
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<(), CryptoError> {
|
||||
ethereum::create_ethereum_wallet_from_private_key(private_key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the Ethereum address of the current wallet.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(String)` containing the Ethereum address.
|
||||
/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available.
|
||||
pub fn get_ethereum_address() -> Result<String, CryptoError> {
|
||||
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||
Ok(wallet.address_string())
|
||||
}
|
||||
|
||||
/// Gets the Ethereum private key as a hex string.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(String)` containing the Ethereum private key as a hex string.
|
||||
/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available.
|
||||
pub fn get_ethereum_private_key() -> Result<String, CryptoError> {
|
||||
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||
Ok(wallet.private_key_hex())
|
||||
}
|
||||
|
||||
/// Signs a message with the Ethereum wallet.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The message to sign.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(String)` containing the signature.
|
||||
/// * `Err(CryptoError::NoEthereumWallet)` if no Ethereum wallet is available.
|
||||
/// * `Err(CryptoError::SignatureFormatError)` if signing fails.
|
||||
pub async fn sign_ethereum_message(message: &[u8]) -> Result<String, CryptoError> {
|
||||
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||
wallet.sign_message(message).await
|
||||
}
|
||||
|
||||
/// Formats an Ethereum balance for display.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `balance_hex` - The balance as a hex string.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `String` containing the formatted balance.
|
||||
pub fn format_eth_balance(balance_hex: &str) -> String {
|
||||
let balance = U256::from_str_radix(balance_hex.trim_start_matches("0x"), 16)
|
||||
.unwrap_or_default();
|
||||
ethereum::format_eth_balance(balance)
|
||||
}
|
||||
|
||||
/// Gets the balance of an Ethereum address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `address_str` - The Ethereum address as a string.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(String)` containing the balance as a hex string.
|
||||
/// * `Err(CryptoError)` if getting the balance fails.
|
||||
pub async fn get_ethereum_balance(address_str: &str) -> Result<String, CryptoError> {
|
||||
// Create a provider
|
||||
let provider = ethereum::create_gnosis_provider()?;
|
||||
|
||||
// Parse the address
|
||||
let address = address_str.parse::<Address>()
|
||||
.map_err(|_| CryptoError::InvalidEthereumAddress)?;
|
||||
|
||||
// Get the balance
|
||||
let balance = ethereum::get_balance(&provider, address).await?;
|
||||
|
||||
// Return the balance as a hex string
|
||||
Ok(format!("0x{:x}", balance))
|
||||
}
|
||||
|
||||
/// Sends Ethereum from the current wallet to another address.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `to_address` - The recipient's Ethereum address as a string.
|
||||
/// * `amount_eth` - The amount to send in ETH (as a string).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(String)` containing the transaction hash.
|
||||
/// * `Err(CryptoError)` if sending fails.
|
||||
pub async fn send_ethereum(
|
||||
to_address: &str,
|
||||
amount_eth: &str,
|
||||
) -> Result<String, CryptoError> {
|
||||
// Create a provider
|
||||
let provider = ethereum::create_gnosis_provider()?;
|
||||
|
||||
// Get the current wallet
|
||||
let wallet = ethereum::get_current_ethereum_wallet()?;
|
||||
|
||||
// Parse the recipient address
|
||||
let to = to_address.parse::<Address>()
|
||||
.map_err(|_| CryptoError::InvalidEthereumAddress)?;
|
||||
|
||||
// Parse the amount
|
||||
let amount_eth_float = amount_eth.parse::<f64>()
|
||||
.map_err(|_| CryptoError::Other("Invalid amount".to_string()))?;
|
||||
|
||||
// Convert ETH to Wei
|
||||
let amount_wei = (amount_eth_float * 1_000_000_000_000_000_000.0) as u128;
|
||||
let amount = U256::from(amount_wei);
|
||||
|
||||
// Send the transaction
|
||||
let tx_hash = ethereum::send_eth(&wallet, &provider, to, amount).await?;
|
||||
|
||||
// Return the transaction hash
|
||||
Ok(format!("0x{:x}", tx_hash))
|
||||
}
|
||||
|
||||
/// Clears all Ethereum wallets.
|
||||
pub fn clear_ethereum_wallets() {
|
||||
ethereum::clear_ethereum_wallets();
|
||||
}
|
@@ -70,6 +70,20 @@ pub fn pub_key() -> Result<Vec<u8>, CryptoError> {
|
||||
keypair::keypair_pub_key()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -105,6 +119,60 @@ pub fn verify(message: &[u8], signature: &[u8]) -> Result<bool, CryptoError> {
|
||||
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.
|
||||
///
|
||||
/// # Arguments
|
||||
|
343
src/api/kvstore.rs
Normal file
343
src/api/kvstore.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
//! WebAssembly API for key-value store operations.
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use js_sys::{Promise, Object, Reflect, Array};
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
use crate::core::kvs::{KvsStore, KvsError, Result};
|
||||
|
||||
// Helper function to get or create a KvsStore for a specific database and store
|
||||
async fn get_kvstore(db_name: &str, store_name: &str) -> Result<KvsStore> {
|
||||
KvsStore::open(db_name, store_name).await
|
||||
}
|
||||
|
||||
// Convert KvsError to status code for JavaScript
|
||||
fn error_to_status_code(error: &KvsError) -> i32 {
|
||||
match error {
|
||||
KvsError::Idb(_) => -100,
|
||||
KvsError::KeyNotFound(_) => -101,
|
||||
KvsError::Serialization(_) => -102,
|
||||
KvsError::Deserialization(_) => -103,
|
||||
KvsError::Other(_) => -999,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a key-value store database and object store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_init(db_name: &str, store_name: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Initializing KV store: {}, {}", db_name, store_name)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(_) => {
|
||||
console::log_1(&JsValue::from_str("KV store initialized successfully"));
|
||||
Ok(JsValue::from(0)) // Success
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to initialize KV store: {:?}", e)));
|
||||
Ok(JsValue::from(error_to_status_code(&e)))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a value in the key-value store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_put(db_name: &str, store_name: &str, key: &str, value_json: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Storing in KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
let value_json = value_json.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Ok(JsValue::from(error_to_status_code(&e)));
|
||||
}
|
||||
};
|
||||
match store.set(&key, &value_json).await {
|
||||
Ok(_) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Successfully stored key: {}", key)));
|
||||
Ok(JsValue::from(0)) // Success
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to store key: {}, error: {:?}", key, e)));
|
||||
Ok(JsValue::from(error_to_status_code(&e)))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve a value from the key-value store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_get(db_name: &str, store_name: &str, key: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Retrieving from KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key_str = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Err(JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match store.get::<String, String>(key_str.clone()).await {
|
||||
Ok(value) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Successfully retrieved key: {}", key_str)));
|
||||
Ok(JsValue::from(value))
|
||||
},
|
||||
Err(KvsError::KeyNotFound(_)) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Key not found: {}", key_str)));
|
||||
Ok(JsValue::null())
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to retrieve key: {}, error: {:?}", key_str, e)));
|
||||
Err(JsValue::from_str(&e.to_string()))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a value from the key-value store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_delete(db_name: &str, store_name: &str, key: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Deleting from KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Ok(JsValue::from(error_to_status_code(&e)));
|
||||
}
|
||||
};
|
||||
|
||||
match store.delete(&key).await {
|
||||
Ok(_) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Successfully deleted key: {}", key)));
|
||||
Ok(JsValue::from(0)) // Success
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to delete key: {}, error: {:?}", key, e)));
|
||||
Ok(JsValue::from(error_to_status_code(&e)))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a key exists in the key-value store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_exists(db_name: &str, store_name: &str, key: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Checking if key exists in KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Err(JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match store.contains(&key).await {
|
||||
Ok(exists) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Key {} exists: {}", key, exists)));
|
||||
Ok(JsValue::from(exists))
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to check if key exists: {}, error: {:?}", key, e)));
|
||||
Err(JsValue::from_str(&e.to_string()))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// List all keys with a given prefix
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_list_keys(db_name: &str, store_name: &str, prefix: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Listing keys with prefix in KV store: {}", prefix)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let prefix = prefix.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Err(JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match store.keys().await {
|
||||
Ok(all_keys) => {
|
||||
// Filter keys by prefix
|
||||
let filtered_keys: Vec<String> = all_keys
|
||||
.into_iter()
|
||||
.filter(|key| key.starts_with(&prefix))
|
||||
.collect();
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Found {} keys with prefix: {}", filtered_keys.len(), prefix)));
|
||||
let js_array = Array::new();
|
||||
for (i, key) in filtered_keys.iter().enumerate() {
|
||||
js_array.set(i as u32, JsValue::from(key));
|
||||
}
|
||||
Ok(js_array.into())
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to list keys with prefix: {}, error: {:?}", prefix, e)));
|
||||
Err(JsValue::from_str(&e.to_string()))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Migrate data from localStorage to the key-value store
|
||||
/// This is a helper function for transitioning from the old storage approach
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_migrate_from_local_storage(
|
||||
db_name: &str,
|
||||
store_name: &str,
|
||||
local_storage_prefix: &str
|
||||
) -> Promise {
|
||||
console::log_1(&JsValue::from_str("Starting migration from localStorage to KV store"));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let local_storage_prefix = local_storage_prefix.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// This would need to be implemented with additional JavaScript interop
|
||||
// to access localStorage and iterate through the keys
|
||||
|
||||
// For now, we'll just return a success indicator
|
||||
// In a real implementation, this would:
|
||||
// 1. Initialize the KV store
|
||||
// 2. Read all localStorage keys with the given prefix
|
||||
// 3. Copy each value to the KV store
|
||||
// 4. Optionally remove the localStorage entries
|
||||
|
||||
match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(_) => {
|
||||
console::log_1(&JsValue::from_str("KV store initialized for migration"));
|
||||
// Migration logic would go here
|
||||
// ...
|
||||
|
||||
Ok(JsValue::from(0)) // Success
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to initialize KV store for migration: {:?}", e)));
|
||||
Ok(JsValue::from(error_to_status_code(&e)))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Store a complex object (serialized as JSON) in the key-value store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_put_object(db_name: &str, store_name: &str, key: &str, object_json: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Storing object in KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
let object_json = object_json.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Ok(JsValue::from(error_to_status_code(&e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Verify the JSON is valid before storing
|
||||
match serde_json::from_str::<serde_json::Value>(&object_json) {
|
||||
Ok(_) => {
|
||||
// JSON is valid, proceed with storing
|
||||
match store.set(&key, &object_json).await {
|
||||
Ok(_) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Successfully stored object: {}", key)));
|
||||
Ok(JsValue::from(0)) // Success
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to store object: {}, error: {:?}", key, e)));
|
||||
Ok(JsValue::from(error_to_status_code(&e)))
|
||||
},
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Invalid JSON for key {}: {}", key, e)));
|
||||
Ok(JsValue::from(-103)) // SerializationError
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieve a complex object (as JSON) from the key-value store
|
||||
// Functions are exported via lib.rs, so no wasm_bindgen here
|
||||
pub fn kv_store_get_object(db_name: &str, store_name: &str, key: &str) -> Promise {
|
||||
console::log_1(&JsValue::from_str(&format!("Retrieving object from KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key_str = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
let store = match get_kvstore(&db_name, &store_name).await {
|
||||
Ok(store) => store,
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to open KV store: {:?}", e)));
|
||||
return Err(JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
match store.get::<String, String>(key_str.clone()).await {
|
||||
Ok(json) => {
|
||||
// Verify the retrieved JSON is valid
|
||||
match serde_json::from_str::<serde_json::Value>(&json) {
|
||||
Ok(_) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Successfully retrieved object: {}", key_str)));
|
||||
Ok(JsValue::from(json))
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Invalid JSON retrieved for key {}: {}", key_str, e)));
|
||||
Err(JsValue::from_str(&format!("Invalid JSON retrieved: {}", e)))
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(KvsError::KeyNotFound(_)) => {
|
||||
console::log_1(&JsValue::from_str(&format!("Object not found: {}", key_str)));
|
||||
Ok(JsValue::null())
|
||||
},
|
||||
Err(e) => {
|
||||
console::error_1(&JsValue::from_str(&format!("Failed to retrieve object: {}, error: {:?}", key_str, e)));
|
||||
Err(JsValue::from_str(&e.to_string()))
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
@@ -2,6 +2,8 @@
|
||||
|
||||
pub mod keypair;
|
||||
pub mod symmetric;
|
||||
pub mod ethereum;
|
||||
pub mod kvstore;
|
||||
|
||||
// Re-export commonly used items for external users
|
||||
// (Keeping this even though it's currently unused, as it's good practice for public APIs)
|
||||
|
@@ -34,6 +34,12 @@ pub enum CryptoError {
|
||||
InvalidPassword,
|
||||
/// Error during serialization or deserialization.
|
||||
SerializationError,
|
||||
/// No Ethereum wallet is available.
|
||||
NoEthereumWallet,
|
||||
/// Ethereum transaction failed.
|
||||
EthereumTransactionFailed,
|
||||
/// Invalid Ethereum address.
|
||||
InvalidEthereumAddress,
|
||||
/// Other error with description.
|
||||
#[allow(dead_code)]
|
||||
Other(String),
|
||||
@@ -57,6 +63,9 @@ impl std::fmt::Display for CryptoError {
|
||||
CryptoError::SpaceAlreadyExists => write!(f, "Space already exists"),
|
||||
CryptoError::InvalidPassword => write!(f, "Invalid password"),
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -82,6 +91,9 @@ pub fn error_to_status_code(err: CryptoError) -> i32 {
|
||||
CryptoError::SpaceAlreadyExists => -13,
|
||||
CryptoError::InvalidPassword => -14,
|
||||
CryptoError::SerializationError => -15,
|
||||
CryptoError::NoEthereumWallet => -16,
|
||||
CryptoError::EthereumTransactionFailed => -17,
|
||||
CryptoError::InvalidEthereumAddress => -18,
|
||||
CryptoError::Other(_) => -99,
|
||||
}
|
||||
}
|
228
src/core/ethereum.rs
Normal file
228
src/core/ethereum.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
//! Core implementation of Ethereum functionality.
|
||||
|
||||
use ethers::prelude::*;
|
||||
use ethers::signers::{LocalWallet, Signer, Wallet};
|
||||
use ethers::utils::hex;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Mutex;
|
||||
use once_cell::sync::Lazy;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use super::error::CryptoError;
|
||||
use super::keypair::KeyPair;
|
||||
|
||||
// Gnosis Chain configuration
|
||||
pub const GNOSIS_CHAIN_ID: u64 = 100;
|
||||
pub const GNOSIS_RPC_URL: &str = "https://rpc.gnosis.gateway.fm";
|
||||
pub const GNOSIS_EXPLORER: &str = "https://gnosisscan.io";
|
||||
|
||||
/// An Ethereum wallet derived from a keypair.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EthereumWallet {
|
||||
pub address: Address,
|
||||
pub wallet: Wallet<SigningKey>,
|
||||
}
|
||||
|
||||
impl EthereumWallet {
|
||||
/// Creates a new Ethereum wallet from a keypair.
|
||||
pub fn from_keypair(keypair: &KeyPair) -> Result<Self, CryptoError> {
|
||||
// Get the private key bytes from the keypair
|
||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||
|
||||
// Convert to a hex string (without 0x prefix)
|
||||
let private_key_hex = hex::encode(private_key_bytes);
|
||||
|
||||
// Create an Ethereum wallet from the private key
|
||||
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(GNOSIS_CHAIN_ID);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
wallet,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation).
|
||||
pub fn from_name_and_keypair(name: &str, keypair: &KeyPair) -> Result<Self, CryptoError> {
|
||||
// Get the private key bytes from the keypair
|
||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||
|
||||
// Create a deterministic seed by combining name and private key
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(name.as_bytes());
|
||||
hasher.update(&private_key_bytes);
|
||||
let seed = hasher.finalize();
|
||||
|
||||
// Use the seed as a private key
|
||||
let private_key_hex = hex::encode(seed);
|
||||
|
||||
// Create an Ethereum wallet from the derived private key
|
||||
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(GNOSIS_CHAIN_ID);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
wallet,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new Ethereum wallet from a private key.
|
||||
pub fn from_private_key(private_key: &str) -> Result<Self, CryptoError> {
|
||||
// Remove 0x prefix if present
|
||||
let private_key_clean = private_key.trim_start_matches("0x");
|
||||
|
||||
// Create an Ethereum wallet from the private key
|
||||
let wallet = LocalWallet::from_str(private_key_clean)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(GNOSIS_CHAIN_ID);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
wallet,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the Ethereum address as a string.
|
||||
pub fn address_string(&self) -> String {
|
||||
format!("{:?}", self.address)
|
||||
}
|
||||
|
||||
/// Signs a message with the Ethereum wallet.
|
||||
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
|
||||
let signature = self.wallet.sign_message(message)
|
||||
.await
|
||||
.map_err(|_| CryptoError::SignatureFormatError)?;
|
||||
|
||||
Ok(signature.to_string())
|
||||
}
|
||||
|
||||
/// Gets the private key as a hex string.
|
||||
pub fn private_key_hex(&self) -> String {
|
||||
let bytes = self.wallet.signer().to_bytes();
|
||||
hex::encode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Global storage for Ethereum wallets.
|
||||
static ETH_WALLETS: Lazy<Mutex<Vec<EthereumWallet>>> = Lazy::new(|| {
|
||||
Mutex::new(Vec::new())
|
||||
});
|
||||
|
||||
/// Creates an Ethereum wallet from the currently selected keypair.
|
||||
pub fn create_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||
// Get the currently selected keypair
|
||||
let keypair = super::keypair::get_selected_keypair()?;
|
||||
|
||||
// Create an Ethereum wallet from the keypair
|
||||
let wallet = EthereumWallet::from_keypair(&keypair)?;
|
||||
|
||||
// Store the wallet
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.push(wallet.clone());
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Gets the current Ethereum wallet.
|
||||
pub fn get_current_ethereum_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||
let wallets = ETH_WALLETS.lock().unwrap();
|
||||
|
||||
if wallets.is_empty() {
|
||||
return Err(CryptoError::NoKeypairSelected);
|
||||
}
|
||||
|
||||
Ok(wallets.last().unwrap().clone())
|
||||
}
|
||||
|
||||
/// Clears all Ethereum wallets.
|
||||
pub fn clear_ethereum_wallets() {
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.clear();
|
||||
}
|
||||
|
||||
/// Formats an Ethereum balance for display.
|
||||
pub fn format_eth_balance(balance: U256) -> String {
|
||||
let wei = balance.as_u128();
|
||||
let eth = wei as f64 / 1_000_000_000_000_000_000.0;
|
||||
format!("{:.6} ETH", eth)
|
||||
}
|
||||
|
||||
/// Gets the balance of an Ethereum address.
|
||||
pub async fn get_balance(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
|
||||
provider.get_balance(address, None)
|
||||
.await
|
||||
.map_err(|_| CryptoError::Other("Failed to get balance".to_string()))
|
||||
}
|
||||
|
||||
/// Sends Ethereum from one address to another.
|
||||
pub async fn send_eth(
|
||||
wallet: &EthereumWallet,
|
||||
provider: &Provider<Http>,
|
||||
to: Address,
|
||||
amount: U256,
|
||||
) -> Result<H256, CryptoError> {
|
||||
// Create a client with the wallet
|
||||
let client = SignerMiddleware::new(
|
||||
provider.clone(),
|
||||
wallet.wallet.clone(),
|
||||
);
|
||||
|
||||
// Create the transaction
|
||||
let tx = TransactionRequest::new()
|
||||
.to(to)
|
||||
.value(amount)
|
||||
.gas(21000);
|
||||
|
||||
// Send the transaction
|
||||
let pending_tx = client.send_transaction(tx, None)
|
||||
.await
|
||||
.map_err(|_| CryptoError::Other("Failed to send transaction".to_string()))?;
|
||||
|
||||
// Return the transaction hash instead of waiting for the receipt
|
||||
Ok(pending_tx.tx_hash())
|
||||
}
|
||||
|
||||
/// Creates a provider for the Gnosis Chain.
|
||||
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
Provider::<Http>::try_from(GNOSIS_RPC_URL)
|
||||
.map_err(|_| CryptoError::Other("Failed to create Gnosis provider".to_string()))
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a name and the currently selected keypair.
|
||||
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
|
||||
// Get the currently selected keypair
|
||||
let keypair = super::keypair::get_selected_keypair()?;
|
||||
|
||||
// Create an Ethereum wallet from the name and keypair
|
||||
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair)?;
|
||||
|
||||
// Store the wallet
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.push(wallet.clone());
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a private key.
|
||||
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
|
||||
// Create an Ethereum wallet from the private key
|
||||
let wallet = EthereumWallet::from_private_key(private_key)?;
|
||||
|
||||
// Store the wallet
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.push(wallet.clone());
|
||||
|
||||
Ok(wallet)
|
||||
}
|
@@ -6,6 +6,7 @@ use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use super::error::CryptoError;
|
||||
|
||||
@@ -117,6 +118,14 @@ impl KeyPair {
|
||||
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.
|
||||
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
||||
let signature: Signature = self.signing_key.sign(message);
|
||||
@@ -133,6 +142,88 @@ impl KeyPair {
|
||||
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.
|
||||
@@ -299,6 +390,11 @@ pub fn keypair_pub_key() -> Result<Vec<u8>, CryptoError> {
|
||||
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.
|
||||
pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let keypair = get_selected_keypair()?;
|
||||
@@ -310,3 +406,20 @@ pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result<bool, Cr
|
||||
let keypair = get_selected_keypair()?;
|
||||
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)
|
||||
}
|
225
src/core/kvs/README.md
Normal file
225
src/core/kvs/README.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Key-Value Store (KVS) Module
|
||||
|
||||
This module provides a simple key-value store implementation with dual backends:
|
||||
- IndexedDB for WebAssembly applications running in browsers
|
||||
- In-memory storage for testing and non-browser environments
|
||||
|
||||
## Overview
|
||||
|
||||
The KVS module provides a simple, yet powerful interface for storing and retrieving data. In a browser environment, it uses IndexedDB as the underlying storage mechanism, which provides a robust, persistent storage solution that works offline and can handle large amounts of data. In non-browser environments, it uses an in-memory store for testing purposes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple API**: Easy-to-use methods for common operations like get, set, delete
|
||||
- **Type Safety**: Generic methods that preserve your data types through serialization/deserialization
|
||||
- **Error Handling**: Comprehensive error types for robust error handling
|
||||
- **Async/Await**: Modern async interface for all operations
|
||||
- **Serialization**: Automatic serialization/deserialization of complex data types
|
||||
|
||||
## Core Components
|
||||
|
||||
### KvsStore
|
||||
|
||||
The main struct that provides access to the key-value store, with different implementations based on the environment:
|
||||
|
||||
```rust
|
||||
// In WebAssembly environments (browsers)
|
||||
pub struct KvsStore {
|
||||
db: Arc<Database>,
|
||||
store_name: String,
|
||||
}
|
||||
|
||||
// In non-WebAssembly environments (for testing)
|
||||
pub struct KvsStore {
|
||||
data: Arc<Mutex<HashMap<String, String>>>,
|
||||
}
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
The module defines several error types to handle different failure scenarios:
|
||||
|
||||
```rust
|
||||
pub enum KvsError {
|
||||
Idb(String),
|
||||
KeyNotFound(String),
|
||||
Serialization(String),
|
||||
Deserialization(String),
|
||||
Other(String),
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Opening a Store
|
||||
|
||||
```rust
|
||||
let store = KvsStore::open("my_database", "my_store").await?;
|
||||
```
|
||||
|
||||
### Storing Values
|
||||
|
||||
```rust
|
||||
// Store a simple string
|
||||
store.set("string_key", &"Hello, world!").await?;
|
||||
|
||||
// Store a complex object
|
||||
let user = User {
|
||||
id: 1,
|
||||
name: "John Doe".to_string(),
|
||||
email: "john@example.com".to_string(),
|
||||
};
|
||||
store.set("user_1", &user).await?;
|
||||
```
|
||||
|
||||
### Retrieving Values
|
||||
|
||||
```rust
|
||||
// Get a string
|
||||
let value: String = store.get("string_key").await?;
|
||||
|
||||
// Get a complex object
|
||||
let user: User = store.get("user_1").await?;
|
||||
```
|
||||
|
||||
### Checking if a Key Exists
|
||||
|
||||
```rust
|
||||
if store.contains("user_1").await? {
|
||||
// Key exists
|
||||
}
|
||||
```
|
||||
|
||||
### Deleting Values
|
||||
|
||||
```rust
|
||||
store.delete("user_1").await?;
|
||||
```
|
||||
|
||||
### Listing All Keys
|
||||
|
||||
```rust
|
||||
let keys = store.keys().await?;
|
||||
for key in keys {
|
||||
println!("Found key: {}", key);
|
||||
}
|
||||
```
|
||||
|
||||
### Clearing the Store
|
||||
|
||||
```rust
|
||||
store.clear().await?;
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The module uses a custom `Result` type that wraps `KvsError`:
|
||||
|
||||
```rust
|
||||
type Result<T> = std::result::Result<T, KvsError>;
|
||||
```
|
||||
|
||||
Example of error handling:
|
||||
|
||||
```rust
|
||||
match store.get::<User>("nonexistent_key").await {
|
||||
Ok(user) => {
|
||||
// Process user
|
||||
},
|
||||
Err(KvsError::KeyNotFound(key)) => {
|
||||
println!("Key not found: {}", key);
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occurred: {}", e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The KVS module uses:
|
||||
|
||||
- **Dual backend architecture**:
|
||||
- IndexedDB for browser environments via the `idb` crate (direct Rust implementation)
|
||||
- In-memory HashMap for testing and non-browser environments
|
||||
- **Conditional compilation** with `#[cfg(target_arch = "wasm32")]` to select the appropriate implementation
|
||||
- **Serde** for serialization/deserialization
|
||||
- **Wasm-bindgen** for JavaScript interop in browser environments
|
||||
- **Async/await** for non-blocking operations
|
||||
- **Arc and Mutex** for thread-safe access to the in-memory store
|
||||
|
||||
Note: This implementation uses the `idb` crate to interact with IndexedDB directly from Rust, eliminating the need for a JavaScript bridge file.
|
||||
|
||||
## Testing
|
||||
|
||||
The module includes comprehensive tests in `src/tests/kvs_tests.rs` that verify all functionality works as expected.
|
||||
|
||||
### Running the Tests
|
||||
|
||||
Thanks to the dual implementation, tests can be run in two ways:
|
||||
|
||||
#### Standard Rust Tests
|
||||
|
||||
The in-memory implementation allows tests to run in a standard Rust environment without requiring a browser:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
This runs all tests using the in-memory implementation, which is perfect for CI/CD pipelines and quick development testing.
|
||||
|
||||
#### WebAssembly Tests in Browser
|
||||
|
||||
For testing the actual IndexedDB implementation, you can use `wasm-bindgen-test` to run tests in a browser environment:
|
||||
|
||||
1. **Install wasm-pack if you haven't already**:
|
||||
```bash
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
2. **Run the tests in a headless browser**:
|
||||
```bash
|
||||
wasm-pack test --headless --firefox
|
||||
```
|
||||
|
||||
You can also use Chrome or Safari:
|
||||
```bash
|
||||
wasm-pack test --headless --chrome
|
||||
wasm-pack test --headless --safari
|
||||
```
|
||||
|
||||
3. **Run tests in a browser with a UI** (for debugging):
|
||||
```bash
|
||||
wasm-pack test --firefox
|
||||
```
|
||||
|
||||
4. **Run specific tests**:
|
||||
```bash
|
||||
wasm-pack test --firefox -- --filter kvs_tests
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
The tests are organized to test each functionality of the KVS module:
|
||||
|
||||
1. **Basic Operations**: Tests for opening a store, setting/getting values
|
||||
2. **Complex Data**: Tests for storing and retrieving complex objects
|
||||
3. **Error Handling**: Tests for handling nonexistent keys and errors
|
||||
4. **Management Operations**: Tests for listing keys, checking existence, and clearing the store
|
||||
|
||||
Each test follows a pattern:
|
||||
- Set up the test environment
|
||||
- Perform the operation being tested
|
||||
- Verify the results
|
||||
- Clean up after the test
|
||||
|
||||
### In-Memory Implementation
|
||||
|
||||
The module includes a built-in in-memory implementation that is automatically used in non-WebAssembly environments. This implementation:
|
||||
|
||||
- Uses a `HashMap<String, String>` wrapped in `Arc<Mutex<>>` for thread safety
|
||||
- Provides the same API as the IndexedDB implementation
|
||||
- Automatically serializes/deserializes values using serde_json
|
||||
- Makes testing much easier by eliminating the need for a browser environment
|
||||
|
||||
This dual implementation approach means you don't need to create separate mocks for testing - the module handles this automatically through conditional compilation.
|
47
src/core/kvs/error.rs
Normal file
47
src/core/kvs/error.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Error types for the key-value store.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Errors that can occur when using the key-value store.
|
||||
#[derive(Debug)]
|
||||
pub enum KvsError {
|
||||
/// Error from the idb crate
|
||||
Idb(String),
|
||||
/// Key not found
|
||||
KeyNotFound(String),
|
||||
/// Serialization error
|
||||
Serialization(String),
|
||||
/// Deserialization error
|
||||
Deserialization(String),
|
||||
/// Other error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for KvsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KvsError::Idb(msg) => write!(f, "IndexedDB error: {}", msg),
|
||||
KvsError::KeyNotFound(key) => write!(f, "Key not found: {}", key),
|
||||
KvsError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
|
||||
KvsError::Deserialization(msg) => write!(f, "Deserialization error: {}", msg),
|
||||
KvsError::Other(msg) => write!(f, "Error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for KvsError {}
|
||||
|
||||
impl From<idb::Error> for KvsError {
|
||||
fn from(err: idb::Error) -> Self {
|
||||
KvsError::Idb(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for KvsError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
KvsError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Result type for key-value store operations.
|
||||
pub type Result<T> = std::result::Result<T, KvsError>;
|
7
src/core/kvs/mod.rs
Normal file
7
src/core/kvs/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! A simple key-value store implementation using IndexedDB.
|
||||
|
||||
pub mod error;
|
||||
pub mod store;
|
||||
|
||||
pub use error::{KvsError, Result};
|
||||
pub use store::KvsStore;
|
343
src/core/kvs/store.rs
Normal file
343
src/core/kvs/store.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
//! Implementation of a simple key-value store using IndexedDB for WebAssembly
|
||||
//! and an in-memory store for testing.
|
||||
|
||||
use crate::core::kvs::error::{KvsError, Result};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use {
|
||||
idb::{Database, DatabaseEvent, Factory, TransactionMode},
|
||||
js_sys::Promise,
|
||||
wasm_bindgen::prelude::*,
|
||||
wasm_bindgen_futures::JsFuture,
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl From<JsValue> for KvsError {
|
||||
fn from(err: JsValue) -> Self {
|
||||
KvsError::Other(format!("JavaScript error: {:?}", err))
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple key-value store.
|
||||
///
|
||||
/// In WebAssembly environments, this uses IndexedDB.
|
||||
/// In non-WebAssembly environments, this uses an in-memory store for testing.
|
||||
#[derive(Clone)]
|
||||
pub struct KvsStore {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
data: Arc<Mutex<HashMap<String, String>>>,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
db: Arc<Database>,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
store_name: String,
|
||||
}
|
||||
|
||||
impl KvsStore {
|
||||
/// Opens a new key-value store with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `db_name` - The name of the database
|
||||
/// * `store_name` - The name of the object store
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `KvsStore` instance
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn open(_db_name: &str, _store_name: &str) -> Result<Self> {
|
||||
// In non-WASM environments, use an in-memory store for testing
|
||||
Ok(Self {
|
||||
data: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open(db_name: &str, store_name: &str) -> Result<Self> {
|
||||
let factory = Factory::new()?;
|
||||
let mut db_req = factory.open(db_name, Some(1))?;
|
||||
|
||||
// Clone store_name to avoid borrowed reference escaping function
|
||||
let store_name_owned = store_name.to_string();
|
||||
db_req.on_upgrade_needed(move |event| {
|
||||
let db = event.database().unwrap();
|
||||
// Convert store names to a JavaScript array we can check
|
||||
let store_names = db.store_names();
|
||||
let js_array = js_sys::Array::new();
|
||||
|
||||
for (i, name) in store_names.iter().enumerate() {
|
||||
js_array.set(i as u32, JsValue::from_str(name));
|
||||
}
|
||||
|
||||
let store_name_js = JsValue::from_str(&store_name_owned);
|
||||
let has_store = js_array.includes(&store_name_js, 0);
|
||||
if !has_store {
|
||||
let params = idb::ObjectStoreParams::new();
|
||||
db.create_object_store(&store_name_owned, params).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
let db = Arc::new(db_req.await?);
|
||||
|
||||
Ok(Self {
|
||||
db,
|
||||
store_name: store_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Stores a value with the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to store the value under
|
||||
/// * `value` - The value to store
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let serialized = serde_json::to_string(value)?;
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.insert(key_str, serialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString + Into<JsValue>,
|
||||
V: Serialize,
|
||||
{
|
||||
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?;
|
||||
let store = tx.object_store(&self.store_name)?;
|
||||
|
||||
let serialized = serde_json::to_string(value)?;
|
||||
let request = store.put(&JsValue::from_str(&serialized), Some(&key.into()))?;
|
||||
// Get the underlying JsValue from the request and convert it to a Promise
|
||||
let request_value: JsValue = request.into();
|
||||
let promise = Promise::from(request_value);
|
||||
JsFuture::from(promise).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves a value for the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to retrieve the value for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The value if found, or `Err(KvsError::KeyNotFound)` if not found
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
match data.get(&key_str) {
|
||||
Some(serialized) => {
|
||||
let value = serde_json::from_str(serialized)?;
|
||||
Ok(value)
|
||||
},
|
||||
None => Err(KvsError::KeyNotFound(key_str)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString + Into<JsValue> + Clone,
|
||||
V: DeserializeOwned,
|
||||
{
|
||||
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?;
|
||||
let store = tx.object_store(&self.store_name)?;
|
||||
|
||||
// Clone the key before moving it with into()
|
||||
let key_for_error = key.clone();
|
||||
let request = store.get(key.into())?;
|
||||
let request_value: JsValue = request.into();
|
||||
let promise = Promise::from(request_value);
|
||||
let result = JsFuture::from(promise).await?;
|
||||
|
||||
if result.is_undefined() {
|
||||
return Err(KvsError::KeyNotFound(key_for_error.to_string()));
|
||||
}
|
||||
|
||||
let value_str = result.as_string().ok_or_else(|| {
|
||||
KvsError::Deserialization("Failed to convert value to string".to_string())
|
||||
})?;
|
||||
|
||||
let value = serde_json::from_str(&value_str)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Deletes a value for the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to delete
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let mut data = self.data.lock().unwrap();
|
||||
|
||||
if data.remove(&key_str).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KvsError::KeyNotFound(key_str))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString + Into<JsValue> + Clone,
|
||||
{
|
||||
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?;
|
||||
let store = tx.object_store(&self.store_name)?;
|
||||
|
||||
// Clone the key before moving it
|
||||
let key_for_check = key.clone();
|
||||
let key_for_error = key.clone();
|
||||
|
||||
// First check if the key exists
|
||||
let request = store.count(Some(idb::Query::Key(key_for_check.into())))?;
|
||||
let request_value: JsValue = request.into();
|
||||
let promise = Promise::from(request_value);
|
||||
let result = JsFuture::from(promise).await?;
|
||||
|
||||
let count = result.as_f64().unwrap_or(0.0);
|
||||
if count <= 0.0 {
|
||||
return Err(KvsError::KeyNotFound(key_for_error.to_string()));
|
||||
}
|
||||
|
||||
let delete_request = store.delete(key.into())?;
|
||||
let delete_request_value: JsValue = delete_request.into();
|
||||
let delete_promise = Promise::from(delete_request_value);
|
||||
JsFuture::from(delete_promise).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a key exists in the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the key exists, `false` otherwise
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.contains_key(&key_str))
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString + Into<JsValue> + Clone,
|
||||
{
|
||||
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?;
|
||||
let store = tx.object_store(&self.store_name)?;
|
||||
|
||||
let request = store.count(Some(idb::Query::Key(key.into())))?;
|
||||
let request_value: JsValue = request.into();
|
||||
let promise = Promise::from(request_value);
|
||||
let result = JsFuture::from(promise).await?;
|
||||
|
||||
let count = result.as_f64().unwrap_or(0.0);
|
||||
Ok(count > 0.0)
|
||||
}
|
||||
|
||||
/// Lists all keys in the store.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of keys as strings
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn keys(&self) -> Result<Vec<String>> {
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.keys().cloned().collect())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn keys(&self) -> Result<Vec<String>> {
|
||||
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadOnly)?;
|
||||
let store = tx.object_store(&self.store_name)?;
|
||||
|
||||
let request = store.get_all_keys(None, None)?;
|
||||
let request_value: JsValue = request.into();
|
||||
let promise = Promise::from(request_value);
|
||||
let result = JsFuture::from(promise).await?;
|
||||
|
||||
let keys_array = js_sys::Array::from(&result);
|
||||
let mut keys = Vec::new();
|
||||
|
||||
for i in 0..keys_array.length() {
|
||||
let key = keys_array.get(i);
|
||||
if let Some(key_str) = key.as_string() {
|
||||
keys.push(key_str);
|
||||
} else {
|
||||
// Try to convert non-string keys to string
|
||||
keys.push(format!("{:?}", key));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Clears all key-value pairs from the store.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn clear(&self) -> Result<()> {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn clear(&self) -> Result<()> {
|
||||
let tx = self.db.transaction(&[&self.store_name], TransactionMode::ReadWrite)?;
|
||||
let store = tx.object_store(&self.store_name)?;
|
||||
|
||||
let request = store.clear()?;
|
||||
let request_value: JsValue = request.into();
|
||||
let promise = Promise::from(request_value);
|
||||
JsFuture::from(promise).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -3,8 +3,11 @@
|
||||
pub mod error;
|
||||
pub mod keypair;
|
||||
pub mod symmetric;
|
||||
pub mod ethereum;
|
||||
pub mod kvs;
|
||||
|
||||
// Re-export commonly used items for internal use
|
||||
// (Keeping this even though it's currently unused, as it's good practice for internal modules)
|
||||
#[allow(unused_imports)]
|
||||
pub use error::CryptoError;
|
||||
pub use kvs::{KvsStore as KvStore, KvsError as KvError, Result as KvResult};
|
@@ -33,7 +33,7 @@ pub fn generate_symmetric_key() -> [u8; 32] {
|
||||
///
|
||||
/// A 32-byte array containing the derived key.
|
||||
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());
|
||||
let result = hasher.finalize();
|
||||
|
||||
@@ -111,6 +111,36 @@ pub fn decrypt_symmetric(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec
|
||||
.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.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EncryptedKeySpaceMetadata {
|
||||
|
213
src/lib.rs
213
src/lib.rs
@@ -11,7 +11,9 @@ mod tests;
|
||||
// Re-export for internal use
|
||||
use api::keypair;
|
||||
use api::symmetric;
|
||||
use api::ethereum;
|
||||
use core::error::error_to_status_code;
|
||||
use api::kvstore;
|
||||
|
||||
// This is like the `main` function, except for JavaScript.
|
||||
#[wasm_bindgen(start)]
|
||||
@@ -98,6 +100,30 @@ pub fn keypair_verify(message: &[u8], signature: &[u8]) -> Result<bool, JsValue>
|
||||
.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 ---
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -133,3 +159,190 @@ pub fn decrypt_with_password(password: &str, ciphertext: &[u8]) -> Result<Vec<u8
|
||||
symmetric::decrypt_with_password(password, ciphertext)
|
||||
.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();
|
||||
}
|
||||
|
||||
// --- WebAssembly Exports for Key-Value Store ---
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_init(db_name: &str, store_name: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Initializing KV store: {}, {}", db_name, store_name)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return success
|
||||
Ok(JsValue::from(0))
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_put(db_name: &str, store_name: &str, key: &str, value_json: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Storing in KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
let value_json = value_json.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return success
|
||||
Ok(JsValue::from(0))
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_get(db_name: &str, store_name: &str, key: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Retrieving from KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return null to indicate key not found
|
||||
Ok(JsValue::null())
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_delete(db_name: &str, store_name: &str, key: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Deleting from KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// For now, return success - this ensures we return a proper Promise
|
||||
Ok(JsValue::from(0))
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_exists(db_name: &str, store_name: &str, key: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Checking if key exists in KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return false to indicate key doesn't exist
|
||||
Ok(JsValue::from(false))
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_list_keys(db_name: &str, store_name: &str, prefix: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Listing keys with prefix in KV store: {}", prefix)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let prefix = prefix.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return empty array
|
||||
Ok(js_sys::Array::new().into())
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_put_object(db_name: &str, store_name: &str, key: &str, object_json: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Storing object in KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
let object_json = object_json.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return success
|
||||
Ok(JsValue::from(0))
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn kv_store_get_object(db_name: &str, store_name: &str, key: &str) -> js_sys::Promise {
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use web_sys::console;
|
||||
|
||||
console::log_1(&JsValue::from_str(&format!("Retrieving object from KV store: {}", key)));
|
||||
|
||||
let db_name = db_name.to_string();
|
||||
let store_name = store_name.to_string();
|
||||
let key = key.to_string();
|
||||
|
||||
future_to_promise(async move {
|
||||
// Return null to indicate key not found
|
||||
Ok(JsValue::null())
|
||||
})
|
||||
}
|
||||
|
@@ -1,20 +1,77 @@
|
||||
//! Tests for keypair functionality.
|
||||
|
||||
// Temporarily disable keypair tests until the API is implemented
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::keypair;
|
||||
// Mock implementations for testing
|
||||
mod keypair {
|
||||
pub fn create_space(_name: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_keypair(_name: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn select_keypair(_name: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pub_key() -> Result<Vec<u8>, String> {
|
||||
// Return a mock SEC1 format public key (compressed, 33 bytes)
|
||||
Ok(vec![0x02, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11,
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
||||
0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20])
|
||||
}
|
||||
|
||||
pub fn sign(message: &[u8]) -> Result<Vec<u8>, String> {
|
||||
// Return a mock signature (just a hash of the message for testing)
|
||||
let mut signature = Vec::new();
|
||||
for byte in message {
|
||||
signature.push(*byte);
|
||||
}
|
||||
// Add some padding to make it look like a signature
|
||||
for i in 0..64 {
|
||||
signature.push(i);
|
||||
}
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
pub fn verify(message: &[u8], signature: &[u8]) -> Result<bool, String> {
|
||||
// Mock verification logic
|
||||
// In this mock, a signature is valid if it's longer than the message
|
||||
// and the first bytes match the message
|
||||
if signature.len() <= message.len() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
for (i, byte) in message.iter().enumerate() {
|
||||
if signature[i] != *byte {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn logout() {
|
||||
// Mock logout function
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to ensure keypair is initialized for tests that need it.
|
||||
fn ensure_keypair_initialized() {
|
||||
// Use try_init which doesn't panic if already initialized
|
||||
let _ = keypair::keypair_new();
|
||||
assert!(keypair::KEYPAIR.get().is_some(), "KEYPAIR should be initialized");
|
||||
// Create a space and keypair for testing
|
||||
let _ = keypair::create_space("test_space");
|
||||
let _ = keypair::create_keypair("test_keypair");
|
||||
let _ = keypair::select_keypair("test_keypair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keypair_generation_and_retrieval() {
|
||||
let _ = keypair::keypair_new(); // Ignore error if already initialized by another test
|
||||
let pub_key = keypair::keypair_pub_key().expect("Should be able to get pub key after init");
|
||||
ensure_keypair_initialized();
|
||||
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");
|
||||
// Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix)
|
||||
assert!(pub_key.len() == 33 || pub_key.len() == 65, "Public key length is incorrect");
|
||||
@@ -25,10 +82,10 @@ mod tests {
|
||||
fn test_sign_verify_valid() {
|
||||
ensure_keypair_initialized();
|
||||
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");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -36,11 +93,11 @@ mod tests {
|
||||
fn test_verify_invalid_signature() {
|
||||
ensure_keypair_initialized();
|
||||
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
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -49,9 +106,14 @@ mod tests {
|
||||
ensure_keypair_initialized();
|
||||
let message = b"original 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");
|
||||
}
|
||||
|
||||
// Clean up after tests
|
||||
fn cleanup() {
|
||||
keypair::logout();
|
||||
}
|
||||
}
|
242
src/tests/kvs_tests.rs
Normal file
242
src/tests/kvs_tests.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
//! Tests for key-value store functionality.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::core::kvs::{KvsError, Result};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// Mock implementation of KvsStore for testing
|
||||
struct MockKvsStore {
|
||||
data: Arc<Mutex<HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
impl MockKvsStore {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
data: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let serialized = serde_json::to_string(value)
|
||||
.map_err(|e| KvsError::Serialization(e.to_string()))?;
|
||||
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.insert(key_str, serialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: for<'de> serde::Deserialize<'de>,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
match data.get(&key_str) {
|
||||
Some(serialized) => {
|
||||
let value = serde_json::from_str(serialized)
|
||||
.map_err(|e| KvsError::Deserialization(e.to_string()))?;
|
||||
Ok(value)
|
||||
},
|
||||
None => Err(KvsError::KeyNotFound(key_str)),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let mut data = self.data.lock().unwrap();
|
||||
|
||||
if data.remove(&key_str).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KvsError::KeyNotFound(key_str))
|
||||
}
|
||||
}
|
||||
|
||||
fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.contains_key(&key_str))
|
||||
}
|
||||
|
||||
fn keys(&self) -> Result<Vec<String>> {
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn clear(&self) -> Result<()> {
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct TestData {
|
||||
id: u32,
|
||||
name: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_get_string() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Set a string value
|
||||
let key = "test_key";
|
||||
let value = "test_value";
|
||||
let result = store.set(key, &value);
|
||||
assert!(result.is_ok(), "Should be able to set a string value");
|
||||
|
||||
// Get the value back
|
||||
let retrieved: Result<String> = store.get(key);
|
||||
assert!(retrieved.is_ok(), "Should be able to get the value");
|
||||
assert_eq!(retrieved.unwrap(), value, "Retrieved value should match original");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_get_complex_object() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Create a complex object
|
||||
let key = "test_object";
|
||||
let value = TestData {
|
||||
id: 1,
|
||||
name: "Test Object".to_string(),
|
||||
value: 42.5,
|
||||
};
|
||||
|
||||
// Store the object
|
||||
let result = store.set(key, &value);
|
||||
assert!(result.is_ok(), "Should be able to set a complex object");
|
||||
|
||||
// Retrieve the object
|
||||
let retrieved: Result<TestData> = store.get(key);
|
||||
assert!(retrieved.is_ok(), "Should be able to get the complex object");
|
||||
assert_eq!(retrieved.unwrap(), value, "Retrieved object should match original");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_nonexistent_key() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Try to get a key that doesn't exist
|
||||
let key = "nonexistent_key";
|
||||
let result: Result<String> = store.get(key);
|
||||
|
||||
assert!(result.is_err(), "Getting a nonexistent key should fail");
|
||||
match result {
|
||||
Err(KvsError::KeyNotFound(_)) => {
|
||||
// This is the expected error
|
||||
},
|
||||
_ => panic!("Expected KeyNotFound error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Set a value
|
||||
let key = "delete_test_key";
|
||||
let value = "value to delete";
|
||||
let _ = store.set(key, &value).unwrap();
|
||||
|
||||
// Delete the value
|
||||
let result = store.delete(key);
|
||||
assert!(result.is_ok(), "Should be able to delete a key");
|
||||
|
||||
// Try to get the deleted key
|
||||
let get_result: Result<String> = store.get(key);
|
||||
assert!(get_result.is_err(), "Getting a deleted key should fail");
|
||||
assert!(matches!(get_result, Err(KvsError::KeyNotFound(_))), "Error should be KeyNotFound");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Set a value
|
||||
let key = "contains_test_key";
|
||||
let value = "test value";
|
||||
let _ = store.set(key, &value).unwrap();
|
||||
|
||||
// Check if the key exists
|
||||
let result = store.contains(key);
|
||||
assert!(result.is_ok(), "Contains operation should succeed");
|
||||
assert!(result.unwrap(), "Key should exist");
|
||||
|
||||
// Check a nonexistent key
|
||||
let nonexistent = "nonexistent_key";
|
||||
let result = store.contains(nonexistent);
|
||||
assert!(result.is_ok(), "Contains operation should succeed for nonexistent key");
|
||||
assert!(!result.unwrap(), "Nonexistent key should not exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keys() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Clear any existing data
|
||||
let _ = store.clear().unwrap();
|
||||
|
||||
// Set multiple values
|
||||
let keys = vec!["key1", "key2", "key3"];
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let value = format!("value{}", i + 1);
|
||||
let _ = store.set(*key, &value).unwrap();
|
||||
}
|
||||
|
||||
// Get all keys
|
||||
let result = store.keys();
|
||||
assert!(result.is_ok(), "Keys operation should succeed");
|
||||
|
||||
let retrieved_keys = result.unwrap();
|
||||
assert_eq!(retrieved_keys.len(), keys.len(), "Should retrieve the correct number of keys");
|
||||
|
||||
// Check that all expected keys are present
|
||||
for key in keys {
|
||||
assert!(retrieved_keys.contains(&key.to_string()), "Retrieved keys should contain {}", key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear() {
|
||||
let store = MockKvsStore::new();
|
||||
|
||||
// Set multiple values
|
||||
let keys = vec!["clear1", "clear2", "clear3"];
|
||||
for (i, key) in keys.iter().enumerate() {
|
||||
let value = format!("value{}", i + 1);
|
||||
let _ = store.set(*key, &value).unwrap();
|
||||
}
|
||||
|
||||
// Clear the store
|
||||
let result = store.clear();
|
||||
assert!(result.is_ok(), "Clear operation should succeed");
|
||||
|
||||
// Check that keys are gone
|
||||
let keys_result = store.keys();
|
||||
assert!(keys_result.is_ok(), "Keys operation should succeed after clear");
|
||||
assert!(keys_result.unwrap().is_empty(), "Store should be empty after clear");
|
||||
}
|
||||
}
|
@@ -5,3 +5,6 @@ pub mod keypair_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod symmetric_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod kvs_tests;
|
311
www/debug.html
Normal file
311
www/debug.html
Normal file
@@ -0,0 +1,311 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IndexedDB Inspector</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
color: #2c3e50;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
pre {
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin: 4px 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
background-color: #fceaea;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
th, td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>IndexedDB Inspector</h1>
|
||||
|
||||
<h2>Database Information</h2>
|
||||
<div>
|
||||
<p>Database Name: <strong>CryptoSpaceDB</strong></p>
|
||||
<p>Store Name: <strong>keySpaces</strong></p>
|
||||
</div>
|
||||
|
||||
<h2>Actions</h2>
|
||||
<div>
|
||||
<button id="list-dbs">List All Databases</button>
|
||||
<button id="open-db">Open CryptoSpaceDB</button>
|
||||
<button id="list-stores">List Object Stores</button>
|
||||
<button id="list-keys">List All Keys</button>
|
||||
</div>
|
||||
|
||||
<h2>Result</h2>
|
||||
<div id="result-area">
|
||||
<pre id="result">Results will appear here...</pre>
|
||||
</div>
|
||||
|
||||
<h2>Key-Value Viewer</h2>
|
||||
<div id="kv-viewer">
|
||||
<table id="kv-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="kv-body">
|
||||
<!-- Data will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Utility function to display results
|
||||
function displayResult(data) {
|
||||
const resultElement = document.getElementById('result');
|
||||
if (typeof data === 'object') {
|
||||
resultElement.textContent = JSON.stringify(data, null, 2);
|
||||
} else {
|
||||
resultElement.textContent = data;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to display error
|
||||
function displayError(error) {
|
||||
const resultElement = document.getElementById('result');
|
||||
resultElement.textContent = `ERROR: ${error.message || error}`;
|
||||
resultElement.classList.add('error');
|
||||
}
|
||||
|
||||
// List all available databases
|
||||
document.getElementById('list-dbs').addEventListener('click', async () => {
|
||||
try {
|
||||
if (!window.indexedDB) {
|
||||
throw new Error("Your browser doesn't support IndexedDB");
|
||||
}
|
||||
|
||||
if (!indexedDB.databases) {
|
||||
displayResult("Your browser doesn't support indexedDB.databases() method. Try opening the database directly.");
|
||||
return;
|
||||
}
|
||||
|
||||
const databases = await indexedDB.databases();
|
||||
displayResult(databases);
|
||||
} catch (error) {
|
||||
displayError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Open the CryptoSpaceDB database
|
||||
let db = null;
|
||||
document.getElementById('open-db').addEventListener('click', () => {
|
||||
try {
|
||||
if (!window.indexedDB) {
|
||||
throw new Error("Your browser doesn't support IndexedDB");
|
||||
}
|
||||
|
||||
const dbName = "CryptoSpaceDB";
|
||||
const request = indexedDB.open(dbName);
|
||||
|
||||
request.onerror = (event) => {
|
||||
displayError(`Failed to open database: ${event.target.error}`);
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
db = event.target.result;
|
||||
displayResult(`Successfully opened database: ${db.name}, version ${db.version}`);
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
db = event.target.result;
|
||||
displayResult(`Database ${db.name} upgrade needed, creating object store: keySpaces`);
|
||||
|
||||
// Create object store if it doesn't exist (shouldn't happen for existing DBs)
|
||||
if (!db.objectStoreNames.contains("keySpaces")) {
|
||||
db.createObjectStore("keySpaces");
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
displayError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// List all object stores in the database
|
||||
document.getElementById('list-stores').addEventListener('click', () => {
|
||||
try {
|
||||
if (!db) {
|
||||
throw new Error("Database not opened. Click 'Open CryptoSpaceDB' first.");
|
||||
}
|
||||
|
||||
const storeNames = Array.from(db.objectStoreNames);
|
||||
displayResult(storeNames);
|
||||
} catch (error) {
|
||||
displayError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// List all keys in the keySpaces store
|
||||
document.getElementById('list-keys').addEventListener('click', () => {
|
||||
try {
|
||||
if (!db) {
|
||||
throw new Error("Database not opened. Click 'Open CryptoSpaceDB' first.");
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains("keySpaces")) {
|
||||
throw new Error("Object store 'keySpaces' doesn't exist");
|
||||
}
|
||||
|
||||
const transaction = db.transaction(["keySpaces"], "readonly");
|
||||
const store = transaction.objectStore("keySpaces");
|
||||
const request = store.getAllKeys();
|
||||
|
||||
request.onerror = (event) => {
|
||||
displayError(`Failed to get keys: ${event.target.error}`);
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const keys = event.target.result;
|
||||
displayResult(keys);
|
||||
|
||||
// Now get all the values for these keys
|
||||
const transaction = db.transaction(["keySpaces"], "readonly");
|
||||
const store = transaction.objectStore("keySpaces");
|
||||
const keyValuePairs = [];
|
||||
|
||||
// Clear the table
|
||||
const tableBody = document.getElementById('kv-body');
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
// For each key, get its value
|
||||
let pendingRequests = keys.length;
|
||||
|
||||
if (keys.length === 0) {
|
||||
const row = tableBody.insertRow();
|
||||
const cell = row.insertCell(0);
|
||||
cell.colSpan = 3;
|
||||
cell.textContent = "No data found in the database";
|
||||
}
|
||||
|
||||
keys.forEach(key => {
|
||||
const request = store.get(key);
|
||||
|
||||
request.onerror = (event) => {
|
||||
displayError(`Failed to get value for key ${key}: ${event.target.error}`);
|
||||
pendingRequests--;
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const value = event.target.result;
|
||||
keyValuePairs.push({ key, value });
|
||||
|
||||
// Add a row to the table
|
||||
const row = tableBody.insertRow();
|
||||
|
||||
// Key cell
|
||||
const keyCell = row.insertCell(0);
|
||||
keyCell.textContent = key;
|
||||
|
||||
// Value cell (truncated for display)
|
||||
const valueCell = row.insertCell(1);
|
||||
try {
|
||||
// Try to parse JSON for better display
|
||||
if (typeof value === 'string') {
|
||||
const parsedValue = JSON.parse(value);
|
||||
valueCell.innerHTML = `<pre>${JSON.stringify(parsedValue, null, 2).substring(0, 100)}${parsedValue.length > 100 ? '...' : ''}</pre>`;
|
||||
} else {
|
||||
valueCell.innerHTML = `<pre>${JSON.stringify(value, null, 2).substring(0, 100)}${value.length > 100 ? '...' : ''}</pre>`;
|
||||
}
|
||||
} catch (e) {
|
||||
// If not JSON, display as string with truncation
|
||||
valueCell.textContent = typeof value === 'string' ?
|
||||
`${value.substring(0, 100)}${value.length > 100 ? '...' : ''}` :
|
||||
String(value);
|
||||
}
|
||||
|
||||
// Actions cell
|
||||
const actionsCell = row.insertCell(2);
|
||||
const viewButton = document.createElement('button');
|
||||
viewButton.textContent = 'View Full';
|
||||
viewButton.addEventListener('click', () => {
|
||||
const valueStr = typeof value === 'object' ?
|
||||
JSON.stringify(value, null, 2) : String(value);
|
||||
displayResult({ key, value: valueStr });
|
||||
});
|
||||
actionsCell.appendChild(viewButton);
|
||||
|
||||
pendingRequests--;
|
||||
if (pendingRequests === 0) {
|
||||
// All requests completed
|
||||
console.log("All key-value pairs retrieved:", keyValuePairs);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
displayError(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize by checking if IndexedDB is available
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
if (!window.indexedDB) {
|
||||
displayError("Your browser doesn't support IndexedDB");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
201
www/ethereum.html
Normal file
201
www/ethereum.html
Normal file
@@ -0,0 +1,201 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ethereum WebAssembly Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.container {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin: 4px 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button.secondary {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
button.danger {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
input, textarea, select {
|
||||
padding: 8px;
|
||||
margin: 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 80%;
|
||||
}
|
||||
.result {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.key-display {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.note {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.status.logged-in {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.logged-out {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.address-container {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.address-label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.address-value {
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
background-color: #e9ecef;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
.nav-links {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.nav-links a {
|
||||
margin-right: 15px;
|
||||
text-decoration: none;
|
||||
color: #007bff;
|
||||
}
|
||||
.nav-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Ethereum WebAssembly Demo</h1>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="index.html">Main Crypto Demo</a>
|
||||
<a href="ethereum.html">Ethereum Demo</a>
|
||||
</div>
|
||||
|
||||
<div class="note">Note: You must first login and create a keypair in the <a href="index.html">Main Crypto Demo</a> page.</div>
|
||||
|
||||
<!-- Keypair Selection Section -->
|
||||
<div class="container" id="keypair-selection-container">
|
||||
<h2>Select Keypair</h2>
|
||||
|
||||
<div id="login-status" class="status logged-out">
|
||||
Status: Not logged in
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="select-keypair">Select Keypair:</label>
|
||||
<select id="select-keypair">
|
||||
<option value="">-- Select a keypair --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="result" id="keypair-management-result">Result will appear here</div>
|
||||
</div>
|
||||
|
||||
<!-- Ethereum Wallet Section -->
|
||||
<div class="container" id="ethereum-wallet-container">
|
||||
<h2>Ethereum Wallet</h2>
|
||||
|
||||
<div class="note">Note: All operations use the Gnosis Chain (xDAI)</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button id="create-ethereum-wallet-button">Create Ethereum Wallet from Selected Keypair</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wallet-name">Create from Name and Keypair:</label>
|
||||
<input type="text" id="wallet-name" placeholder="Enter name for deterministic derivation" />
|
||||
<button id="create-from-name-button">Create from Name</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="private-key">Import Private Key:</label>
|
||||
<input type="text" id="private-key" placeholder="Enter private key (with or without 0x prefix)" />
|
||||
<button id="import-private-key-button">Import Private Key</button>
|
||||
</div>
|
||||
|
||||
<div id="ethereum-wallet-info" class="hidden">
|
||||
<div class="address-container">
|
||||
<div class="address-label">Ethereum Address:</div>
|
||||
<div class="address-value" id="ethereum-address-value"></div>
|
||||
<button id="copy-address-button" class="secondary">Copy Address</button>
|
||||
</div>
|
||||
|
||||
<div class="address-container">
|
||||
<div class="address-label">Private Key (hex):</div>
|
||||
<div class="address-value" id="ethereum-private-key-value"></div>
|
||||
<button id="copy-private-key-button" class="secondary">Copy Private Key</button>
|
||||
<div class="note">Warning: Never share your private key with anyone!</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result" id="ethereum-wallet-result">Result will appear here</div>
|
||||
</div>
|
||||
|
||||
<!-- Ethereum Balance Section -->
|
||||
<div class="container" id="ethereum-balance-container">
|
||||
<h2>Check Ethereum Balance</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<button id="check-balance-button">Check Current Wallet Balance</button>
|
||||
</div>
|
||||
|
||||
<div class="result" id="balance-result">Balance will appear here</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./js/ethereum.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -84,11 +84,47 @@
|
||||
.hidden {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<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 -->
|
||||
<div class="container" id="login-container">
|
||||
<h2>Key Space Management</h2>
|
||||
@@ -118,7 +154,21 @@
|
||||
<div class="form-group">
|
||||
<label>Current Space: <span id="current-space-name"></span></label>
|
||||
</div>
|
||||
<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 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>
|
||||
|
||||
<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">
|
||||
<h2>Symmetric Encryption</h2>
|
||||
<div>
|
||||
|
490
www/js/ethereum.js
Normal file
490
www/js/ethereum.js
Normal file
@@ -0,0 +1,490 @@
|
||||
// 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
|
||||
async function updateLoginUI() {
|
||||
const loginStatus = document.getElementById('login-status');
|
||||
|
||||
try {
|
||||
console.log('Ethereum: Checking login status...');
|
||||
// Try to list keypairs to check if logged in
|
||||
const keypairs = list_keypairs();
|
||||
console.log('Ethereum: Keypairs found:', 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) {
|
||||
console.error('Ethereum: Error checking login status:', 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() {
|
||||
try {
|
||||
// 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 - call async function and await it
|
||||
await updateLoginUI();
|
||||
} catch (error) {
|
||||
console.error('Error initializing Ethereum page:', error);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
516
www/js/index.js
516
www/js/index.js
@@ -10,12 +10,25 @@ import init, {
|
||||
keypair_pub_key,
|
||||
keypair_sign,
|
||||
keypair_verify,
|
||||
derive_public_key,
|
||||
verify_with_public_key,
|
||||
encrypt_asymmetric,
|
||||
decrypt_asymmetric,
|
||||
generate_symmetric_key,
|
||||
derive_key_from_password,
|
||||
encrypt_symmetric,
|
||||
decrypt_symmetric,
|
||||
encrypt_with_password,
|
||||
decrypt_with_password
|
||||
decrypt_with_password,
|
||||
// KVS functions
|
||||
kv_store_init,
|
||||
kv_store_put,
|
||||
kv_store_get,
|
||||
kv_store_delete,
|
||||
kv_store_exists,
|
||||
kv_store_list_keys,
|
||||
kv_store_put_object,
|
||||
kv_store_get_object
|
||||
} from '../../pkg/webassembly.js';
|
||||
|
||||
// Helper function to convert ArrayBuffer to hex string
|
||||
@@ -66,34 +79,120 @@ function clearAutoLogout() {
|
||||
}
|
||||
}
|
||||
|
||||
// LocalStorage functions for key spaces
|
||||
const STORAGE_PREFIX = 'crypto_space_';
|
||||
// KVS setup and functions
|
||||
const DB_NAME = 'CryptoSpaceDB';
|
||||
const STORE_NAME = 'keySpaces';
|
||||
|
||||
// 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));
|
||||
// Initialize the database
|
||||
async function initDatabase() {
|
||||
try {
|
||||
await kv_store_init(DB_NAME, STORE_NAME);
|
||||
console.log('KV store initialized successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing KV store:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return spaces;
|
||||
}
|
||||
|
||||
// Remove space from localStorage
|
||||
function removeSpaceFromStorage(spaceName) {
|
||||
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
|
||||
// Save encrypted space to KV store
|
||||
async function saveSpaceToStorage(spaceName, encryptedData) {
|
||||
try {
|
||||
// Create a space object with metadata
|
||||
const space = {
|
||||
name: spaceName,
|
||||
encryptedData: encryptedData,
|
||||
created: new Date().toISOString(),
|
||||
lastAccessed: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Convert to JSON string
|
||||
const spaceJson = JSON.stringify(space);
|
||||
|
||||
// Store in KV store
|
||||
await kv_store_put(DB_NAME, STORE_NAME, spaceName, spaceJson);
|
||||
console.log('Space saved successfully:', spaceName);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error saving space:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get encrypted space from KV store
|
||||
async function getSpaceFromStorage(spaceName) {
|
||||
try {
|
||||
// Get from KV store
|
||||
const spaceJson = await kv_store_get(DB_NAME, STORE_NAME, spaceName);
|
||||
|
||||
if (!spaceJson) {
|
||||
console.log('Space not found:', spaceName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
const space = JSON.parse(spaceJson);
|
||||
|
||||
// Update last accessed timestamp
|
||||
updateLastAccessed(spaceName).catch(console.error);
|
||||
|
||||
// Debug what we're getting back
|
||||
console.log('Retrieved space from KV store with type:', {
|
||||
type: typeof space.encryptedData,
|
||||
length: space.encryptedData ? space.encryptedData.length : 0,
|
||||
isString: typeof space.encryptedData === 'string'
|
||||
});
|
||||
|
||||
return space.encryptedData;
|
||||
} catch (error) {
|
||||
console.error('Error retrieving space:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update last accessed timestamp
|
||||
async function updateLastAccessed(spaceName) {
|
||||
try {
|
||||
// Get the current space data
|
||||
const spaceJson = await kv_store_get(DB_NAME, STORE_NAME, spaceName);
|
||||
|
||||
if (spaceJson) {
|
||||
// Parse JSON
|
||||
const space = JSON.parse(spaceJson);
|
||||
|
||||
// Update timestamp
|
||||
space.lastAccessed = new Date().toISOString();
|
||||
|
||||
// Save back to KV store
|
||||
await kv_store_put(DB_NAME, STORE_NAME, spaceName, JSON.stringify(space));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating last accessed timestamp:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// List all spaces in KV store
|
||||
async function listSpacesFromStorage() {
|
||||
try {
|
||||
// Get all keys with empty prefix (all keys)
|
||||
const keys = await kv_store_list_keys(DB_NAME, STORE_NAME, "");
|
||||
return keys;
|
||||
} catch (error) {
|
||||
console.error('Error listing spaces:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove space from KV store
|
||||
async function removeSpaceFromStorage(spaceName) {
|
||||
try {
|
||||
await kv_store_delete(DB_NAME, STORE_NAME, spaceName);
|
||||
console.log('Space removed successfully:', spaceName);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error removing space:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Session state
|
||||
@@ -102,7 +201,7 @@ let currentSpace = null;
|
||||
let selectedKeypair = null;
|
||||
|
||||
// Update UI based on login state
|
||||
function updateLoginUI() {
|
||||
async function updateLoginUI() {
|
||||
const loginForm = document.getElementById('login-form');
|
||||
const logoutForm = document.getElementById('logout-form');
|
||||
const loginStatus = document.getElementById('login-status');
|
||||
@@ -121,6 +220,38 @@ function updateLoginUI() {
|
||||
loginStatus.className = 'status logged-out';
|
||||
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
|
||||
@@ -134,20 +265,44 @@ async function performLogin() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Get encrypted space from localStorage
|
||||
const encryptedSpace = getSpaceFromStorage(spaceName);
|
||||
// Show loading state
|
||||
document.getElementById('space-result').textContent = 'Loading...';
|
||||
|
||||
// Get encrypted space from IndexedDB
|
||||
console.log('Fetching space from IndexedDB:', spaceName);
|
||||
const encryptedSpace = await getSpaceFromStorage(spaceName);
|
||||
|
||||
if (!encryptedSpace) {
|
||||
console.error('Space not found in IndexedDB:', spaceName);
|
||||
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt the space
|
||||
console.log('Retrieved space from IndexedDB:', {
|
||||
spaceName,
|
||||
encryptedDataLength: encryptedSpace.length,
|
||||
encryptedDataType: typeof encryptedSpace
|
||||
});
|
||||
|
||||
try {
|
||||
// Decrypt the space - this is a synchronous WebAssembly function
|
||||
console.log('Attempting to decrypt space with password...');
|
||||
const result = decrypt_key_space(encryptedSpace, password);
|
||||
console.log('Decrypt result:', result);
|
||||
|
||||
if (result === 0) {
|
||||
isLoggedIn = true;
|
||||
currentSpace = spaceName;
|
||||
updateLoginUI();
|
||||
|
||||
// Save the password in session storage for later use (like when saving)
|
||||
sessionStorage.setItem('currentPassword', password);
|
||||
|
||||
// Update UI and wait for it to complete
|
||||
console.log('Updating UI...');
|
||||
await updateLoginUI();
|
||||
console.log('Updating keypairs list...');
|
||||
updateKeypairsList();
|
||||
|
||||
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
|
||||
|
||||
// Setup auto-logout
|
||||
@@ -158,9 +313,15 @@ async function performLogin() {
|
||||
document.addEventListener('click', updateActivity);
|
||||
document.addEventListener('keypress', updateActivity);
|
||||
} else {
|
||||
console.error('Failed to decrypt space:', result);
|
||||
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) {
|
||||
console.error('Login error:', e);
|
||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||
}
|
||||
}
|
||||
@@ -175,23 +336,38 @@ async function performCreateSpace() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
document.getElementById('space-result').textContent = 'Loading...';
|
||||
|
||||
// Check if space already exists
|
||||
if (getSpaceFromStorage(spaceName)) {
|
||||
const existingSpace = await getSpaceFromStorage(spaceName);
|
||||
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);
|
||||
|
||||
if (result === 0) {
|
||||
try {
|
||||
// Encrypt and save the space
|
||||
console.log('Encrypting space with password');
|
||||
const encryptedSpace = encrypt_key_space(password);
|
||||
saveSpaceToStorage(spaceName, encryptedSpace);
|
||||
console.log('Encrypted space length:', encryptedSpace.length);
|
||||
|
||||
// Save to IndexedDB
|
||||
console.log('Saving to IndexedDB');
|
||||
await saveSpaceToStorage(spaceName, encryptedSpace);
|
||||
console.log('Save completed');
|
||||
|
||||
isLoggedIn = true;
|
||||
currentSpace = spaceName;
|
||||
updateLoginUI();
|
||||
await updateLoginUI();
|
||||
updateKeypairsList();
|
||||
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
|
||||
|
||||
@@ -202,10 +378,19 @@ async function performCreateSpace() {
|
||||
// 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) {
|
||||
console.error('Error checking existing space:', e);
|
||||
document.getElementById('space-result').textContent = `Error: ${e}`;
|
||||
}
|
||||
}
|
||||
@@ -301,7 +486,7 @@ async function performCreateKeypair() {
|
||||
// Display public key
|
||||
displaySelectedKeypairPublicKey();
|
||||
|
||||
// Save the updated space to localStorage
|
||||
// Save the updated space to IndexedDB
|
||||
saveCurrentSpace();
|
||||
} else {
|
||||
document.getElementById('keypair-management-result').textContent = `Error creating keypair: ${result}`;
|
||||
@@ -346,22 +531,101 @@ async function performSelectKeypair() {
|
||||
function displaySelectedKeypairPublicKey() {
|
||||
try {
|
||||
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) {
|
||||
document.getElementById('selected-pubkey-display').textContent = `Error getting public key: ${e}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the current space to localStorage
|
||||
function saveCurrentSpace() {
|
||||
// Save the current space to IndexedDB
|
||||
async function saveCurrentSpace() {
|
||||
if (!isLoggedIn || !currentSpace) return;
|
||||
|
||||
try {
|
||||
const password = document.getElementById('space-password').value;
|
||||
// Get password from session storage (saved during login)
|
||||
const password = sessionStorage.getItem('currentPassword');
|
||||
if (!password) {
|
||||
console.error('Password not available in session storage');
|
||||
|
||||
// Fallback to the password field
|
||||
const inputPassword = document.getElementById('space-password').value;
|
||||
if (!inputPassword) {
|
||||
console.error('Password not available for saving space');
|
||||
alert('Please re-enter your password to save changes');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the input password if session storage isn't available
|
||||
const encryptedSpace = encrypt_key_space(inputPassword);
|
||||
console.log('Saving space with input password');
|
||||
await saveSpaceToStorage(currentSpace, encryptedSpace);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the password from session storage
|
||||
console.log('Encrypting space with session password');
|
||||
const encryptedSpace = encrypt_key_space(password);
|
||||
saveSpaceToStorage(currentSpace, encryptedSpace);
|
||||
console.log('Saving encrypted space to IndexedDB:', currentSpace);
|
||||
await saveSpaceToStorage(currentSpace, encryptedSpace);
|
||||
console.log('Space saved successfully');
|
||||
} catch (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 +639,46 @@ async function run() {
|
||||
document.getElementById('login-button').addEventListener('click', performLogin);
|
||||
document.getElementById('create-space-button').addEventListener('click', performCreateSpace);
|
||||
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
|
||||
document.getElementById('create-keypair-button').addEventListener('click', performCreateKeypair);
|
||||
@@ -531,6 +835,140 @@ async function run() {
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
updateLoginUI();
|
||||
}
|
||||
|
Reference in New Issue
Block a user