add cli with rhai scripting engine
This commit is contained in:
parent
452bae3a18
commit
0890db4810
807
CLI_IMPLEMENTATION_PLAN.md
Normal file
807
CLI_IMPLEMENTATION_PLAN.md
Normal file
@ -0,0 +1,807 @@
|
||||
# CLI and Rhai Scripting Implementation Plan
|
||||
|
||||
This document outlines the technical implementation plan for adding CLI and Rhai scripting capabilities to the WebAssembly Cryptography Module.
|
||||
|
||||
## 1. Project Structure Updates
|
||||
|
||||
### 1.1 Directory Structure
|
||||
|
||||
```
|
||||
webassembly/
|
||||
├── Cargo.toml (updated)
|
||||
├── src/
|
||||
│ ├── lib.rs (existing WebAssembly exports)
|
||||
│ ├── main.rs (new CLI entry point)
|
||||
│ ├── core/ (existing cryptographic core)
|
||||
│ ├── api/ (existing API layer)
|
||||
│ ├── cli/ (new CLI module)
|
||||
│ │ ├── commands.rs
|
||||
│ │ ├── config.rs
|
||||
│ │ ├── error.rs
|
||||
│ │ └── mod.rs
|
||||
│ ├── scripting/ (new Rhai scripting module)
|
||||
│ │ ├── engine.rs
|
||||
│ │ ├── api.rs
|
||||
│ │ ├── sandbox.rs
|
||||
│ │ └── mod.rs
|
||||
│ └── messaging/ (new messaging module)
|
||||
│ ├── mycelium.rs (or nats.rs)
|
||||
│ ├── error.rs
|
||||
│ └── mod.rs
|
||||
├── scripts/ (example Rhai scripts)
|
||||
└── www/ (existing WebAssembly frontend)
|
||||
```
|
||||
|
||||
### 1.2 Cargo.toml Updates
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "webassembly"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Your Name <your.email@example.com>"]
|
||||
description = "Cryptographic module with CLI, Rhai scripting, and WebAssembly support"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "crypto-cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Existing dependencies
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["console"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
k256 = { version = "0.13", features = ["ecdsa", "serde"] }
|
||||
chacha20poly1305 = "0.10"
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
once_cell = "1.17"
|
||||
sha2 = "0.10"
|
||||
ethers = { version = "2.0", features = ["legacy"] }
|
||||
|
||||
# New dependencies for CLI
|
||||
clap = { version = "4.3", features = ["derive"] }
|
||||
colored = "2.0"
|
||||
dirs = "5.0"
|
||||
rustyline = "11.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
rpassword = "7.2"
|
||||
|
||||
# Rhai scripting
|
||||
rhai = { version = "1.14", features = ["sync", "serde"] }
|
||||
|
||||
# Messaging system (choose one)
|
||||
# Option 1: Mycelium
|
||||
mycelium = "0.1"
|
||||
# Option 2: NATS
|
||||
async-nats = "0.29"
|
||||
tokio = { version = "1.28", features = ["full"] }
|
||||
|
||||
[features]
|
||||
default = ["cli", "wasm"]
|
||||
cli = []
|
||||
wasm = []
|
||||
mycelium = []
|
||||
nats = []
|
||||
```
|
||||
|
||||
## 2. CLI Implementation
|
||||
|
||||
### 2.1 Main Entry Point (src/main.rs)
|
||||
|
||||
```rust
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use log::info;
|
||||
|
||||
mod core;
|
||||
mod api;
|
||||
mod cli;
|
||||
mod scripting;
|
||||
mod messaging;
|
||||
|
||||
use cli::{Cli, Commands};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logger
|
||||
env_logger::init();
|
||||
|
||||
// Parse command line arguments
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Set up verbose logging if requested
|
||||
if cli.verbose {
|
||||
std::env::set_var("RUST_LOG", "debug");
|
||||
}
|
||||
|
||||
// Execute the appropriate command
|
||||
match &cli.command {
|
||||
Commands::Key { command } => {
|
||||
cli::commands::execute_key_command(command)?;
|
||||
},
|
||||
Commands::Crypto { command } => {
|
||||
cli::commands::execute_crypto_command(command)?;
|
||||
},
|
||||
Commands::Ethereum { command } => {
|
||||
cli::commands::execute_ethereum_command(command)?;
|
||||
},
|
||||
Commands::Script { path, inline } => {
|
||||
let mut engine = scripting::ScriptEngine::new();
|
||||
|
||||
if let Some(script_path) = path {
|
||||
info!("Executing script from file: {}", script_path);
|
||||
engine.eval_file(script_path)?;
|
||||
} else if let Some(script) = inline {
|
||||
info!("Executing inline script");
|
||||
engine.eval(script)?;
|
||||
} else {
|
||||
println!("Error: No script provided");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Commands::Listen { server, subject } => {
|
||||
// Implementation depends on chosen messaging system
|
||||
#[cfg(feature = "mycelium")]
|
||||
{
|
||||
let listener = messaging::mycelium::MyceliumNetwork::new().await?;
|
||||
listener.listen().await?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "nats")]
|
||||
{
|
||||
let listener = messaging::nats::NatsListener::new(server, subject).await?;
|
||||
listener.listen().await?;
|
||||
}
|
||||
},
|
||||
Commands::Shell => {
|
||||
cli::shell::run_interactive_shell()?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 CLI Module (src/cli/mod.rs)
|
||||
|
||||
```rust
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod shell;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "crypto-cli")]
|
||||
#[command(about = "Cryptographic operations CLI with Rhai scripting support", long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
|
||||
#[arg(short, long, help = "Enable verbose output")]
|
||||
pub verbose: bool,
|
||||
|
||||
#[arg(short, long, help = "Config file path")]
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Key management commands
|
||||
Key {
|
||||
#[command(subcommand)]
|
||||
command: KeyCommands,
|
||||
},
|
||||
|
||||
/// Encryption/decryption commands
|
||||
Crypto {
|
||||
#[command(subcommand)]
|
||||
command: CryptoCommands,
|
||||
},
|
||||
|
||||
/// Ethereum wallet commands
|
||||
Ethereum {
|
||||
#[command(subcommand)]
|
||||
command: EthereumCommands,
|
||||
},
|
||||
|
||||
/// Execute Rhai script
|
||||
Script {
|
||||
#[arg(help = "Path to Rhai script file")]
|
||||
path: Option<String>,
|
||||
|
||||
#[arg(short, long, help = "Execute script from string")]
|
||||
inline: Option<String>,
|
||||
},
|
||||
|
||||
/// Start listener for scripts
|
||||
Listen {
|
||||
#[arg(short, long, help = "Server URL", default_value = "localhost")]
|
||||
server: String,
|
||||
|
||||
#[arg(short, long, help = "Subject to subscribe to", default_value = "crypto.scripts")]
|
||||
subject: String,
|
||||
},
|
||||
|
||||
/// Interactive shell
|
||||
Shell,
|
||||
}
|
||||
|
||||
// Define subcommands for each category
|
||||
#[derive(Subcommand)]
|
||||
pub enum KeyCommands {
|
||||
// Key management commands
|
||||
CreateSpace { name: String, password: Option<String> },
|
||||
ListSpaces,
|
||||
CreateKeypair { name: String },
|
||||
ListKeypairs,
|
||||
Export { name: String, output: Option<String> },
|
||||
Import { name: String, input: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum CryptoCommands {
|
||||
// Cryptographic operation commands
|
||||
Sign { message: Option<String>, input: Option<String>, keypair: String, output: Option<String> },
|
||||
Verify { message: Option<String>, input: Option<String>, signature: String, keypair: Option<String>, pubkey: Option<String> },
|
||||
Encrypt { data: Option<String>, input: Option<String>, recipient: String, output: Option<String> },
|
||||
Decrypt { data: Option<String>, input: Option<String>, keypair: String, output: Option<String> },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum EthereumCommands {
|
||||
// Ethereum wallet commands
|
||||
Create { keypair: String },
|
||||
Address { keypair: String },
|
||||
Balance { address: Option<String>, network: String },
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 CLI Error Handling (src/cli/error.rs)
|
||||
|
||||
```rust
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CliError {
|
||||
IoError(String),
|
||||
CryptoError(crate::core::error::CryptoError),
|
||||
ScriptError(String),
|
||||
MessagingError(String),
|
||||
ConfigError(String),
|
||||
NotImplemented,
|
||||
}
|
||||
|
||||
impl fmt::Display for CliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CliError::IoError(msg) => write!(f, "I/O Error: {}", msg),
|
||||
CliError::CryptoError(err) => write!(f, "Crypto Error: {}", err),
|
||||
CliError::ScriptError(msg) => write!(f, "Script Error: {}", msg),
|
||||
CliError::MessagingError(msg) => write!(f, "Messaging Error: {}", msg),
|
||||
CliError::ConfigError(msg) => write!(f, "Configuration Error: {}", msg),
|
||||
CliError::NotImplemented => write!(f, "Command not implemented yet"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for CliError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
CliError::IoError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::core::error::CryptoError> for CliError {
|
||||
fn from(err: crate::core::error::CryptoError) -> Self {
|
||||
CliError::CryptoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rhai::EvalAltResult> for CliError {
|
||||
fn from(err: rhai::EvalAltResult) -> Self {
|
||||
CliError::ScriptError(err.to_string())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Rhai Scripting Implementation
|
||||
|
||||
### 3.1 Scripting Engine (src/scripting/engine.rs)
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, AST, Scope, EvalAltResult};
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
|
||||
use crate::scripting::api::register_crypto_api;
|
||||
use crate::cli::error::CliError;
|
||||
|
||||
pub struct ScriptEngine {
|
||||
engine: Engine,
|
||||
scope: Scope<'static>,
|
||||
}
|
||||
|
||||
impl ScriptEngine {
|
||||
pub fn new() -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Set up sandboxing
|
||||
engine.set_max_operations(100_000);
|
||||
engine.set_max_modules(10);
|
||||
engine.set_max_string_size(10_000);
|
||||
engine.set_max_array_size(1_000);
|
||||
engine.set_max_map_size(1_000);
|
||||
|
||||
// Disable potentially dangerous operations
|
||||
engine.disable_symbol("eval");
|
||||
engine.disable_symbol("source");
|
||||
|
||||
// Register crypto API
|
||||
let mut scope = Scope::new();
|
||||
register_crypto_api(&mut engine, &mut scope);
|
||||
|
||||
ScriptEngine { engine, scope }
|
||||
}
|
||||
|
||||
pub fn eval_file<P: AsRef<Path>>(&mut self, path: P) -> Result<(), CliError> {
|
||||
let script = fs::read_to_string(path)
|
||||
.map_err(|e| CliError::IoError(format!("Failed to read script file: {}", e)))?;
|
||||
|
||||
self.eval(&script)
|
||||
}
|
||||
|
||||
pub fn eval(&mut self, script: &str) -> Result<(), CliError> {
|
||||
self.engine.eval_with_scope::<()>(&mut self.scope, script)
|
||||
.map_err(|e| CliError::ScriptError(e.to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Scripting API (src/scripting/api.rs)
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, Scope, Dynamic, FnPtr};
|
||||
use crate::api::{keypair, symmetric, ethereum};
|
||||
|
||||
pub fn register_crypto_api(engine: &mut Engine, scope: &mut Scope) {
|
||||
// Register key space functions
|
||||
engine.register_fn("create_key_space", |name: &str| -> bool {
|
||||
keypair::create_space(name).is_ok()
|
||||
});
|
||||
|
||||
engine.register_fn("encrypt_key_space", |password: &str| -> Dynamic {
|
||||
match keypair::encrypt_space(password) {
|
||||
Ok(encrypted) => Dynamic::from(encrypted),
|
||||
Err(_) => Dynamic::UNIT,
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_fn("decrypt_key_space", |encrypted: &str, password: &str| -> bool {
|
||||
keypair::decrypt_space(encrypted, password).is_ok()
|
||||
});
|
||||
|
||||
// Register keypair functions
|
||||
engine.register_fn("create_keypair", |name: &str| -> bool {
|
||||
keypair::create_keypair(name).is_ok()
|
||||
});
|
||||
|
||||
engine.register_fn("select_keypair", |name: &str| -> bool {
|
||||
keypair::select_keypair(name).is_ok()
|
||||
});
|
||||
|
||||
engine.register_fn("list_keypairs", || -> Dynamic {
|
||||
match keypair::list_keypairs() {
|
||||
Ok(keypairs) => {
|
||||
let array: Vec<Dynamic> = keypairs.into_iter()
|
||||
.map(Dynamic::from)
|
||||
.collect();
|
||||
Dynamic::from(array)
|
||||
},
|
||||
Err(_) => Dynamic::from(Vec::<Dynamic>::new()),
|
||||
}
|
||||
});
|
||||
|
||||
// Register signing/verification functions
|
||||
engine.register_fn("sign", |message: &str| -> Dynamic {
|
||||
let message_bytes = message.as_bytes();
|
||||
match keypair::sign(message_bytes) {
|
||||
Ok(signature) => {
|
||||
// Convert to hex string for easier handling in scripts
|
||||
let hex = signature.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<String>();
|
||||
Dynamic::from(hex)
|
||||
},
|
||||
Err(_) => Dynamic::UNIT,
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_fn("verify", |message: &str, signature_hex: &str| -> bool {
|
||||
let message_bytes = message.as_bytes();
|
||||
|
||||
// Convert hex string back to bytes
|
||||
let signature_bytes = hex_to_bytes(signature_hex);
|
||||
if signature_bytes.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match keypair::verify(message_bytes, &signature_bytes) {
|
||||
Ok(result) => result,
|
||||
Err(_) => false,
|
||||
}
|
||||
});
|
||||
|
||||
// Register symmetric encryption functions
|
||||
engine.register_fn("generate_key", || -> Dynamic {
|
||||
let key = symmetric::generate_key();
|
||||
let hex = key.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<String>();
|
||||
Dynamic::from(hex)
|
||||
});
|
||||
|
||||
engine.register_fn("encrypt", |key_hex: &str, message: &str| -> Dynamic {
|
||||
let key = hex_to_bytes(key_hex);
|
||||
if key.is_empty() {
|
||||
return Dynamic::UNIT;
|
||||
}
|
||||
|
||||
let message_bytes = message.as_bytes();
|
||||
match symmetric::encrypt(&key, message_bytes) {
|
||||
Ok(ciphertext) => {
|
||||
let hex = ciphertext.iter()
|
||||
.map(|b| format!("{:02x}", b))
|
||||
.collect::<String>();
|
||||
Dynamic::from(hex)
|
||||
},
|
||||
Err(_) => Dynamic::UNIT,
|
||||
}
|
||||
});
|
||||
|
||||
engine.register_fn("decrypt", |key_hex: &str, ciphertext_hex: &str| -> Dynamic {
|
||||
let key = hex_to_bytes(key_hex);
|
||||
let ciphertext = hex_to_bytes(ciphertext_hex);
|
||||
if key.is_empty() || ciphertext.is_empty() {
|
||||
return Dynamic::UNIT;
|
||||
}
|
||||
|
||||
match symmetric::decrypt(&key, &ciphertext) {
|
||||
Ok(plaintext) => {
|
||||
match String::from_utf8(plaintext) {
|
||||
Ok(text) => Dynamic::from(text),
|
||||
Err(_) => Dynamic::UNIT,
|
||||
}
|
||||
},
|
||||
Err(_) => Dynamic::UNIT,
|
||||
}
|
||||
});
|
||||
|
||||
// Register Ethereum functions
|
||||
engine.register_fn("create_ethereum_wallet", || -> bool {
|
||||
ethereum::create_ethereum_wallet().is_ok()
|
||||
});
|
||||
|
||||
engine.register_fn("get_ethereum_address", || -> Dynamic {
|
||||
match ethereum::get_ethereum_address() {
|
||||
Ok(address) => Dynamic::from(address),
|
||||
Err(_) => Dynamic::UNIT,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to convert hex string to bytes
|
||||
fn hex_to_bytes(hex: &str) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
let mut chars = hex.chars();
|
||||
|
||||
while let (Some(a), Some(b)) = (chars.next(), chars.next()) {
|
||||
if let (Some(high), Some(low)) = (a.to_digit(16), b.to_digit(16)) {
|
||||
bytes.push(((high << 4) | low) as u8);
|
||||
} else {
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Messaging System Implementation
|
||||
|
||||
### 4.1 Mycelium Implementation (src/messaging/mycelium.rs)
|
||||
|
||||
```rust
|
||||
use mycelium::{Node, Identity, Message};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::scripting::ScriptEngine;
|
||||
use crate::cli::error::CliError;
|
||||
|
||||
pub struct MyceliumNetwork {
|
||||
node: Node,
|
||||
identity: Identity,
|
||||
}
|
||||
|
||||
impl MyceliumNetwork {
|
||||
pub async fn new() -> Result<Self, CliError> {
|
||||
let identity = Identity::random();
|
||||
let node = Node::new(identity.clone())
|
||||
.map_err(|e| CliError::MessagingError(format!("Failed to create Mycelium node: {}", e)))?;
|
||||
|
||||
Ok(MyceliumNetwork {
|
||||
node,
|
||||
identity,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn listen(&self) -> Result<(), CliError> {
|
||||
println!("Listening for scripts on Mycelium network");
|
||||
|
||||
let mut receiver = self.node.subscribe("crypto.scripts")
|
||||
.map_err(|e| CliError::MessagingError(format!("Failed to subscribe: {}", e)))?;
|
||||
|
||||
while let Some(msg) = receiver.recv().await {
|
||||
let script = String::from_utf8_lossy(&msg.payload);
|
||||
println!("Received script: {}", script);
|
||||
|
||||
let mut engine = ScriptEngine::new();
|
||||
match engine.eval(&script) {
|
||||
Ok(_) => {
|
||||
println!("Script executed successfully");
|
||||
self.node.publish(
|
||||
"crypto.results",
|
||||
msg.sender.clone(),
|
||||
"Script executed successfully".as_bytes().to_vec(),
|
||||
).await.map_err(|e| CliError::MessagingError(format!("Failed to send result: {}", e)))?;
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Script execution failed: {}", e);
|
||||
self.node.publish(
|
||||
"crypto.results",
|
||||
msg.sender.clone(),
|
||||
format!("Script execution failed: {}", e).as_bytes().to_vec(),
|
||||
).await.map_err(|e| CliError::MessagingError(format!("Failed to send result: {}", e)))?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 NATS Implementation (src/messaging/nats.rs)
|
||||
|
||||
```rust
|
||||
use async_nats::Client;
|
||||
use tokio::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::scripting::ScriptEngine;
|
||||
use crate::cli::error::CliError;
|
||||
|
||||
pub struct NatsListener {
|
||||
client: Client,
|
||||
subject: String,
|
||||
}
|
||||
|
||||
impl NatsListener {
|
||||
pub async fn new(server: &str, subject: &str) -> Result<Self, CliError> {
|
||||
let client = async_nats::connect(server)
|
||||
.await
|
||||
.map_err(|e| CliError::MessagingError(format!("Failed to connect to NATS: {}", e)))?;
|
||||
|
||||
Ok(NatsListener {
|
||||
client,
|
||||
subject: subject.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn listen(&self) -> Result<(), CliError> {
|
||||
println!("Listening for scripts on subject: {}", self.subject);
|
||||
|
||||
let mut subscriber = self.client.subscribe(self.subject.clone())
|
||||
.await
|
||||
.map_err(|e| CliError::MessagingError(format!("Failed to subscribe: {}", e)))?;
|
||||
|
||||
while let Some(msg) = subscriber.next().await {
|
||||
let script = String::from_utf8_lossy(&msg.payload);
|
||||
println!("Received script: {}", script);
|
||||
|
||||
let mut engine = ScriptEngine::new();
|
||||
let result = match engine.eval(&script) {
|
||||
Ok(_) => {
|
||||
println!("Script executed successfully");
|
||||
"Script executed successfully"
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Script execution failed: {}", e);
|
||||
&format!("Script execution failed: {}", e)
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(reply) = msg.reply {
|
||||
self.client.publish(reply, result.into())
|
||||
.await
|
||||
.map_err(|e| CliError::MessagingError(format!("Failed to send result: {}", e)))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Example Rhai Scripts
|
||||
|
||||
### 5.1 Key Management Script (scripts/key_management.rhai)
|
||||
|
||||
```rhai
|
||||
// Create a key space
|
||||
let space_name = "test_space";
|
||||
let password = "secure_password";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name) {
|
||||
print("Key space created successfully");
|
||||
|
||||
// Encrypt the key space
|
||||
let encrypted = encrypt_key_space(password);
|
||||
print("Encrypted key space: " + encrypted);
|
||||
|
||||
// Create a keypair
|
||||
if create_keypair("test_keypair") {
|
||||
print("Keypair created successfully");
|
||||
|
||||
// List keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Select the keypair
|
||||
if select_keypair("test_keypair") {
|
||||
print("Keypair selected successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Signing Script (scripts/signing.rhai)
|
||||
|
||||
```rhai
|
||||
// Select a keypair
|
||||
if select_keypair("test_keypair") {
|
||||
print("Keypair selected successfully");
|
||||
|
||||
// Sign a message
|
||||
let message = "Hello, this is a test message";
|
||||
let signature = sign(message);
|
||||
|
||||
print("Message: " + message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
print("Signature valid: " + is_valid);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Encryption Script (scripts/encryption.rhai)
|
||||
|
||||
```rhai
|
||||
// Generate a symmetric key
|
||||
let key = generate_key();
|
||||
print("Generated key: " + key);
|
||||
|
||||
// Encrypt a message
|
||||
let message = "This is a secret message";
|
||||
let encrypted = encrypt(key, message);
|
||||
print("Encrypted: " + encrypted);
|
||||
|
||||
// Decrypt the message
|
||||
let decrypted = decrypt(key, encrypted);
|
||||
print("Decrypted: " + decrypted);
|
||||
|
||||
// Verify the decryption worked
|
||||
if decrypted == message {
|
||||
print("Encryption/decryption successful!");
|
||||
} else {
|
||||
print("Encryption/decryption failed!");
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Implementation Steps
|
||||
|
||||
1. **Update Cargo.toml**
|
||||
- Add new dependencies
|
||||
- Configure features
|
||||
- Add binary target
|
||||
|
||||
2. **Create CLI Structure**
|
||||
- Implement CLI module
|
||||
- Define commands and subcommands
|
||||
- Set up error handling
|
||||
|
||||
3. **Implement Rhai Scripting**
|
||||
- Create scripting engine
|
||||
- Register API functions
|
||||
- Implement sandboxing
|
||||
|
||||
4. **Implement Messaging System**
|
||||
- Choose between Mycelium and NATS
|
||||
- Implement listener
|
||||
- Set up script execution
|
||||
|
||||
5. **Create Example Scripts**
|
||||
- Key management scripts
|
||||
- Signing scripts
|
||||
- Encryption scripts
|
||||
|
||||
6. **Testing**
|
||||
- Unit tests for CLI commands
|
||||
- Integration tests for script execution
|
||||
- End-to-end tests for messaging
|
||||
|
||||
7. **Documentation**
|
||||
- Update README.md
|
||||
- Add CLI help text
|
||||
- Document script API
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 Unit Tests
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cli_key_commands() {
|
||||
// Test key management commands
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_crypto_commands() {
|
||||
// Test cryptographic operation commands
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_engine() {
|
||||
// Test script execution
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Integration Tests
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod integration_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_script_execution() {
|
||||
// Test executing a script file
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_workflow() {
|
||||
// Test a complete CLI workflow
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Conclusion
|
||||
|
||||
This implementation plan provides a detailed roadmap for adding CLI and Rhai scripting capabilities to the WebAssembly Cryptography Module. By following this plan, the module will be transformed into a versatile cryptographic toolkit that can operate across multiple contexts 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 are included in this plan to provide flexibility.
|
164
CLI_README.md
Normal file
164
CLI_README.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Crypto CLI and Rhai Scripting
|
||||
|
||||
This module adds CLI and Rhai scripting capabilities to the WebAssembly Cryptography Module, allowing for command-line operations and scripting of cryptographic functions.
|
||||
|
||||
## Features
|
||||
|
||||
- Command-line interface for cryptographic operations
|
||||
- Interactive shell mode
|
||||
- Rhai scripting engine for automation
|
||||
- Key management (create, list, import, export)
|
||||
- Cryptographic operations (sign, verify, encrypt, decrypt)
|
||||
- Ethereum wallet integration
|
||||
|
||||
## Installation
|
||||
|
||||
Build the CLI tool using Cargo:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary will be available at `target/release/crypto-cli`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
The CLI provides several subcommands for different operations:
|
||||
|
||||
#### Key Management
|
||||
|
||||
```bash
|
||||
# Create a new key space
|
||||
crypto-cli key create-space <name> [password]
|
||||
|
||||
# List available key spaces
|
||||
crypto-cli key list-spaces
|
||||
|
||||
# Create a new keypair
|
||||
crypto-cli key create-keypair <name>
|
||||
|
||||
# List available keypairs
|
||||
crypto-cli key list-keypairs
|
||||
|
||||
# Export a keypair
|
||||
crypto-cli key export <name> [output-file]
|
||||
|
||||
# Import a keypair
|
||||
crypto-cli key import <name> [input-file]
|
||||
```
|
||||
|
||||
#### Cryptographic Operations
|
||||
|
||||
```bash
|
||||
# Sign a message
|
||||
crypto-cli crypto sign <keypair> <message> [output-file]
|
||||
|
||||
# Verify a signature
|
||||
crypto-cli crypto verify <signature> <message> [keypair]
|
||||
|
||||
# Encrypt data
|
||||
crypto-cli crypto encrypt <recipient> <data> [output-file]
|
||||
|
||||
# Decrypt data
|
||||
crypto-cli crypto decrypt <keypair> <data> [output-file]
|
||||
```
|
||||
|
||||
#### Ethereum Operations
|
||||
|
||||
```bash
|
||||
# Create an Ethereum wallet from a keypair
|
||||
crypto-cli eth create <keypair>
|
||||
|
||||
# Get the Ethereum address for a keypair
|
||||
crypto-cli eth address <keypair>
|
||||
|
||||
# Get the balance of an Ethereum address
|
||||
crypto-cli eth balance <address> <network>
|
||||
```
|
||||
|
||||
### Interactive Shell
|
||||
|
||||
Launch the interactive shell with:
|
||||
|
||||
```bash
|
||||
crypto-cli shell
|
||||
```
|
||||
|
||||
In the shell, you can run the same commands as in the CLI but without the `crypto-cli` prefix.
|
||||
|
||||
### Rhai Scripting
|
||||
|
||||
Execute Rhai scripts with:
|
||||
|
||||
```bash
|
||||
# Execute a script file
|
||||
crypto-cli script <path-to-script>
|
||||
|
||||
# Execute an inline script
|
||||
crypto-cli script --inline "create_keypair('test'); sign('Hello, world!');"
|
||||
```
|
||||
|
||||
## Rhai Scripting API
|
||||
|
||||
The Rhai scripting engine provides access to the following functions:
|
||||
|
||||
### Key Management
|
||||
|
||||
- `create_key_space(name)` - Create a new key space
|
||||
- `encrypt_key_space(password)` - Encrypt the current key space
|
||||
- `decrypt_key_space(encrypted, password)` - Decrypt a key space
|
||||
- `create_keypair(name)` - Create a new keypair
|
||||
- `select_keypair(name)` - Select a keypair for operations
|
||||
- `list_keypairs()` - List available keypairs
|
||||
|
||||
### Cryptographic Operations
|
||||
|
||||
- `sign(message)` - Sign a message with the selected keypair
|
||||
- `verify(message, signature)` - Verify a signature
|
||||
- `generate_key()` - Generate a symmetric encryption key
|
||||
- `encrypt(key, message)` - Encrypt a message with a symmetric key
|
||||
- `decrypt(key, ciphertext)` - Decrypt a message with a symmetric key
|
||||
|
||||
### Ethereum Operations
|
||||
|
||||
- `create_ethereum_wallet()` - Create an Ethereum wallet
|
||||
- `get_ethereum_address()` - Get the Ethereum address for the selected keypair
|
||||
|
||||
## Example Script
|
||||
|
||||
```rhai
|
||||
// Create a key space and keypair
|
||||
create_key_space("demo");
|
||||
create_keypair("alice");
|
||||
select_keypair("alice");
|
||||
|
||||
// Sign and verify a message
|
||||
let message = "Hello, world!";
|
||||
let signature = sign(message);
|
||||
let is_valid = verify(message, signature);
|
||||
print("Signature valid: " + is_valid);
|
||||
|
||||
// Symmetric encryption
|
||||
let key = generate_key();
|
||||
let ciphertext = encrypt(key, "Secret message");
|
||||
let plaintext = decrypt(key, ciphertext);
|
||||
print("Decrypted: " + plaintext);
|
||||
|
||||
// Ethereum operations
|
||||
create_ethereum_wallet();
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The CLI uses a configuration file located at `~/.crypto-cli/config.json`. You can specify a different configuration file with the `--config` option.
|
||||
|
||||
## Verbose Mode
|
||||
|
||||
Use the `--verbose` flag to enable verbose output:
|
||||
|
||||
```bash
|
||||
crypto-cli --verbose <command>
|
29
Cargo.toml
29
Cargo.toml
@ -2,19 +2,24 @@
|
||||
name = "webassembly"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "A WebAssembly module for web integration"
|
||||
description = "Cryptographic module with CLI, Rhai scripting, and WebAssembly support"
|
||||
repository = "https://github.com/yourusername/webassembly"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "crypto-cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Existing 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"] }
|
||||
k256 = { version = "0.13", features = ["ecdsa", "serde"] }
|
||||
rand = { version = "0.8", features = ["getrandom"] }
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
chacha20poly1305 = "0.10"
|
||||
@ -27,6 +32,21 @@ ethers = { version = "2.0", features = ["abigen", "legacy"] }
|
||||
hex = "0.4"
|
||||
idb = "0.6.4"
|
||||
|
||||
# New dependencies for CLI
|
||||
clap = { version = "4.3", features = ["derive"] }
|
||||
colored = "2.0"
|
||||
dirs = "5.0"
|
||||
rustyline = "11.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
rpassword = "7.2"
|
||||
|
||||
# Rhai scripting
|
||||
rhai = { version = "1.14", features = ["sync", "serde"] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1.28", features = ["rt", "rt-multi-thread"] }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
@ -40,6 +60,11 @@ features = [
|
||||
"Performance"
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["cli", "wasm"]
|
||||
cli = []
|
||||
wasm = []
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
|
30
build.sh
Executable file
30
build.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for the Crypto CLI
|
||||
|
||||
# Set colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}Building Crypto CLI...${NC}"
|
||||
|
||||
# Build the CLI
|
||||
cargo build --release
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}Build successful!${NC}"
|
||||
echo -e "Binary available at: ${YELLOW}target/release/crypto-cli${NC}"
|
||||
|
||||
# Create a symlink for easier access
|
||||
echo -e "${YELLOW}Creating symlink...${NC}"
|
||||
ln -sf "$(pwd)/target/release/crypto-cli" ./crypto-cli
|
||||
|
||||
echo -e "${GREEN}Done!${NC}"
|
||||
echo -e "You can now run the CLI with: ${YELLOW}./crypto-cli${NC}"
|
||||
echo -e "Or try the example script: ${YELLOW}./crypto-cli script examples/scripts/crypto_demo.rhai${NC}"
|
||||
else
|
||||
echo -e "${RED}Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
77
scripts/README.md
Normal file
77
scripts/README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# WebAssembly Cryptography Module Scripts
|
||||
|
||||
This directory contains example scripts and documentation for the WebAssembly Cryptography Module's scripting and messaging capabilities.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `rhai/`: Example Rhai scripts that demonstrate the cryptographic operations
|
||||
- `examples/`: Documentation and code examples for messaging system integration
|
||||
|
||||
## Rhai Scripts
|
||||
|
||||
The `rhai/` directory contains example Rhai scripts that can be executed using the CLI:
|
||||
|
||||
```bash
|
||||
crypto-cli script --path scripts/rhai/example.rhai
|
||||
```
|
||||
|
||||
These scripts demonstrate how to use the cryptographic functions exposed to the Rhai scripting engine, including:
|
||||
|
||||
- Key space management
|
||||
- Keypair operations
|
||||
- Signing and verification
|
||||
- Symmetric encryption and decryption
|
||||
- Ethereum wallet operations
|
||||
|
||||
## Messaging Examples
|
||||
|
||||
The `examples/` directory contains documentation and code examples for integrating the WebAssembly Cryptography Module with messaging systems:
|
||||
|
||||
- `mycelium_example.md`: Example of using Mycelium for peer-to-peer, end-to-end encrypted messaging
|
||||
- `nats_example.md`: Example of using NATS for high-performance, client-server messaging
|
||||
|
||||
These examples demonstrate how to:
|
||||
|
||||
1. Start a listener for remote script execution
|
||||
2. Send scripts from remote systems
|
||||
3. Process the results of script execution
|
||||
4. Implement security measures for remote execution
|
||||
|
||||
## Creating Your Own Scripts
|
||||
|
||||
You can create your own Rhai scripts to automate cryptographic operations. The following functions are available in the scripting API:
|
||||
|
||||
### Key Space Management
|
||||
|
||||
- `create_key_space(name)`: Create a new key space
|
||||
- `encrypt_key_space(password)`: Encrypt the current key space
|
||||
- `decrypt_key_space(encrypted, password)`: Decrypt and load a key space
|
||||
|
||||
### Keypair Operations
|
||||
|
||||
- `create_keypair(name)`: Create a new keypair
|
||||
- `select_keypair(name)`: Select a keypair for use
|
||||
- `list_keypairs()`: List all keypairs in the current space
|
||||
|
||||
### Cryptographic Operations
|
||||
|
||||
- `sign(message)`: Sign a message with the selected keypair
|
||||
- `verify(message, signature)`: Verify a signature
|
||||
- `generate_key()`: Generate a symmetric key
|
||||
- `encrypt(key, message)`: Encrypt a message with a symmetric key
|
||||
- `decrypt(key, ciphertext)`: Decrypt a message with a symmetric key
|
||||
|
||||
### Ethereum Operations
|
||||
|
||||
- `create_ethereum_wallet()`: Create an Ethereum wallet
|
||||
- `get_ethereum_address()`: Get the Ethereum address of the current wallet
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When using scripts, especially with remote execution via messaging systems, consider the following security measures:
|
||||
|
||||
1. **Script Validation**: Validate scripts before execution to prevent malicious code
|
||||
2. **Resource Limits**: Set appropriate limits on script execution to prevent denial of service
|
||||
3. **Authentication**: Ensure that only authorized users or systems can execute scripts
|
||||
4. **Sensitive Data**: Be careful about what data is returned in script results
|
||||
5. **Encryption**: Use encrypted communication channels for remote script execution
|
137
scripts/examples/mycelium_example.md
Normal file
137
scripts/examples/mycelium_example.md
Normal file
@ -0,0 +1,137 @@
|
||||
# Mycelium Integration Example
|
||||
|
||||
This document demonstrates how to use the Mycelium messaging system with the WebAssembly Cryptography Module for remote script execution.
|
||||
|
||||
## Overview
|
||||
|
||||
Mycelium is a peer-to-peer, end-to-end encrypted messaging system that allows for secure communication between nodes. When integrated with the WebAssembly Cryptography Module, it enables remote execution of Rhai scripts, allowing for distributed cryptographic operations.
|
||||
|
||||
## Example Scenario
|
||||
|
||||
In this example, we'll demonstrate how a remote system can send a Rhai script to the cryptographic module for execution, and receive the results.
|
||||
|
||||
### Step 1: Start the Listener
|
||||
|
||||
First, start the cryptographic module's Mycelium listener:
|
||||
|
||||
```bash
|
||||
crypto-cli listen
|
||||
```
|
||||
|
||||
This will start a Mycelium node that listens for scripts on the "crypto.scripts" topic.
|
||||
|
||||
### Step 2: Send a Script from a Remote System
|
||||
|
||||
From another system, send a Rhai script to the listener:
|
||||
|
||||
```rust
|
||||
use mycelium::{Node, Identity};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a Mycelium node
|
||||
let identity = Identity::random();
|
||||
let node = Node::new(identity)?;
|
||||
|
||||
// Connect to the network
|
||||
node.start().await?;
|
||||
|
||||
// Define the script to execute
|
||||
let script = r#"
|
||||
// Create a key space
|
||||
if create_key_space("remote_space") {
|
||||
print("Key space created successfully");
|
||||
|
||||
// Create a keypair
|
||||
if create_keypair("remote_keypair") {
|
||||
print("Keypair created successfully");
|
||||
|
||||
// Select the keypair
|
||||
if select_keypair("remote_keypair") {
|
||||
print("Keypair selected successfully");
|
||||
|
||||
// Sign a message
|
||||
let message = "Hello from remote system";
|
||||
let signature = sign(message);
|
||||
|
||||
print("Message: " + message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Return the signature as the result
|
||||
signature
|
||||
} else {
|
||||
"Failed to select keypair"
|
||||
}
|
||||
} else {
|
||||
"Failed to create keypair"
|
||||
}
|
||||
} else {
|
||||
"Failed to create key space"
|
||||
}
|
||||
"#;
|
||||
|
||||
// Send the script to the crypto module
|
||||
println!("Sending script to crypto module...");
|
||||
let target_id = "RECIPIENT_ID"; // The ID of the crypto module's Mycelium node
|
||||
node.publish("crypto.scripts", target_id, script.as_bytes().to_vec()).await?;
|
||||
|
||||
// Subscribe to receive the result
|
||||
let mut receiver = node.subscribe("crypto.results").await?;
|
||||
|
||||
// Wait for the result
|
||||
println!("Waiting for result...");
|
||||
if let Some(msg) = receiver.recv().await {
|
||||
let result = String::from_utf8_lossy(&msg.payload);
|
||||
println!("Received result: {}", result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Process the Result
|
||||
|
||||
The remote system can then process the result of the script execution:
|
||||
|
||||
```rust
|
||||
// Continue from the previous example...
|
||||
|
||||
// Parse the signature from the result
|
||||
let signature_hex = result.trim();
|
||||
|
||||
// Use the signature for further operations
|
||||
println!("Signature received: {}", signature_hex);
|
||||
|
||||
// Verify the signature locally
|
||||
let message = "Hello from remote system";
|
||||
let message_bytes = message.as_bytes();
|
||||
let signature_bytes = hex_to_bytes(signature_hex);
|
||||
|
||||
// Assuming we have the public key of the remote keypair
|
||||
let is_valid = verify_with_public_key(public_key, message_bytes, &signature_bytes);
|
||||
println!("Signature valid: {}", is_valid);
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When using Mycelium for remote script execution, consider the following security measures:
|
||||
|
||||
1. **Authentication**: Ensure that only authorized nodes can send scripts to your crypto module.
|
||||
2. **Script Validation**: Validate scripts before execution to prevent malicious code.
|
||||
3. **Resource Limits**: Set appropriate limits on script execution to prevent denial of service.
|
||||
4. **Sensitive Data**: Be careful about what data is returned in script results.
|
||||
5. **End-to-End Encryption**: Mycelium provides end-to-end encryption, but ensure your node IDs are properly secured.
|
||||
|
||||
## Benefits of Mycelium Integration
|
||||
|
||||
- **Decentralized**: No central server required, making the system more resilient.
|
||||
- **End-to-End Encrypted**: All communication is encrypted by default.
|
||||
- **NAT Traversal**: Works across different network environments without complex configuration.
|
||||
- **Rust Native**: Seamless integration with the WebAssembly Cryptography Module.
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
1. **Distributed Key Management**: Manage cryptographic keys across multiple systems.
|
||||
2. **Secure Communication**: Establish secure communication channels between systems.
|
||||
3. **Remote Signing**: Sign messages or transactions remotely without exposing private keys.
|
||||
4. **Automated Cryptographic Operations**: Schedule and execute cryptographic operations from remote systems.
|
156
scripts/examples/nats_example.md
Normal file
156
scripts/examples/nats_example.md
Normal file
@ -0,0 +1,156 @@
|
||||
# NATS Integration Example
|
||||
|
||||
This document demonstrates how to use the NATS messaging system with the WebAssembly Cryptography Module for remote script execution.
|
||||
|
||||
## Overview
|
||||
|
||||
NATS is a high-performance, cloud-native messaging system that provides a simple, secure, and scalable communication layer. When integrated with the WebAssembly Cryptography Module, it enables remote execution of Rhai scripts, allowing for distributed cryptographic operations.
|
||||
|
||||
## Example Scenario
|
||||
|
||||
In this example, we'll demonstrate how a remote system can send a Rhai script to the cryptographic module for execution, and receive the results.
|
||||
|
||||
### Step 1: Start the NATS Server
|
||||
|
||||
First, start a NATS server:
|
||||
|
||||
```bash
|
||||
# Install NATS server if not already installed
|
||||
# For example, on Ubuntu:
|
||||
# sudo apt-get install nats-server
|
||||
|
||||
# Start the NATS server
|
||||
nats-server
|
||||
```
|
||||
|
||||
### Step 2: Start the Listener
|
||||
|
||||
Next, start the cryptographic module's NATS listener:
|
||||
|
||||
```bash
|
||||
crypto-cli listen --server nats://localhost:4222 --subject crypto.scripts
|
||||
```
|
||||
|
||||
This will connect to the NATS server and listen for scripts on the "crypto.scripts" subject.
|
||||
|
||||
### Step 3: Send a Script from a Remote System
|
||||
|
||||
From another system, send a Rhai script to the listener:
|
||||
|
||||
```rust
|
||||
use async_nats::Client;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Connect to the NATS server
|
||||
let client = async_nats::connect("nats://localhost:4222").await?;
|
||||
|
||||
// Define the script to execute
|
||||
let script = r#"
|
||||
// Create a key space
|
||||
if create_key_space("remote_space") {
|
||||
print("Key space created successfully");
|
||||
|
||||
// Create a keypair
|
||||
if create_keypair("remote_keypair") {
|
||||
print("Keypair created successfully");
|
||||
|
||||
// Select the keypair
|
||||
if select_keypair("remote_keypair") {
|
||||
print("Keypair selected successfully");
|
||||
|
||||
// Sign a message
|
||||
let message = "Hello from remote system";
|
||||
let signature = sign(message);
|
||||
|
||||
print("Message: " + message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Return the signature as the result
|
||||
signature
|
||||
} else {
|
||||
"Failed to select keypair"
|
||||
}
|
||||
} else {
|
||||
"Failed to create keypair"
|
||||
}
|
||||
} else {
|
||||
"Failed to create key space"
|
||||
}
|
||||
"#;
|
||||
|
||||
// Send the script to the crypto module with a reply subject
|
||||
println!("Sending script to crypto module...");
|
||||
let reply = client.request("crypto.scripts", script.into()).await?;
|
||||
|
||||
// Process the reply
|
||||
let result = String::from_utf8_lossy(&reply.payload);
|
||||
println!("Received result: {}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Process the Result
|
||||
|
||||
The remote system can then process the result of the script execution:
|
||||
|
||||
```rust
|
||||
// Continue from the previous example...
|
||||
|
||||
// Parse the signature from the result
|
||||
let signature_hex = result.trim();
|
||||
|
||||
// Use the signature for further operations
|
||||
println!("Signature received: {}", signature_hex);
|
||||
|
||||
// Verify the signature locally
|
||||
let message = "Hello from remote system";
|
||||
let message_bytes = message.as_bytes();
|
||||
let signature_bytes = hex_to_bytes(signature_hex);
|
||||
|
||||
// Assuming we have the public key of the remote keypair
|
||||
let is_valid = verify_with_public_key(public_key, message_bytes, &signature_bytes);
|
||||
println!("Signature valid: {}", is_valid);
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When using NATS for remote script execution, consider the following security measures:
|
||||
|
||||
1. **TLS**: Configure NATS to use TLS for secure communication.
|
||||
2. **Authentication**: Set up user authentication for the NATS server.
|
||||
3. **Authorization**: Configure permissions to control which clients can publish/subscribe to which subjects.
|
||||
4. **Script Validation**: Validate scripts before execution to prevent malicious code.
|
||||
5. **Resource Limits**: Set appropriate limits on script execution to prevent denial of service.
|
||||
6. **Sensitive Data**: Be careful about what data is returned in script results.
|
||||
|
||||
## Benefits of NATS Integration
|
||||
|
||||
- **High Performance**: NATS is designed for high throughput and low latency.
|
||||
- **Scalability**: NATS can scale to handle millions of messages per second.
|
||||
- **Mature Ecosystem**: NATS has a mature ecosystem with clients for many languages.
|
||||
- **Flexible Deployment**: NATS can be deployed in various configurations, from a single server to a distributed cluster.
|
||||
- **Quality of Service**: NATS supports different quality of service levels, including at-most-once, at-least-once, and exactly-once delivery.
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
1. **Centralized Key Management**: Manage cryptographic keys from a central service.
|
||||
2. **Secure API**: Provide a secure API for cryptographic operations.
|
||||
3. **Remote Signing Service**: Offer signing as a service without exposing private keys.
|
||||
4. **Automated Cryptographic Operations**: Schedule and execute cryptographic operations from remote systems.
|
||||
|
||||
## Comparison with Mycelium
|
||||
|
||||
| Feature | NATS | Mycelium |
|
||||
|---------|------|----------|
|
||||
| Architecture | Client-server | Peer-to-peer |
|
||||
| Deployment | Requires server setup | No central server needed |
|
||||
| Security | TLS, authentication, authorization | End-to-end encryption by default |
|
||||
| Performance | Optimized for high throughput | Good for P2P scenarios |
|
||||
| Maturity | Established project | Newer project |
|
||||
| Documentation | Extensive | Limited |
|
||||
| Language Support | Multiple language clients | Rust native |
|
||||
| NAT Traversal | Requires configuration | Built-in |
|
||||
|
||||
Choose NATS if you prefer a centralized, high-performance messaging system with extensive documentation and language support. Choose Mycelium if you prefer a decentralized, peer-to-peer approach with built-in end-to-end encryption and NAT traversal.
|
57
scripts/rhai/README.md
Normal file
57
scripts/rhai/README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Rhai Scripting for WebAssembly Cryptography Module
|
||||
|
||||
This directory contains example Rhai scripts that demonstrate how to use the WebAssembly Cryptography Module's scripting capabilities.
|
||||
|
||||
## Key Space Persistence
|
||||
|
||||
|
||||
The Rhai API now supports key space persistence, allowing you to create key spaces and keypairs in one script and use them in another. This is achieved through the following functions:
|
||||
|
||||
### Key Space Management Functions
|
||||
|
||||
- `load_key_space(name, password)`: Loads a key space from disk by name and decrypts it with the provided password.
|
||||
- `create_key_space(name, password)`: Creates a new key space with the given name and automatically saves it to disk encrypted with the provided password.
|
||||
- `encrypt_key_space(password)`: Encrypts the current key space and returns the encrypted data as a string.
|
||||
- `decrypt_key_space(encrypted_data, password)`: Decrypts an encrypted key space and sets it as the current key space.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```rhai
|
||||
// Create a key space (automatically saves to disk)
|
||||
let space_name = "my_space";
|
||||
let password = "secure_password";
|
||||
|
||||
if create_key_space(space_name, password) {
|
||||
// Create keypairs (automatically saves to disk)
|
||||
create_keypair("my_keypair", password);
|
||||
}
|
||||
|
||||
// Later, in another script:
|
||||
if load_key_space(space_name, password) {
|
||||
// Use the keypair
|
||||
select_keypair("my_keypair");
|
||||
let signature = sign("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
## Example Scripts
|
||||
|
||||
1. **example.rhai**: Basic example demonstrating key management, signing, and encryption.
|
||||
2. **advanced_example.rhai**: Advanced example with error handling and more complex operations.
|
||||
3. **key_persistence_example.rhai**: Demonstrates creating and saving a key space to disk.
|
||||
4. **load_existing_space.rhai**: Shows how to load a previously created key space and use its keypairs.
|
||||
|
||||
## Key Space Storage
|
||||
|
||||
Key spaces are stored in the `~/.crypto-cli/key-spaces/` directory by default. Each key space is stored in a separate JSON file named after the key space (e.g., `my_space.json`).
|
||||
|
||||
## Security
|
||||
|
||||
Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password. The encryption ensures that the key material is secure at rest.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Strong Passwords**: Since the security of your key spaces depends on the strength of your passwords, use strong, unique passwords.
|
||||
2. **Backup Key Spaces**: Regularly backup your key spaces directory to prevent data loss.
|
||||
3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage.
|
||||
4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding.
|
233
scripts/rhai/advanced_example.rhai
Normal file
233
scripts/rhai/advanced_example.rhai
Normal file
@ -0,0 +1,233 @@
|
||||
// Advanced Rhai script example for WebAssembly Cryptography Module
|
||||
// This script demonstrates conditional logic, error handling, and more complex operations
|
||||
|
||||
// Function to create a key space with error handling
|
||||
fn setup_key_space(name, password) {
|
||||
print("Attempting: Create key space: " + name);
|
||||
let result = create_key_space(name, password);
|
||||
|
||||
if result {
|
||||
print("✅ Create key space succeeded!");
|
||||
return true;
|
||||
} else {
|
||||
print("❌ Create key space failed!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to create and select a keypair
|
||||
fn setup_keypair(name, password) {
|
||||
print("Attempting: Create keypair: " + name);
|
||||
let result = create_keypair(name, password);
|
||||
|
||||
if result {
|
||||
print("✅ Create keypair succeeded!");
|
||||
|
||||
print("Attempting: Select keypair: " + name);
|
||||
let selected = select_keypair(name);
|
||||
|
||||
if selected {
|
||||
print("✅ Select keypair succeeded!");
|
||||
return true;
|
||||
} else {
|
||||
print("❌ Select keypair failed!");
|
||||
}
|
||||
} else {
|
||||
print("❌ Create keypair failed!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to sign multiple messages
|
||||
fn sign_messages(messages) {
|
||||
let signatures = [];
|
||||
|
||||
for message in messages {
|
||||
print("Signing message: " + message);
|
||||
print("Attempting: Sign message");
|
||||
let signature = sign(message);
|
||||
|
||||
if signature != "" {
|
||||
print("✅ Sign message succeeded!");
|
||||
signatures.push(#{
|
||||
message: message,
|
||||
signature: signature
|
||||
});
|
||||
} else {
|
||||
print("❌ Sign message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
// Function to verify signatures
|
||||
fn verify_signatures(signed_messages) {
|
||||
let results = [];
|
||||
|
||||
for item in signed_messages {
|
||||
let message = item.message;
|
||||
let signature = item.signature;
|
||||
|
||||
print("Verifying signature for: " + message);
|
||||
print("Attempting: Verify signature");
|
||||
let is_valid = verify(message, signature);
|
||||
|
||||
if is_valid {
|
||||
print("✅ Verify signature succeeded!");
|
||||
} else {
|
||||
print("❌ Verify signature failed!");
|
||||
}
|
||||
|
||||
results.push(#{
|
||||
message: message,
|
||||
valid: is_valid
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Function to encrypt multiple messages
|
||||
fn encrypt_messages(messages) {
|
||||
// Generate a symmetric key
|
||||
print("Attempting: Generate symmetric key");
|
||||
let key = generate_key();
|
||||
|
||||
if key == "" {
|
||||
print("❌ Generate symmetric key failed!");
|
||||
return [];
|
||||
}
|
||||
|
||||
print("✅ Generate symmetric key succeeded!");
|
||||
print("Using key: " + key);
|
||||
let encrypted_messages = [];
|
||||
|
||||
for message in messages {
|
||||
print("Encrypting message: " + message);
|
||||
print("Attempting: Encrypt message");
|
||||
let encrypted = encrypt(key, message);
|
||||
|
||||
if encrypted != "" {
|
||||
print("✅ Encrypt message succeeded!");
|
||||
encrypted_messages.push(#{
|
||||
original: message,
|
||||
encrypted: encrypted,
|
||||
key: key
|
||||
});
|
||||
} else {
|
||||
print("❌ Encrypt message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return encrypted_messages;
|
||||
}
|
||||
|
||||
// Function to decrypt messages
|
||||
fn decrypt_messages(encrypted_messages) {
|
||||
let decrypted_messages = [];
|
||||
|
||||
for item in encrypted_messages {
|
||||
let encrypted = item.encrypted;
|
||||
let key = item.key;
|
||||
let original = item.original;
|
||||
|
||||
print("Decrypting message...");
|
||||
print("Attempting: Decrypt message");
|
||||
let decrypted = decrypt(key, encrypted);
|
||||
|
||||
if decrypted != false {
|
||||
let success = decrypted == original;
|
||||
|
||||
decrypted_messages.push(#{
|
||||
decrypted: decrypted,
|
||||
original: original,
|
||||
success: success
|
||||
});
|
||||
|
||||
if success {
|
||||
print("Decryption matched original ✅");
|
||||
} else {
|
||||
print("Decryption did not match original ❌");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted_messages;
|
||||
}
|
||||
|
||||
// Main script execution
|
||||
print("=== Advanced Cryptography Script ===");
|
||||
|
||||
// Set up key space
|
||||
let space_name = "advanced_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
if setup_key_space(space_name, password) {
|
||||
print("\n--- Key space setup complete ---\n");
|
||||
|
||||
// Set up keypair
|
||||
if setup_keypair("advanced_keypair", password) {
|
||||
print("\n--- Keypair setup complete ---\n");
|
||||
|
||||
// Define messages to sign
|
||||
let messages = [
|
||||
"This is the first message to sign",
|
||||
"Here's another message that needs signing",
|
||||
"And a third message for good measure"
|
||||
];
|
||||
|
||||
// Sign messages
|
||||
print("\n--- Signing Messages ---\n");
|
||||
let signed_messages = sign_messages(messages);
|
||||
|
||||
// Verify signatures
|
||||
print("\n--- Verifying Signatures ---\n");
|
||||
let verification_results = verify_signatures(signed_messages);
|
||||
|
||||
// Count successful verifications
|
||||
let successful_verifications = verification_results.filter(|r| r.valid).len();
|
||||
print("Successfully verified " + successful_verifications + " out of " + verification_results.len() + " signatures");
|
||||
|
||||
// Encrypt messages
|
||||
print("\n--- Encrypting Messages ---\n");
|
||||
let encrypted_messages = encrypt_messages(messages);
|
||||
|
||||
// Decrypt messages
|
||||
print("\n--- Decrypting Messages ---\n");
|
||||
let decryption_results = decrypt_messages(encrypted_messages);
|
||||
|
||||
// Count successful decryptions
|
||||
let successful_decryptions = decryption_results.filter(|r| r.success).len();
|
||||
print("Successfully decrypted " + successful_decryptions + " out of " + decryption_results.len() + " messages");
|
||||
|
||||
// Create Ethereum wallet
|
||||
print("\n--- Creating Ethereum Wallet ---\n");
|
||||
print("Attempting: Create Ethereum wallet");
|
||||
let wallet_created = create_ethereum_wallet();
|
||||
|
||||
if wallet_created {
|
||||
print("✅ Create Ethereum wallet succeeded!");
|
||||
|
||||
print("Attempting: Get Ethereum address");
|
||||
let address = get_ethereum_address();
|
||||
|
||||
if address != "" {
|
||||
print("✅ Get Ethereum address succeeded!");
|
||||
print("Ethereum wallet address: " + address);
|
||||
} else {
|
||||
print("❌ Get Ethereum address failed!");
|
||||
}
|
||||
} else {
|
||||
print("❌ Create Ethereum wallet failed!");
|
||||
}
|
||||
|
||||
print("\n=== Script execution completed successfully! ===");
|
||||
} else {
|
||||
print("Failed to set up keypair. Aborting script.");
|
||||
}
|
||||
} else {
|
||||
print("Failed to set up key space. Aborting script.");
|
||||
}
|
85
scripts/rhai/example.rhai
Normal file
85
scripts/rhai/example.rhai
Normal file
@ -0,0 +1,85 @@
|
||||
// Example Rhai script for WebAssembly Cryptography Module
|
||||
// This script demonstrates key management, signing, and encryption
|
||||
|
||||
// Step 1: Create and manage a key space
|
||||
let space_name = "demo_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Step 2: Create and use keypairs
|
||||
print("\nCreating keypairs...");
|
||||
if create_keypair("signing_key", password) {
|
||||
print("✓ Created signing keypair");
|
||||
}
|
||||
|
||||
if create_keypair("encryption_key", password) {
|
||||
print("✓ Created encryption keypair");
|
||||
}
|
||||
|
||||
// List all keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Step 3: Sign a message
|
||||
print("\nPerforming signing operations...");
|
||||
if select_keypair("signing_key") {
|
||||
print("✓ Selected signing keypair");
|
||||
|
||||
let message = "This is a secure message that needs to be signed";
|
||||
print("Message: " + message);
|
||||
|
||||
let signature = sign(message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
if is_valid {
|
||||
print("Signature verification: ✓ Valid");
|
||||
} else {
|
||||
print("Signature verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Encrypt and decrypt data
|
||||
print("\nPerforming encryption operations...");
|
||||
|
||||
// Generate a symmetric key
|
||||
let sym_key = generate_key();
|
||||
print("Generated symmetric key: " + sym_key);
|
||||
|
||||
// Encrypt a message
|
||||
let secret = "This is a top secret message that must be encrypted";
|
||||
print("Original message: " + secret);
|
||||
|
||||
let encrypted_data = encrypt(sym_key, secret);
|
||||
print("Encrypted data: " + encrypted_data);
|
||||
|
||||
// Decrypt the message
|
||||
let decrypted_data = decrypt(sym_key, encrypted_data);
|
||||
print("Decrypted message: " + decrypted_data);
|
||||
|
||||
// Verify decryption was successful
|
||||
if decrypted_data == secret {
|
||||
print("✓ Encryption/decryption successful");
|
||||
} else {
|
||||
print("✗ Encryption/decryption failed");
|
||||
}
|
||||
|
||||
// Step 5: Create an Ethereum wallet
|
||||
print("\nCreating Ethereum wallet...");
|
||||
if select_keypair("encryption_key") {
|
||||
print("✓ Selected keypair for Ethereum wallet");
|
||||
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
}
|
||||
}
|
||||
|
||||
print("\nScript execution completed successfully!");
|
||||
}
|
65
scripts/rhai/key_persistence_example.rhai
Normal file
65
scripts/rhai/key_persistence_example.rhai
Normal file
@ -0,0 +1,65 @@
|
||||
// Example Rhai script demonstrating key space persistence
|
||||
// This script shows how to create, save, and load key spaces
|
||||
|
||||
// Step 1: Create a key space
|
||||
let space_name = "persistent_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Creating key space: " + space_name);
|
||||
if create_key_space(space_name, password) {
|
||||
print("✓ Key space created successfully");
|
||||
|
||||
// Step 2: Create keypairs in this space
|
||||
print("\nCreating keypairs...");
|
||||
if create_keypair("persistent_key1", password) {
|
||||
print("✓ Created first keypair");
|
||||
}
|
||||
|
||||
if create_keypair("persistent_key2", password) {
|
||||
print("✓ Created second keypair");
|
||||
}
|
||||
|
||||
// List all keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Step 3: Clear the session (simulate closing and reopening the CLI)
|
||||
print("\nClearing session (simulating restart)...");
|
||||
// Note: In a real script, you would exit here and run a new script
|
||||
// For demonstration purposes, we'll continue in the same script
|
||||
|
||||
// Step 4: Load the key space from disk
|
||||
print("\nLoading key space from disk...");
|
||||
if load_key_space(space_name, password) {
|
||||
print("✓ Key space loaded successfully");
|
||||
|
||||
// Verify the keypairs are still available
|
||||
let loaded_keypairs = list_keypairs();
|
||||
print("Keypairs after loading: " + loaded_keypairs);
|
||||
|
||||
// Step 5: Use a keypair from the loaded space
|
||||
print("\nSelecting and using a keypair...");
|
||||
if select_keypair("persistent_key1") {
|
||||
print("✓ Selected keypair");
|
||||
|
||||
let message = "This message was signed using a keypair from a loaded key space";
|
||||
let signature = sign(message);
|
||||
print("Message: " + message);
|
||||
print("Signature: " + signature);
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = verify(message, signature);
|
||||
if is_valid {
|
||||
print("Signature verification: ✓ Valid");
|
||||
} else {
|
||||
print("Signature verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load key space");
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to create key space");
|
||||
}
|
||||
|
||||
print("\nScript execution completed!");
|
65
scripts/rhai/load_existing_space.rhai
Normal file
65
scripts/rhai/load_existing_space.rhai
Normal file
@ -0,0 +1,65 @@
|
||||
// Example Rhai script demonstrating loading an existing key space
|
||||
// This script shows how to load a previously created key space and use its keypairs
|
||||
|
||||
// Define the key space name and password
|
||||
let space_name = "persistent_space";
|
||||
let password = "secure_password123";
|
||||
|
||||
print("Loading existing key space: " + space_name);
|
||||
|
||||
// Load the key space from disk
|
||||
if load_key_space(space_name, password) {
|
||||
print("✓ Key space loaded successfully");
|
||||
|
||||
// List available keypairs
|
||||
let keypairs = list_keypairs();
|
||||
print("Available keypairs: " + keypairs);
|
||||
|
||||
// Use both keypairs to sign different messages
|
||||
if select_keypair("persistent_key1") {
|
||||
print("\nUsing persistent_key1:");
|
||||
let message1 = "Message signed with the first keypair";
|
||||
let signature1 = sign(message1);
|
||||
print("Message: " + message1);
|
||||
print("Signature: " + signature1);
|
||||
|
||||
let is_valid1 = verify(message1, signature1);
|
||||
if is_valid1 {
|
||||
print("Verification: ✓ Valid");
|
||||
} else {
|
||||
print("Verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
if select_keypair("persistent_key2") {
|
||||
print("\nUsing persistent_key2:");
|
||||
let message2 = "Message signed with the second keypair";
|
||||
let signature2 = sign(message2);
|
||||
print("Message: " + message2);
|
||||
print("Signature: " + signature2);
|
||||
|
||||
let is_valid2 = verify(message2, signature2);
|
||||
if is_valid2 {
|
||||
print("Verification: ✓ Valid");
|
||||
} else {
|
||||
print("Verification: ✗ Invalid");
|
||||
}
|
||||
}
|
||||
|
||||
// Create an Ethereum wallet using one of the keypairs
|
||||
print("\nCreating Ethereum wallet from persistent keypair:");
|
||||
if select_keypair("persistent_key1") {
|
||||
if create_ethereum_wallet() {
|
||||
print("✓ Ethereum wallet created");
|
||||
|
||||
let address = get_ethereum_address();
|
||||
print("Ethereum address: " + address);
|
||||
} else {
|
||||
print("✗ Failed to create Ethereum wallet");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print("✗ Failed to load key space. Make sure you've run key_persistence_example.rhai first.");
|
||||
}
|
||||
|
||||
print("\nScript execution completed!");
|
103
scripts/run_examples.sh
Executable file
103
scripts/run_examples.sh
Executable file
@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Script to run the example Rhai scripts and demonstrate the WebAssembly Cryptography Module
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print section headers
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}======================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}======================================${NC}\n"
|
||||
}
|
||||
|
||||
# Function to run a Rhai script
|
||||
run_script() {
|
||||
echo -e "${YELLOW}Running script: $1${NC}"
|
||||
echo -e "${YELLOW}------------------------${NC}"
|
||||
|
||||
if [ -f "$1" ]; then
|
||||
echo -e "${GREEN}Script output:${NC}"
|
||||
crypto-cli script "$1"
|
||||
echo -e "\n${GREEN}Script execution completed.${NC}"
|
||||
else
|
||||
echo -e "${RED}Error: Script file not found: $1${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if crypto-cli is installed
|
||||
if ! command -v crypto-cli &> /dev/null; then
|
||||
echo -e "${RED}Error: crypto-cli is not installed or not in PATH.${NC}"
|
||||
echo -e "${YELLOW}Please build and install the CLI first:${NC}"
|
||||
echo -e " cargo build --bin crypto-cli"
|
||||
echo -e " cargo install --path ."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Print welcome message
|
||||
print_header "WebAssembly Cryptography Module Examples"
|
||||
echo -e "This script will run the example Rhai scripts to demonstrate the functionality of the WebAssembly Cryptography Module."
|
||||
echo -e "Make sure you have built and installed the CLI before running this script.\n"
|
||||
|
||||
# Ask user which example to run
|
||||
echo -e "${YELLOW}Which example would you like to run?${NC}"
|
||||
echo -e "1. Basic example (key management, signing, encryption)"
|
||||
echo -e "2. Advanced example (error handling, multiple operations)"
|
||||
echo -e "3. Multi-script workflows (chaining scripts)"
|
||||
echo -e "4. Run all examples"
|
||||
echo -e "5. Exit"
|
||||
|
||||
read -p "Enter your choice (1-4): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
print_header "Running Basic Example"
|
||||
run_script "scripts/rhai/example.rhai"
|
||||
;;
|
||||
2)
|
||||
print_header "Running Advanced Example"
|
||||
run_script "scripts/rhai/advanced_example.rhai"
|
||||
;;
|
||||
3)
|
||||
print_header "Running Multi-Script Workflows"
|
||||
run_script "scripts/rhai/key_persistence_example.rhai"
|
||||
echo -e "\n"
|
||||
run_script "scripts/rhai/load_existing_space.rhai"
|
||||
;;
|
||||
4)
|
||||
print_header "Running All Examples"
|
||||
run_script "scripts/rhai/example.rhai"
|
||||
echo -e "\n"
|
||||
run_script "scripts/rhai/advanced_example.rhai"
|
||||
echo -e "\n"
|
||||
run_script "scripts/rhai/key_persistence_example.rhai"
|
||||
echo -e "\n"
|
||||
run_script "scripts/rhai/load_existing_space.rhai"
|
||||
;;
|
||||
5)
|
||||
echo -e "${YELLOW}Exiting...${NC}"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Invalid choice. Exiting...${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Print information about messaging examples
|
||||
print_header "Messaging System Examples"
|
||||
echo -e "To try the messaging system examples, you can:"
|
||||
echo -e "1. Start a listener for remote script execution:"
|
||||
echo -e " ${YELLOW}crypto-cli listen${NC}"
|
||||
echo -e ""
|
||||
echo -e "2. For Mycelium integration, see:"
|
||||
echo -e " ${YELLOW}scripts/examples/mycelium_example.md${NC}"
|
||||
echo -e ""
|
||||
echo -e "3. For NATS integration, see:"
|
||||
echo -e " ${YELLOW}scripts/examples/nats_example.md${NC}"
|
||||
|
||||
echo -e "\n${GREEN}Thank you for trying the WebAssembly Cryptography Module examples!${NC}"
|
66
src/bin/test_serialization.rs
Normal file
66
src/bin/test_serialization.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use webassembly::core::keypair::{KeySpace, KeyPair};
|
||||
use webassembly::core::symmetric;
|
||||
use std::fs;
|
||||
use serde_json;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a key space
|
||||
let mut space = KeySpace::new("test-space");
|
||||
|
||||
// Add a keypair
|
||||
let keypair = KeyPair::new("test-keypair");
|
||||
space.keypairs.insert("test-keypair".to_string(), keypair);
|
||||
|
||||
// Print the key space
|
||||
println!("Key space: {:?}", space);
|
||||
|
||||
// Serialize the key space directly to see what it looks like
|
||||
let direct_serialized = serde_json::to_string_pretty(&space)?;
|
||||
println!("Direct serialized key space:\n{}", direct_serialized);
|
||||
|
||||
// Encrypt the key space
|
||||
let password = "test123";
|
||||
let encrypted_space = symmetric::encrypt_key_space(&space, password)?;
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = symmetric::serialize_encrypted_space(&encrypted_space)?;
|
||||
|
||||
// Write to file
|
||||
fs::write("test_keyspace.json", &serialized)?;
|
||||
println!("Wrote encrypted key space to test_keyspace.json");
|
||||
|
||||
// Read from file
|
||||
let serialized = fs::read_to_string("test_keyspace.json")?;
|
||||
|
||||
// Deserialize the encrypted space
|
||||
let encrypted_space = symmetric::deserialize_encrypted_space(&serialized)?;
|
||||
println!("Deserialized encrypted space: {:?}", encrypted_space.metadata);
|
||||
|
||||
// Decrypt the key space
|
||||
let decrypted_data = symmetric::decrypt_symmetric(
|
||||
&symmetric::derive_key_from_password(password),
|
||||
&encrypted_space.encrypted_data
|
||||
)?;
|
||||
|
||||
println!("Decrypted data length: {}", decrypted_data.len());
|
||||
println!("Decrypted data preview: {:?}", &decrypted_data[..20]);
|
||||
|
||||
// Try to deserialize manually
|
||||
match serde_json::from_slice::<KeySpace>(&decrypted_data) {
|
||||
Ok(space) => {
|
||||
println!("Manual deserialization successful!");
|
||||
println!("Decrypted key space: {:?}", space);
|
||||
println!("Keypairs: {:?}", space.list_keypairs());
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Manual deserialization error: {}", e);
|
||||
// Try to print the decrypted data as a string to see what's wrong
|
||||
match std::str::from_utf8(&decrypted_data) {
|
||||
Ok(s) => println!("Decrypted data as string: {}", s),
|
||||
Err(_) => println!("Decrypted data is not valid UTF-8"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
772
src/cli/commands.rs
Normal file
772
src/cli/commands.rs
Normal file
@ -0,0 +1,772 @@
|
||||
use colored::Colorize;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::cli::error::{CliError, Result};
|
||||
use crate::cli::{CryptoCommands, EthereumCommands, KeyCommands};
|
||||
use webassembly::core::keypair;
|
||||
use webassembly::core::symmetric;
|
||||
use webassembly::core::ethereum;
|
||||
use webassembly::core::error::CryptoError;
|
||||
|
||||
// Load a key space from disk
|
||||
fn load_key_space(name: &str, password: &str) -> Result<()> {
|
||||
// Load config to get key spaces directory
|
||||
let config = crate::cli::config::Config::default();
|
||||
|
||||
// Check if directory exists
|
||||
if !config.key_spaces_dir.exists() {
|
||||
return Err(CliError::ConfigError("Key spaces directory does not exist".to_string()));
|
||||
}
|
||||
|
||||
// Get the key space file path
|
||||
let space_path = config.key_spaces_dir.join(format!("{}.json", name));
|
||||
|
||||
// Check if file exists
|
||||
if !space_path.exists() {
|
||||
return Err(CliError::ConfigError(format!("Key space file not found: {}", space_path.display())));
|
||||
}
|
||||
|
||||
// Read the file
|
||||
let serialized = fs::read_to_string(&space_path)?;
|
||||
|
||||
// Deserialize the encrypted space
|
||||
let encrypted_space = match symmetric::deserialize_encrypted_space(&serialized) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
println!("Error deserializing key space: {}", e);
|
||||
return Err(CliError::CryptoError(format!("Failed to deserialize key space: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Decrypt the space
|
||||
let space = match symmetric::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
println!("Error decrypting key space: {}", e);
|
||||
return Err(CliError::CryptoError(format!("Failed to decrypt key space: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Set as current space
|
||||
keypair::set_current_space(space)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Execute key management commands
|
||||
pub fn execute_key_command(command: &KeyCommands) -> Result<()> {
|
||||
match command {
|
||||
KeyCommands::Load { name, password } => {
|
||||
println!("Loading key space: {}", name);
|
||||
|
||||
// Get password
|
||||
let pwd = match password {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
println!("Enter password for key space {}:", name);
|
||||
rpassword::read_password()?
|
||||
}
|
||||
};
|
||||
|
||||
// Load the key space
|
||||
load_key_space(name, &pwd)?;
|
||||
println!("{}", "Key space loaded successfully".green());
|
||||
Ok(())
|
||||
},
|
||||
KeyCommands::CreateSpace { name, password } => {
|
||||
println!("Creating key space: {}", name);
|
||||
|
||||
// Create the key space
|
||||
keypair::create_space(name)?;
|
||||
|
||||
// Get the current space
|
||||
let space = keypair::get_current_space()?;
|
||||
|
||||
// Encrypt the key space with the provided password
|
||||
let encrypted_space = symmetric::encrypt_key_space(&space, &password)?;
|
||||
|
||||
// Load config to get key spaces directory
|
||||
let config = crate::cli::config::Config::default();
|
||||
config.ensure_key_spaces_dir()?;
|
||||
|
||||
// Store the encrypted space to disk
|
||||
let space_path = config.key_spaces_dir.join(format!("{}.json", name));
|
||||
let serialized = symmetric::serialize_encrypted_space(&encrypted_space)?;
|
||||
fs::write(&space_path, serialized)?;
|
||||
|
||||
println!("Key space encrypted with password and saved to {}", space_path.display());
|
||||
println!("{}", "Key space created successfully".green());
|
||||
Ok(())
|
||||
},
|
||||
KeyCommands::ListSpaces => {
|
||||
println!("Listing key spaces:");
|
||||
|
||||
// Load config to get key spaces directory
|
||||
let config = crate::cli::config::Config::default();
|
||||
|
||||
// Check if directory exists
|
||||
if !config.key_spaces_dir.exists() {
|
||||
println!("No key spaces found (directory does not exist)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// List all space files
|
||||
let mut spaces_found = false;
|
||||
for entry in fs::read_dir(&config.key_spaces_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "json") {
|
||||
if let Some(name) = path.file_stem() {
|
||||
println!("- {}", name.to_string_lossy());
|
||||
spaces_found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !spaces_found {
|
||||
println!("No key spaces found in {}", config.key_spaces_dir.display());
|
||||
}
|
||||
|
||||
// Show the current space if it exists
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
println!("\nCurrent active space: {}", space.name.green());
|
||||
},
|
||||
Err(_) => {
|
||||
println!("\nNo active key space");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
KeyCommands::CreateKeypair { name, space, password } => {
|
||||
println!("Creating keypair: {}", name);
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
// If space is provided, try to load it
|
||||
if let Some(space_name) = space {
|
||||
// Get password
|
||||
let pwd = match password {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
rpassword::read_password()?
|
||||
}
|
||||
};
|
||||
|
||||
// Load the key space
|
||||
load_key_space(&space_name, &pwd)?;
|
||||
println!("Loaded key space: {}", space_name);
|
||||
} else {
|
||||
// Try to load the default space
|
||||
println!("No active key space. Enter the name of the key space to use:");
|
||||
let mut space_name = String::new();
|
||||
io::stdin().read_line(&mut space_name)?;
|
||||
space_name = space_name.trim().to_string();
|
||||
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
let pwd = rpassword::read_password()?;
|
||||
|
||||
// Load the key space
|
||||
load_key_space(&space_name, &pwd)?;
|
||||
println!("Loaded key space: {}", space_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the keypair in the current space
|
||||
keypair::create_keypair(name)?;
|
||||
|
||||
// Save the key space to disk
|
||||
let space = keypair::get_current_space()?;
|
||||
|
||||
// Get the space name
|
||||
let space_name = space.name.clone();
|
||||
|
||||
// Get password
|
||||
let pwd = match password {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
rpassword::read_password()?
|
||||
}
|
||||
};
|
||||
|
||||
// Encrypt the key space
|
||||
let encrypted_space = symmetric::encrypt_key_space(&space, &pwd)?;
|
||||
|
||||
// Load config to get key spaces directory
|
||||
let config = crate::cli::config::Config::default();
|
||||
config.ensure_key_spaces_dir()?;
|
||||
|
||||
// Store the encrypted space to disk
|
||||
let space_path = config.key_spaces_dir.join(format!("{}.json", space_name));
|
||||
let serialized = symmetric::serialize_encrypted_space(&encrypted_space)?;
|
||||
fs::write(&space_path, serialized)?;
|
||||
|
||||
println!("Key space saved to {}", space_path.display());
|
||||
|
||||
println!("{}", "Keypair created successfully".green());
|
||||
Ok(())
|
||||
},
|
||||
KeyCommands::ListKeypairs { space, password } => {
|
||||
println!("Listing keypairs:");
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
// If space is provided, try to load it
|
||||
if let Some(space_name) = space {
|
||||
// Get password
|
||||
let pwd = match password {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
rpassword::read_password()?
|
||||
}
|
||||
};
|
||||
|
||||
// Load the key space
|
||||
match load_key_space(&space_name, &pwd) {
|
||||
Ok(_) => println!("Loaded key space: {}", space_name),
|
||||
Err(e) => {
|
||||
println!("Error loading key space: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to load the default space
|
||||
println!("No active key space. Enter the name of the key space to use:");
|
||||
let mut space_name = String::new();
|
||||
io::stdin().read_line(&mut space_name)?;
|
||||
space_name = space_name.trim().to_string();
|
||||
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
let pwd = rpassword::read_password()?;
|
||||
|
||||
// Load the key space
|
||||
match load_key_space(&space_name, &pwd) {
|
||||
Ok(_) => println!("Loaded key space: {}", space_name),
|
||||
Err(e) => {
|
||||
println!("Error loading key space: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current space
|
||||
let space = match keypair::get_current_space() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("Error getting current key space: {}", e);
|
||||
return Err(CliError::CryptoError(format!("Failed to get current key space: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// List keypairs directly from the space
|
||||
let keypairs = space.list_keypairs();
|
||||
if keypairs.is_empty() {
|
||||
println!("No keypairs found");
|
||||
} else {
|
||||
for (i, name) in keypairs.iter().enumerate() {
|
||||
println!("{}. {}", i + 1, name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
KeyCommands::Export { name, output, space, password } => {
|
||||
println!("Exporting keypair: {}", name);
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
keypair::select_keypair(name)?;
|
||||
|
||||
// Get the keypair
|
||||
let kp = keypair::get_selected_keypair()?;
|
||||
|
||||
// Serialize the keypair
|
||||
let serialized = serde_json::to_string_pretty(&kp)
|
||||
.map_err(|e| CliError::CryptoError(format!("Failed to serialize keypair: {}", e)))?;
|
||||
|
||||
// Output the serialized keypair
|
||||
match output {
|
||||
Some(path) => {
|
||||
fs::write(path, &serialized)?;
|
||||
println!("{}", format!("Keypair exported to {}", path).green());
|
||||
},
|
||||
None => {
|
||||
println!("{}", serialized);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
KeyCommands::Import { name, input, space: space_name, password } => {
|
||||
println!("Importing keypair: {}", name);
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
// If space is provided, try to load it
|
||||
if let Some(space_name) = space_name {
|
||||
// Get password
|
||||
let pwd = match password {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
rpassword::read_password()?
|
||||
}
|
||||
};
|
||||
|
||||
// Load the key space
|
||||
load_key_space(&space_name, &pwd)?;
|
||||
println!("Loaded key space: {}", space_name);
|
||||
} else {
|
||||
// Try to load the default space
|
||||
println!("No active key space. Enter the name of the key space to use:");
|
||||
let mut space_name = String::new();
|
||||
io::stdin().read_line(&mut space_name)?;
|
||||
space_name = space_name.trim().to_string();
|
||||
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
let pwd = rpassword::read_password()?;
|
||||
|
||||
// Load the key space
|
||||
load_key_space(&space_name, &pwd)?;
|
||||
println!("Loaded key space: {}", space_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Get input data
|
||||
let import_data = match input {
|
||||
Some(path) => fs::read_to_string(path)?,
|
||||
None => {
|
||||
println!("Enter keypair data (end with Ctrl+D on Unix or Ctrl+Z on Windows):");
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
// Deserialize the keypair
|
||||
let kp: keypair::KeyPair = serde_json::from_str(&import_data)
|
||||
.map_err(|e| CliError::CryptoError(format!("Failed to deserialize keypair: {}", e)))?;
|
||||
|
||||
// Get the current space
|
||||
let mut space = keypair::get_current_space()?;
|
||||
|
||||
// Add the keypair to the space
|
||||
space.keypairs.insert(name.to_string(), kp);
|
||||
|
||||
// Update the space
|
||||
keypair::set_current_space(space)?;
|
||||
|
||||
// Auto-save the key space
|
||||
let space = keypair::get_current_space()?;
|
||||
let space_name = space.name.clone();
|
||||
|
||||
// Get password
|
||||
let pwd = match password {
|
||||
Some(p) => p.clone(),
|
||||
None => {
|
||||
println!("Enter password for key space {}:", space_name);
|
||||
rpassword::read_password()?
|
||||
}
|
||||
};
|
||||
|
||||
// Encrypt the key space
|
||||
let encrypted_space = symmetric::encrypt_key_space(&space, &pwd)?;
|
||||
|
||||
// Load config to get key spaces directory
|
||||
let config = crate::cli::config::Config::default();
|
||||
config.ensure_key_spaces_dir()?;
|
||||
|
||||
// Store the encrypted space to disk
|
||||
let space_path = config.key_spaces_dir.join(format!("{}.json", space_name));
|
||||
let serialized = symmetric::serialize_encrypted_space(&encrypted_space)?;
|
||||
fs::write(&space_path, serialized)?;
|
||||
|
||||
println!("Key space saved to {}", space_path.display());
|
||||
println!("{}", "Keypair imported successfully".green());
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute cryptographic operation commands
|
||||
pub fn execute_crypto_command(command: &CryptoCommands) -> Result<()> {
|
||||
match command {
|
||||
CryptoCommands::Sign { message, input, keypair, output, space, password } => {
|
||||
println!("Signing with keypair: {}", keypair);
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
keypair::select_keypair(keypair)?;
|
||||
|
||||
// Get message to sign
|
||||
let msg = match (message, input) {
|
||||
(Some(m), _) => m.clone(),
|
||||
(_, Some(path)) => fs::read_to_string(path)?,
|
||||
_ => {
|
||||
println!("Enter message to sign (end with Ctrl+D on Unix or Ctrl+Z on Windows):");
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
// Sign the message
|
||||
let signature_bytes = keypair::keypair_sign(msg.as_bytes())?;
|
||||
|
||||
// Encode the signature as base64
|
||||
let signature = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature_bytes);
|
||||
|
||||
// Output signature
|
||||
match output {
|
||||
Some(path) => {
|
||||
fs::write(path, &signature)?;
|
||||
println!("{}", format!("Signature written to {}", path).green());
|
||||
},
|
||||
None => {
|
||||
println!("Signature: {}", signature);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
CryptoCommands::Verify { message, input, signature, keypair, pubkey, space, password } => {
|
||||
println!("Verifying signature");
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space and a keypair is specified
|
||||
if let Some(kp) = keypair {
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// Get message to verify
|
||||
let msg = match (message, input) {
|
||||
(Some(m), _) => m.clone(),
|
||||
(_, Some(path)) => fs::read_to_string(path)?,
|
||||
_ => {
|
||||
println!("Enter message to verify (end with Ctrl+D on Unix or Ctrl+Z on Windows):");
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
// Decode the signature from base64
|
||||
let signature_bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, signature) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return Err(CliError::CryptoError(format!("Invalid signature format: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Verify the signature
|
||||
let is_valid = if let Some(kp) = keypair {
|
||||
// Select the keypair and verify
|
||||
keypair::select_keypair(kp)?;
|
||||
keypair::keypair_verify(msg.as_bytes(), &signature_bytes)?
|
||||
} else if let Some(pk) = pubkey {
|
||||
// Decode the public key from base64
|
||||
let pubkey_bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, pk) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return Err(CliError::CryptoError(format!("Invalid public key format: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Verify with the public key
|
||||
keypair::verify_with_public_key(&pubkey_bytes, msg.as_bytes(), &signature_bytes)?
|
||||
} else {
|
||||
// Use the currently selected keypair
|
||||
keypair::keypair_verify(msg.as_bytes(), &signature_bytes)?
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
println!("{}", "Signature is valid".green());
|
||||
} else {
|
||||
println!("{}", "Signature is invalid".red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
CryptoCommands::Encrypt { data, input, recipient, output, space, password } => {
|
||||
println!("Encrypting for recipient: {}", recipient);
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
// Get data to encrypt
|
||||
let plaintext = match (data, input) {
|
||||
(Some(d), _) => d.clone(),
|
||||
(_, Some(path)) => fs::read_to_string(path)?,
|
||||
_ => {
|
||||
println!("Enter data to encrypt (end with Ctrl+D on Unix or Ctrl+Z on Windows):");
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
// Get the recipient's public key
|
||||
// For now, we'll assume the recipient is a keypair name
|
||||
keypair::select_keypair(&recipient)?;
|
||||
let recipient_pubkey = keypair::keypair_pub_key()?;
|
||||
|
||||
// Encrypt the data
|
||||
let ciphertext_bytes = keypair::encrypt_asymmetric(&recipient_pubkey, plaintext.as_bytes())?;
|
||||
|
||||
// Encode the ciphertext as base64
|
||||
let ciphertext = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &ciphertext_bytes);
|
||||
|
||||
// Output ciphertext
|
||||
match output {
|
||||
Some(path) => {
|
||||
fs::write(path, &ciphertext)?;
|
||||
println!("{}", format!("Encrypted data written to {}", path).green());
|
||||
},
|
||||
None => {
|
||||
println!("Encrypted data: {}", ciphertext);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
CryptoCommands::Decrypt { data, input, keypair, output, space, password } => {
|
||||
println!("Decrypting with keypair: {}", keypair);
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
keypair::select_keypair(keypair)?;
|
||||
|
||||
// Get data to decrypt
|
||||
let ciphertext = match (data, input) {
|
||||
(Some(d), _) => d.clone(),
|
||||
(_, Some(path)) => fs::read_to_string(path)?,
|
||||
_ => {
|
||||
println!("Enter data to decrypt (end with Ctrl+D on Unix or Ctrl+Z on Windows):");
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
// Decode the ciphertext from base64
|
||||
let ciphertext_bytes = match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &ciphertext) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
return Err(CliError::CryptoError(format!("Invalid ciphertext format: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Decrypt the data
|
||||
let plaintext_bytes = keypair::decrypt_asymmetric(&ciphertext_bytes)?;
|
||||
|
||||
// Convert the plaintext to a string
|
||||
let plaintext = match String::from_utf8(plaintext_bytes) {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
return Err(CliError::CryptoError(format!("Invalid UTF-8 in decrypted data: {}", e)));
|
||||
}
|
||||
};
|
||||
|
||||
// Output plaintext
|
||||
match output {
|
||||
Some(path) => {
|
||||
fs::write(path, &plaintext)?;
|
||||
println!("{}", format!("Decrypted data written to {}", path).green());
|
||||
},
|
||||
None => {
|
||||
println!("Decrypted data: {}", plaintext);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute Ethereum wallet commands
|
||||
pub fn execute_ethereum_command(command: &EthereumCommands) -> Result<()> {
|
||||
match command {
|
||||
EthereumCommands::Create { keypair, space, password } => {
|
||||
println!("Creating Ethereum wallet from keypair: {}", keypair);
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
keypair::select_keypair(keypair)?;
|
||||
|
||||
// Create the Ethereum wallet
|
||||
let wallet = ethereum::create_ethereum_wallet()?;
|
||||
|
||||
println!("{}", "Ethereum wallet created successfully".green());
|
||||
println!("Address: {}", wallet.address_string());
|
||||
Ok(())
|
||||
},
|
||||
EthereumCommands::Address { keypair, space, password } => {
|
||||
println!("Getting Ethereum address for keypair: {}", keypair);
|
||||
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
// Select the keypair
|
||||
keypair::select_keypair(keypair)?;
|
||||
|
||||
// Get the Ethereum address
|
||||
match ethereum::get_current_ethereum_wallet() {
|
||||
Ok(wallet) => {
|
||||
println!("Ethereum address: {}", wallet.address_string());
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
// If no wallet exists, create one
|
||||
if let CryptoError::NoKeypairSelected = e {
|
||||
println!("No Ethereum wallet found for this keypair. Creating one...");
|
||||
let wallet = ethereum::create_ethereum_wallet()?;
|
||||
println!("Ethereum address: {}", wallet.address_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
EthereumCommands::Balance { address, network, space, password } => {
|
||||
// If space and password are provided, load the key space
|
||||
if let (Some(s), Some(p)) = (space, password) {
|
||||
load_key_space(s, p)?;
|
||||
println!("Loaded key space: {}", s);
|
||||
}
|
||||
|
||||
// Check if we have an active space
|
||||
if let Err(_) = keypair::get_current_space() {
|
||||
return Err(CliError::CryptoError("No active key space. Please use 'key load' command first or provide --space and --password".to_string()));
|
||||
}
|
||||
|
||||
let addr = match address {
|
||||
Some(a) => a.clone(),
|
||||
None => {
|
||||
// Try to get the address from the current wallet
|
||||
match ethereum::get_current_ethereum_wallet() {
|
||||
Ok(wallet) => wallet.address_string(),
|
||||
Err(_) => {
|
||||
println!("Enter Ethereum address:");
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
buffer.trim().to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
println!("Getting balance for address {} on network {}", addr, network);
|
||||
|
||||
// Create provider based on network
|
||||
let provider = match network.to_lowercase().as_str() {
|
||||
"gnosis" => ethereum::create_gnosis_provider()?,
|
||||
_ => {
|
||||
return Err(CliError::CryptoError(format!("Unsupported network: {}", network)));
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the address
|
||||
let eth_address = addr.parse::<ethers::types::Address>()
|
||||
.map_err(|_| CliError::CryptoError("Invalid Ethereum address".to_string()))?;
|
||||
|
||||
// Get the balance
|
||||
let rt = tokio::runtime::Runtime::new()
|
||||
.map_err(|e| CliError::IoError(format!("Failed to create runtime: {}", e)))?;
|
||||
|
||||
let balance = rt.block_on(ethereum::get_balance(&provider, eth_address))?;
|
||||
|
||||
// Format the balance
|
||||
let formatted_balance = ethereum::format_eth_balance(balance);
|
||||
|
||||
println!("Balance: {}", formatted_balance);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to read file contents
|
||||
fn read_file<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
// Helper function to write file contents
|
||||
fn write_file<P: AsRef<Path>>(path: P, data: &[u8]) -> io::Result<()> {
|
||||
let mut file = fs::File::create(path)?;
|
||||
file.write_all(data)?;
|
||||
Ok(())
|
||||
}
|
81
src/cli/config.rs
Normal file
81
src/cli/config.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::cli::error::{CliError, Result};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub default_key_space: Option<String>,
|
||||
pub default_keypair: Option<String>,
|
||||
pub key_spaces_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".crypto-cli").join("key-spaces");
|
||||
|
||||
Config {
|
||||
default_key_space: None,
|
||||
default_keypair: None,
|
||||
key_spaces_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load<P: AsRef<Path>>(path: Option<P>) -> Result<Self> {
|
||||
let config_path = match path {
|
||||
Some(p) => PathBuf::from(p.as_ref()),
|
||||
None => {
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
home_dir.join(".crypto-cli").join("config.json")
|
||||
}
|
||||
};
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(Config::default());
|
||||
}
|
||||
|
||||
let config_str = fs::read_to_string(&config_path)
|
||||
.map_err(|e| CliError::ConfigError(format!("Failed to read config file: {}", e)))?;
|
||||
|
||||
serde_json::from_str(&config_str)
|
||||
.map_err(|e| CliError::ConfigError(format!("Failed to parse config file: {}", e)))
|
||||
}
|
||||
|
||||
pub fn save<P: AsRef<Path>>(&self, path: Option<P>) -> Result<()> {
|
||||
let config_path = match path {
|
||||
Some(p) => PathBuf::from(p.as_ref()),
|
||||
None => {
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let config_dir = home_dir.join(".crypto-cli");
|
||||
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)
|
||||
.map_err(|e| CliError::ConfigError(format!("Failed to create config directory: {}", e)))?;
|
||||
}
|
||||
|
||||
config_dir.join("config.json")
|
||||
}
|
||||
};
|
||||
|
||||
let config_str = serde_json::to_string_pretty(self)
|
||||
.map_err(|e| CliError::ConfigError(format!("Failed to serialize config: {}", e)))?;
|
||||
|
||||
fs::write(&config_path, config_str)
|
||||
.map_err(|e| CliError::ConfigError(format!("Failed to write config file: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_key_spaces_dir(&self) -> Result<()> {
|
||||
if !self.key_spaces_dir.exists() {
|
||||
fs::create_dir_all(&self.key_spaces_dir)
|
||||
.map_err(|e| CliError::ConfigError(format!("Failed to create key spaces directory: {}", e)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
47
src/cli/error.rs
Normal file
47
src/cli/error.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use webassembly::core::error::CryptoError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CliError {
|
||||
IoError(String),
|
||||
CryptoError(String),
|
||||
ScriptError(String),
|
||||
ConfigError(String),
|
||||
NotImplemented,
|
||||
}
|
||||
|
||||
impl fmt::Display for CliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CliError::IoError(msg) => write!(f, "I/O Error: {}", msg),
|
||||
CliError::CryptoError(msg) => write!(f, "Crypto Error: {}", msg),
|
||||
CliError::ScriptError(msg) => write!(f, "Script Error: {}", msg),
|
||||
CliError::ConfigError(msg) => write!(f, "Configuration Error: {}", msg),
|
||||
CliError::NotImplemented => write!(f, "Command not implemented yet"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CliError {}
|
||||
|
||||
impl From<io::Error> for CliError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
CliError::IoError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CryptoError> for CliError {
|
||||
fn from(err: CryptoError) -> Self {
|
||||
CliError::CryptoError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rhai::EvalAltResult> for CliError {
|
||||
fn from(err: rhai::EvalAltResult) -> Self {
|
||||
CliError::ScriptError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Define a Result type alias for convenience
|
||||
pub type Result<T> = std::result::Result<T, CliError>;
|
254
src/cli/mod.rs
Normal file
254
src/cli/mod.rs
Normal file
@ -0,0 +1,254 @@
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod shell;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "crypto-cli")]
|
||||
#[command(about = "Cryptographic operations CLI with Rhai scripting support", long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
|
||||
#[arg(short, long, help = "Enable verbose output")]
|
||||
pub verbose: bool,
|
||||
|
||||
#[arg(short, long, help = "Config file path")]
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Key management commands
|
||||
Key {
|
||||
#[command(subcommand)]
|
||||
command: KeyCommands,
|
||||
},
|
||||
|
||||
/// Encryption/decryption commands
|
||||
Crypto {
|
||||
#[command(subcommand)]
|
||||
command: CryptoCommands,
|
||||
},
|
||||
|
||||
/// Ethereum wallet commands
|
||||
Ethereum {
|
||||
#[command(subcommand)]
|
||||
command: EthereumCommands,
|
||||
},
|
||||
|
||||
/// Execute Rhai script
|
||||
Script {
|
||||
#[arg(help = "Path to Rhai script file")]
|
||||
path: Option<String>,
|
||||
|
||||
#[arg(short, long, help = "Execute script from string")]
|
||||
inline: Option<String>,
|
||||
},
|
||||
|
||||
/// Interactive shell
|
||||
Shell,
|
||||
}
|
||||
|
||||
// Define subcommands for each category
|
||||
#[derive(Subcommand)]
|
||||
pub enum KeyCommands {
|
||||
/// Create a new key space
|
||||
CreateSpace {
|
||||
/// Name of the key space
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
/// Password to encrypt the key space (required)
|
||||
password: String
|
||||
},
|
||||
/// List available key spaces
|
||||
ListSpaces,
|
||||
/// Load a key space
|
||||
Load {
|
||||
/// Name of the key space to load
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Create a new keypair in the current key space
|
||||
CreateKeypair {
|
||||
/// Name of the keypair
|
||||
name: String,
|
||||
#[arg(long)]
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
password: Option<String>
|
||||
},
|
||||
/// List keypairs in the current key space
|
||||
ListKeypairs {
|
||||
#[arg(long)]
|
||||
/// Key space to list keypairs from
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Export a keypair
|
||||
Export {
|
||||
/// Name of the keypair to export
|
||||
name: String,
|
||||
/// Output file path (prints to stdout if not specified)
|
||||
output: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Import a keypair
|
||||
Import {
|
||||
/// Name to give the imported keypair
|
||||
name: String,
|
||||
/// Input file path (reads from stdin if not specified)
|
||||
input: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Key space to import the keypair into
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum CryptoCommands {
|
||||
/// Sign a message with a keypair
|
||||
Sign {
|
||||
#[arg(long)]
|
||||
/// Message to sign (as a string)
|
||||
message: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Input file containing the message to sign
|
||||
input: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Name of the keypair to use for signing
|
||||
keypair: String,
|
||||
#[arg(long)]
|
||||
/// Output file for the signature (prints to stdout if not specified)
|
||||
output: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Verify a signature
|
||||
Verify {
|
||||
#[arg(long)]
|
||||
/// Message to verify (as a string)
|
||||
message: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Input file containing the message to verify
|
||||
input: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Signature to verify (base64 encoded)
|
||||
signature: String,
|
||||
#[arg(long)]
|
||||
/// Name of the keypair to use for verification
|
||||
keypair: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Public key to use for verification (base64 encoded)
|
||||
pubkey: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Encrypt data for a recipient
|
||||
Encrypt {
|
||||
#[arg(long)]
|
||||
/// Data to encrypt (as a string)
|
||||
data: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Input file containing the data to encrypt
|
||||
input: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Name of the recipient keypair
|
||||
recipient: String,
|
||||
#[arg(long)]
|
||||
/// Output file for the encrypted data (prints to stdout if not specified)
|
||||
output: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Decrypt data with a keypair
|
||||
Decrypt {
|
||||
#[arg(long)]
|
||||
/// Data to decrypt (as a string, base64 encoded)
|
||||
data: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Input file containing the data to decrypt
|
||||
input: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Name of the keypair to use for decryption
|
||||
keypair: String,
|
||||
#[arg(long)]
|
||||
/// Output file for the decrypted data (prints to stdout if not specified)
|
||||
output: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum EthereumCommands {
|
||||
/// Create an Ethereum wallet from a keypair
|
||||
Create {
|
||||
#[arg(long)]
|
||||
/// Name of the keypair to use
|
||||
keypair: String,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Get the Ethereum address for a keypair
|
||||
Address {
|
||||
#[arg(long)]
|
||||
/// Name of the keypair
|
||||
keypair: String,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
/// Get the balance of an Ethereum address
|
||||
Balance {
|
||||
#[arg(long)]
|
||||
/// Ethereum address (uses the current wallet if not specified)
|
||||
address: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Network to use (e.g., "gnosis")
|
||||
network: String,
|
||||
#[arg(long)]
|
||||
/// Key space containing the keypair
|
||||
space: Option<String>,
|
||||
#[arg(long)]
|
||||
/// Password to decrypt the key space
|
||||
password: Option<String>
|
||||
},
|
||||
}
|
284
src/cli/shell.rs
Normal file
284
src/cli/shell.rs
Normal file
@ -0,0 +1,284 @@
|
||||
use colored::Colorize;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::DefaultEditor;
|
||||
use std::process;
|
||||
|
||||
use crate::cli::commands::{execute_crypto_command, execute_ethereum_command, execute_key_command};
|
||||
use crate::cli::error::{CliError, Result};
|
||||
use crate::cli::{CryptoCommands, EthereumCommands, KeyCommands};
|
||||
|
||||
pub fn run_interactive_shell() -> Result<()> {
|
||||
println!("{}", "Crypto CLI Interactive Shell".green().bold());
|
||||
println!("Type 'help' for a list of commands, 'exit' to quit");
|
||||
|
||||
let mut rl = DefaultEditor::new().map_err(|e| CliError::IoError(e.to_string()))?;
|
||||
|
||||
if rl.load_history("history.txt").is_err() {
|
||||
println!("No previous history.");
|
||||
}
|
||||
|
||||
loop {
|
||||
let readline = rl.readline("crypto> ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
rl.add_history_entry(line.as_str());
|
||||
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match line {
|
||||
"exit" | "quit" => {
|
||||
println!("Goodbye!");
|
||||
break;
|
||||
},
|
||||
"help" => {
|
||||
print_help();
|
||||
},
|
||||
_ => {
|
||||
if let Err(e) = process_command(line) {
|
||||
println!("{}: {}", "Error".red().bold(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("CTRL-C");
|
||||
break;
|
||||
},
|
||||
Err(ReadlineError::Eof) => {
|
||||
println!("CTRL-D");
|
||||
break;
|
||||
},
|
||||
Err(err) => {
|
||||
println!("Error: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rl.save_history("history.txt").map_err(|e| CliError::IoError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("{}", "Available Commands:".green().bold());
|
||||
println!(" {}", "Key Management:".yellow());
|
||||
println!(" key create-space <name> [password]");
|
||||
println!(" key list-spaces");
|
||||
println!(" key load <name> [password]");
|
||||
println!(" key create-keypair <name>");
|
||||
println!(" key list-keypairs");
|
||||
println!(" key export <name> [output-file]");
|
||||
println!(" key import <name> [input-file]");
|
||||
|
||||
println!(" {}", "Cryptographic Operations:".yellow());
|
||||
println!(" crypto sign <keypair> <message> [output-file]");
|
||||
println!(" crypto verify <signature> <message> [keypair]");
|
||||
println!(" crypto encrypt <recipient> <data> [output-file]");
|
||||
println!(" crypto decrypt <keypair> <data> [output-file]");
|
||||
|
||||
println!(" {}", "Ethereum Operations:".yellow());
|
||||
println!(" eth create <keypair>");
|
||||
println!(" eth address <keypair>");
|
||||
println!(" eth balance <address> <network>");
|
||||
|
||||
println!(" {}", "General:".yellow());
|
||||
println!(" help - Show this help message");
|
||||
println!(" exit - Exit the shell");
|
||||
}
|
||||
|
||||
fn process_command(cmd: &str) -> Result<()> {
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match parts[0] {
|
||||
"key" => {
|
||||
if parts.len() < 2 {
|
||||
println!("Missing key subcommand. Try 'help' for a list of commands.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match parts[1] {
|
||||
"create-space" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing space name. Usage: key create-space <name> [password]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = parts[2].to_string();
|
||||
let password = if parts.len() > 3 { parts[3].to_string() } else { String::new() };
|
||||
|
||||
execute_key_command(&KeyCommands::CreateSpace { name, password })
|
||||
},
|
||||
"list-spaces" => {
|
||||
execute_key_command(&KeyCommands::ListSpaces)
|
||||
},
|
||||
"load" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing space name. Usage: key load <name> [password]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = parts[2].to_string();
|
||||
let password = if parts.len() > 3 { Some(parts[3].to_string()) } else { None };
|
||||
|
||||
execute_key_command(&KeyCommands::Load { name, password })
|
||||
},
|
||||
"create-keypair" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing keypair name. Usage: key create-keypair <name>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = parts[2].to_string();
|
||||
|
||||
execute_key_command(&KeyCommands::CreateKeypair { name, space: None, password: None })
|
||||
},
|
||||
"list-keypairs" => {
|
||||
execute_key_command(&KeyCommands::ListKeypairs { space: None, password: None })
|
||||
},
|
||||
"export" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing keypair name. Usage: key export <name> [output-file]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = parts[2].to_string();
|
||||
let output = if parts.len() > 3 { Some(parts[3].to_string()) } else { None };
|
||||
|
||||
execute_key_command(&KeyCommands::Export { name, output, space: None, password: None })
|
||||
},
|
||||
"import" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing keypair name. Usage: key import <name> [input-file]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let name = parts[2].to_string();
|
||||
let input = if parts.len() > 3 { Some(parts[3].to_string()) } else { None };
|
||||
|
||||
execute_key_command(&KeyCommands::Import { name, input, space: None, password: None })
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown key subcommand: {}. Try 'help' for a list of commands.", parts[1]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
"crypto" => {
|
||||
if parts.len() < 2 {
|
||||
println!("Missing crypto subcommand. Try 'help' for a list of commands.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match parts[1] {
|
||||
"sign" => {
|
||||
if parts.len() < 4 {
|
||||
println!("Missing arguments. Usage: crypto sign <keypair> <message> [output-file]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let keypair = parts[2].to_string();
|
||||
let message = Some(parts[3].to_string());
|
||||
let output = if parts.len() > 4 { Some(parts[4].to_string()) } else { None };
|
||||
|
||||
execute_crypto_command(&CryptoCommands::Sign { message, input: None, keypair, output, space: None, password: None })
|
||||
},
|
||||
"verify" => {
|
||||
if parts.len() < 4 {
|
||||
println!("Missing arguments. Usage: crypto verify <signature> <message> [keypair]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let signature = parts[2].to_string();
|
||||
let message = Some(parts[3].to_string());
|
||||
let keypair = if parts.len() > 4 { Some(parts[4].to_string()) } else { None };
|
||||
|
||||
execute_crypto_command(&CryptoCommands::Verify { message, input: None, signature, keypair, pubkey: None, space: None, password: None })
|
||||
},
|
||||
"encrypt" => {
|
||||
if parts.len() < 4 {
|
||||
println!("Missing arguments. Usage: crypto encrypt <recipient> <data> [output-file]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let recipient = parts[2].to_string();
|
||||
let data = Some(parts[3].to_string());
|
||||
let output = if parts.len() > 4 { Some(parts[4].to_string()) } else { None };
|
||||
|
||||
execute_crypto_command(&CryptoCommands::Encrypt { data, input: None, recipient, output, space: None, password: None })
|
||||
},
|
||||
"decrypt" => {
|
||||
if parts.len() < 4 {
|
||||
println!("Missing arguments. Usage: crypto decrypt <keypair> <data> [output-file]");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let keypair = parts[2].to_string();
|
||||
let data = Some(parts[3].to_string());
|
||||
let output = if parts.len() > 4 { Some(parts[4].to_string()) } else { None };
|
||||
|
||||
execute_crypto_command(&CryptoCommands::Decrypt { data, input: None, keypair, output, space: None, password: None })
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown crypto subcommand: {}. Try 'help' for a list of commands.", parts[1]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
"eth" => {
|
||||
if parts.len() < 2 {
|
||||
println!("Missing eth subcommand. Try 'help' for a list of commands.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match parts[1] {
|
||||
"create" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing keypair name. Usage: eth create <keypair>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let keypair = parts[2].to_string();
|
||||
|
||||
execute_ethereum_command(&EthereumCommands::Create { keypair, space: None, password: None })
|
||||
},
|
||||
"address" => {
|
||||
if parts.len() < 3 {
|
||||
println!("Missing keypair name. Usage: eth address <keypair>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let keypair = parts[2].to_string();
|
||||
|
||||
execute_ethereum_command(&EthereumCommands::Address { keypair, space: None, password: None })
|
||||
},
|
||||
"balance" => {
|
||||
if parts.len() < 4 {
|
||||
println!("Missing arguments. Usage: eth balance <address> <network>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let address = Some(parts[2].to_string());
|
||||
let network = parts[3].to_string();
|
||||
|
||||
execute_ethereum_command(&EthereumCommands::Balance { address, network, space: None, password: None })
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown eth subcommand: {}. Try 'help' for a list of commands.", parts[1]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown command: {}. Try 'help' for a list of commands.", parts[0]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,8 @@ mod verifying_key_serde {
|
||||
S: Serializer,
|
||||
{
|
||||
let bytes = key.to_sec1_bytes();
|
||||
serializer.serialize_bytes(&bytes)
|
||||
// Convert bytes to a Vec<u8> and serialize that instead
|
||||
serializer.collect_seq(bytes)
|
||||
}
|
||||
|
||||
struct VerifyingKeyVisitor;
|
||||
@ -48,7 +49,26 @@ mod verifying_key_serde {
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
VerifyingKey::from_sec1_bytes(v).map_err(|_| E::custom("invalid verifying key"))
|
||||
VerifyingKey::from_sec1_bytes(v).map_err(|e| {
|
||||
eprintln!("Error deserializing verifying key: {:?}", e);
|
||||
E::custom(format!("invalid verifying key: {:?}", e))
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
// Collect all bytes from the sequence
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(byte) = seq.next_element()? {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
VerifyingKey::from_sec1_bytes(&bytes).map_err(|e| {
|
||||
eprintln!("Error deserializing verifying key from seq: {:?}", e);
|
||||
de::Error::custom(format!("invalid verifying key from seq: {:?}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +76,8 @@ mod verifying_key_serde {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_bytes(VerifyingKeyVisitor)
|
||||
// Try to deserialize as bytes first, then as a sequence
|
||||
deserializer.deserialize_any(VerifyingKeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +93,8 @@ mod signing_key_serde {
|
||||
S: Serializer,
|
||||
{
|
||||
let bytes = key.to_bytes();
|
||||
serializer.serialize_bytes(&bytes)
|
||||
// Convert bytes to a Vec<u8> and serialize that instead
|
||||
serializer.collect_seq(bytes)
|
||||
}
|
||||
|
||||
struct SigningKeyVisitor;
|
||||
@ -88,7 +110,26 @@ mod signing_key_serde {
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
SigningKey::from_bytes(v.into()).map_err(|_| E::custom("invalid signing key"))
|
||||
SigningKey::from_bytes(v.into()).map_err(|e| {
|
||||
eprintln!("Error deserializing signing key: {:?}", e);
|
||||
E::custom(format!("invalid signing key: {:?}", e))
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
// Collect all bytes from the sequence
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(byte) = seq.next_element()? {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
SigningKey::from_bytes(bytes.as_slice().into()).map_err(|e| {
|
||||
eprintln!("Error deserializing signing key from seq: {:?}", e);
|
||||
de::Error::custom(format!("invalid signing key from seq: {:?}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +137,8 @@ mod signing_key_serde {
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_bytes(SigningKeyVisitor)
|
||||
// Try to deserialize as bytes first, then as a sequence
|
||||
deserializer.deserialize_any(SigningKeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ pub fn decrypt_with_key(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec<
|
||||
}
|
||||
|
||||
/// Metadata for an encrypted key space.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EncryptedKeySpaceMetadata {
|
||||
pub name: String,
|
||||
pub created_at: u64,
|
||||
@ -150,7 +150,7 @@ pub struct EncryptedKeySpaceMetadata {
|
||||
}
|
||||
|
||||
/// An encrypted key space with metadata.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EncryptedKeySpace {
|
||||
pub metadata: EncryptedKeySpaceMetadata,
|
||||
pub encrypted_data: Vec<u8>,
|
||||
@ -169,8 +169,13 @@ pub struct EncryptedKeySpace {
|
||||
/// * `Err(CryptoError)` if encryption fails.
|
||||
pub fn encrypt_key_space(space: &KeySpace, password: &str) -> Result<EncryptedKeySpace, CryptoError> {
|
||||
// Serialize the key space
|
||||
let serialized = serde_json::to_vec(space)
|
||||
.map_err(|_| CryptoError::SerializationError)?;
|
||||
let serialized = match serde_json::to_vec(space) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("Serialization error during encryption: {}", e);
|
||||
return Err(CryptoError::SerializationError);
|
||||
}
|
||||
};
|
||||
|
||||
// Derive key from password
|
||||
let key = derive_key_from_password(password);
|
||||
@ -179,7 +184,10 @@ pub fn encrypt_key_space(space: &KeySpace, password: &str) -> Result<EncryptedKe
|
||||
let encrypted_data = encrypt_symmetric(&key, &serialized)?;
|
||||
|
||||
// Create metadata
|
||||
let now = js_sys::Date::now() as u64;
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
let metadata = EncryptedKeySpaceMetadata {
|
||||
name: space.name.clone(),
|
||||
created_at: now,
|
||||
@ -211,8 +219,13 @@ pub fn decrypt_key_space(encrypted_space: &EncryptedKeySpace, password: &str) ->
|
||||
let decrypted_data = decrypt_symmetric(&key, &encrypted_space.encrypted_data)?;
|
||||
|
||||
// Deserialize the key space
|
||||
let space: KeySpace = serde_json::from_slice(&decrypted_data)
|
||||
.map_err(|_| CryptoError::SerializationError)?;
|
||||
let space: KeySpace = match serde_json::from_slice(&decrypted_data) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
eprintln!("Deserialization error: {}", e);
|
||||
return Err(CryptoError::SerializationError);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(space)
|
||||
}
|
||||
@ -243,6 +256,11 @@ pub fn serialize_encrypted_space(encrypted_space: &EncryptedKeySpace) -> Result<
|
||||
/// * `Ok(EncryptedKeySpace)` containing the deserialized encrypted key space.
|
||||
/// * `Err(CryptoError)` if deserialization fails.
|
||||
pub fn deserialize_encrypted_space(serialized: &str) -> Result<EncryptedKeySpace, CryptoError> {
|
||||
serde_json::from_str(serialized)
|
||||
.map_err(|_| CryptoError::SerializationError)
|
||||
match serde_json::from_str(serialized) {
|
||||
Ok(space) => Ok(space),
|
||||
Err(e) => {
|
||||
eprintln!("Error deserializing encrypted space: {}", e);
|
||||
Err(CryptoError::SerializationError)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ use web_sys::console;
|
||||
|
||||
// Import modules
|
||||
mod api;
|
||||
mod core;
|
||||
pub mod core;
|
||||
mod tests;
|
||||
|
||||
// Re-export for internal use
|
||||
|
58
src/main.rs
Normal file
58
src/main.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use env_logger::Builder;
|
||||
use log::{info, LevelFilter};
|
||||
|
||||
// Import the webassembly crate for access to core functionality
|
||||
extern crate webassembly;
|
||||
|
||||
mod cli;
|
||||
mod scripting;
|
||||
|
||||
use cli::{Cli, Commands};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Parse command line arguments
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize logger with appropriate level
|
||||
let mut builder = Builder::from_default_env();
|
||||
if cli.verbose {
|
||||
builder.filter_level(LevelFilter::Debug);
|
||||
} else {
|
||||
builder.filter_level(LevelFilter::Info);
|
||||
}
|
||||
builder.init();
|
||||
|
||||
// Execute the appropriate command
|
||||
match &cli.command {
|
||||
Commands::Key { command } => {
|
||||
cli::commands::execute_key_command(command)?;
|
||||
},
|
||||
Commands::Crypto { command } => {
|
||||
cli::commands::execute_crypto_command(command)?;
|
||||
},
|
||||
Commands::Ethereum { command } => {
|
||||
cli::commands::execute_ethereum_command(command)?;
|
||||
},
|
||||
Commands::Script { path, inline } => {
|
||||
let mut engine = scripting::ScriptEngine::new();
|
||||
|
||||
if let Some(script_path) = path {
|
||||
info!("Executing script from file: {}", script_path);
|
||||
engine.eval_file(script_path)?;
|
||||
} else if let Some(script) = inline {
|
||||
info!("Executing inline script");
|
||||
engine.eval(script)?;
|
||||
} else {
|
||||
println!("Error: No script provided");
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Commands::Shell => {
|
||||
cli::shell::run_interactive_shell()?;
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
416
src/scripting/api.rs
Normal file
416
src/scripting/api.rs
Normal file
@ -0,0 +1,416 @@
|
||||
use rhai::{Engine, Scope, Dynamic, FnPtr};
|
||||
use webassembly::core::keypair;
|
||||
use webassembly::core::symmetric;
|
||||
use webassembly::core::ethereum;
|
||||
use webassembly::core::error::CryptoError;
|
||||
use std::str::FromStr;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Key space management functions
|
||||
fn load_key_space(name: &str, password: &str) -> bool {
|
||||
// Get the key spaces directory from config
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".crypto-cli").join("key-spaces");
|
||||
|
||||
// Check if directory exists
|
||||
if !key_spaces_dir.exists() {
|
||||
println!("Key spaces directory does not exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the key space file path
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||
|
||||
// Check if file exists
|
||||
if !space_path.exists() {
|
||||
println!("Key space file not found: {}", space_path.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the file
|
||||
let serialized = match fs::read_to_string(&space_path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
println!("Error reading key space file: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Deserialize the encrypted space
|
||||
let encrypted_space = match symmetric::deserialize_encrypted_space(&serialized) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
println!("Error deserializing key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Decrypt the space
|
||||
let space = match symmetric::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
println!("Error decrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set as current space
|
||||
match keypair::set_current_space(space) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
println!("Error setting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_key_space(name: &str, password: &str) -> bool {
|
||||
match keypair::create_space(name) {
|
||||
Ok(_) => {
|
||||
// Get the current space
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
// Encrypt the key space
|
||||
let encrypted_space = match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
println!("Error encrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
println!("Error serializing encrypted space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the key spaces directory
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".crypto-cli").join("key-spaces");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if !key_spaces_dir.exists() {
|
||||
match fs::create_dir_all(&key_spaces_dir) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
println!("Error creating key spaces directory: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", name));
|
||||
match fs::write(&space_path, serialized) {
|
||||
Ok(_) => {
|
||||
println!("Key space created and saved to {}", space_path.display());
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error writing key space file: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error getting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error creating key space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-save function for internal use
|
||||
fn auto_save_key_space(password: &str) -> bool {
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
// Encrypt the key space
|
||||
let encrypted_space = match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted) => encrypted,
|
||||
Err(e) => {
|
||||
println!("Error encrypting key space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize the encrypted space
|
||||
let serialized = match symmetric::serialize_encrypted_space(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
println!("Error serializing encrypted space: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the key spaces directory
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
let key_spaces_dir = home_dir.join(".crypto-cli").join("key-spaces");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if !key_spaces_dir.exists() {
|
||||
match fs::create_dir_all(&key_spaces_dir) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
println!("Error creating key spaces directory: {}", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
let space_path = key_spaces_dir.join(format!("{}.json", space.name));
|
||||
match fs::write(&space_path, serialized) {
|
||||
Ok(_) => {
|
||||
println!("Key space saved to {}", space_path.display());
|
||||
true
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error writing key space file: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error getting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt_key_space(password: &str) -> String {
|
||||
match keypair::get_current_space() {
|
||||
Ok(space) => {
|
||||
match symmetric::encrypt_key_space(&space, password) {
|
||||
Ok(encrypted_space) => {
|
||||
match serde_json::to_string(&encrypted_space) {
|
||||
Ok(json) => json,
|
||||
Err(e) => {
|
||||
println!("Error serializing encrypted space: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error encrypting key space: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error getting current space: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
|
||||
match serde_json::from_str(encrypted) {
|
||||
Ok(encrypted_space) => {
|
||||
match symmetric::decrypt_key_space(&encrypted_space, password) {
|
||||
Ok(space) => {
|
||||
match keypair::set_current_space(space) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
println!("Error setting current space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error decrypting key space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error parsing encrypted space: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keypair management functions
|
||||
fn create_keypair(name: &str, password: &str) -> bool {
|
||||
match keypair::create_keypair(name) {
|
||||
Ok(_) => {
|
||||
// Auto-save the key space after creating a keypair
|
||||
auto_save_key_space(password)
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error creating keypair: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_keypair(name: &str) -> bool {
|
||||
match keypair::select_keypair(name) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
println!("Error selecting keypair: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_keypairs() -> Vec<String> {
|
||||
match keypair::list_keypairs() {
|
||||
Ok(keypairs) => keypairs,
|
||||
Err(e) => {
|
||||
println!("Error listing keypairs: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cryptographic operations
|
||||
fn sign(message: &str) -> String {
|
||||
let message_bytes = message.as_bytes();
|
||||
match keypair::keypair_sign(message_bytes) {
|
||||
Ok(signature) => BASE64.encode(signature),
|
||||
Err(e) => {
|
||||
println!("Error signing message: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify(message: &str, signature: &str) -> bool {
|
||||
let message_bytes = message.as_bytes();
|
||||
match BASE64.decode(signature) {
|
||||
Ok(signature_bytes) => {
|
||||
match keypair::keypair_verify(message_bytes, &signature_bytes) {
|
||||
Ok(is_valid) => is_valid,
|
||||
Err(e) => {
|
||||
println!("Error verifying signature: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error decoding signature: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Symmetric encryption
|
||||
fn generate_key() -> String {
|
||||
let key = symmetric::generate_symmetric_key();
|
||||
BASE64.encode(key)
|
||||
}
|
||||
|
||||
fn encrypt(key: &str, message: &str) -> String {
|
||||
match BASE64.decode(key) {
|
||||
Ok(key_bytes) => {
|
||||
let message_bytes = message.as_bytes();
|
||||
match symmetric::encrypt_symmetric(&key_bytes, message_bytes) {
|
||||
Ok(ciphertext) => BASE64.encode(ciphertext),
|
||||
Err(e) => {
|
||||
println!("Error encrypting message: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error decoding key: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt(key: &str, ciphertext: &str) -> String {
|
||||
match BASE64.decode(key) {
|
||||
Ok(key_bytes) => {
|
||||
match BASE64.decode(ciphertext) {
|
||||
Ok(ciphertext_bytes) => {
|
||||
match symmetric::decrypt_symmetric(&key_bytes, &ciphertext_bytes) {
|
||||
Ok(plaintext) => {
|
||||
match String::from_utf8(plaintext) {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
println!("Error converting plaintext to string: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error decrypting ciphertext: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error decoding ciphertext: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error decoding key: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ethereum operations
|
||||
fn create_ethereum_wallet() -> bool {
|
||||
match ethereum::create_ethereum_wallet() {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
println!("Error creating Ethereum wallet: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ethereum_address() -> String {
|
||||
match ethereum::get_current_ethereum_wallet() {
|
||||
Ok(wallet) => wallet.address_string(),
|
||||
Err(e) => {
|
||||
println!("Error getting Ethereum address: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_crypto_api(engine: &mut Engine, scope: &mut Scope<'_>) {
|
||||
// Register key space functions
|
||||
engine.register_fn("load_key_space", load_key_space);
|
||||
engine.register_fn("create_key_space", create_key_space);
|
||||
engine.register_fn("encrypt_key_space", encrypt_key_space);
|
||||
engine.register_fn("decrypt_key_space", decrypt_key_space);
|
||||
|
||||
// Register keypair functions
|
||||
engine.register_fn("create_keypair", create_keypair);
|
||||
engine.register_fn("select_keypair", select_keypair);
|
||||
engine.register_fn("list_keypairs", list_keypairs);
|
||||
|
||||
// Register signing/verification functions
|
||||
engine.register_fn("sign", sign);
|
||||
engine.register_fn("verify", verify);
|
||||
|
||||
// Register symmetric encryption functions
|
||||
engine.register_fn("generate_key", generate_key);
|
||||
engine.register_fn("encrypt", encrypt);
|
||||
engine.register_fn("decrypt", decrypt);
|
||||
|
||||
// Register Ethereum functions
|
||||
engine.register_fn("create_ethereum_wallet", create_ethereum_wallet);
|
||||
engine.register_fn("get_ethereum_address", get_ethereum_address);
|
||||
|
||||
// Add any additional functions or variables to the scope
|
||||
scope.push("VERSION", "1.0.0");
|
||||
}
|
46
src/scripting/engine.rs
Normal file
46
src/scripting/engine.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use rhai::{Engine, AST, Scope, EvalAltResult};
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
|
||||
use crate::cli::error::{CliError, Result};
|
||||
use crate::scripting::api::register_crypto_api;
|
||||
|
||||
pub struct ScriptEngine {
|
||||
engine: Engine,
|
||||
scope: Scope<'static>,
|
||||
}
|
||||
|
||||
impl ScriptEngine {
|
||||
pub fn new() -> Self {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Set up sandboxing
|
||||
engine.set_max_operations(100_000);
|
||||
engine.set_max_modules(10);
|
||||
engine.set_max_string_size(10_000);
|
||||
engine.set_max_array_size(1_000);
|
||||
engine.set_max_map_size(1_000);
|
||||
|
||||
// Disable potentially dangerous operations
|
||||
engine.disable_symbol("eval");
|
||||
engine.disable_symbol("source");
|
||||
|
||||
// Register crypto API
|
||||
let mut scope = Scope::new();
|
||||
register_crypto_api(&mut engine, &mut scope);
|
||||
|
||||
ScriptEngine { engine, scope }
|
||||
}
|
||||
|
||||
pub fn eval_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
|
||||
let script = fs::read_to_string(path)
|
||||
.map_err(|e| CliError::IoError(format!("Failed to read script file: {}", e)))?;
|
||||
|
||||
self.eval(&script)
|
||||
}
|
||||
|
||||
pub fn eval(&mut self, script: &str) -> Result<()> {
|
||||
self.engine.eval_with_scope::<()>(&mut self.scope, script)
|
||||
.map_err(|e| CliError::ScriptError(e.to_string()))
|
||||
}
|
||||
}
|
4
src/scripting/mod.rs
Normal file
4
src/scripting/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod engine;
|
||||
pub mod api;
|
||||
|
||||
pub use self::engine::ScriptEngine;
|
Loading…
Reference in New Issue
Block a user