22 KiB
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
[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)
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)
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)
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)
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)
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)
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)
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)
// 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)
// 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)
// 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
-
Update Cargo.toml
- Add new dependencies
- Configure features
- Add binary target
-
Create CLI Structure
- Implement CLI module
- Define commands and subcommands
- Set up error handling
-
Implement Rhai Scripting
- Create scripting engine
- Register API functions
- Implement sandboxing
-
Implement Messaging System
- Choose between Mycelium and NATS
- Implement listener
- Set up script execution
-
Create Example Scripts
- Key management scripts
- Signing scripts
- Encryption scripts
-
Testing
- Unit tests for CLI commands
- Integration tests for script execution
- End-to-end tests for messaging
-
Documentation
- Update README.md
- Add CLI help text
- Document script API
7. Testing Strategy
7.1 Unit Tests
#[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
#[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.