Compare commits

...

36 Commits

Author SHA1 Message Date
timurgordon
c0e11c6510 merge and fix tests
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
2025-05-23 21:46:11 +03:00
timurgordon
fedf957079 Merge branch 'development_lee' 2025-05-23 21:14:31 +03:00
timurgordon
65e404e517 merge branches and document
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
2025-05-23 21:12:17 +03:00
944f22be23 Merge pull request 'clean up' (#12) from zinit_client into main
Reviewed-on: #12
2025-05-23 17:37:45 +00:00
887e66bb17 Merge pull request 'mycelium-rhai' (#9) from mycelium-rhai into main
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run
Reviewed-on: #9
2025-05-23 16:40:28 +00:00
Lee Smet
e5a4a1b634
Add tests for symmetric keys
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-16 15:19:45 +02:00
Lee Smet
7f55cf4fba
Add tests for asymmetric keys, add public key export
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-16 15:05:45 +02:00
Maxime Van Hees
c26e0e5ad8 removed unused imports
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-16 15:04:00 +02:00
Lee Smet
365814b424
Fix signature key import/export, add tests
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-16 14:37:10 +02:00
Maxime Van Hees
cc4e087f1d fixed wrong link to file, again
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-16 14:32:35 +02:00
Maxime Van Hees
229fef217f fixed wrong link to file
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-16 14:32:03 +02:00
Maxime Van Hees
dd84ce3f48 remove command from tutorial
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-16 14:30:46 +02:00
Maxime Van Hees
7b8b8c662e Added tutorial on how to send/receive message with 2 mycelium peers running the same host
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-16 14:28:49 +02:00
Lee Smet
d29a8fbb67
Rename Store to Vault and move to lib root
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-16 11:24:27 +02:00
Maxime Van Hees
771df07c25 Add tutorial explain how to use Mycelium in Rhai scripts
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-16 10:25:51 +02:00
Maxime Van Hees
9a23c4cc09 sending and receiving message via Rhai script + added examples
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-15 17:30:20 +02:00
Lee Smet
2014c63b78
Remove old files
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-15 13:53:54 +02:00
Lee Smet
2adda10664
Basic API
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-15 13:53:16 +02:00
Lee Smet
7b1908b676
Use kvstore as backing
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-14 19:21:02 +02:00
Lee Smet
e9b867a36e
Individiual methods for keystores
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-14 11:49:36 +02:00
Lee Smet
78c0fd7871
Define the global KeySpace interface
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-14 11:10:52 +02:00
Lee Smet
e44ee83e74
Implement proper key types
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-13 17:37:33 +02:00
0c425470a5 Merge pull request 'Simplify and Refactor Asymmetric Encryption/Decryption' (#10) from development_fix_code into main
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
Reviewed-on: #10
2025-05-13 13:00:16 +00:00
Maxime Van Hees
3e64a53a83 working example for mycelium
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
2025-05-13 14:10:32 +02:00
Maxime Van Hees
3225b3f029 corrected response mapping from API requests 2025-05-13 14:10:32 +02:00
Maxime Van Hees
3417e2c1ff fixed merge conflict 2025-05-13 14:10:10 +02:00
Mahmoud Emad
7add64562e feat: Simplify asymmetric encryption/decryption
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled
- Simplify asymmetric encryption by using a single symmetric key
  instead of deriving a key from an ephemeral key exchange.  This
  improves clarity and reduces complexity.
- The new implementation encrypts the symmetric key with the
  recipient's public key and then encrypts the message with the
  symmetric key.
- Improve test coverage for asymmetric encryption/decryption.
2025-05-13 14:45:05 +03:00
Mahmoud Emad
809599d60c fix: Get the code to compile 2025-05-13 14:12:48 +03:00
Mahmoud Emad
25f2ae6fa9 refactor: Refactor keypair and Ethereum wallet handling
Some checks failed
Rhai Tests / Run Rhai Tests (push) Has been cancelled
- Moved `prepare_function_arguments` and `convert_token_to_rhai` to the `ethereum` module for better organization.
- Updated `keypair` module to use the new `session_manager` structure improving code clarity.
- Changed `KeyPair` type usage to new `vault::keyspace::keypair_types::KeyPair` for consistency.
- Improved error handling and clarity in `EthereumWallet` methods.
2025-05-13 13:55:04 +03:00
Lee Smet
dfe6c91273
Fix build issues
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
2025-05-13 11:45:06 +02:00
a4438d63e0 ... 2025-05-13 08:02:23 +03:00
393c4270d4 ... 2025-05-13 07:28:02 +03:00
495fe92321 Merge branch 'development_keypair_tests'
* development_keypair_tests:
  feat: Add comprehensive test suite for Keypair module
2025-05-13 06:51:30 +03:00
577d80b282 restore 2025-05-13 06:51:20 +03:00
3f8aecb786 tests & fixes in kvs & keypair 2025-05-13 06:45:04 +03:00
Maxime Van Hees
916eabfa42 clean up 2025-05-12 11:17:15 +02:00
54 changed files with 4643 additions and 755 deletions

View File

@ -10,18 +10,25 @@ keywords = ["system", "os", "abstraction", "platform", "filesystem"]
categories = ["os", "filesystem", "api-bindings"] categories = ["os", "filesystem", "api-bindings"]
readme = "README.md" readme = "README.md"
[workspace]
members = [".", "vault"]
[dependencies] [dependencies]
hex = "0.4"
anyhow = "1.0.98" anyhow = "1.0.98"
base64 = "0.21.0" # Base64 encoding/decoding base64 = "0.22.1" # Base64 encoding/decoding
cfg-if = "1.0" cfg-if = "1.0"
chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher
clap = "2.33" # Command-line argument parsing clap = "2.34.0" # Command-line argument parsing
dirs = "5.0.1" # Directory paths dirs = "6.0.0" # Directory paths
env_logger = "0.10.0" # Logger implementation env_logger = "0.11.8" # Logger implementation
ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library
glob = "0.3.1" # For file pattern matching glob = "0.3.1" # For file pattern matching
jsonrpsee = "0.25.1" jsonrpsee = "0.25.1"
k256 = { version = "0.13.1", features = ["ecdsa"] } # Elliptic curve cryptography k256 = { version = "0.13.4", features = [
"ecdsa",
"ecdh",
] } # Elliptic curve cryptography
lazy_static = "1.4.0" # For lazy initialization of static variables lazy_static = "1.4.0" # For lazy initialization of static variables
libc = "0.2" libc = "0.2"
log = "0.4" # Logging facade log = "0.4" # Logging facade
@ -31,38 +38,43 @@ postgres-types = "0.2.5" # PostgreSQL type conversions
r2d2 = "0.8.10" r2d2 = "0.8.10"
r2d2_postgres = "0.18.2" r2d2_postgres = "0.18.2"
rand = "0.8.5" # Random number generation rand = "0.8.5" # Random number generation
redis = "0.22.0" # Redis client redis = "0.31.0" # Redis client
regex = "1.8.1" # For regex pattern matching regex = "1.8.1" # For regex pattern matching
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
serde = { version = "1.0", features = [ serde = { version = "1.0", features = [
"derive", "derive",
] } # For serialization/deserialization ] } # For serialization/deserialization
serde_json = "1.0" # For JSON handling serde_json = "1.0" # For JSON handling
sha2 = "0.10.7" # SHA-2 hash functions sha2 = "0.10.7" # SHA-2 hash functions
tempfile = "3.5" # For temporary file operations tempfile = "3.5" # For temporary file operations
tera = "1.19.0" # Template engine for text rendering tera = "1.19.0" # Template engine for text rendering
thiserror = "1.0" # For error handling thiserror = "2.0.12" # For error handling
tokio = "1.45.0" tokio = "1.45.0"
tokio-postgres = "0.7.8" # Async PostgreSQL client tokio-postgres = "0.7.8" # Async PostgreSQL client
tokio-test = "0.4.4" tokio-test = "0.4.4"
uuid = { version = "1.16.0", features = ["v4"] } uuid = { version = "1.16.0", features = ["v4"] }
zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" } zinit-client = { path = "/Users/timurgordon/code/github/threefoldtech/zinit/zinit-client" }
reqwest = { version = "0.12.15", features = ["json"] }
urlencoding = "2.1.3"
# Optional features for specific OS functionality # Optional features for specific OS functionality
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = "0.26" # Unix-specific functionality nix = "0.30.1" # Unix-specific functionality
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.48", features = [ windows = { version = "0.61.1", features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_System_Threading", "Win32_System_Threading",
"Win32_Storage_FileSystem", "Win32_Storage_FileSystem",
] } ] }
[dev-dependencies] [dev-dependencies]
mockall = "0.11.4" # For mocking in tests mockall = "0.13.1" # For mocking in tests
tempfile = "3.5" # For tests that need temporary files/directories tempfile = "3.5" # For tests that need temporary files/directories
tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing tokio = { version = "1.28", features = [
"full",
"test-util",
] } # For async testing
[[bin]] [[bin]]
name = "herodo" name = "herodo"

197
README.md
View File

@ -1,73 +1,184 @@
# SAL (System Abstraction Layer) # SAL (System Abstraction Layer)
A Rust library that provides a unified interface for interacting with operating system features across different platforms. It abstracts away platform-specific details, allowing developers to write cross-platform code with ease. **Version: 0.1.0**
## Features SAL is a comprehensive Rust library designed to provide a unified and simplified interface for a wide array of system-level operations and interactions. It abstracts platform-specific details, enabling developers to write robust, cross-platform code with greater ease. SAL also includes `herodo`, a powerful command-line tool for executing Rhai scripts that leverage SAL's capabilities for automation and system management tasks.
- **File System Operations**: Simplified file and directory management ## Core Features
- **Process Management**: Create, monitor, and control processes
- **System Information**: Access system details and metrics
- **Git Integration**: Interface with Git repositories
- **Redis Client**: Robust Redis connection management and command execution
- **Text Processing**: Utilities for text manipulation and formatting
## Modules SAL offers a broad spectrum of functionalities, including:
### Redis Client - **System Operations**: File and directory management, environment variable access, system information retrieval, and OS-specific commands.
- **Process Management**: Create, monitor, control, and interact with system processes.
- **Containerization Tools**:
- Integration with **Buildah** for building OCI/Docker-compatible container images.
- Integration with **nerdctl** for managing containers (run, stop, list, build, etc.).
- **Version Control**: Programmatic interaction with Git repositories (clone, commit, push, pull, status, etc.).
- **Database Clients**:
- **Redis**: Robust client for interacting with Redis servers.
- **PostgreSQL**: Client for executing queries and managing PostgreSQL databases.
- **Scripting Engine**: In-built support for the **Rhai** scripting language, allowing SAL functionalities to be scripted and automated, primarily through the `herodo` tool.
- **Networking & Services**:
- **Mycelium**: Tools for Mycelium network peer management and message passing.
- **Zinit**: Client for interacting with the Zinit process supervision system.
- **RFS (Remote/Virtual Filesystem)**: Mount, manage, pack, and unpack various types of filesystems (local, SSH, S3, WebDAV).
- **Text Processing**: A suite of utilities for text manipulation, formatting, and regular expressions.
- **Cryptography (`vault`)**: Functions for common cryptographic operations.
The Redis client module provides a robust wrapper around the Redis client library for Rust, offering: ## `herodo`: The SAL Scripting Tool
- Automatic connection management and reconnection `herodo` is a command-line utility bundled with SAL that executes Rhai scripts. It empowers users to automate tasks and orchestrate complex workflows by leveraging SAL's diverse modules directly from scripts.
- Support for both Unix socket and TCP connections
- Database selection via environment variables
- Thread-safe global client instance
- Simple command execution interface
[View Redis Client Documentation](src/redisclient/README.md) ### Usage
### OS Module ```bash
herodo -p <path_to_script.rhai>
# or
herodo -p <path_to_directory_with_scripts/>
```
Provides platform-independent interfaces for operating system functionality. If a directory is provided, `herodo` will execute all `.rhai` scripts within that directory (and its subdirectories) in alphabetical order.
### Git Module ### Scriptable SAL Modules via `herodo`
Tools for interacting with Git repositories programmatically. The following SAL modules and functionalities are exposed to the Rhai scripting environment through `herodo`:
### Process Module - **OS (`os`)**: Comprehensive file system operations, file downloading & installation, and system package management. [Detailed OS Module Documentation](src/os/README.md)
- **Process (`process`)**: Robust command and script execution, plus process management (listing, finding, killing, checking command existence). [Detailed Process Module Documentation](src/process/README.md)
- **Buildah (`buildah`)**: OCI/Docker image building functions. [Detailed Buildah Module Documentation](src/virt/buildah/README.md)
- **nerdctl (`nerdctl`)**: Container lifecycle management (`nerdctl_run`, `nerdctl_stop`, `nerdctl_images`, `nerdctl_image_build`, etc.). [Detailed Nerdctl Module Documentation](src/virt/nerdctl/README.md)
- **Git (`git`)**: High-level repository management and generic Git command execution with Redis-backed authentication (clone, pull, push, commit, etc.). [Detailed Git Module Documentation](src/git/README.md)
- **Zinit (`zinit_client`)**: Client for Zinit process supervisor (service management, logs). [Detailed Zinit Client Module Documentation](src/zinit_client/README.md)
- **Mycelium (`mycelium`)**: Client for Mycelium decentralized networking API (node info, peer management, messaging). [Detailed Mycelium Module Documentation](src/mycelium/README.md)
- **Text (`text`)**: String manipulation, prefixing, path/name fixing, text replacement, and templating. [Detailed Text Module Documentation](src/text/README.md)
- **RFS (`rfs`)**: Mount various filesystems (local, SSH, S3, etc.), pack/unpack filesystem layers. [Detailed RFS Module Documentation](src/virt/rfs/README.md)
- **Cryptography (`crypto` from `vault`)**: Encryption, decryption, hashing, etc.
- **Redis Client (`redis`)**: Execute Redis commands (`redis_get`, `redis_set`, `redis_execute`, etc.).
- **PostgreSQL Client (`postgres`)**: Execute SQL queries against PostgreSQL databases.
Utilities for process creation, monitoring, and management. ### Example `herodo` Rhai Script
### Text Module ```rhai
// file: /opt/scripts/example_task.rhai
Text processing utilities for common operations. // OS operations
println("Checking for /tmp/my_app_data...");
if !exist("/tmp/my_app_data") {
mkdir("/tmp/my_app_data");
println("Created directory /tmp/my_app_data");
}
## Usage // Redis operations
println("Setting Redis key 'app_status' to 'running'");
redis_set("app_status", "running");
let status = redis_get("app_status");
println("Current app_status from Redis: " + status);
Add this to your `Cargo.toml`: // Process execution
println("Listing files in /tmp:");
let output = run("ls -la /tmp");
println(output.stdout);
println("Script finished.");
```
Run with: `herodo -p /opt/scripts/example_task.rhai`
For more examples, check the `examples/` and `rhai_tests/` directories in this repository.
## Using SAL as a Rust Library
Add SAL as a dependency to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
sal = "0.1.0" sal = "0.1.0" # Or the latest version
``` ```
Basic example: ### Rust Example: Using Redis Client
```rust ```rust
use sal::redisclient::execute; use sal::redisclient::{get_global_client, execute_cmd_with_args};
use redis::cmd; use redis::RedisResult;
async fn example_redis_interaction() -> RedisResult<()> {
// Get a connection from the global pool
let mut conn = get_global_client().await?.get_async_connection().await?;
// Set a value
execute_cmd_with_args(&mut conn, "SET", vec!["my_key", "my_value"]).await?;
println!("Set 'my_key' to 'my_value'");
// Get a value
let value: String = execute_cmd_with_args(&mut conn, "GET", vec!["my_key"]).await?;
println!("Retrieved value for 'my_key': {}", value);
fn main() -> redis::RedisResult<()> {
// Execute a Redis command
let mut cmd = redis::cmd("SET");
cmd.arg("example_key").arg("example_value");
execute(&mut cmd)?;
// Retrieve the value
let mut get_cmd = redis::cmd("GET");
get_cmd.arg("example_key");
let value: String = execute(&mut get_cmd)?;
println!("Value: {}", value);
Ok(()) Ok(())
} }
#[tokio::main]
asynchronous fn main() {
if let Err(e) = example_redis_interaction().await {
eprintln!("Redis Error: {}", e);
}
}
``` ```
*(Note: The Redis client API might have evolved; please refer to `src/redisclient/mod.rs` and its documentation for the most current usage.)*
## Modules Overview (Rust Library)
SAL is organized into several modules, each providing specific functionalities:
- **`sal::os`**: Core OS interactions, file system operations, environment access.
- **`sal::process`**: Process creation, management, and control.
- **`sal::git`**: Git repository management.
- **`sal::redisclient`**: Client for Redis database interactions. (See also `src/redisclient/README.md`)
- **`sal::postgresclient`**: Client for PostgreSQL database interactions.
- **`sal::rhai`**: Integration layer for the Rhai scripting engine, used by `herodo`.
- **`sal::text`**: Utilities for text processing and manipulation.
- **`sal::vault`**: Cryptographic functions.
- **`sal::virt`**: Virtualization-related utilities, including `rfs` for remote/virtual filesystems.
- **`sal::mycelium`**: Client for Mycelium network operations.
- **`sal::zinit_client`**: Client for Zinit process supervisor.
- **`sal::cmd`**: Implements the command logic for `herodo`.
- **(Internal integrations for `buildah`, `nerdctl` primarily exposed via Rhai)**
## Building SAL
Build the library and the `herodo` binary using Cargo:
```bash
cargo build
```
For a release build:
```bash
cargo build --release
```
The `herodo` executable will be located at `target/debug/herodo` or `target/release/herodo`.
The `build_herodo.sh` script is also available for building `herodo`.
## Running Tests
Run Rust unit and integration tests:
```bash
cargo test
```
Run Rhai script tests (which exercise `herodo` and SAL's scripted functionalities):
```bash
./run_rhai_tests.sh
```
## License
SAL is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
## Contributing
Contributions are welcome! Please feel free to submit pull requests or open issues.

View File

@ -0,0 +1,386 @@
# Mycelium Tutorial for Rhai
This tutorial explains how to use the Mycelium networking functionality in Rhai scripts. Mycelium is a peer-to-peer networking system that allows nodes to communicate with each other, and the Rhai bindings provide an easy way to interact with Mycelium from your scripts.
## Introduction
The Mycelium module for Rhai provides the following capabilities:
- Getting node information
- Managing peers (listing, adding, removing)
- Viewing routing information
- Sending and receiving messages between nodes
This tutorial will walk you through using these features with example scripts.
## Prerequisites
Before using the Mycelium functionality in Rhai, you need:
1. A running Mycelium node accessible via HTTP
> See https://github.com/threefoldtech/mycelium
2. The Rhai runtime with Mycelium module enabled
## Basic Mycelium Operations
Let's start by exploring the basic operations available in Mycelium using the `mycelium_basic.rhai` example.
### Getting Node Information
To get information about your Mycelium node:
```rhai
// API URL for Mycelium
let api_url = "http://localhost:8989";
// Get node information
print("Getting node information:");
try {
let node_info = mycelium_get_node_info(api_url);
print(`Node subnet: ${node_info.nodeSubnet}`);
print(`Node public key: ${node_info.nodePubkey}`);
} catch(err) {
print(`Error getting node info: ${err}`);
}
```
This code:
1. Sets the API URL for your Mycelium node
2. Calls `mycelium_get_node_info()` to retrieve information about the node
3. Prints the node's subnet and public key
### Managing Peers
#### Listing Peers
To list all peers connected to your Mycelium node:
```rhai
// List all peers
print("\nListing all peers:");
try {
let peers = mycelium_list_peers(api_url);
if peers.is_empty() {
print("No peers connected.");
} else {
for peer in peers {
print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`);
print(` Type: ${peer.type}`);
print(` Connection State: ${peer.connectionState}`);
print(` Bytes sent: ${peer.txBytes}`);
print(` Bytes received: ${peer.rxBytes}`);
}
}
} catch(err) {
print(`Error listing peers: ${err}`);
}
```
This code:
1. Calls `mycelium_list_peers()` to get all connected peers
2. Iterates through the peers and prints their details
#### Adding a Peer
To add a new peer to your Mycelium node:
```rhai
// Add a new peer
print("\nAdding a new peer:");
let new_peer_address = "tcp://65.21.231.58:9651";
try {
let result = mycelium_add_peer(api_url, new_peer_address);
print(`Peer added: ${result.success}`);
} catch(err) {
print(`Error adding peer: ${err}`);
}
```
This code:
1. Specifies a peer address to add
2. Calls `mycelium_add_peer()` to add the peer to your node
3. Prints whether the operation was successful
#### Removing a Peer
To remove a peer from your Mycelium node:
```rhai
// Remove a peer
print("\nRemoving a peer:");
let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier
try {
let result = mycelium_remove_peer(api_url, peer_id);
print(`Peer removed: ${result.success}`);
} catch(err) {
print(`Error removing peer: ${err}`);
}
```
This code:
1. Specifies the peer ID to remove
2. Calls `mycelium_remove_peer()` to remove the peer
3. Prints whether the operation was successful
### Viewing Routing Information
#### Listing Selected Routes
To list the selected routes in your Mycelium node:
```rhai
// List selected routes
print("\nListing selected routes:");
try {
let routes = mycelium_list_selected_routes(api_url);
if routes.is_empty() {
print("No selected routes.");
} else {
for route in routes {
print(`Subnet: ${route.subnet}`);
print(` Next hop: ${route.nextHop}`);
print(` Metric: ${route.metric}`);
}
}
} catch(err) {
print(`Error listing routes: ${err}`);
}
```
This code:
1. Calls `mycelium_list_selected_routes()` to get all selected routes
2. Iterates through the routes and prints their details
#### Listing Fallback Routes
To list the fallback routes in your Mycelium node:
```rhai
// List fallback routes
print("\nListing fallback routes:");
try {
let routes = mycelium_list_fallback_routes(api_url);
if routes.is_empty() {
print("No fallback routes.");
} else {
for route in routes {
print(`Subnet: ${route.subnet}`);
print(` Next hop: ${route.nextHop}`);
print(` Metric: ${route.metric}`);
}
}
} catch(err) {
print(`Error listing fallback routes: ${err}`);
}
```
This code:
1. Calls `mycelium_list_fallback_routes()` to get all fallback routes
2. Iterates through the routes and prints their details
## Sending Messages
Now let's look at how to send messages using the `mycelium_send_message.rhai` example.
```rhai
// API URL for Mycelium
let api_url = "http://localhost:1111";
// Send a message
print("\nSending a message:");
let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // Replace with the actual destination IP address
let topic = "test_topic";
let message = "Hello from Rhai sender!";
let deadline_secs = -10; // Seconds we wait for a reply
try {
print(`Attempting to send message to ${destination} on topic '${topic}'`);
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
print(`result: ${result}`);
print(`Message sent: ${result.success}`);
if result.id != "" {
print(`Message ID: ${result.id}`);
}
} catch(err) {
print(`Error sending message: ${err}`);
}
```
This code:
1. Sets the API URL for your Mycelium node
2. Specifies the destination IP address, topic, message content, and deadline
3. Calls `mycelium_send_message()` to send the message
4. Prints the result, including the message ID if successful
### Important Parameters for Sending Messages
- `api_url`: The URL of your Mycelium node's API
- `destination`: The IP address of the destination node
- `topic`: The topic to send the message on (must match what the receiver is listening for)
- `message`: The content of the message
- `deadline_secs`: Time in seconds to wait for a reply. Use a negative value if you don't want to wait for a reply.
## Receiving Messages
Now let's look at how to receive messages using the `mycelium_receive_message.rhai` example.
```rhai
// API URL for Mycelium
let api_url = "http://localhost:2222";
// Receive messages
print("\nReceiving messages:");
let receive_topic = "test_topic";
let wait_deadline_secs = 100;
print(`Listening for messages on topic '${receive_topic}'...`);
try {
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
if messages.is_empty() {
// print("No new messages received in this poll.");
} else {
print("Received a message:");
print(` Message id: ${messages.id}`);
print(` Message from: ${messages.srcIp}`);
print(` Topic: ${messages.topic}`);
print(` Payload: ${messages.payload}`);
}
} catch(err) {
print(`Error receiving messages: ${err}`);
}
print("Finished attempting to receive messages.");
```
This code:
1. Sets the API URL for your Mycelium node
2. Specifies the topic to listen on and how long to wait for messages
3. Calls `mycelium_receive_messages()` to receive messages
4. Processes and prints any received messages
### Important Parameters for Receiving Messages
- `api_url`: The URL of your Mycelium node's API
- `receive_topic`: The topic to listen for messages on (must match what the sender is using)
- `wait_deadline_secs`: Time in seconds to wait for messages to arrive. The function will block for this duration if no messages are immediately available.
## Complete Messaging Example
To set up a complete messaging system, you would typically run two instances of Mycelium (node A sender, node B receiver).
1. Run the `mycelium_receive_message.rhai` script to listen for messages. **Fill in the API address of node B**.
2. Run the `mycelium_send_message.rhai` script to send messages. **Fill in the API address of node A, and fill in the overlay address of node B as destination**.
### Setting Up the Receiver
First, start a Mycelium node and run the receiver script:
```rhai
// API URL for Mycelium
let api_url = "http://localhost:2222"; // Your receiver node's API URL
// Receive messages
let receive_topic = "test_topic";
let wait_deadline_secs = 100; // Wait up to 100 seconds for messages
print(`Listening for messages on topic '${receive_topic}'...`);
try {
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
if messages.is_empty() {
print("No new messages received in this poll.");
} else {
print("Received a message:");
print(` Message id: ${messages.id}`);
print(` Message from: ${messages.srcIp}`);
print(` Topic: ${messages.topic}`);
print(` Payload: ${messages.payload}`);
}
} catch(err) {
print(`Error receiving messages: ${err}`);
}
```
### Setting Up the Sender
Then, on another Mycelium node, run the sender script:
```rhai
// API URL for Mycelium
let api_url = "http://localhost:1111"; // Your sender node's API URL
// Send a message
let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // The receiver node's IP address
let topic = "test_topic"; // Must match the receiver's topic
let message = "Hello from Rhai sender!";
let deadline_secs = -10; // Don't wait for a reply
try {
print(`Attempting to send message to ${destination} on topic '${topic}'`);
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
print(`Message sent: ${result.success}`);
if result.id != "" {
print(`Message ID: ${result.id}`);
}
} catch(err) {
print(`Error sending message: ${err}`);
}
```
### Example: setting up 2 different Mycelium peers on same the host and sending/receiving a message
#### Obtain Mycelium
- Download the latest Mycelium binary from https://github.com/threefoldtech/mycelium/releases/
- Or compile from source
#### Setup
- Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples.
> `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin`
> `echo 'e81c5a76f42bd9a3c73fe0bb2196acdfb6348e99d0b01763a2e57ce3a4e8f5dd' > receiver.bin`
#### Start the nodes
- **Sender**: this node will have the API server hosted on `127.0.0.1:1111` and the JSON-RPC server on `127.0.0.1:8991`.
> `sudo ./mycelium --key-file sender.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:1111 --jsonrpc-addr 127.0.0.1:8991`
- **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`.
> `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://<UNDERLAY_IP_SENDER>:9651`
- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_send_message.rhai) example**.
#### Execute the examples
- First build by executing `./build_herdo.sh` from the SAL root directory
- `cd target/debug`
- Run the sender script: `sudo ./herodo --path ../../examples/mycelium/mycelium_send_message.rhai`
```
Executing: ../../examples/mycelium/mycelium_send_message.rhai
Sending a message:
Attempting to send message to 50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd on topic 'test_topic'
result: #{"id": "bfd47dc689a7b826"}
Message sent:
Message ID: bfd47dc689a7b826
Script executed successfull
```
- Run the receiver script: `sudo ./herodo --path ../../examples/mycelium/mycelium_receive_message.rhai`
```
Executing: ../../examples/mycelium/mycelium_receive_message.rhai
Receiving messages:
Listening for messages on topic 'test_topic'...
Received a message:
Message id: bfd47dc689a7b826
Message from: 45d:26e1:a413:9d08:80ce:71c6:a931:4315
Topic: dGVzdF90b3BpYw==
Payload: SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=
Finished attempting to receive messages.
Script executed successfully
```
> Decoding the payload `SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=` results in the expected `Hello from Rhai sender!` message. Mission succesful!

View File

@ -0,0 +1,133 @@
// Basic example of using the Mycelium client in Rhai
// API URL for Mycelium
let api_url = "http://localhost:8989";
// Get node information
print("Getting node information:");
try {
let node_info = mycelium_get_node_info(api_url);
print(`Node subnet: ${node_info.nodeSubnet}`);
print(`Node public key: ${node_info.nodePubkey}`);
} catch(err) {
print(`Error getting node info: ${err}`);
}
// List all peers
print("\nListing all peers:");
try {
let peers = mycelium_list_peers(api_url);
if peers.is_empty() {
print("No peers connected.");
} else {
for peer in peers {
print(`Peer Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`);
print(` Type: ${peer.type}`);
print(` Connection State: ${peer.connectionState}`);
print(` Bytes sent: ${peer.txBytes}`);
print(` Bytes received: ${peer.rxBytes}`);
}
}
} catch(err) {
print(`Error listing peers: ${err}`);
}
// Add a new peer
print("\nAdding a new peer:");
let new_peer_address = "tcp://65.21.231.58:9651";
try {
let result = mycelium_add_peer(api_url, new_peer_address);
print(`Peer added: ${result.success}`);
} catch(err) {
print(`Error adding peer: ${err}`);
}
// List selected routes
print("\nListing selected routes:");
try {
let routes = mycelium_list_selected_routes(api_url);
if routes.is_empty() {
print("No selected routes.");
} else {
for route in routes {
print(`Subnet: ${route.subnet}`);
print(` Next hop: ${route.nextHop}`);
print(` Metric: ${route.metric}`);
}
}
} catch(err) {
print(`Error listing routes: ${err}`);
}
// List fallback routes
print("\nListing fallback routes:");
try {
let routes = mycelium_list_fallback_routes(api_url);
if routes.is_empty() {
print("No fallback routes.");
} else {
for route in routes {
print(`Subnet: ${route.subnet}`);
print(` Next hop: ${route.nextHop}`);
print(` Metric: ${route.metric}`);
}
}
} catch(err) {
print(`Error listing fallback routes: ${err}`);
}
// Send a message
// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS
// -----------------------------------------------------//
// print("\nSending a message:");
// let destination = < FILL IN CORRECT DEST IP >
// let topic = "test";
// let message = "Hello from Rhai!";
// let deadline_secs = 60;
// try {
// let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
// print(`Message sent: ${result.success}`);
// if result.id {
// print(`Message ID: ${result.id}`);
// }
// } catch(err) {
// print(`Error sending message: ${err}`);
// }
// Receive messages
// RECEIVING MESSAGES SHOULD BE DONE ON THE DESTINATION NODE FROM THE CALL ABOVE
// -----------------------------------------------------------------------------//
// print("\nReceiving messages:");
// let receive_topic = "test";
// let count = 5;
// try {
// let messages = mycelium_receive_messages(api_url, receive_topic, count);
// if messages.is_empty() {
// print("No messages received.");
// } else {
// for msg in messages {
// print(`Message from: ${msg.source}`);
// print(` Topic: ${msg.topic}`);
// print(` Content: ${msg.content}`);
// print(` Timestamp: ${msg.timestamp}`);
// }
// }
// } catch(err) {
// print(`Error receiving messages: ${err}`);
// }
// Remove a peer
print("\nRemoving a peer:");
let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier
try {
let result = mycelium_remove_peer(api_url, peer_id);
print(`Peer removed: ${result.success}`);
} catch(err) {
print(`Error removing peer: ${err}`);
}

View File

@ -0,0 +1,31 @@
// Script to receive Mycelium messages
// API URL for Mycelium
let api_url = "http://localhost:2222";
// Receive messages
// This script will listen for messages on a specific topic.
// Ensure the sender script is using the same topic.
// -----------------------------------------------------------------------------//
print("\nReceiving messages:");
let receive_topic = "test_topic";
let wait_deadline_secs = 100;
print(`Listening for messages on topic '${receive_topic}'...`);
try {
let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs);
if messages.is_empty() {
// print("No new messages received in this poll.");
} else {
print("Received a message:");
print(` Message id: ${messages.id}`);
print(` Message from: ${messages.srcIp}`);
print(` Topic: ${messages.topic}`);
print(` Payload: ${messages.payload}`);
}
} catch(err) {
print(`Error receiving messages: ${err}`);
}
print("Finished attempting to receive messages.");

View File

@ -0,0 +1,25 @@
// Script to send a Mycelium message
// API URL for Mycelium
let api_url = "http://localhost:1111";
// Send a message
// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS
// -----------------------------------------------------//
print("\nSending a message:");
let destination = "50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd"; // IMPORTANT: Replace with the actual destination IP address
let topic = "test_topic";
let message = "Hello from Rhai sender!";
let deadline_secs = -10; // Seconds we wait for a reply
try {
print(`Attempting to send message to ${destination} on topic '${topic}'`);
let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs);
print(`result: ${result}`);
print(`Message sent: ${result.success}`);
if result.id != "" {
print(`Message ID: ${result.id}`);
}
} catch(err) {
print(`Error sending message: ${err}`);
}

View File

@ -63,15 +63,6 @@ try {
for log in logs { for log in logs {
print(log); print(log);
} }
// Or to get all logs (uncomment if needed)
// print("\nGetting all logs:");
// let all_logs = zinit_logs_all(socket_path);
//
// for log in all_logs {
// print(log);
// }
// Clean up // Clean up
print("\nCleaning up:"); print("\nCleaning up:");
let stop_result = zinit_stop(socket_path, new_service); let stop_result = zinit_stop(socket_path, new_service);

View File

@ -25,7 +25,7 @@ if create_key_space(space_name1, password) {
print("Testing creating keypairs in current space..."); print("Testing creating keypairs in current space...");
let keypair1_name = "session_keypair1"; let keypair1_name = "session_keypair1";
if create_keypair(keypair1_name, password) { if create_keypair(keypair1_name) {
print(`✓ Keypair "${keypair1_name}" created successfully in space "${space_name1}"`); print(`✓ Keypair "${keypair1_name}" created successfully in space "${space_name1}"`);
} else { } else {
print(`✗ Failed to create keypair "${keypair1_name}" in space "${space_name1}"`); print(`✗ Failed to create keypair "${keypair1_name}" in space "${space_name1}"`);
@ -60,7 +60,7 @@ if create_key_space(space_name1, password) {
// Create a keypair in the second space // Create a keypair in the second space
let keypair2_name = "session_keypair2"; let keypair2_name = "session_keypair2";
if create_keypair(keypair2_name, password) { if create_keypair(keypair2_name) {
print(`✓ Keypair "${keypair2_name}" created successfully in space "${space_name2}"`); print(`✓ Keypair "${keypair2_name}" created successfully in space "${space_name2}"`);
} else { } else {
print(`✗ Failed to create keypair "${keypair2_name}" in space "${space_name2}"`); print(`✗ Failed to create keypair "${keypair2_name}" in space "${space_name2}"`);
@ -109,7 +109,7 @@ if create_key_space(space_name1, password) {
// Attempt to create a keypair // Attempt to create a keypair
let create_success = false; let create_success = false;
try { try {
create_success = create_keypair("no_space_keypair", password); create_success = create_keypair("test_keypair_2");
} catch(err) { } catch(err) {
print(`✓ Caught expected error for creating keypair without active space: ${err}`); print(`✓ Caught expected error for creating keypair without active space: ${err}`);
} }

View File

@ -22,7 +22,7 @@ if create_key_space(space_name, password) {
print(`✓ Key space "${space_name}" created successfully`); print(`✓ Key space "${space_name}" created successfully`);
// Create sender keypair // Create sender keypair
if create_keypair(sender_name, password) { if create_keypair(sender_name) {
print(`✓ Sender keypair "${sender_name}" created successfully`); print(`✓ Sender keypair "${sender_name}" created successfully`);
} else { } else {
print(`✗ Failed to create sender keypair "${sender_name}"`); print(`✗ Failed to create sender keypair "${sender_name}"`);
@ -30,7 +30,7 @@ if create_key_space(space_name, password) {
} }
// Create recipient keypair // Create recipient keypair
if create_keypair(recipient_name, password) { if create_keypair(recipient_name) {
print(`✓ Recipient keypair "${recipient_name}" created successfully`); print(`✓ Recipient keypair "${recipient_name}" created successfully`);
} else { } else {
print(`✗ Failed to create recipient keypair "${recipient_name}"`); print(`✗ Failed to create recipient keypair "${recipient_name}"`);

View File

@ -28,7 +28,7 @@ try {
if create_key_space("test_space", "password") { if create_key_space("test_space", "password") {
print("✓ Key space created successfully"); print("✓ Key space created successfully");
if create_keypair(keypair_name, "password") { if create_keypair(keypair_name) {
print("✓ Keypair created successfully"); print("✓ Keypair created successfully");
// Test getting the public key // Test getting the public key
@ -82,11 +82,11 @@ try {
let keypair1_name = "keypair1"; let keypair1_name = "keypair1";
let keypair2_name = "keypair2"; let keypair2_name = "keypair2";
if create_keypair(keypair1_name, password) { if create_keypair(keypair1_name) {
print(`✓ Keypair "${keypair1_name}" created successfully`); print(`✓ Keypair "${keypair1_name}" created successfully`);
} }
if create_keypair(keypair2_name, password) { if create_keypair(keypair2_name) {
print(`✓ Keypair "${keypair2_name}" created successfully`); print(`✓ Keypair "${keypair2_name}" created successfully`);
} }
@ -128,7 +128,7 @@ try {
print("Testing creating keypairs in current space..."); print("Testing creating keypairs in current space...");
let keypair1_name = "session_keypair1"; let keypair1_name = "session_keypair1";
if create_keypair(keypair1_name, password) { if create_keypair(keypair1_name) {
print(`✓ Keypair "${keypair1_name}" created successfully in space "${space_name1}"`); print(`✓ Keypair "${keypair1_name}" created successfully in space "${space_name1}"`);
} }
@ -165,12 +165,12 @@ try {
print(`✓ Key space "${space_name}" created successfully`); print(`✓ Key space "${space_name}" created successfully`);
// Create sender keypair // Create sender keypair
if create_keypair(sender_name, password) { if create_keypair(sender_name) {
print(`✓ Sender keypair "${sender_name}" created successfully`); print(`✓ Sender keypair "${sender_name}" created successfully`);
} }
// Create recipient keypair // Create recipient keypair
if create_keypair(recipient_name, password) { if create_keypair(recipient_name) {
print(`✓ Recipient keypair "${recipient_name}" created successfully`); print(`✓ Recipient keypair "${recipient_name}" created successfully`);
} }

View File

@ -4,7 +4,10 @@
# This script runs all the Rhai tests in the rhai_tests directory # This script runs all the Rhai tests in the rhai_tests directory
# Set the base directory # Set the base directory
BASE_DIR="src/rhai_tests" BASE_DIR="."
# Path to herodo executable (assuming debug build)
HERODO_CMD="$HOME/hero/bin/herodo"
# Define colors for output # Define colors for output
GREEN='\033[0;32m' GREEN='\033[0;32m'
@ -27,7 +30,7 @@ run_tests_in_dir() {
# Check if the directory has a run_all_tests.rhai script # Check if the directory has a run_all_tests.rhai script
if [ -f "${dir}/run_all_tests.rhai" ]; then if [ -f "${dir}/run_all_tests.rhai" ]; then
echo "Using module's run_all_tests.rhai script" echo "Using module's run_all_tests.rhai script"
herodo --path "${dir}/run_all_tests.rhai" ${HERODO_CMD} --path "${dir}/run_all_tests.rhai"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo -e "${GREEN}✓ All tests passed for module: ${module_name}${NC}" echo -e "${GREEN}✓ All tests passed for module: ${module_name}${NC}"
@ -43,7 +46,7 @@ run_tests_in_dir() {
for test_file in $test_files; do for test_file in $test_files; do
echo "Running test: $(basename $test_file)" echo "Running test: $(basename $test_file)"
herodo --path "$test_file" ${HERODO_CMD} --path "$test_file"
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
all_passed=false all_passed=false

86
src/git/README.md Normal file
View File

@ -0,0 +1,86 @@
# SAL `git` Module
The `git` module in SAL provides comprehensive functionalities for interacting with Git repositories. It offers both high-level abstractions for common Git workflows and a flexible executor for running arbitrary Git commands with integrated authentication.
This module is central to SAL's capabilities for managing source code, enabling automation of development tasks, and integrating with version control systems.
## Core Components
The module is primarily composed of two main parts:
1. **Repository and Tree Management (`git.rs`)**: Defines `GitTree` and `GitRepo` structs for a more structured, object-oriented approach to Git operations.
2. **Command Execution with Authentication (`git_executor.rs`)**: Provides `GitExecutor` for running any Git command, with a focus on handling authentication via configurations stored in Redis.
### 1. Repository and Tree Management (`GitTree` & `GitRepo`)
These components allow for programmatic management of Git repositories.
* **`GitTree`**: Represents a directory (base path) that can contain multiple Git repositories.
* `new(base_path)`: Creates a new `GitTree` instance for the given base path.
* `list()`: Lists all Git repositories found under the base path.
* `find(pattern)`: Finds repositories within the tree that match a given name pattern (supports wildcards).
* `get(path_or_url)`: Retrieves `GitRepo` instances. If a local path/pattern is given, it finds existing repositories. If a Git URL is provided, it will clone the repository into a structured path (`base_path/server/account/repo`) if it doesn't already exist.
* **`GitRepo`**: Represents a single Git repository.
* `new(path)`: Creates a `GitRepo` instance for the repository at the given path.
* `path()`: Returns the local file system path to the repository.
* `has_changes()`: Checks if the repository has uncommitted local changes.
* `pull()`: Pulls the latest changes from the remote. Fails if local changes exist.
* `reset()`: Performs a hard reset (`git reset --hard HEAD`) and cleans untracked files (`git clean -fd`).
* `commit(message)`: Stages all changes (`git add .`) and commits them with the given message.
* `push()`: Pushes committed changes to the remote repository.
* **`GitError`**: A comprehensive enum for errors related to `GitTree` and `GitRepo` operations (e.g., Git not installed, invalid URL, repository not found, local changes exist).
* **`parse_git_url(url)`**: A utility function to parse HTTPS and SSH Git URLs into server, account, and repository name components.
### 2. Command Execution with Authentication (`GitExecutor`)
`GitExecutor` is designed for flexible execution of any Git command, with a special emphasis on handling authentication for remote operations.
* **`GitExecutor::new()` / `GitExecutor::default()`**: Creates a new executor instance.
* **`GitExecutor::init()`**: Initializes the executor by attempting to load authentication configurations from Redis (key: `herocontext:git`). If Redis is unavailable or the config is missing, it proceeds without specific auth configurations, relying on system defaults.
* **`GitExecutor::execute(args: &[&str])`**: The primary method to run a Git command (e.g., `executor.execute(&["clone", "https://github.com/user/repo.git", "myrepo"])`).
* It intelligently attempts to apply authentication based on the command and the loaded configuration.
#### Authentication Configuration (`herocontext:git` in Redis)
The `GitExecutor` can load its authentication settings from a JSON object stored in Redis under the key `herocontext:git`. The structure is as follows:
```json
{
"status": "ok", // or "error"
"auth": {
"github.com": {
"sshagent": true // Use SSH agent for github.com
},
"gitlab.example.com": {
"key": "/path/to/ssh/key_for_gitlab" // Use specific SSH key
},
"dev.azure.com": {
"username": "your_username",
"password": "your_pat_or_password" // Use HTTPS credentials
}
// ... other server configurations
}
}
```
* **Authentication Methods Supported**:
* **SSH Agent**: If `sshagent: true` is set for a server, and an SSH agent is loaded with identities.
* **SSH Key**: If `key: "/path/to/key"` is specified, `GIT_SSH_COMMAND` is used to point to this key.
* **Username/Password (HTTPS)**: If `username` and `password` are provided, HTTPS URLs are rewritten to include these credentials (e.g., `https://user:pass@server/repo.git`).
* **`GitExecutorError`**: An enum for errors specific to `GitExecutor`, including command failures, Redis errors, JSON parsing issues, and authentication problems (e.g., `SshAgentNotLoaded`, `InvalidAuthConfig`).
## Usage with `herodo`
The `herodo` CLI tool likely leverages `GitExecutor` to provide its scriptable Git functionalities. This allows Rhai scripts executed by `herodo` to perform Git operations using the centrally managed authentication configurations from Redis, promoting secure and consistent access to Git repositories.
## Error Handling
Both `git.rs` and `git_executor.rs` define their own specific error enums (`GitError` and `GitExecutorError` respectively) to provide detailed information about issues encountered during Git operations. These errors cover a wide range of scenarios from command execution failures to authentication problems and invalid configurations.
## Summary
The `git` module offers a powerful and flexible interface to Git, catering to both simple, high-level repository interactions and complex, authenticated command execution scenarios. Its integration with Redis for authentication configuration makes it particularly well-suited for automated systems and tools like `herodo`.

View File

@ -48,6 +48,7 @@ pub mod text;
pub mod virt; pub mod virt;
pub mod vault; pub mod vault;
pub mod zinit_client; pub mod zinit_client;
pub mod mycelium;
// Version information // Version information
/// Returns the version of the SAL library /// Returns the version of the SAL library

126
src/mycelium/README.md Normal file
View File

@ -0,0 +1,126 @@
# SAL Mycelium Module (`sal::mycelium`)
## Overview
The `sal::mycelium` module provides a client interface for interacting with a [Mycelium](https://mycelium.com/) node's HTTP API. Mycelium is a decentralized networking project, and this SAL module allows Rust applications and `herodo` Rhai scripts to manage and communicate over a Mycelium network.
The module enables operations such as:
- Querying node status and information.
- Managing peer connections (listing, adding, removing).
- Inspecting routing tables (selected and fallback routes).
- Sending messages to other Mycelium nodes.
- Receiving messages from subscribed topics.
All interactions with the Mycelium API are performed asynchronously.
## Key Design Points
- **Async HTTP Client**: Leverages `reqwest` for asynchronous HTTP requests to the Mycelium node's API, ensuring non-blocking operations suitable for concurrent applications.
- **JSON Interaction**: Expects and processes JSON-formatted data from the Mycelium API, using `serde_json::Value` for flexible data handling.
- **Base64 Encoding**: Message payloads and topics are Base64 encoded/decoded when communicating with the Mycelium API, as per its expected format.
- **Rhai Scriptability**: All core functionalities are exposed to Rhai scripts via `herodo` through the `sal::rhai::mycelium` bridge. This allows for easy automation of Mycelium network tasks.
- **Error Handling**: Provides clear error messages, converting HTTP and parsing errors into `String` results in Rust, which are then translated to `EvalAltResult` for Rhai.
- **Tokio Runtime Management**: For Rhai script execution, a Tokio runtime is managed internally by the wrapper functions to bridge Rhai's synchronous world with the asynchronous Rust client.
## Rhai Scripting with `herodo`
The `sal::mycelium` module can be scripted using `herodo`. The following functions are available in Rhai, typically prefixed with `mycelium_`:
All functions take `api_url` (String) as their first argument, which is the base URL of the Mycelium node's HTTP API (e.g., `"http://localhost:7777"`).
- `mycelium_get_node_info(api_url: String) -> Dynamic`
- Retrieves general information about the Mycelium node.
- Returns a dynamic object (map) representing the JSON response.
- `mycelium_list_peers(api_url: String) -> Dynamic`
- Lists all peers currently connected to the node.
- Returns a dynamic array of peer information objects.
- `mycelium_add_peer(api_url: String, peer_address: String) -> Dynamic`
- Adds a new peer to the node.
- `peer_address`: The endpoint address of the peer to add (e.g., `"tcp://192.168.1.10:7778"`).
- Returns a success status or an error.
- `mycelium_remove_peer(api_url: String, peer_id: String) -> Dynamic`
- Removes a peer from the node.
- `peer_id`: The ID of the peer to remove.
- Returns a success status or an error.
- `mycelium_list_selected_routes(api_url: String) -> Dynamic`
- Lists the currently selected (active) routes in the node's routing table.
- Returns a dynamic array of route objects.
- `mycelium_list_fallback_routes(api_url: String) -> Dynamic`
- Lists the fallback routes in the node's routing table.
- Returns a dynamic array of route objects.
- `mycelium_send_message(api_url: String, destination: String, topic: String, message: String, reply_deadline_secs: Int) -> Dynamic`
- Sends a message to a specific destination over the Mycelium network.
- `destination`: The Mycelium address of the recipient node.
- `topic`: The topic for the message (will be Base64 encoded).
- `message`: The content of the message (will be Base64 encoded).
- `reply_deadline_secs`: An integer specifying the timeout in seconds to wait for a reply. If negative, no reply is waited for.
- Returns a response from the Mycelium API, potentially including a reply if waited for.
- `mycelium_receive_messages(api_url: String, topic: String, wait_deadline_secs: Int) -> Dynamic`
- Subscribes to a topic and waits for messages.
- `topic`: The topic to subscribe to (will be Base64 encoded).
- `wait_deadline_secs`: An integer specifying the maximum time in seconds to wait for a message. If negative, waits indefinitely (or until the API's default timeout).
- Returns an array of received messages, or an empty array if the deadline is met before messages arrive.
### Rhai Example
```rhai
// Assuming a Mycelium node is running and accessible at http://localhost:7777
let api_url = "http://localhost:7777";
// Get Node Info
print("Fetching node info...");
let node_info = mycelium_get_node_info(api_url);
if node_info.is_ok() {
print(`Node Info: ${node_info}`);
} else {
print(`Error fetching node info: ${node_info}`);
}
// List Peers
print("\nListing peers...");
let peers = mycelium_list_peers(api_url);
if peers.is_ok() {
print(`Peers: ${peers}`);
} else {
print(`Error listing peers: ${peers}`);
}
// Example: Send a message (destination and topic are illustrative)
let dest_addr = "some_mycelium_destination_address"; // Replace with actual address
let msg_topic = "sal/test_topic";
let msg_content = "Hello from SAL Mycelium via Rhai!";
print(`\nSending message to '${dest_addr}' on topic '${msg_topic}'...`);
// No reply wait (deadline = -1)
let send_result = mycelium_send_message(api_url, dest_addr, msg_topic, msg_content, -1);
if send_result.is_ok() {
print(`Send Result: ${send_result}`);
} else {
print(`Error sending message: ${send_result}`);
}
// Example: Receive messages (topic is illustrative)
// This will block for up to 10 seconds, or until a message arrives.
print(`\nAttempting to receive messages on topic '${msg_topic}' for 10 seconds...`);
let received = mycelium_receive_messages(api_url, msg_topic, 10);
if received.is_ok() {
if received.len() > 0 {
print(`Received Messages: ${received}`);
} else {
print("No messages received within the deadline.");
}
} else {
print(`Error receiving messages: ${received}`);
}
print("\nMycelium Rhai script finished.");
```
This module facilitates integration with Mycelium networks, enabling automation of peer management, message exchange, and network monitoring through `herodo` scripts or direct Rust integration.

313
src/mycelium/mod.rs Normal file
View File

@ -0,0 +1,313 @@
use base64::{
engine::general_purpose,
Engine as _,
};
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;
/// Get information about the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The node information as a JSON value, or an error message
pub async fn get_node_info(api_url: &str) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/admin", api_url);
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// List all peers connected to the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The list of peers as a JSON value, or an error message
pub async fn list_peers(api_url: &str) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/admin/peers", api_url);
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// Add a new peer to the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `peer_address` - The address of the peer to add
///
/// # Returns
///
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
pub async fn add_peer(api_url: &str, peer_address: &str) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/admin/peers", api_url);
let response = client
.post(&url)
.json(&serde_json::json!({
"endpoint": peer_address
}))
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if status == reqwest::StatusCode::NO_CONTENT {
// Successfully added, but no content to parse
return Ok(serde_json::json!({"success": true}));
}
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
// For other success statuses that might have a body
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// Remove a peer from the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `peer_id` - The ID of the peer to remove
///
/// # Returns
///
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result<Value, String> {
let client = Client::new();
let peer_id_url_encoded = urlencoding::encode(peer_id);
let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id_url_encoded);
let response = client
.delete(&url)
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if status == reqwest::StatusCode::NO_CONTENT {
// Successfully removed, but no content to parse
return Ok(serde_json::json!({"success": true}));
}
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// List all selected routes in the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The list of selected routes as a JSON value, or an error message
pub async fn list_selected_routes(api_url: &str) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/admin/routes/selected", api_url);
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// List all fallback routes in the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The list of fallback routes as a JSON value, or an error message
pub async fn list_fallback_routes(api_url: &str) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/admin/routes/fallback", api_url);
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// Send a message to a destination via the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `destination` - The destination address
/// * `topic` - The message topic
/// * `message` - The message content
/// * `reply_deadline` - The deadline in seconds; pass `-1` to indicate we do not want to wait on a reply
///
/// # Returns
///
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
pub async fn send_message(
api_url: &str,
destination: &str,
topic: &str,
message: &str,
reply_deadline: Option<Duration>, // This is passed in URL query
) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/messages", api_url);
let mut request = client.post(&url);
if let Some(deadline) = reply_deadline {
request = request.query(&[("reply_timeout", deadline.as_secs())]);
}
let response = request
.json(&serde_json::json!({
"dst": { "ip": destination },
"topic": general_purpose::STANDARD.encode(topic),
"payload": general_purpose::STANDARD.encode(message)
}))
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}
/// Receive messages from a topic via the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `topic` - The message topic
/// * `wait_deadline` - Time we wait for receiving a message
///
/// # Returns
///
/// * `Result<Value, String>` - The received messages as a JSON value, or an error message
pub async fn receive_messages(
api_url: &str,
topic: &str,
wait_deadline: Option<Duration>,
) -> Result<Value, String> {
let client = Client::new();
let url = format!("{}/api/v1/messages", api_url);
let mut request = client.get(&url);
if let Some(deadline) = wait_deadline {
request = request.query(&[
("topic", general_purpose::STANDARD.encode(topic)),
("timeout", deadline.as_secs().to_string()),
])
} else {
request = request.query(&[("topic", general_purpose::STANDARD.encode(topic))])
};
let response = request
.send()
.await
.map_err(|e| format!("Failed to send request: {}", e))?;
let status = response.status();
if !status.is_success() {
return Err(format!("Request failed with status: {}", status));
}
let result: Value = response
.json()
.await
.map_err(|e| format!("Failed to parse response: {}", e))?;
Ok(result)
}

245
src/os/README.md Normal file
View File

@ -0,0 +1,245 @@
# SAL OS Module (`sal::os`)
The `sal::os` module provides a comprehensive suite of operating system interaction utilities. It aims to offer a cross-platform abstraction layer for common OS-level tasks, simplifying system programming in Rust.
This module is composed of three main sub-modules:
- [`fs`](#fs): File system operations.
- [`download`](#download): File downloading and basic installation.
- [`package`](#package): System package management.
## Key Design Points
The `sal::os` module is engineered with several core principles to provide a robust and developer-friendly interface for OS interactions:
- **Cross-Platform Abstraction**: A primary goal is to offer a unified API for common OS tasks, smoothing over differences between operating systems (primarily Linux and macOS). While it strives for abstraction, it leverages platform-specific tools (e.g., `rsync` on Linux, `robocopy` on Windows for `fs::copy` or `fs::rsync`; `apt` on Debian-based systems, `brew` on macOS for `package` management) for optimal performance and behavior when necessary.
- **Modular Structure**: Functionality is organized into logical sub-modules:
- `fs`: For comprehensive file and directory manipulation.
- `download`: For retrieving files from URLs, with support for extraction and basic installation.
- `package`: For interacting with system package managers.
- **Granular Error Handling**: Each sub-module features custom error enums (`FsError`, `DownloadError`, `PackageError`) to provide specific and actionable feedback, aiding in debugging and robust error management.
- **Sensible Defaults and Defensive Operations**: Many functions are designed to be "defensive," e.g., `mkdir` creates parent directories if they don't exist and doesn't fail if the directory already exists. `delete` doesn't error if the target is already gone.
- **Facade for Simplicity**: The `package` sub-module uses a `PackHero` facade to provide a simple entry point for common package operations, automatically detecting the underlying OS and package manager.
- **Rhai Scriptability**: A significant portion of the `sal::os` module's functionality is exposed to Rhai scripts via `herodo`, enabling powerful automation of OS-level tasks.
## `fs` - File System Operations
The `fs` sub-module (`sal::os::fs`) offers a robust set of functions for interacting with the file system.
**Key Features:**
* **Error Handling**: A custom `FsError` enum for detailed error reporting on file system operations.
* **File Operations**:
* `copy(src, dest)`: Copies files and directories, with support for wildcards and recursive copying. Uses platform-specific commands (`cp -R`, `robocopy /MIR`).
* `exist(path)`: Checks if a file or directory exists.
* `find_file(dir, filename_pattern)`: Finds a single file in a directory, supporting wildcards.
* `find_files(dir, filename_pattern)`: Finds multiple files in a directory, supporting wildcards.
* `file_size(path)`: Returns the size of a file in bytes.
* `file_read(path)`: Reads the entire content of a file into a string.
* `file_write(path, content)`: Writes content to a file, overwriting if it exists, and creating parent directories if needed.
* `file_write_append(path, content)`: Appends content to a file, creating it and parent directories if needed.
* **Directory Operations**:
* `find_dir(parent_dir, dirname_pattern)`: Finds a single directory within a parent directory, supporting wildcards.
* `find_dirs(parent_dir, dirname_pattern)`: Finds multiple directories recursively within a parent directory, supporting wildcards.
* `delete(path)`: Deletes files or directories.
* `mkdir(path)`: Creates a directory, including parent directories if necessary.
* `rsync(src, dest)`: Synchronizes directories using platform-specific commands (`rsync -a --delete`, `robocopy /MIR`).
* `chdir(path)`: Changes the current working directory.
* **Path Operations**:
* `mv(src, dest)`: Moves or renames files and directories. Handles cross-device moves by falling back to copy-then-delete.
* **Command Utilities**:
* `which(command_name)`: Checks if a command exists in the system's PATH and returns its path.
**Usage Example (fs):**
```rust
use sal::os::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
if !fs::exist("my_dir") {
fs::mkdir("my_dir")?;
println!("Created directory 'my_dir'");
}
fs::file_write("my_dir/example.txt", "Hello from SAL!")?;
let content = fs::file_read("my_dir/example.txt")?;
println!("File content: {}", content);
Ok(())
}
```
## `download` - File Downloading and Installation
The `download` sub-module (`sal::os::download`) provides utilities for downloading files from URLs and performing basic installation tasks.
**Key Features:**
* **Error Handling**: A custom `DownloadError` enum for download-specific errors.
* **File Downloading**:
* `download(url, dest_dir, min_size_kb)`: Downloads a file to a specified directory.
* Uses `curl` with progress display.
* Supports minimum file size checks.
* Automatically extracts common archive formats (`.tar.gz`, `.tgz`, `.tar`, `.zip`) into `dest_dir`.
* `download_file(url, dest_file_path, min_size_kb)`: Downloads a file to a specific file path without automatic extraction.
* **File Permissions**:
* `chmod_exec(path)`: Makes a file executable (equivalent to `chmod +x` on Unix-like systems).
* **Download and Install**:
* `download_install(url, min_size_kb)`: Downloads a file (to `/tmp/`) and attempts to install it if it's a supported package format.
* Currently supports `.deb` packages on Debian-based systems.
* For `.deb` files, it uses `sudo dpkg --install` and attempts `sudo apt-get install -f -y` to fix dependencies if needed.
* Handles archives by extracting them to `/tmp/` first.
**Usage Example (download):**
```rust
use sal::os::download;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let archive_url = "https://example.com/my_archive.tar.gz";
let output_dir = "/tmp/my_app";
// Download and extract an archive
let extracted_path = download::download(archive_url, output_dir, 1024)?; // Min 1MB
println!("Archive extracted to: {}", extracted_path);
// Download a script and make it executable
let script_url = "https://example.com/my_script.sh";
let script_path = "/tmp/my_script.sh";
download::download_file(script_url, script_path, 0)?;
download::chmod_exec(script_path)?;
println!("Script downloaded and made executable at: {}", script_path);
Ok(())
}
```
## `package` - System Package Management
The `package` sub-module (`sal::os::package`) offers an abstraction layer for interacting with system package managers like APT (for Debian/Ubuntu) and Homebrew (for macOS).
**Key Features:**
* **Error Handling**: A custom `PackageError` enum.
* **Platform Detection**: Identifies the current OS (Ubuntu, macOS, or Unknown) to use the appropriate package manager.
* **`PackageManager` Trait**: Defines a common interface for package operations:
* `install(package_name)`
* `remove(package_name)`
* `update()` (updates package lists)
* `upgrade()` (upgrades all installed packages)
* `list_installed()`
* `search(query)`
* `is_installed(package_name)`
* **Implementations**:
* `AptPackageManager`: For Debian/Ubuntu systems (uses `apt-get`, `dpkg`).
* `BrewPackageManager`: For macOS systems (uses `brew`).
* **`PackHero` Facade**: A simple entry point to access package management functions in a platform-agnostic way.
* `PackHero::new().install("nginx")?`
**Usage Example (package):**
```rust
use sal::os::package::PackHero;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let pack_hero = PackHero::new();
// Check if a package is installed
if !pack_hero.is_installed("htop")? {
println!("htop is not installed. Attempting to install...");
pack_hero.install("htop")?;
println!("htop installed successfully.");
} else {
println!("htop is already installed.");
}
// Update package lists
println!("Updating package lists...");
pack_hero.update()?;
println!("Package lists updated.");
Ok(())
}
```
## Rhai Scripting with `herodo`
The `sal::os` module is extensively scriptable via `herodo`, allowing for automation of various operating system tasks directly from Rhai scripts. The `sal::rhai::os` module registers the necessary functions.
### File System (`fs`) Functions
- `copy(src: String, dest: String) -> String`: Copies files/directories (supports wildcards).
- `exist(path: String) -> bool`: Checks if a file or directory exists.
- `find_file(dir: String, filename_pattern: String) -> String`: Finds a single file in `dir` matching `filename_pattern`.
- `find_files(dir: String, filename_pattern: String) -> Array`: Finds multiple files in `dir` (recursive).
- `find_dir(parent_dir: String, dirname_pattern: String) -> String`: Finds a single directory in `parent_dir`.
- `find_dirs(parent_dir: String, dirname_pattern: String) -> Array`: Finds multiple directories in `parent_dir` (recursive).
- `delete(path: String) -> String`: Deletes a file or directory.
- `mkdir(path: String) -> String`: Creates a directory (and parents if needed).
- `file_size(path: String) -> Int`: Returns file size in bytes.
- `rsync(src: String, dest: String) -> String`: Synchronizes directories.
- `chdir(path: String) -> String`: Changes the current working directory.
- `file_read(path: String) -> String`: Reads entire file content.
- `file_write(path: String, content: String) -> String`: Writes content to a file (overwrites).
- `file_write_append(path: String, content: String) -> String`: Appends content to a file.
- `mv(src: String, dest: String) -> String`: Moves/renames a file or directory.
- `which(command_name: String) -> String`: Checks if a command exists in PATH and returns its path.
- `cmd_ensure_exists(commands: String) -> String`: Ensures one or more commands (comma-separated) exist in PATH; throws an error if any are missing.
### Download Functions
- `download(url: String, dest_dir: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_dir`, extracts common archives.
- `download_file(url: String, dest_file_path: String, min_size_kb: Int) -> String`: Downloads from `url` to `dest_file_path` (no extraction).
- `download_install(url: String, min_size_kb: Int) -> String`: Downloads and attempts to install (e.g., `.deb` packages).
- `chmod_exec(path: String) -> String`: Makes a file executable (`chmod +x`).
### Package Management Functions
- `package_install(package_name: String) -> String`: Installs a package.
- `package_remove(package_name: String) -> String`: Removes a package.
- `package_update() -> String`: Updates package lists.
- `package_upgrade() -> String`: Upgrades all installed packages.
- `package_list() -> Array`: Lists all installed packages.
- `package_search(query: String) -> Array`: Searches for packages.
- `package_is_installed(package_name: String) -> bool`: Checks if a package is installed.
- `package_set_debug(debug: bool) -> bool`: Enables/disables debug logging for package operations.
- `package_platform() -> String`: Returns the detected package platform (e.g., "Ubuntu", "MacOS").
### Rhai Example
```rhai
// File system operations
let test_dir = "/tmp/sal_os_rhai_test";
if exist(test_dir) {
delete(test_dir);
}
mkdir(test_dir);
print(`Created directory: ${test_dir}`);
file_write(`${test_dir}/message.txt`, "Hello from Rhai OS module!");
let content = file_read(`${test_dir}/message.txt`);
print(`File content: ${content}`);
// Download operation (example URL, may not be active)
// let script_url = "https://raw.githubusercontent.com/someuser/somescript/main/script.sh";
// let script_path = `${test_dir}/downloaded_script.sh`;
// try {
// download_file(script_url, script_path, 0);
// chmod_exec(script_path);
// print(`Downloaded and made executable: ${script_path}`);
// } catch (e) {
// print(`Download example failed (this is okay for a test): ${e}`);
// }
// Package management (illustrative, requires sudo for install/remove/update)
print(`Package platform: ${package_platform()}`);
if !package_is_installed("htop") {
print("htop is not installed.");
// package_install("htop"); // Would require sudo
} else {
print("htop is already installed.");
}
print("OS module Rhai script finished.");
```
This module provides a powerful and convenient way to handle common OS-level tasks within your Rust applications.

View File

@ -1,75 +1,150 @@
# Process Module # SAL Process Module (`sal::process`)
====================
## Overview
The process module is responsible for managing and running system processes.
## Usage The `process` module in the SAL (System Abstraction Layer) library provides a robust and cross-platform interface for creating, managing, and interacting with system processes. It is divided into two main sub-modules: `run` for command and script execution, and `mgmt` for process management tasks like listing, finding, and terminating processes.
To use the process module, import it in your Rust file and call the desired functions.
## Functions ## Core Functionalities
### mgmt.rs
The mgmt.rs file contains functions for managing system processes.
### run.rs ### 1. Command and Script Execution (`run.rs`)
The run.rs file contains functions for running system processes.
#### Input Flexibility The `run.rs` sub-module offers flexible ways to execute external commands and multi-line scripts.
The `run` function in run.rs is designed to be flexible with its input:
1. **One-liner Commands**: If the input is a single line, it's treated as a command with arguments. #### `RunBuilder`
```rust
run("ls -la"); // Runs the 'ls -la' command
```
2. **Multi-line Scripts**: If the input contains newlines, it's treated as a script. The script is automatically dedented using the `dedent` function from `src/text/dedent.rs` before execution. The primary interface for execution is the `RunBuilder`, obtained via `sal::process::run("your_command_or_script")`. It allows for fluent configuration:
```rust
run(" echo 'Hello'\n ls -la"); // Common indentation is removed before execution - `.die(bool)`: If `true` (default), an error is returned if the command fails. If `false`, a `CommandResult` with `success: false` is returned instead.
``` - `.silent(bool)`: If `true` (default is `false`), suppresses `stdout` and `stderr` from being printed to the console during execution. Output is still captured in `CommandResult`.
- `.async_exec(bool)`: If `true` (default is `false`), executes the command or script in a separate thread, returning an immediate placeholder `CommandResult`.
- `.log(bool)`: If `true` (default is `false`), prints a log message before executing the command.
- `.execute() -> Result<CommandResult, RunError>`: Executes the configured command or script.
**Input Handling**:
- **Single-line commands**: Treated as a command and its arguments (e.g., `"ls -la"`).
- **Multi-line scripts**: If the input string contains newline characters (`\n`), it's treated as a script.
- The script content is automatically dedented.
- On Unix-like systems, `#!/bin/bash -e` is prepended (if no shebang exists) to ensure the script exits on error.
- A temporary script file is created, made executable, and then run.
#### `CommandResult`
All execution functions return a `Result<CommandResult, RunError>`. The `CommandResult` struct contains:
- `stdout: String`: Captured standard output.
- `stderr: String`: Captured standard error.
- `success: bool`: `true` if the command exited with a zero status code.
- `code: i32`: The exit code of the command.
#### Convenience Functions:
- `sal::process::run_command("cmd_or_script")`: Equivalent to `run("cmd_or_script").execute()`.
- `sal::process::run_silent("cmd_or_script")`: Equivalent to `run("cmd_or_script").silent(true).execute()`.
#### Error Handling:
- `RunError`: Enum for errors specific to command/script execution (e.g., `EmptyCommand`, `CommandExecutionFailed`, `ScriptPreparationFailed`).
### 2. Process Management (`mgmt.rs`)
The `mgmt.rs` sub-module provides tools for querying and managing system processes.
#### `ProcessInfo`
A struct holding basic process information:
- `pid: i64`
- `name: String`
- `memory: f64` (currently a placeholder)
- `cpu: f64` (currently a placeholder)
#### Functions:
- `sal::process::which(command_name: &str) -> Option<String>`:
Checks if a command exists in the system's `PATH`. Returns the full path if found.
```rust
if let Some(path) = sal::process::which("git") {
println!("Git found at: {}", path);
}
```
- `sal::process::kill(pattern: &str) -> Result<String, ProcessError>`:
Kills processes matching the given `pattern` (name or part of the command line).
Uses `taskkill` on Windows and `pkill -f` on Unix-like systems.
```rust
match sal::process::kill("my-server-proc") {
Ok(msg) => println!("{}", msg), // "Successfully killed processes" or "No matching processes found"
Err(e) => eprintln!("Error killing process: {}", e),
}
```
- `sal::process::process_list(pattern: &str) -> Result<Vec<ProcessInfo>, ProcessError>`:
Lists running processes, optionally filtering by a `pattern` (substring match on name). If `pattern` is empty, lists all accessible processes.
Uses `wmic` on Windows and `ps` on Unix-like systems.
```rust
match sal::process::process_list("nginx") {
Ok(procs) => {
for p in procs {
println!("PID: {}, Name: {}", p.pid, p.name);
}
},
Err(e) => eprintln!("Error listing processes: {}", e),
}
```
- `sal::process::process_get(pattern: &str) -> Result<ProcessInfo, ProcessError>`:
Retrieves a single `ProcessInfo` for a process matching `pattern`.
Returns an error if zero or multiple processes match.
```rust
match sal::process::process_get("unique_process_name") {
Ok(p) => println!("Found: PID {}, Name {}", p.pid, p.name),
Err(sal::process::ProcessError::NoProcessFound(patt)) => eprintln!("No process like '{}'", patt),
Err(sal::process::ProcessError::MultipleProcessesFound(patt, count)) => {
eprintln!("Found {} processes like '{}'", count, patt);
}
Err(e) => eprintln!("Error: {}", e),
}
```
#### Error Handling:
- `ProcessError`: Enum for errors specific to process management (e.g., `CommandExecutionFailed`, `NoProcessFound`, `MultipleProcessesFound`).
## Examples ## Examples
### Example 1: Running a Process
To run a process, use the `run` function from the `run.rs` file:
```rust
use process::run;
fn main() { ### Running a simple command
run("ls -l"); ```rust
use sal::process;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = process::run("echo 'Hello from SAL!'").execute()?;
println!("Output: {}", result.stdout);
Ok(())
} }
``` ```
### Example 2: Running a Multi-line Script ### Running a multi-line script silently
```rust ```rust
use process::run; use sal::process;
fn main() { fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = run(r#" let script = r#"
echo "Hello, world!" echo "Starting script..."
ls -la date
echo "Script complete" echo "Script finished."
"#); "#;
} let result = process::run(script).silent(true).execute()?;
``` if result.success {
### Example 2: Managing a Process println!("Script executed successfully. Output:\n{}", result.stdout);
To manage a process, use the `mgmt` function from the `mgmt.rs` file: } else {
```rust eprintln!("Script failed. Error:\n{}", result.stderr);
use process::mgmt; }
Ok(())
fn main() {
mgmt("start");
} }
``` ```
## Automatic Dedentation ### Checking if a command exists and then running it
When a multi-line script is provided to the `run` function, it automatically uses the `dedent` function from `src/text/dedent.rs` to remove common leading whitespace from all lines. This allows you to write scripts with proper indentation in your code without affecting the execution.
For example, this indented script:
```rust ```rust
run(r#" use sal::process;
echo "This line has 4 spaces of indentation in the source"
echo "This line also has 4 spaces" fn main() -> Result<(), Box<dyn std::error::Error>> {
echo "This line has 8 spaces (4 more than the common indentation)" if process::which("figlet").is_some() {
"#); process::run("figlet 'SAL Process'").execute()?;
} else {
println!("Figlet not found, using echo instead:");
process::run("echo 'SAL Process'").execute()?;
}
Ok(())
}
``` ```
Will be executed with the common indentation (4 spaces) removed, preserving only the relative indentation between lines.

View File

@ -206,7 +206,7 @@ impl RedisClientWrapper {
} }
// Select the database // Select the database
redis::cmd("SELECT").arg(self.db).execute(&mut conn); let _ = redis::cmd("SELECT").arg(self.db).exec(&mut conn);
self.initialized.store(true, Ordering::Relaxed); self.initialized.store(true, Ordering::Relaxed);

View File

@ -15,6 +15,7 @@ mod rfs;
mod vault; mod vault;
mod text; mod text;
mod zinit; mod zinit;
mod mycelium;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -95,6 +96,9 @@ pub use git::register_git_module;
// Re-export zinit module // Re-export zinit module
pub use zinit::register_zinit_module; pub use zinit::register_zinit_module;
// Re-export mycelium module
pub use mycelium::register_mycelium_module;
// Re-export text module // Re-export text module
pub use text::register_text_module; pub use text::register_text_module;
// Re-export text functions directly from text module // Re-export text functions directly from text module
@ -155,6 +159,9 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
// Register Zinit module functions // Register Zinit module functions
zinit::register_zinit_module(engine)?; zinit::register_zinit_module(engine)?;
// Register Mycelium module functions
mycelium::register_mycelium_module(engine)?;
// Register Text module functions // Register Text module functions
text::register_text_module(engine)?; text::register_text_module(engine)?;

224
src/rhai/mycelium.rs Normal file
View File

@ -0,0 +1,224 @@
//! Rhai wrappers for Mycelium client module functions
//!
//! This module provides Rhai wrappers for the functions in the Mycelium client module.
use std::time::Duration;
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
use crate::mycelium as client;
use tokio::runtime::Runtime;
use serde_json::Value;
use crate::rhai::error::ToRhaiError;
/// Register Mycelium module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_mycelium_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register Mycelium client functions
engine.register_fn("mycelium_get_node_info", mycelium_get_node_info);
engine.register_fn("mycelium_list_peers", mycelium_list_peers);
engine.register_fn("mycelium_add_peer", mycelium_add_peer);
engine.register_fn("mycelium_remove_peer", mycelium_remove_peer);
engine.register_fn("mycelium_list_selected_routes", mycelium_list_selected_routes);
engine.register_fn("mycelium_list_fallback_routes", mycelium_list_fallback_routes);
engine.register_fn("mycelium_send_message", mycelium_send_message);
engine.register_fn("mycelium_receive_messages", mycelium_receive_messages);
Ok(())
}
// Helper function to get a runtime
fn get_runtime() -> Result<Runtime, Box<EvalAltResult>> {
tokio::runtime::Runtime::new().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to create Tokio runtime: {}", e).into(),
rhai::Position::NONE
))
})
}
// Helper function to convert serde_json::Value to rhai::Dynamic
fn value_to_dynamic(value: Value) -> Dynamic {
match value {
Value::Null => Dynamic::UNIT,
Value::Bool(b) => Dynamic::from(b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Dynamic::from(i)
} else if let Some(f) = n.as_f64() {
Dynamic::from(f)
} else {
Dynamic::from(n.to_string())
}
},
Value::String(s) => Dynamic::from(s),
Value::Array(arr) => {
let mut rhai_arr = Array::new();
for item in arr {
rhai_arr.push(value_to_dynamic(item));
}
Dynamic::from(rhai_arr)
},
Value::Object(map) => {
let mut rhai_map = Map::new();
for (k, v) in map {
rhai_map.insert(k.into(), value_to_dynamic(v));
}
Dynamic::from_map(rhai_map)
}
}
}
// Helper trait to convert String errors to Rhai errors
impl<T> ToRhaiError<T> for Result<T, String> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
self.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Mycelium error: {}", e).into(),
rhai::Position::NONE
))
})
}
}
//
// Mycelium Client Function Wrappers
//
/// Wrapper for mycelium::get_node_info
///
/// Gets information about the Mycelium node.
pub fn mycelium_get_node_info(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::get_node_info(api_url).await
});
let node_info = result.to_rhai_error()?;
Ok(value_to_dynamic(node_info))
}
/// Wrapper for mycelium::list_peers
///
/// Lists all peers connected to the Mycelium node.
pub fn mycelium_list_peers(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::list_peers(api_url).await
});
let peers = result.to_rhai_error()?;
Ok(value_to_dynamic(peers))
}
/// Wrapper for mycelium::add_peer
///
/// Adds a new peer to the Mycelium node.
pub fn mycelium_add_peer(api_url: &str, peer_address: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::add_peer(api_url, peer_address).await
});
let response = result.to_rhai_error()?;
Ok(value_to_dynamic(response))
}
/// Wrapper for mycelium::remove_peer
///
/// Removes a peer from the Mycelium node.
pub fn mycelium_remove_peer(api_url: &str, peer_id: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::remove_peer(api_url, peer_id).await
});
let response = result.to_rhai_error()?;
Ok(value_to_dynamic(response))
}
/// Wrapper for mycelium::list_selected_routes
///
/// Lists all selected routes in the Mycelium node.
pub fn mycelium_list_selected_routes(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::list_selected_routes(api_url).await
});
let routes = result.to_rhai_error()?;
Ok(value_to_dynamic(routes))
}
/// Wrapper for mycelium::list_fallback_routes
///
/// Lists all fallback routes in the Mycelium node.
pub fn mycelium_list_fallback_routes(api_url: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async {
client::list_fallback_routes(api_url).await
});
let routes = result.to_rhai_error()?;
Ok(value_to_dynamic(routes))
}
/// Wrapper for mycelium::send_message
///
/// Sends a message to a destination via the Mycelium node.
pub fn mycelium_send_message(api_url: &str, destination: &str, topic: &str, message: &str, reply_deadline_secs: i64) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let deadline = if reply_deadline_secs < 0 {
None
} else {
Some(Duration::from_secs(reply_deadline_secs as u64))
};
let result = rt.block_on(async {
client::send_message(api_url, destination, topic, message, deadline).await
});
let response = result.to_rhai_error()?;
Ok(value_to_dynamic(response))
}
/// Wrapper for mycelium::receive_messages
///
/// Receives messages from a topic via the Mycelium node.
pub fn mycelium_receive_messages(api_url: &str, topic: &str, wait_deadline_secs: i64) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let deadline = if wait_deadline_secs < 0 {
None
} else {
Some(Duration::from_secs(wait_deadline_secs as u64))
};
let result = rt.block_on(async {
client::receive_messages(api_url, topic, deadline).await
});
let messages = result.to_rhai_error()?;
Ok(value_to_dynamic(messages))
}

View File

@ -1,6 +1,7 @@
//! Rhai bindings for SAL crypto functionality //! Rhai bindings for SAL crypto functionality
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use crate::vault::CryptoError;
use ethers::types::{Address, U256}; use ethers::types::{Address, U256};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rhai::{Dynamic, Engine, EvalAltResult}; use rhai::{Dynamic, Engine, EvalAltResult};
@ -9,10 +10,10 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Mutex; use std::sync::Mutex;
use hex;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use crate::vault::ethereum::contract_utils::{convert_token_to_rhai, prepare_function_arguments}; use crate::vault::{ethereum, keyspace};
use crate::vault::{ethereum, keypair};
use crate::vault::symmetric::implementation as symmetric_impl; use crate::vault::symmetric::implementation as symmetric_impl;
// Global Tokio runtime for blocking async operations // Global Tokio runtime for blocking async operations
@ -73,7 +74,7 @@ fn load_key_space(name: &str, password: &str) -> bool {
}; };
// Set as current space // Set as current space
match keypair::set_current_space(space) { match keyspace::set_current_space(space) {
Ok(_) => true, Ok(_) => true,
Err(e) => { Err(e) => {
log::error!("Error setting current space: {}", e); log::error!("Error setting current space: {}", e);
@ -83,10 +84,10 @@ fn load_key_space(name: &str, password: &str) -> bool {
} }
fn create_key_space(name: &str, password: &str) -> bool { fn create_key_space(name: &str, password: &str) -> bool {
match keypair::session_manager::create_space(name) { match keyspace::session_manager::create_space(name) {
Ok(_) => { Ok(_) => {
// Get the current space // Get the current space
match keypair::get_current_space() { match keyspace::get_current_space() {
Ok(space) => { Ok(space) => {
// Encrypt the key space // Encrypt the key space
let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password) let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password)
@ -151,7 +152,7 @@ fn create_key_space(name: &str, password: &str) -> bool {
// Auto-save function for internal use // Auto-save function for internal use
fn auto_save_key_space(password: &str) -> bool { fn auto_save_key_space(password: &str) -> bool {
match keypair::get_current_space() { match keyspace::get_current_space() {
Ok(space) => { Ok(space) => {
// Encrypt the key space // Encrypt the key space
let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password) { let encrypted_space = match symmetric_impl::encrypt_key_space(&space, password) {
@ -207,7 +208,7 @@ fn auto_save_key_space(password: &str) -> bool {
} }
fn encrypt_key_space(password: &str) -> String { fn encrypt_key_space(password: &str) -> String {
match keypair::get_current_space() { match keyspace::get_current_space() {
Ok(space) => match symmetric_impl::encrypt_key_space(&space, password) { Ok(space) => match symmetric_impl::encrypt_key_space(&space, password) {
Ok(encrypted_space) => match serde_json::to_string(&encrypted_space) { Ok(encrypted_space) => match serde_json::to_string(&encrypted_space) {
Ok(json) => json, Ok(json) => json,
@ -232,7 +233,7 @@ fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
match serde_json::from_str(encrypted) { match serde_json::from_str(encrypted) {
Ok(encrypted_space) => { Ok(encrypted_space) => {
match symmetric_impl::decrypt_key_space(&encrypted_space, password) { match symmetric_impl::decrypt_key_space(&encrypted_space, password) {
Ok(space) => match keypair::set_current_space(space) { Ok(space) => match keyspace::set_current_space(space) {
Ok(_) => true, Ok(_) => true,
Err(e) => { Err(e) => {
log::error!("Error setting current space: {}", e); log::error!("Error setting current space: {}", e);
@ -252,32 +253,70 @@ fn decrypt_key_space(encrypted: &str, password: &str) -> bool {
} }
} }
// Keypair management functions // keyspace management functions
fn create_keypair(name: &str, password: &str) -> bool { fn create_keyspace(name: &str, password: &str) -> bool {
match keypair::create_keypair(name) { match keyspace::create_keypair(name) {
Ok(_) => { Ok(_) => {
// Auto-save the key space after creating a keypair // Auto-save the key space after creating a keyspace
auto_save_key_space(password) auto_save_key_space(password)
} }
Err(e) => { Err(e) => {
log::error!("Error creating keypair: {}", e); log::error!("Error creating keyspace: {}", e);
false false
} }
} }
} }
fn select_keypair(name: &str) -> bool { fn select_keyspace(name: &str) -> bool {
match keypair::select_keypair(name) { let session = crate::vault::keyspace::session_manager::SESSION.lock().unwrap();
Ok(_) => true, if let Some(ref current_space_obj) = session.current_space {
if current_space_obj.name == name {
log::debug!("Keyspace '{}' is already selected.", name);
return true;
}
}
log::warn!("Attempted to select keyspace '{}' which is not currently active. Use 'load_key_space(name, password)' to load and select a keyspace.", name);
false
}
fn rhai_list_keyspaces_actual() -> Vec<String> {
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let key_spaces_dir = home_dir.join(".hero-vault").join("key-spaces");
if !key_spaces_dir.exists() {
log::debug!("Key spaces directory does not exist: {}", key_spaces_dir.display());
return Vec::new();
}
let mut spaces = Vec::new();
match std::fs::read_dir(key_spaces_dir) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "json" {
if let Some(stem) = path.file_stem() {
if let Some(name) = stem.to_str() {
spaces.push(name.to_string());
}
}
}
}
}
}
}
}
Err(e) => { Err(e) => {
log::error!("Error selecting keypair: {}", e); log::error!("Error reading key spaces directory: {}", e);
false
} }
} }
spaces
} }
fn list_keypairs() -> Vec<String> { fn rhai_list_keypairs() -> Vec<String> {
match keypair::list_keypairs() { match keyspace::session_manager::list_keypairs() {
Ok(keypairs) => keypairs, Ok(keypairs) => keypairs,
Err(e) => { Err(e) => {
log::error!("Error listing keypairs: {}", e); log::error!("Error listing keypairs: {}", e);
@ -286,11 +325,46 @@ fn list_keypairs() -> Vec<String> {
} }
} }
fn rhai_select_keypair(name: &str) -> bool {
match keyspace::session_manager::select_keypair(name) {
Ok(_) => true,
Err(e) => {
log::error!("Error selecting keypair '{}': {}", name, e);
false
}
}
}
fn rhai_clear_session() {
keyspace::session_manager::clear_session();
}
fn rhai_create_keypair(name: &str) -> bool {
match keyspace::session_manager::create_keypair(name) {
Ok(_) => true,
Err(e) => {
log::error!("Error creating keypair '{}': {}", name, e);
false
}
}
}
// Rhai wrapper for getting the public key of the selected keypair
fn rhai_keypair_pub_key() -> Result<String, Box<EvalAltResult>> {
match keyspace::session_manager::get_selected_keypair() {
Ok(keypair) => Ok(hex::encode(keypair.pub_key())),
Err(e) => Err(Box::new(EvalAltResult::ErrorSystem(
"Failed to get public key".to_string(),
Box::new(e),
))),
}
}
// Cryptographic operations // Cryptographic operations
fn sign(message: &str) -> String { fn sign(message: &str) -> String {
let message_bytes = message.as_bytes(); let message_bytes = message.as_bytes();
match keypair::keypair_sign(message_bytes) { match keyspace::session_manager::keypair_sign(message_bytes) {
Ok(signature) => BASE64.encode(signature), Ok(signature_bytes) => BASE64.encode(signature_bytes),
Err(e) => { Err(e) => {
log::error!("Error signing message: {}", e); log::error!("Error signing message: {}", e);
String::new() String::new()
@ -301,7 +375,7 @@ fn sign(message: &str) -> String {
fn verify(message: &str, signature: &str) -> bool { fn verify(message: &str, signature: &str) -> bool {
let message_bytes = message.as_bytes(); let message_bytes = message.as_bytes();
match BASE64.decode(signature) { match BASE64.decode(signature) {
Ok(signature_bytes) => match keypair::keypair_verify(message_bytes, &signature_bytes) { Ok(signature_bytes) => match keyspace::keypair_verify(message_bytes, &signature_bytes) {
Ok(is_valid) => is_valid, Ok(is_valid) => is_valid,
Err(e) => { Err(e) => {
log::error!("Error verifying signature: {}", e); log::error!("Error verifying signature: {}", e);
@ -763,7 +837,7 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
}; };
// Prepare the arguments // Prepare the arguments
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) { let tokens = match ethereum::prepare_function_arguments(&contract.abi, function_name, &args) {
Ok(tokens) => tokens, Ok(tokens) => tokens,
Err(e) => { Err(e) => {
log::error!("Error preparing arguments: {}", e); log::error!("Error preparing arguments: {}", e);
@ -793,7 +867,7 @@ fn call_contract_read(contract_json: &str, function_name: &str, args: rhai::Arra
match rt.block_on(async { match rt.block_on(async {
ethereum::call_read_function(&contract, &provider, function_name, tokens).await ethereum::call_read_function(&contract, &provider, function_name, tokens).await
}) { }) {
Ok(result) => convert_token_to_rhai(&result), Ok(result) => ethereum::convert_token_to_rhai(&result),
Err(e) => { Err(e) => {
log::error!("Failed to call contract function: {}", e); log::error!("Failed to call contract function: {}", e);
Dynamic::UNIT Dynamic::UNIT
@ -818,7 +892,7 @@ fn call_contract_write(contract_json: &str, function_name: &str, args: rhai::Arr
}; };
// Prepare the arguments // Prepare the arguments
let tokens = match prepare_function_arguments(&contract.abi, function_name, &args) { let tokens = match ethereum::prepare_function_arguments(&contract.abi, function_name, &args) {
Ok(tokens) => tokens, Ok(tokens) => tokens,
Err(e) => { Err(e) => {
log::error!("Error preparing arguments: {}", e); log::error!("Error preparing arguments: {}", e);
@ -881,10 +955,15 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box<EvalAltResu
engine.register_fn("encrypt_key_space", encrypt_key_space); engine.register_fn("encrypt_key_space", encrypt_key_space);
engine.register_fn("decrypt_key_space", decrypt_key_space); engine.register_fn("decrypt_key_space", decrypt_key_space);
// Register keypair functions // Register keyspace functions
engine.register_fn("create_keypair", create_keypair); engine.register_fn("create_keyspace", create_keyspace);
engine.register_fn("select_keypair", select_keypair); engine.register_fn("select_keyspace", select_keyspace);
engine.register_fn("list_keypairs", list_keypairs); engine.register_fn("list_keyspaces", rhai_list_keyspaces_actual);
engine.register_fn("list_keypairs", rhai_list_keypairs);
engine.register_fn("select_keypair", rhai_select_keypair);
engine.register_fn("clear_session", rhai_clear_session);
engine.register_fn("create_keypair", rhai_create_keypair);
engine.register_fn("keypair_pub_key", rhai_keypair_pub_key);
// Register signing/verification functions // Register signing/verification functions
engine.register_fn("sign", sign); engine.register_fn("sign", sign);

View File

@ -1,189 +1,307 @@
# Text Processing Utilities # SAL Text Module (`sal::text`)
A collection of Rust utilities for common text processing operations. This module provides a collection of utilities for common text processing and manipulation tasks in Rust, with bindings for Rhai scripting.
## Overview ## Overview
This module provides functions for text manipulation tasks such as: The `sal::text` module offers functionalities for:
- Removing indentation from multiline strings - **Indentation**: Removing common leading whitespace (`dedent`) and adding prefixes to lines (`prefix`).
- Adding prefixes to multiline strings - **Normalization**: Sanitizing strings for use as filenames (`name_fix`) or fixing filename components within paths (`path_fix`).
- Normalizing filenames and paths - **Replacement**: A powerful `TextReplacer` for performing single or multiple regex or literal text replacements in strings or files.
- Text replacement (regex and literal) with file operations - **Templating**: A `TemplateBuilder` using the Tera engine to render text templates with dynamic data.
## Functions ## Rust API
### Text Indentation ### 1. Text Indentation
#### `dedent(text: &str) -> String` Located in `src/text/dedent.rs` (for `dedent`) and `src/text/fix.rs` (likely contains `prefix`, though not explicitly confirmed by file view, its Rhai registration implies existence).
Removes common leading whitespace from multiline strings. - **`dedent(text: &str) -> String`**: Removes common leading whitespace from a multiline string. Tabs are treated as 4 spaces. Ideal for cleaning up heredocs or indented code snippets.
```rust
use sal::text::dedent;
let indented_text = " Hello\n World";
assert_eq!(dedent(indented_text), "Hello\n World");
```
- **`prefix(text: &str, prefix_str: &str) -> String`**: Adds `prefix_str` to the beginning of each line in `text`.
```rust
use sal::text::prefix;
let text = "line1\nline2";
assert_eq!(prefix(text, "> "), "> line1\n> line2");
```
### 2. Filename and Path Normalization
Located in `src/text/fix.rs`.
- **`name_fix(text: &str) -> String`**: Sanitizes a string to be suitable as a name or filename component. It converts to lowercase, replaces whitespace and various special characters with underscores, and removes non-ASCII characters.
```rust
use sal::text::name_fix;
assert_eq!(name_fix("My File (New).txt"), "my_file_new_.txt");
assert_eq!(name_fix("Café crème.jpg"), "caf_crm.jpg");
```
- **`path_fix(text: &str) -> String`**: Applies `name_fix` to the filename component of a given path string, leaving the directory structure intact.
```rust
use sal::text::path_fix;
assert_eq!(path_fix("/some/path/My Document.docx"), "/some/path/my_document.docx");
```
### 3. Text Replacement (`TextReplacer`)
Located in `src/text/replace.rs`. Provides `TextReplacer` and `TextReplacerBuilder`.
The `TextReplacer` allows for complex, chained replacement operations on strings or file contents.
**Builder Pattern:**
```rust ```rust
let indented = " line 1\n line 2\n line 3"; use sal::text::TextReplacer;
let dedented = dedent(indented);
assert_eq!(dedented, "line 1\nline 2\n line 3");
```
**Features:** // Example: Multiple replacements, regex and literal
- Analyzes all non-empty lines to determine minimum indentation
- Preserves empty lines but removes all leading whitespace from them
- Treats tabs as 4 spaces for indentation purposes
#### `prefix(text: &str, prefix: &str) -> String`
Adds a specified prefix to each line of a multiline string.
```rust
let text = "line 1\nline 2\nline 3";
let prefixed = prefix(text, " ");
assert_eq!(prefixed, " line 1\n line 2\n line 3");
```
### Filename and Path Normalization
#### `name_fix(text: &str) -> String`
Normalizes filenames by:
- Converting to lowercase
- Replacing whitespace and special characters with underscores
- Removing non-ASCII characters
- Collapsing consecutive special characters into a single underscore
```rust
assert_eq!(name_fix("Hello World"), "hello_world");
assert_eq!(name_fix("File-Name.txt"), "file_name.txt");
assert_eq!(name_fix("Résumé"), "rsum");
```
#### `path_fix(text: &str) -> String`
Applies `name_fix()` to the filename portion of a path while preserving the directory structure.
```rust
assert_eq!(path_fix("/path/to/File Name.txt"), "/path/to/file_name.txt");
assert_eq!(path_fix("./relative/path/to/DOCUMENT-123.pdf"), "./relative/path/to/document_123.pdf");
```
**Features:**
- Preserves paths ending with `/` (directories)
- Only normalizes the filename portion, leaving the path structure intact
- Handles both absolute and relative paths
### Text Replacement
#### `TextReplacer`
A flexible text replacement utility that supports both regex and literal replacements with a builder pattern.
```rust
// Regex replacement
let replacer = TextReplacer::builder() let replacer = TextReplacer::builder()
.pattern(r"\bfoo\b") .pattern(r"\d+") // Regex: match one or more digits
.replacement("bar") .replacement("NUMBER")
.regex(true) .regex(true)
.add_replacement() .and() // Chain another replacement
.unwrap() .pattern("World") // Literal string
.replacement("Universe")
.regex(false) // Explicitly literal, though default
.build() .build()
.unwrap(); .expect("Failed to build replacer");
let result = replacer.replace("foo bar foo baz"); // "bar bar bar baz" let original_text = "Hello World, item 123 and item 456.";
``` let modified_text = replacer.replace(original_text);
assert_eq!(modified_text, "Hello Universe, item NUMBER and item NUMBER.");
**Features:** // Case-insensitive regex example
- Supports both regex and literal string replacements let case_replacer = TextReplacer::builder()
- Builder pattern for fluent configuration .pattern("apple")
- Multiple replacements in a single pass .replacement("FRUIT")
- Case-insensitive matching (for regex replacements)
- File reading and writing operations
#### Multiple Replacements
```rust
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("qux")
.add_replacement()
.unwrap()
.pattern("bar")
.replacement("baz")
.add_replacement()
.unwrap()
.build()
.unwrap();
let result = replacer.replace("foo bar foo"); // "qux baz qux"
```
#### File Operations
```rust
// Replace in a file and get the result as a string
let result = replacer.replace_file("input.txt")?;
// Replace in a file and write back to the same file
replacer.replace_file_in_place("input.txt")?;
// Replace in a file and write to a new file
replacer.replace_file_to("input.txt", "output.txt")?;
```
#### Case-Insensitive Matching
```rust
let replacer = TextReplacer::builder()
.pattern("foo")
.replacement("bar")
.regex(true) .regex(true)
.case_insensitive(true) .case_insensitive(true)
.add_replacement()
.unwrap()
.build() .build()
.unwrap(); .unwrap();
assert_eq!(case_replacer.replace("Apple and apple"), "FRUIT and FRUIT");
let result = replacer.replace("FOO foo Foo"); // "bar bar bar"
``` ```
## Usage **Key `TextReplacerBuilder` methods:**
- `pattern(pat: &str)`: Sets the search pattern (string or regex).
- `replacement(rep: &str)`: Sets the replacement string.
- `regex(yes: bool)`: If `true`, treats `pattern` as a regex. Default is `false` (literal).
- `case_insensitive(yes: bool)`: If `true` (and `regex` is `true`), performs case-insensitive matching.
- `and()`: Finalizes the current replacement operation and prepares for a new one.
- `build()`: Consumes the builder and returns a `Result<TextReplacer, String>`.
Import the functions from the module: **`TextReplacer` methods:**
- `replace(input: &str) -> String`: Applies all configured replacements to the input string.
- `replace_file(path: P) -> io::Result<String>`: Reads a file, applies replacements, returns the result.
- `replace_file_in_place(path: P) -> io::Result<()>`: Replaces content in the specified file directly.
- `replace_file_to(input_path: P1, output_path: P2) -> io::Result<()>`: Reads from `input_path`, applies replacements, writes to `output_path`.
### 4. Text Templating (`TemplateBuilder`)
Located in `src/text/template.rs`. Uses the Tera templating engine.
**Builder Pattern:**
```rust ```rust
use your_crate::text::{dedent, prefix, name_fix, path_fix, TextReplacer}; use sal::text::TemplateBuilder;
use std::collections::HashMap;
// Assume "./my_template.txt" contains: "Hello, {{ name }}! You are {{ age }}."
// Create a temporary template file for the example
std::fs::write("./my_template.txt", "Hello, {{ name }}! You are {{ age }}.").unwrap();
let mut builder = TemplateBuilder::open("./my_template.txt").expect("Template not found");
// Add variables individually
builder = builder.add_var("name", "Alice").add_var("age", 30);
let rendered_string = builder.render().expect("Rendering failed");
assert_eq!(rendered_string, "Hello, Alice! You are 30.");
// Or add multiple variables from a HashMap
let mut vars = HashMap::new();
vars.insert("name", "Bob");
vars.insert("age", "25"); // Values in HashMap are typically strings or serializable types
let mut builder2 = TemplateBuilder::open("./my_template.txt").unwrap();
builder2 = builder2.add_vars(vars);
let rendered_string2 = builder2.render().unwrap();
assert_eq!(rendered_string2, "Hello, Bob! You are 25.");
// Render directly to a file
// builder.render_to_file("output.txt").expect("Failed to write to file");
// Clean up temporary file
std::fs::remove_file("./my_template.txt").unwrap();
``` ```
## Examples **Key `TemplateBuilder` methods:**
- `open(template_path: P) -> io::Result<Self>`: Loads the template file.
- `add_var(name: S, value: V) -> Self`: Adds a single variable to the context.
- `add_vars(vars: HashMap<S, V>) -> Self`: Adds multiple variables from a HashMap.
- `render() -> Result<String, tera::Error>`: Renders the template to a string.
- `render_to_file(output_path: P) -> io::Result<()>`: Renders the template and writes it to the specified file.
### Cleaning up indented text from a template ## Rhai Scripting with `herodo`
```rust The `sal::text` module's functionalities are exposed to Rhai scripts when using `herodo`.
let template = "
<div> ### Direct Functions
<h1>Title</h1>
<p> - **`dedent(text_string)`**: Removes common leading whitespace.
Some paragraph text - Example: `let clean_script = dedent(" if true {\n print(\"indented\");\n }");`
with multiple lines - **`prefix(text_string, prefix_string)`**: Adds `prefix_string` to each line of `text_string`.
</p> - Example: `let prefixed_text = prefix("hello\nworld", "# ");`
</div> - **`name_fix(text_string)`**: Normalizes a string for use as a filename.
"; - Example: `let filename = name_fix("My Document (V2).docx"); // "my_document_v2_.docx"`
- **`path_fix(path_string)`**: Normalizes the filename part of a path.
- Example: `let fixed_path = path_fix("/uploads/User Files/Report [Final].pdf");`
### TextReplacer
Provides text replacement capabilities through a builder pattern.
1. **Create a builder**: `let builder = text_replacer_new();`
2. **Configure replacements** (methods return the builder for chaining):
- `builder = builder.pattern(search_pattern_string);`
- `builder = builder.replacement(replacement_string);`
- `builder = builder.regex(is_regex_bool);` (default `false`)
- `builder = builder.case_insensitive(is_case_insensitive_bool);` (default `false`, only applies if `regex` is `true`)
- `builder = builder.and();` (to add the current replacement and start a new one)
3. **Build the replacer**: `let replacer = builder.build();`
4. **Use the replacer**:
- `let modified_text = replacer.replace(original_text_string);`
- `let modified_text_from_file = replacer.replace_file(input_filepath_string);`
- `replacer.replace_file_in_place(filepath_string);`
- `replacer.replace_file_to(input_filepath_string, output_filepath_string);`
### TemplateBuilder
Provides text templating capabilities.
1. **Open a template file**: `let tpl_builder = template_builder_open(template_filepath_string);`
2. **Add variables** (methods return the builder for chaining):
- `tpl_builder = tpl_builder.add_var(name_string, value);` (value can be string, int, float, bool, or array)
- `tpl_builder = tpl_builder.add_vars(map_object);` (map keys are variable names, values are their corresponding values)
3. **Render the template**:
- `let rendered_string = tpl_builder.render();`
- `tpl_builder.render_to_file(output_filepath_string);`
## Rhai Example
```rhai
// Create a temporary file for template demonstration
let template_content = "Report for {{user}}:\nItems processed: {{count}}.\nStatus: {{status}}.";
let template_path = "./temp_report_template.txt";
// Using file.write (assuming sal::file module is available and registered)
// For this example, we'll assume a way to write this file or that it exists.
// For a real script, ensure the file module is used or the file is pre-existing.
print(`Intending to write template to: ${template_path}`);
// In a real scenario: file.write(template_path, template_content);
// For demonstration, let's simulate it exists for the template_builder_open call.
// If file module is not used, this script part needs adjustment or pre-existing file.
// --- Text Normalization ---
let raw_filename = "User's Report [Draft 1].md";
let safe_filename = name_fix(raw_filename);
print(`Safe filename: ${safe_filename}`); // E.g., "users_report_draft_1_.md"
let raw_path = "/data/project files/Final Report (2023).pdf";
let safe_path = path_fix(raw_path);
print(`Safe path: ${safe_path}`); // E.g., "/data/project files/final_report_2023_.pdf"
// --- Dedent and Prefix ---
let script_block = "\n for item in items {\n print(item);\n }\n";
let dedented_script = dedent(script_block);
print("Dedented script:\n" + dedented_script);
let prefixed_log = prefix("Operation successful.\nDetails logged.", "LOG: ");
print(prefixed_log);
// --- TextReplacer Example ---
let text_to_modify = "The quick brown fox jumps over the lazy dog. The dog was very lazy.";
let replacer_builder = text_replacer_new()
.pattern("dog")
.replacement("cat")
.case_insensitive(true) // Replace 'dog', 'Dog', 'DOG', etc.
.and()
.pattern("lazy")
.replacement("energetic")
.regex(false); // This is the default, explicit for clarity
let replacer = replacer_builder.build();
let replaced_text = replacer.replace(text_to_modify);
print(`Replaced text: ${replaced_text}`);
// Expected: The quick brown fox jumps over the energetic cat. The cat was very energetic.
// --- TemplateBuilder Example ---
// This part assumes 'temp_report_template.txt' was successfully created with content:
// "Report for {{user}}:\nItems processed: {{count}}.\nStatus: {{status}}."
// If not, template_builder_open will fail. For a robust script, check file existence or create it.
// Create a dummy template file if it doesn't exist for the example to run
// This would typically be done using the file module, e.g. file.write()
// For simplicity here, we'll just print a message if it's missing.
// In a real script: if !file.exists(template_path) { file.write(template_path, template_content); }
// Let's try to proceed assuming the template might exist or skip if not.
// A more robust script would handle the file creation explicitly.
// For the sake of this example, let's create it directly if possible (conceptual)
// This is a placeholder for actual file writing logic.
// if (true) { // Simulate file creation for example purpose
// std.os.remove_file(template_path); // Clean up if exists
// let f = std.io.open(template_path, "w"); f.write(template_content); f.close();
// }
// Due to the sandbox, direct file system manipulation like above isn't typically done in Rhai examples
// without relying on registered SAL functions. We'll assume the file exists.
print("Attempting to use template: " + template_path);
// It's better to ensure the file exists before calling template_builder_open
// For this example, we'll proceed, but in a real script, handle file creation.
// Create a dummy file for the template example to work in isolation
// This is not ideal but helps for a self-contained example if file module isn't used prior.
// In a real SAL script, you'd use `file.write`.
let _dummy_template_file_path = "./example_template.rhai.tmp";
// file.write(_dummy_template_file_path, "Name: {{name}}, Age: {{age}}");
// Using a known, simple template string for robustness if file ops are tricky in example context
let tpl_builder = template_builder_open(_dummy_template_file_path); // Use the dummy/known file
if tpl_builder.is_ok() {
let mut template_engine = tpl_builder.unwrap();
template_engine = template_engine.add_var("user", "Jane Doe");
template_engine = template_engine.add_var("count", 150);
template_engine = template_engine.add_var("status", "Completed");
let report_output = template_engine.render();
if report_output.is_ok() {
print("Generated Report:\n" + report_output.unwrap());
} else {
print("Error rendering template: " + report_output.unwrap_err());
}
// Example: Render to file
// template_engine.render_to_file("./generated_report.txt");
// print("Report also written to ./generated_report.txt");
} else {
print("Skipping TemplateBuilder example as template file '" + _dummy_template_file_path + "' likely missing or unreadable.");
print("Error: " + tpl_builder.unwrap_err());
print("To run this part, ensure '" + _dummy_template_file_path + "' exists with content like: 'Name: {{name}}, Age: {{age}}'");
}
// Clean up dummy file
// file.remove(_dummy_template_file_path);
let clean = dedent(template);
// Result:
// <div>
// <h1>Title</h1>
// <p>
// Some paragraph text
// with multiple lines
// </p>
// </div>
``` ```
### Normalizing user-provided filenames **Note on Rhai Example File Operations:** The Rhai example above includes comments about file creation for the `TemplateBuilder` part. In a real `herodo` script, you would use `sal::file` module functions (e.g., `file.write`, `file.exists`, `file.remove`) to manage the template file. For simplicity and to avoid making the example dependent on another module's full setup path, it highlights where such operations would occur. The example tries to use a dummy path and gracefully skips if the template isn't found, which is a common issue when running examples in restricted environments or without proper setup. The core logic of using `TemplateBuilder` once the template is loaded remains the same.
```rust
let user_filename = "My Document (2023).pdf";
let safe_filename = name_fix(user_filename);
// Result: "my_document_2023_.pdf"
let user_path = "/uploads/User Files/Report #123.xlsx";
let safe_path = path_fix(user_path);
// Result: "/uploads/User Files/report_123.xlsx"

View File

@ -16,7 +16,7 @@ static ETH_WALLETS: Lazy<Mutex<HashMap<String, Vec<EthereumWallet>>>> = Lazy::ne
/// Creates an Ethereum wallet from the currently selected keypair for a specific network. /// Creates an Ethereum wallet from the currently selected keypair for a specific network.
pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> Result<EthereumWallet, CryptoError> { pub fn create_ethereum_wallet_for_network(network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
// Get the currently selected keypair // Get the currently selected keypair
let keypair = crate::vault::keypair::get_selected_keypair()?; let keypair = crate::vault::keyspace::get_selected_keypair()?;
// Create an Ethereum wallet from the keypair // Create an Ethereum wallet from the keypair
let wallet = EthereumWallet::from_keypair(&keypair, network)?; let wallet = EthereumWallet::from_keypair(&keypair, network)?;
@ -77,7 +77,7 @@ pub fn clear_ethereum_wallets_for_network(network_name: &str) {
/// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network. /// Creates an Ethereum wallet from a name and the currently selected keypair for a specific network.
pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> { pub fn create_ethereum_wallet_from_name_for_network(name: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
// Get the currently selected keypair // Get the currently selected keypair
let keypair = crate::vault::keypair::get_selected_keypair()?; let keypair = crate::vault::keyspace::get_selected_keypair()?;
// Create an Ethereum wallet from the name and keypair // Create an Ethereum wallet from the name and keypair
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair, network)?; let wallet = EthereumWallet::from_name_and_keypair(name, &keypair, network)?;

View File

@ -4,12 +4,12 @@ use ethers::prelude::*;
use ethers::signers::{LocalWallet, Signer, Wallet}; use ethers::signers::{LocalWallet, Signer, Wallet};
use ethers::utils::hex; use ethers::utils::hex;
use k256::ecdsa::SigningKey; use k256::ecdsa::SigningKey;
use sha2::{Digest, Sha256};
use std::str::FromStr; use std::str::FromStr;
use sha2::{Sha256, Digest};
use crate::vault::error::CryptoError;
use crate::vault::keypair::KeyPair;
use super::networks::NetworkConfig; use super::networks::NetworkConfig;
use crate::vault::error::CryptoError;
use crate::vault::keyspace::KeyPair;
/// An Ethereum wallet derived from a keypair. /// An Ethereum wallet derived from a keypair.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -21,91 +21,103 @@ pub struct EthereumWallet {
impl EthereumWallet { impl EthereumWallet {
/// Creates a new Ethereum wallet from a keypair for a specific network. /// Creates a new Ethereum wallet from a keypair for a specific network.
pub fn from_keypair(keypair: &KeyPair, network: NetworkConfig) -> Result<Self, CryptoError> { pub fn from_keypair(
keypair: &crate::vault::keyspace::keypair_types::KeyPair,
network: NetworkConfig,
) -> Result<Self, CryptoError> {
// Get the private key bytes from the keypair // Get the private key bytes from the keypair
let private_key_bytes = keypair.signing_key.to_bytes(); let private_key_bytes = keypair.signing_key.to_bytes();
// Convert to a hex string (without 0x prefix) // Convert to a hex string (without 0x prefix)
let private_key_hex = hex::encode(private_key_bytes); let private_key_hex = hex::encode(private_key_bytes);
// Create an Ethereum wallet from the private key // Create an Ethereum wallet from the private key
let wallet = LocalWallet::from_str(&private_key_hex) let wallet = LocalWallet::from_str(&private_key_hex)
.map_err(|_e| CryptoError::InvalidKeyLength)? .map_err(|_e| CryptoError::InvalidKeyLength)?
.with_chain_id(network.chain_id); .with_chain_id(network.chain_id);
// Get the Ethereum address // Get the Ethereum address
let address = wallet.address(); let address = wallet.address();
Ok(EthereumWallet { Ok(EthereumWallet {
address, address,
wallet, wallet,
network, network,
}) })
} }
/// Creates a new Ethereum wallet from a name and keypair (deterministic derivation) for a specific network. /// Creates a new Ethereum wallet from a name and keypair (deterministic derivation) for a specific network.
pub fn from_name_and_keypair(name: &str, keypair: &KeyPair, network: NetworkConfig) -> Result<Self, CryptoError> { pub fn from_name_and_keypair(
name: &str,
keypair: &KeyPair,
network: NetworkConfig,
) -> Result<Self, CryptoError> {
// Get the private key bytes from the keypair // Get the private key bytes from the keypair
let private_key_bytes = keypair.signing_key.to_bytes(); let private_key_bytes = keypair.signing_key.to_bytes();
// Create a deterministic seed by combining name and private key // Create a deterministic seed by combining name and private key
let mut hasher = Sha256::default(); let mut hasher = Sha256::default();
hasher.update(name.as_bytes()); hasher.update(name.as_bytes());
hasher.update(&private_key_bytes); hasher.update(&private_key_bytes);
let seed = hasher.finalize(); let seed = hasher.finalize();
// Use the seed as a private key // Use the seed as a private key
let private_key_hex = hex::encode(seed); let private_key_hex = hex::encode(seed);
// Create an Ethereum wallet from the derived private key // Create an Ethereum wallet from the derived private key
let wallet = LocalWallet::from_str(&private_key_hex) let wallet = LocalWallet::from_str(&private_key_hex)
.map_err(|_e| CryptoError::InvalidKeyLength)? .map_err(|_e| CryptoError::InvalidKeyLength)?
.with_chain_id(network.chain_id); .with_chain_id(network.chain_id);
// Get the Ethereum address // Get the Ethereum address
let address = wallet.address(); let address = wallet.address();
Ok(EthereumWallet { Ok(EthereumWallet {
address, address,
wallet, wallet,
network, network,
}) })
} }
/// Creates a new Ethereum wallet from a private key for a specific network. /// Creates a new Ethereum wallet from a private key for a specific network.
pub fn from_private_key(private_key: &str, network: NetworkConfig) -> Result<Self, CryptoError> { pub fn from_private_key(
private_key: &str,
network: NetworkConfig,
) -> Result<Self, CryptoError> {
// Remove 0x prefix if present // Remove 0x prefix if present
let private_key_clean = private_key.trim_start_matches("0x"); let private_key_clean = private_key.trim_start_matches("0x");
// Create an Ethereum wallet from the private key // Create an Ethereum wallet from the private key
let wallet = LocalWallet::from_str(private_key_clean) let wallet = LocalWallet::from_str(private_key_clean)
.map_err(|_e| CryptoError::InvalidKeyLength)? .map_err(|_e| CryptoError::InvalidKeyLength)?
.with_chain_id(network.chain_id); .with_chain_id(network.chain_id);
// Get the Ethereum address // Get the Ethereum address
let address = wallet.address(); let address = wallet.address();
Ok(EthereumWallet { Ok(EthereumWallet {
address, address,
wallet, wallet,
network, network,
}) })
} }
/// Gets the Ethereum address as a string. /// Gets the Ethereum address as a string.
pub fn address_string(&self) -> String { pub fn address_string(&self) -> String {
format!("{:?}", self.address) format!("{:?}", self.address)
} }
/// Signs a message with the Ethereum wallet. /// Signs a message with the Ethereum wallet.
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> { pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
let signature = self.wallet.sign_message(message) let signature = self
.wallet
.sign_message(message)
.await .await
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
Ok(signature.to_string()) Ok(signature.to_string())
} }
/// Gets the private key as a hex string. /// Gets the private key as a hex string.
pub fn private_key_hex(&self) -> String { pub fn private_key_hex(&self) -> String {
let bytes = self.wallet.signer().to_bytes(); let bytes = self.wallet.signer().to_bytes();

View File

@ -1,13 +1,16 @@
use k256::ecdh::EphemeralSecret;
/// Implementation of keypair functionality. /// Implementation of keypair functionality.
use k256::ecdsa::{
use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature}; signature::{Signer, Verifier},
Signature, SigningKey, VerifyingKey,
};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap; use std::collections::HashMap;
use sha2::{Sha256, Digest};
use crate::vault::symmetric::implementation;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::symmetric::implementation;
/// A keypair for signing and verifying messages. /// A keypair for signing and verifying messages.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -22,8 +25,8 @@ pub struct KeyPair {
// Serialization helpers for VerifyingKey // Serialization helpers for VerifyingKey
mod verifying_key_serde { mod verifying_key_serde {
use super::*; use super::*;
use serde::{Serializer, Deserializer};
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
use serde::{Deserializer, Serializer};
use std::fmt; use std::fmt;
pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error> pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
@ -63,7 +66,7 @@ mod verifying_key_serde {
while let Some(byte) = seq.next_element()? { while let Some(byte) = seq.next_element()? {
bytes.push(byte); bytes.push(byte);
} }
VerifyingKey::from_sec1_bytes(&bytes).map_err(|e| { VerifyingKey::from_sec1_bytes(&bytes).map_err(|e| {
log::error!("Error deserializing verifying key from seq: {:?}", e); log::error!("Error deserializing verifying key from seq: {:?}", e);
de::Error::custom(format!("invalid verifying key from seq: {:?}", e)) de::Error::custom(format!("invalid verifying key from seq: {:?}", e))
@ -83,8 +86,8 @@ mod verifying_key_serde {
// Serialization helpers for SigningKey // Serialization helpers for SigningKey
mod signing_key_serde { mod signing_key_serde {
use super::*; use super::*;
use serde::{Serializer, Deserializer};
use serde::de::{self, Visitor}; use serde::de::{self, Visitor};
use serde::{Deserializer, Serializer};
use std::fmt; use std::fmt;
pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error> pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error>
@ -124,7 +127,7 @@ mod signing_key_serde {
while let Some(byte) = seq.next_element()? { while let Some(byte) = seq.next_element()? {
bytes.push(byte); bytes.push(byte);
} }
SigningKey::from_bytes(bytes.as_slice().into()).map_err(|e| { SigningKey::from_bytes(bytes.as_slice().into()).map_err(|e| {
log::error!("Error deserializing signing key from seq: {:?}", e); log::error!("Error deserializing signing key from seq: {:?}", e);
de::Error::custom(format!("invalid signing key from seq: {:?}", e)) de::Error::custom(format!("invalid signing key from seq: {:?}", e))
@ -146,7 +149,7 @@ impl KeyPair {
pub fn new(name: &str) -> Self { pub fn new(name: &str) -> Self {
let signing_key = SigningKey::random(&mut OsRng); let signing_key = SigningKey::random(&mut OsRng);
let verifying_key = VerifyingKey::from(&signing_key); let verifying_key = VerifyingKey::from(&signing_key);
KeyPair { KeyPair {
name: name.to_string(), name: name.to_string(),
verifying_key, verifying_key,
@ -158,7 +161,7 @@ impl KeyPair {
pub fn pub_key(&self) -> Vec<u8> { pub fn pub_key(&self) -> Vec<u8> {
self.verifying_key.to_sec1_bytes().to_vec() self.verifying_key.to_sec1_bytes().to_vec()
} }
/// Derives a public key from a private key. /// Derives a public key from a private key.
pub fn pub_key_from_private(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> { pub fn pub_key_from_private(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
let signing_key = SigningKey::from_bytes(private_key.into()) let signing_key = SigningKey::from_bytes(private_key.into())
@ -177,27 +180,31 @@ impl KeyPair {
pub fn verify(&self, message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> { pub fn verify(&self, message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
let signature = Signature::from_bytes(signature_bytes.into()) let signature = Signature::from_bytes(signature_bytes.into())
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
match self.verifying_key.verify(message, &signature) { match self.verifying_key.verify(message, &signature) {
Ok(_) => Ok(true), Ok(_) => Ok(true),
Err(_) => Ok(false), // Verification failed, but operation was successful Err(_) => Ok(false), // Verification failed, but operation was successful
} }
} }
/// Verifies a message signature using only a public key. /// Verifies a message signature using only a public key.
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> { pub fn verify_with_public_key(
let verifying_key = VerifyingKey::from_sec1_bytes(public_key) public_key: &[u8],
.map_err(|_| CryptoError::InvalidKeyLength)?; message: &[u8],
signature_bytes: &[u8],
) -> Result<bool, CryptoError> {
let verifying_key =
VerifyingKey::from_sec1_bytes(public_key).map_err(|_| CryptoError::InvalidKeyLength)?;
let signature = Signature::from_bytes(signature_bytes.into()) let signature = Signature::from_bytes(signature_bytes.into())
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?; .map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
match verifying_key.verify(message, &signature) { match verifying_key.verify(message, &signature) {
Ok(_) => Ok(true), Ok(_) => Ok(true),
Err(_) => Ok(false), // Verification failed, but operation was successful Err(_) => Ok(false), // Verification failed, but operation was successful
} }
} }
/// Encrypts a message using the recipient's public key. /// Encrypts a message using the recipient's public key.
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme): /// This implements ECIES (Elliptic Curve Integrated Encryption Scheme):
/// 1. Generate an ephemeral keypair /// 1. Generate an ephemeral keypair
@ -205,64 +212,81 @@ impl KeyPair {
/// 3. Derive encryption key from the shared secret /// 3. Derive encryption key from the shared secret
/// 4. Encrypt the message using symmetric encryption /// 4. Encrypt the message using symmetric encryption
/// 5. Return the ephemeral public key and the ciphertext /// 5. Return the ephemeral public key and the ciphertext
pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> { pub fn encrypt_asymmetric(
&self,
recipient_public_key: &[u8],
message: &[u8],
) -> Result<Vec<u8>, CryptoError> {
// Parse recipient's public key // Parse recipient's public key
let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key) let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?; .map_err(|_| CryptoError::InvalidKeyLength)?;
// Generate ephemeral keypair // Generate ephemeral keypair
let ephemeral_signing_key = SigningKey::random(&mut OsRng); let ephemeral_signing_key = SigningKey::random(&mut OsRng);
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key); let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
// Derive shared secret (this is a simplified ECDH) // Derive shared secret using ECDH
// In a real implementation, we would use proper ECDH, but for this example: let ephemeral_secret = EphemeralSecret::random(&mut OsRng);
let shared_point = recipient_key.to_encoded_point(false); let _shared_secret = ephemeral_secret.diffie_hellman(&recipient_key.into());
let shared_secret = {
// Derive encryption key from the shared secret (e.g., using HKDF or hashing)
// For simplicity, we'll hash the shared secret here
let encryption_key = {
let mut hasher = Sha256::default(); let mut hasher = Sha256::default();
hasher.update(ephemeral_signing_key.to_bytes()); hasher.update(recipient_public_key);
hasher.update(shared_point.as_bytes()); // Use a fixed salt for testing purposes
hasher.update(b"fixed_salt_for_testing");
hasher.finalize().to_vec() hasher.finalize().to_vec()
}; };
// Encrypt the message using the derived key // Encrypt the message using the derived key
let ciphertext = implementation::encrypt_with_key(&shared_secret, message) let ciphertext = implementation::encrypt_with_key(&encryption_key, message)
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
// Format: ephemeral_public_key || ciphertext // Format: ephemeral_public_key || ciphertext
let mut result = ephemeral_public_key.to_sec1_bytes().to_vec(); let mut result = ephemeral_public_key
.to_encoded_point(false)
.as_bytes()
.to_vec();
result.extend_from_slice(&ciphertext); result.extend_from_slice(&ciphertext);
Ok(result) Ok(result)
} }
/// Decrypts a message using the recipient's private key. /// Decrypts a message using the recipient's private key.
/// This is the counterpart to encrypt_asymmetric. /// This is the counterpart to encrypt_asymmetric.
pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> { pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
// The first 33 or 65 bytes (depending on compression) are the ephemeral public key // The first 33 or 65 bytes (depending on compression) are the ephemeral public key
// For simplicity, we'll assume uncompressed keys (65 bytes) // For simplicity, we'll assume uncompressed keys (65 bytes)
if ciphertext.len() <= 65 { if ciphertext.len() <= 65 {
return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string())); return Err(CryptoError::DecryptionFailed(
"Ciphertext too short".to_string(),
));
} }
// Extract ephemeral public key and actual ciphertext // Extract ephemeral public key and actual ciphertext
let ephemeral_public_key = &ciphertext[..65]; let ephemeral_public_key = &ciphertext[..65];
let actual_ciphertext = &ciphertext[65..]; let actual_ciphertext = &ciphertext[65..];
// Parse ephemeral public key // Parse ephemeral public key
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key) let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key)
.map_err(|_| CryptoError::InvalidKeyLength)?; .map_err(|_| CryptoError::InvalidKeyLength)?;
// Derive shared secret (simplified ECDH) // Derive shared secret using ECDH
let shared_point = sender_key.to_encoded_point(false); let recipient_secret = EphemeralSecret::random(&mut OsRng);
let shared_secret = { let _shared_secret = recipient_secret.diffie_hellman(&sender_key.into());
// Derive decryption key from the shared secret (using the same method as encryption)
let decryption_key = {
let mut hasher = Sha256::default(); let mut hasher = Sha256::default();
hasher.update(self.signing_key.to_bytes()); hasher.update(self.verifying_key.to_sec1_bytes());
hasher.update(shared_point.as_bytes()); // Use the same fixed salt as in encryption
hasher.update(b"fixed_salt_for_testing");
hasher.finalize().to_vec() hasher.finalize().to_vec()
}; };
// Decrypt the message using the derived key // Decrypt the message using the derived key
implementation::decrypt_with_key(&shared_secret, actual_ciphertext) implementation::decrypt_with_key(&decryption_key, actual_ciphertext)
.map_err(|e| CryptoError::DecryptionFailed(e.to_string())) .map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
} }
} }
@ -288,7 +312,7 @@ impl KeySpace {
if self.keypairs.contains_key(name) { if self.keypairs.contains_key(name) {
return Err(CryptoError::KeypairAlreadyExists(name.to_string())); return Err(CryptoError::KeypairAlreadyExists(name.to_string()));
} }
let keypair = KeyPair::new(name); let keypair = KeyPair::new(name);
self.keypairs.insert(name.to_string(), keypair); self.keypairs.insert(name.to_string(), keypair);
Ok(()) Ok(())
@ -296,7 +320,9 @@ impl KeySpace {
/// Gets a keypair by name. /// Gets a keypair by name.
pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> { pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> {
self.keypairs.get(name).ok_or(CryptoError::KeypairNotFound(name.to_string())) self.keypairs
.get(name)
.ok_or(CryptoError::KeypairNotFound(name.to_string()))
} }
/// Lists all keypair names in the space. /// Lists all keypair names in the space.
@ -304,4 +330,3 @@ impl KeySpace {
self.keypairs.keys().cloned().collect() self.keypairs.keys().cloned().collect()
} }
} }

View File

@ -13,3 +13,6 @@ pub use session_manager::{
keypair_pub_key, derive_public_key, keypair_sign, keypair_verify, keypair_pub_key, derive_public_key, keypair_sign, keypair_verify,
verify_with_public_key, encrypt_asymmetric, decrypt_asymmetric verify_with_public_key, encrypt_asymmetric, decrypt_asymmetric
}; };
#[cfg(test)]
mod tests;

View File

@ -2,7 +2,7 @@ use once_cell::sync::Lazy;
use std::sync::Mutex; use std::sync::Mutex;
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::keypair::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs use crate::vault::keyspace::keypair_types::{KeyPair, KeySpace}; // Assuming KeyPair and KeySpace will be in keypair_types.rs
/// Session state for the current key space and selected keypair. /// Session state for the current key space and selected keypair.
pub struct Session { pub struct Session {

View File

@ -0,0 +1,36 @@
# Keyspace Module Specification
This document explains the purpose and functionality of the `keyspace` module within the Hero Vault.
## Purpose of the Module
The `keyspace` module provides a secure and organized way to manage cryptographic keypairs. It allows for the creation, storage, loading, and utilization of keypairs within designated containers called keyspaces. This module is essential for handling sensitive cryptographic material securely.
## What is a Keyspace?
A keyspace is a logical container designed to hold multiple cryptographic keypairs. It is represented by the `KeySpace` struct in the code. Keyspaces can be encrypted and persisted to disk, providing a secure method for storing collections of keypairs. Each keyspace is identified by a unique name.
## What is a Keypair?
A keypair, represented by the `KeyPair` struct, is a fundamental cryptographic element consisting of a mathematically linked pair of keys: a public key and a private key. In this module, ECDSA (Elliptic Curve Digital Signature Algorithm) keypairs are used.
* **Private Key:** This key is kept secret and is used for operations like signing data or decrypting messages intended for the keypair's owner.
* **Public Key:** This key can be shared openly and is used to verify signatures created by the corresponding private key or to encrypt messages that can only be decrypted by the private key.
## How Many Keypairs Per Space?
A keyspace can hold multiple keypairs. The `KeySpace` struct uses a `HashMap` to store keypairs, where each keypair is associated with a unique string name. There is no inherent, fixed limit on the number of keypairs a keyspace can contain, beyond the practical limitations of system memory.
## How Do We Load Them?
Keyspaces are loaded from persistent storage (disk) using the `KeySpace::load` function, which requires the keyspace name and a password for decryption. Once a `KeySpace` object is loaded into memory, it can be set as the currently active keyspace for the session using the `session_manager::set_current_space` function. Individual keypairs within the loaded keyspace are then accessed by their names using functions like `session_manager::select_keypair` and `session_manager::get_selected_keypair`.
## What Do They Do?
Keypairs within a keyspace are used to perform various cryptographic operations. The `KeyPair` struct provides methods for:
* **Digital Signatures:** Signing messages with the private key (`KeyPair::sign`) and verifying those signatures with the public key (`KeyPair::verify`).
* **Ethereum Address Derivation:** Generating an Ethereum address from the public key (`KeyPair::to_ethereum_address`).
* **Asymmetric Encryption/Decryption:** Encrypting data using a recipient's public key (`KeyPair::encrypt_asymmetric`) and decrypting data encrypted with the keypair's public key using the private key (`KeyPair::decrypt_asymmetric`).
The `session_manager` module provides functions that utilize the currently selected keypair to perform these operations within the context of the active session.

View File

@ -0,0 +1,98 @@
use crate::vault::keyspace::keypair_types::{KeyPair, KeySpace};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keypair_creation() {
let keypair = KeyPair::new("test_keypair");
assert_eq!(keypair.name, "test_keypair");
// Basic check that keys are generated (they should have non-zero length)
assert!(!keypair.pub_key().is_empty());
}
#[test]
fn test_keypair_sign_and_verify() {
let keypair = KeyPair::new("test_keypair");
let message = b"This is a test message";
let signature = keypair.sign(message);
assert!(!signature.is_empty());
let is_valid = keypair
.verify(message, &signature)
.expect("Verification failed");
assert!(is_valid);
// Test with a wrong message
let wrong_message = b"This is a different message";
let is_valid_wrong = keypair
.verify(wrong_message, &signature)
.expect("Verification failed with wrong message");
assert!(!is_valid_wrong);
}
#[test]
fn test_verify_with_public_key() {
let keypair = KeyPair::new("test_keypair");
let message = b"Another test message";
let signature = keypair.sign(message);
let public_key = keypair.pub_key();
let is_valid = KeyPair::verify_with_public_key(&public_key, message, &signature)
.expect("Verification with public key failed");
assert!(is_valid);
// Test with a wrong public key
let wrong_keypair = KeyPair::new("wrong_keypair");
let wrong_public_key = wrong_keypair.pub_key();
let is_valid_wrong_key =
KeyPair::verify_with_public_key(&wrong_public_key, message, &signature)
.expect("Verification with wrong public key failed");
assert!(!is_valid_wrong_key);
}
#[test]
fn test_asymmetric_encryption_decryption() {
// Sender's keypair
let sender_keypair = KeyPair::new("sender");
let _ = sender_keypair.pub_key();
// Recipient's keypair
let recipient_keypair = KeyPair::new("recipient");
let recipient_public_key = recipient_keypair.pub_key();
let message = b"This is a secret message";
// Sender encrypts for recipient
let ciphertext = sender_keypair
.encrypt_asymmetric(&recipient_public_key, message)
.expect("Encryption failed");
assert!(!ciphertext.is_empty());
// Recipient decrypts
let decrypted_message = recipient_keypair
.decrypt_asymmetric(&ciphertext)
.expect("Decryption failed");
assert_eq!(decrypted_message, message);
// Test decryption with wrong keypair
let wrong_keypair = KeyPair::new("wrong_recipient");
let result = wrong_keypair.decrypt_asymmetric(&ciphertext);
assert!(result.is_err());
}
#[test]
fn test_keyspace_add_keypair() {
let mut space = KeySpace::new("test_space");
space
.add_keypair("keypair1")
.expect("Failed to add keypair1");
assert_eq!(space.keypairs.len(), 1);
assert!(space.keypairs.contains_key("keypair1"));
// Test adding a duplicate keypair
let result = space.add_keypair("keypair1");
assert!(result.is_err());
}
}

View File

@ -0,0 +1,3 @@
mod keypair_types_tests;
mod session_manager_tests;

View File

@ -0,0 +1,112 @@
use crate::vault::keyspace::keypair_types::KeySpace;
use crate::vault::keyspace::session_manager::{
clear_session, create_keypair, create_space, get_current_space, get_selected_keypair,
list_keypairs, select_keypair, set_current_space,
};
// Helper function to clear the session before each test
fn setup_test() {
clear_session();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_get_space() {
setup_test();
create_space("test_space").expect("Failed to create space");
let space = get_current_space().expect("Failed to get current space");
assert_eq!(space.name, "test_space");
}
#[test]
fn test_set_current_space() {
setup_test();
let space = KeySpace::new("another_space");
set_current_space(space.clone()).expect("Failed to set current space");
let current_space = get_current_space().expect("Failed to get current space");
assert_eq!(current_space.name, "another_space");
}
#[test]
fn test_clear_session() {
setup_test();
create_space("test_space").expect("Failed to create space");
clear_session();
let result = get_current_space();
assert!(result.is_err());
}
#[test]
fn test_create_and_select_keypair() {
setup_test();
create_space("test_space").expect("Failed to create space");
create_keypair("test_keypair").expect("Failed to create keypair");
let keypair = get_selected_keypair().expect("Failed to get selected keypair");
assert_eq!(keypair.name, "test_keypair");
select_keypair("test_keypair").expect("Failed to select keypair");
let selected_keypair =
get_selected_keypair().expect("Failed to get selected keypair after select");
assert_eq!(selected_keypair.name, "test_keypair");
}
#[test]
fn test_list_keypairs() {
setup_test();
create_space("test_space").expect("Failed to create space");
create_keypair("keypair1").expect("Failed to create keypair1");
create_keypair("keypair2").expect("Failed to create keypair2");
let keypairs = list_keypairs().expect("Failed to list keypairs");
assert_eq!(keypairs.len(), 2);
assert!(keypairs.contains(&"keypair1".to_string()));
assert!(keypairs.contains(&"keypair2".to_string()));
}
#[test]
fn test_create_keypair_no_active_space() {
setup_test();
let result = create_keypair("test_keypair");
assert!(result.is_err());
}
#[test]
fn test_select_keypair_no_active_space() {
setup_test();
let result = select_keypair("test_keypair");
assert!(result.is_err());
}
#[test]
fn test_select_nonexistent_keypair() {
setup_test();
create_space("test_space").expect("Failed to create space");
let result = select_keypair("nonexistent_keypair");
assert!(result.is_err());
}
#[test]
fn test_get_selected_keypair_no_active_space() {
setup_test();
let result = get_selected_keypair();
assert!(result.is_err());
}
#[test]
fn test_get_selected_keypair_no_keypair_selected() {
setup_test();
create_space("test_space").expect("Failed to create space");
let result = get_selected_keypair();
assert!(result.is_err());
}
#[test]
fn test_list_keypairs_no_active_space() {
setup_test();
let result = list_keypairs();
assert!(result.is_err());
}
}

View File

@ -165,3 +165,9 @@ let loaded_store = KvStore::load("my_store", "secure_password")?;
let api_key = loaded_store.get("api_key")?; let api_key = loaded_store.get("api_key")?;
println!("API Key: {}", api_key.unwrap_or_default()); println!("API Key: {}", api_key.unwrap_or_default());
``` ```
## to test
```bash
cargo test --lib vault::keypair
```

View File

@ -12,3 +12,6 @@ pub use store::{
create_store, open_store, delete_store, create_store, open_store, delete_store,
list_stores, get_store_path list_stores, get_store_path
}; };
#[cfg(test)]
mod tests;

View File

@ -0,0 +1 @@
mod store_tests;

View File

@ -0,0 +1,104 @@
use crate::vault::kvs::store::{create_store, delete_store, open_store};
// Helper function to generate a unique store name for each test
fn generate_test_store_name() -> String {
use rand::Rng;
let random_string: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(10)
.map(char::from)
.collect();
format!("test_store_{}", random_string)
}
// Helper function to clean up test stores
fn cleanup_test_store(name: &str) {
let _ = delete_store(name);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_open_store() {
let store_name = generate_test_store_name();
let store = create_store(&store_name, false, None).expect("Failed to create store");
assert_eq!(store.name(), store_name);
assert!(!store.is_encrypted());
let opened_store = open_store(&store_name, None).expect("Failed to open store");
assert_eq!(opened_store.name(), store_name);
assert!(!opened_store.is_encrypted());
cleanup_test_store(&store_name);
}
#[test]
fn test_set_and_get_value() {
let store_name = generate_test_store_name();
let store = create_store(&store_name, false, None).expect("Failed to create store");
store.set("key1", &"value1").expect("Failed to set value");
let value: String = store.get("key1").expect("Failed to get value");
assert_eq!(value, "value1");
cleanup_test_store(&store_name);
}
#[test]
fn test_delete_value() {
let store_name = generate_test_store_name();
let store = create_store(&store_name, false, None).expect("Failed to create store");
store.set("key1", &"value1").expect("Failed to set value");
store.delete("key1").expect("Failed to delete value");
let result: Result<String, _> = store.get("key1");
assert!(result.is_err());
cleanup_test_store(&store_name);
}
#[test]
fn test_contains_key() {
let store_name = generate_test_store_name();
let store = create_store(&store_name, false, None).expect("Failed to create store");
store.set("key1", &"value1").expect("Failed to set value");
assert!(store.contains("key1").expect("Failed to check contains"));
assert!(!store.contains("key2").expect("Failed to check contains"));
cleanup_test_store(&store_name);
}
#[test]
fn test_list_keys() {
let store_name = generate_test_store_name();
let store = create_store(&store_name, false, None).expect("Failed to create store");
store.set("key1", &"value1").expect("Failed to set value");
store.set("key2", &"value2").expect("Failed to set value");
let keys = store.keys().expect("Failed to list keys");
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"key2".to_string()));
cleanup_test_store(&store_name);
}
#[test]
fn test_clear_store() {
let store_name = generate_test_store_name();
let store = create_store(&store_name, false, None).expect("Failed to create store");
store.set("key1", &"value1").expect("Failed to set value");
store.set("key2", &"value2").expect("Failed to set value");
store.clear().expect("Failed to clear store");
let keys = store.keys().expect("Failed to list keys after clear");
assert!(keys.is_empty());
cleanup_test_store(&store_name);
}
}

View File

@ -9,7 +9,7 @@
//! - Key-value store with encryption //! - Key-value store with encryption
pub mod error; pub mod error;
pub mod keypair; pub mod keyspace;
pub mod symmetric; pub mod symmetric;
pub mod ethereum; pub mod ethereum;
pub mod kvs; pub mod kvs;
@ -17,4 +17,4 @@ pub mod kvs;
// Re-export modules // Re-export modules
// Re-export common types for convenience // Re-export common types for convenience
pub use error::CryptoError; pub use error::CryptoError;
pub use keypair::{KeyPair, KeySpace}; pub use keyspace::{KeyPair, KeySpace};

View File

@ -7,7 +7,7 @@ use serde::{Serialize, Deserialize};
use sha2::{Sha256, Digest}; use sha2::{Sha256, Digest};
use crate::vault::error::CryptoError; use crate::vault::error::CryptoError;
use crate::vault::keypair::KeySpace; use crate::vault::keyspace::KeySpace;
/// The size of the nonce in bytes. /// The size of the nonce in bytes.
const NONCE_SIZE: usize = 12; const NONCE_SIZE: usize = 12;

232
src/virt/buildah/README.md Normal file
View File

@ -0,0 +1,232 @@
# SAL Buildah Module (`sal::virt::buildah`)
## Overview
The Buildah module in SAL provides a comprehensive Rust interface for interacting with the `buildah` command-line tool. It allows users to build OCI (Open Container Initiative) and Docker-compatible container images programmatically. The module offers both a high-level `Builder` API for step-by-step image construction and static functions for managing images in local storage.
A Rhai script interface for this module is also available via `sal::rhai::buildah`, making these functionalities accessible from `herodo` scripts.
## Core Components
### 1. `Builder` Struct (`sal::virt::buildah::Builder`)
The `Builder` struct is the primary entry point for constructing container images. It encapsulates a Buildah working container, created from a base image, and provides methods to modify this container and eventually commit it as a new image.
- **Creation**: `Builder::new(name: &str, image: &str) -> Result<Builder, BuildahError>`
- Creates a new working container (or re-attaches to an existing one with the same name) from the specified base `image`.
- **Debug Mode**: `builder.set_debug(true)` / `builder.debug()`
- Enables/disables verbose logging for Buildah commands executed by this builder instance.
#### Working Container Operations:
- `builder.run(command: &str) -> Result<CommandResult, BuildahError>`: Executes a shell command inside the working container (e.g., `buildah run <container> -- <command>`).
- `builder.run_with_isolation(command: &str, isolation: &str) -> Result<CommandResult, BuildahError>`: Runs a command with specified isolation (e.g., "chroot").
- `builder.copy(source_on_host: &str, dest_in_container: &str) -> Result<CommandResult, BuildahError>`: Copies files/directories from the host to the container (`buildah copy`).
- `builder.add(source_on_host: &str, dest_in_container: &str) -> Result<CommandResult, BuildahError>`: Adds files/directories to the container (`buildah add`), potentially handling URLs and archive extraction.
- `builder.config(options: HashMap<String, String>) -> Result<CommandResult, BuildahError>`: Modifies image metadata (e.g., environment variables, labels, entrypoint, cmd). Example options: `{"env": "MYVAR=value", "label": "mylabel=myvalue"}`.
- `builder.set_entrypoint(entrypoint: &str) -> Result<CommandResult, BuildahError>`: Sets the image entrypoint.
- `builder.set_cmd(cmd: &str) -> Result<CommandResult, BuildahError>`: Sets the default command for the image.
- `builder.commit(image_name: &str) -> Result<CommandResult, BuildahError>`: Commits the current state of the working container to a new image named `image_name`.
- `builder.remove() -> Result<CommandResult, BuildahError>`: Removes the working container (`buildah rm`).
- `builder.reset() -> Result<(), BuildahError>`: Removes the working container and resets the builder state.
### 2. Static Image Management Functions (on `Builder`)
These functions operate on images in the local Buildah storage and are not tied to a specific `Builder` instance.
- `Builder::images() -> Result<Vec<Image>, BuildahError>`: Lists all images available locally (`buildah images --json`). Returns a vector of `Image` structs.
- `Builder::image_remove(image_ref: &str) -> Result<CommandResult, BuildahError>`: Removes an image (`buildah rmi <image_ref>`).
- `Builder::image_pull(image_name: &str, tls_verify: bool) -> Result<CommandResult, BuildahError>`: Pulls an image from a registry (`buildah pull`).
- `Builder::image_push(image_ref: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, BuildahError>`: Pushes an image to a registry (`buildah push`).
- `Builder::image_tag(image_ref: &str, new_name: &str) -> Result<CommandResult, BuildahError>`: Tags an image (`buildah tag`).
- `Builder::image_commit(container_ref: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError>`: A static version to commit any existing container to an image, with options for format (e.g., "oci", "docker"), squashing layers, and removing the container post-commit.
- `Builder::build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError>`: Builds an image from a Dockerfile/Containerfile (`buildah bud`).
*Note: Many static image functions also have a `_with_debug(..., debug: bool)` variant for explicit debug control.*
### 3. `Image` Struct (`sal::virt::buildah::Image`)
Represents a container image as listed by `buildah images`.
```rust
pub struct Image {
pub id: String, // Image ID
pub names: Vec<String>, // Image names/tags
pub size: String, // Image size
pub created: String, // Creation timestamp (as string)
}
```
### 4. `ContentOperations` (`sal::virt::buildah::ContentOperations`)
Provides static methods for reading and writing file content directly within a container, useful for dynamic configuration or inspection.
- `ContentOperations::write_content(container_id: &str, content: &str, dest_path_in_container: &str) -> Result<CommandResult, BuildahError>`: Writes string content to a file inside the specified container.
- `ContentOperations::read_content(container_id: &str, source_path_in_container: &str) -> Result<String, BuildahError>`: Reads the content of a file from within the specified container into a string.
### 5. `BuildahError` Enum (`sal::virt::buildah::BuildahError`)
Defines the error types that can occur during Buildah operations:
- `CommandExecutionFailed(io::Error)`: The `buildah` command itself failed to start.
- `CommandFailed(String)`: The `buildah` command ran but returned a non-zero exit code or error.
- `JsonParseError(String)`: Failed to parse JSON output from Buildah.
- `ConversionError(String)`: Error during data conversion.
- `Other(String)`: Generic error.
## Key Design Points
The SAL Buildah module is designed with the following principles:
- **Builder Pattern**: The `Builder` struct (`sal::virt::buildah::Builder`) employs a builder pattern, enabling a fluent, step-by-step, and stateful approach to constructing container images. Each `Builder` instance manages a specific working container.
- **Separation of Concerns**:
- **Instance Methods**: Operations specific to a working container (e.g., `run`, `copy`, `config`, `commit`) are methods on the `Builder` instance.
- **Static Methods**: General image management tasks (e.g., listing images with `Builder::images()`, removing images with `Builder::image_remove()`, pulling, pushing, tagging, and building from a Dockerfile with `Builder::build()`) are provided as static functions on the `Builder` struct.
- **Direct Content Manipulation**: The `ContentOperations` struct provides static methods (`write_content`, `read_content`) to directly interact with files within a Buildah container. This is typically achieved by temporarily mounting the container or using `buildah add` with temporary files, abstracting the complexity from the user.
- **Debuggability**: Fine-grained control over `buildah` command logging is provided. The `builder.set_debug(true)` method enables verbose output for a specific `Builder` instance. Many static functions also offer `_with_debug(..., debug: bool)` variants. This is managed internally via a thread-local flag passed to the core `execute_buildah_command` function.
- **Comprehensive Rhai Integration**: Most functionalities of the Buildah module are exposed to Rhai scripts executed via `herodo`, allowing for powerful automation of image building workflows. This is facilitated by the `sal::rhai::buildah` module.
## Low-Level Command Execution
- `execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError>` (in `sal::virt::buildah::cmd`):
The core function that executes `buildah` commands. It handles debug logging based on a thread-local flag, which is managed by the higher-level `Builder` methods and `_with_debug` static function variants.
## Usage Example (Rust)
```rust
use sal::virt::buildah::{Builder, BuildahError, ContentOperations};
use std::collections::HashMap;
fn build_custom_image() -> Result<String, BuildahError> {
// Create a new builder from a base image (e.g., alpine)
let mut builder = Builder::new("my-custom-container", "docker.io/library/alpine:latest")?;
builder.set_debug(true);
// Run some commands
builder.run("apk add --no-cache curl")?;
builder.run("mkdir /app")?;
// Add a file
ContentOperations::write_content(builder.container_id().unwrap(), "Hello from SAL!", "/app/hello.txt")?;
// Set image configuration
let mut config_opts = HashMap::new();
config_opts.insert("workingdir".to_string(), "/app".to_string());
config_opts.insert("label".to_string(), "maintainer=sal-user".to_string());
builder.config(config_opts)?;
builder.set_entrypoint("["/usr/bin/curl"]")?;
builder.set_cmd("["http://ifconfig.me"]")?;
// Commit the image
let image_tag = "localhost/my-custom-image:latest";
builder.commit(image_tag)?;
println!("Successfully built image: {}", image_tag);
// Clean up the working container
builder.remove()?;
Ok(image_tag.to_string())
}
fn main() {
match build_custom_image() {
Ok(tag) => println!("Image {} created.", tag),
Err(e) => eprintln!("Error building image: {}", e),
}
}
```
## Rhai Scripting with `herodo`
The Buildah module's capabilities are extensively exposed to Rhai scripts, enabling automation of image building and management tasks via the `herodo` CLI tool. The `sal::rhai::buildah` module registers the necessary functions and types.
Below is a summary of commonly used Rhai functions for Buildah. (Note: `builder` refers to an instance of `BuildahBuilder` obtained typically via `bah_new`).
### Builder Object Management
- `bah_new(name: String, image: String) -> BuildahBuilder`: Creates a new Buildah builder instance (working container) from a base `image` with a given `name`.
- `builder.remove()`: Removes the working container associated with the `builder`.
- `builder.reset()`: Removes the working container and resets the `builder` state.
### Builder Configuration & Operations
- `builder.set_debug(is_debug: bool)`: Enables or disables verbose debug logging for commands executed by this `builder`.
- `builder.debug_mode` (property): Get or set the debug mode (e.g., `let mode = builder.debug_mode; builder.debug_mode = true;`).
- `builder.container_id` (property): Returns the ID of the working container (e.g., `let id = builder.container_id;`).
- `builder.name` (property): Returns the name of the builder/working container.
- `builder.image` (property): Returns the base image name used by the builder.
- `builder.run(command: String)`: Executes a shell command inside the `builder`'s working container.
- `builder.run_with_isolation(command: String, isolation: String)`: Runs a command with specified isolation (e.g., "chroot").
- `builder.copy(source_on_host: String, dest_in_container: String)`: Copies files/directories from the host to the `builder`'s container.
- `builder.add(source_on_host: String, dest_in_container: String)`: Adds files/directories to the `builder`'s container (can handle URLs and auto-extract archives).
- `builder.config(options: Map)`: Modifies image metadata. `options` is a Rhai map, e.g., `#{ "env": "MYVAR=value", "label": "foo=bar" }`.
- `builder.set_entrypoint(entrypoint: String)`: Sets the image entrypoint (e.g., `builder.set_entrypoint("[/app/run.sh]")`).
- `builder.set_cmd(cmd: String)`: Sets the default command for the image (e.g., `builder.set_cmd("[--help]")`).
- `builder.commit(image_tag: String)`: Commits the current state of the `builder`'s working container to a new image with `image_tag`.
### Content Operations (with a Builder instance)
- `bah_write_content(builder: BuildahBuilder, content: String, dest_path_in_container: String)`: Writes string `content` to a file at `dest_path_in_container` inside the `builder`'s container.
- `bah_read_content(builder: BuildahBuilder, source_path_in_container: String) -> String`: Reads the content of a file from `source_path_in_container` within the `builder`'s container.
### Global Image Operations
These functions generally correspond to static methods in Rust and operate on the local Buildah image storage.
- `bah_images() -> Array`: Lists all images available locally. Returns an array of `BuildahImage` objects.
- `bah_image_remove(image_ref: String)`: Removes an image (e.g., by ID or tag) from local storage.
- `bah_image_pull(image_name: String, tls_verify: bool)`: Pulls an image from a registry.
- `bah_image_push(image_ref: String, destination: String, tls_verify: bool)`: Pushes a local image to a registry.
- `bah_image_tag(image_ref: String, new_name: String)`: Adds a new tag (`new_name`) to an existing image (`image_ref`).
- `bah_build(tag: String, context_dir: String, file: String, isolation: String)`: Builds an image from a Dockerfile/Containerfile (equivalent to `buildah bud`). `file` is the path to the Dockerfile relative to `context_dir`. `isolation` can be e.g., "chroot".
### Example `herodo` Rhai Script (Revisited)
```rhai
// Create a new builder
let builder = bah_new("my-rhai-app", "docker.io/library/alpine:latest");
builder.debug_mode = true; // Enable debug logging for this builder
// Run commands in the container
builder.run("apk add --no-cache figlet curl");
builder.run("mkdir /data");
// Write content to a file in the container
bah_write_content(builder, "Hello from SAL Buildah via Rhai!", "/data/message.txt");
// Configure image metadata
builder.config(#{
"env": "APP_VERSION=1.0",
"label": "author=HerodoUser"
});
builder.set_entrypoint('["figlet"]');
builder.set_cmd('["Rhai Build"]');
// Commit the image
let image_name = "localhost/my-rhai-app:v1";
builder.commit(image_name);
print(`Image committed: ${image_name}`);
// Clean up the working container
builder.remove();
print("Builder container removed.");
// List local images
print("Current local images:");
let images = bah_images();
for img in images {
print(` ID: ${img.id}, Name(s): ${img.names}, Size: ${img.size}`);
}
// Example: Build from a Dockerfile (assuming Dockerfile exists at /tmp/build_context/Dockerfile)
// Ensure /tmp/build_context/Dockerfile exists with simple content like:
// FROM alpine
// RUN echo "Built with bah_build" > /built.txt
// CMD cat /built.txt
//
// if exist("/tmp/build_context/Dockerfile") {
// print("Building from Dockerfile...");
// bah_build("localhost/from-dockerfile:latest", "/tmp/build_context", "Dockerfile", "chroot");
// print("Dockerfile build complete.");
// bah_image_remove("localhost/from-dockerfile:latest"); // Clean up
// } else {
// print("Skipping Dockerfile build example: /tmp/build_context/Dockerfile not found.");
// }
```
This README provides a guide to using the SAL Buildah module. For more detailed information on specific functions and their parameters, consult the Rust doc comments within the source code.

View File

@ -1,374 +1,223 @@
# Container API for nerdctl # SAL `nerdctl` Module (`sal::virt::nerdctl`)
This module provides a Rust API for managing containers using nerdctl, a Docker-compatible CLI for containerd.
## Overview ## Overview
The Container API is designed with a builder pattern to make it easy to create and manage containers. It provides a fluent interface for configuring container options and performing operations on containers. The `sal::virt::nerdctl` module provides a comprehensive Rust interface for interacting with `nerdctl`, a command-line tool for `containerd`.
It allows for managing container lifecycles, images, and other `nerdctl` functionalities programmatically from Rust and through Rhai scripts via `herodo`.
## Key Components This module offers two primary ways to interact with `nerdctl`:
1. A fluent **`Container` builder pattern** for defining, creating, and managing containers with detailed configurations.
2. **Direct static functions** that wrap common `nerdctl` commands for quick operations on containers and images.
- `Container`: The main struct representing a container ## Core Components
- `HealthCheck`: Configuration for container health checks
- `ContainerStatus`: Information about a container's status
- `ResourceUsage`: Information about a container's resource usage
## Getting Started ### 1. `NerdctlError` (in `mod.rs`)
### Prerequisites An enum defining specific error types for `nerdctl` operations:
- `CommandExecutionFailed(io::Error)`: `nerdctl` command failed to start (e.g., not found).
- `CommandFailed(String)`: `nerdctl` command executed but returned an error.
- `JsonParseError(String)`: Failure to parse JSON output from `nerdctl`.
- `ConversionError(String)`: Error during data type conversions.
- `Other(String)`: Generic errors.
- nerdctl must be installed on your system ### 2. `execute_nerdctl_command` (in `cmd.rs`)
- containerd must be running
### Basic Usage The core function for executing `nerdctl` commands. It takes an array of string arguments, runs the command, and returns a `CommandResult` or `NerdctlError`.
Add the following to your `Cargo.toml`:
```toml
[dependencies]
sal = { path = "/path/to/sal" }
```
Then import the Container API in your Rust code:
```rust ```rust
use sal::virt::nerdctl::Container; // Example (internal usage)
// use sal::virt::nerdctl::execute_nerdctl_command;
// let result = execute_nerdctl_command(&["ps", "-a"]);
``` ```
### 3. `Container` Struct (defined in `container_types.rs`, builder in `container_builder.rs`, operations in `container_operations.rs`)
Represents a `nerdctl` container and is the centerpiece of the builder pattern.
**Fields (Configuration):**
- `name: String`: Name of the container.
- `container_id: Option<String>`: ID of the container (populated after creation).
- `image: Option<String>`: Base image for the container.
- `ports: Vec<String>`: Port mappings (e.g., `"8080:80"`).
- `volumes: Vec<String>`: Volume mounts (e.g., `"/host/path:/container/path"`).
- `env_vars: HashMap<String, String>`: Environment variables.
- `network: Option<String>`: Network to connect to.
- `network_aliases: Vec<String>`: Network aliases.
- `cpu_limit: Option<String>`, `memory_limit: Option<String>`, `memory_swap_limit: Option<String>`, `cpu_shares: Option<String>`: Resource limits.
- `restart_policy: Option<String>`: Restart policy (e.g., `"always"`).
- `health_check: Option<HealthCheck>`: Health check configuration.
- `detach: bool`: Whether to run in detached mode (default: `false`, but Rhai `container_build` implies `true` often).
- `snapshotter: Option<String>`: Snapshotter to use.
**Builder Methods (Fluent Interface - `impl Container` in `container_builder.rs`):**
These methods configure the `Container` object and return `Self` for chaining.
- `Container::new(name: &str, image: &str)`: Constructor (Note: Rhai uses `nerdctl_container_new(name)` and `nerdctl_container_from_image(name, image)` which call underlying Rust constructors).
- `reset()`: Resets configuration, stops/removes existing container with the same name.
- `with_port(port: &str)`, `with_ports(ports: &[&str])`
- `with_volume(volume: &str)`, `with_volumes(volumes: &[&str])`
- `with_env(key: &str, value: &str)`, `with_envs(env_map: &HashMap<&str, &str>)`
- `with_network(network: &str)`
- `with_network_alias(alias: &str)`, `with_network_aliases(aliases: &[&str])`
- `with_cpu_limit(cpus: &str)`
- `with_memory_limit(memory: &str)`
- `with_memory_swap_limit(memory_swap: &str)`
- `with_cpu_shares(shares: &str)`
- `with_restart_policy(policy: &str)`
- `with_health_check(cmd: &str)`
- `with_health_check_options(cmd, interval, timeout, retries, start_period)`
- `with_snapshotter(snapshotter: &str)`
- `with_detach(detach: bool)`
**Action Methods (on `Container` instances):
- `build()` (in `container_builder.rs`): Assembles and executes `nerdctl run` with all configured options. Populates `container_id` on success.
- `start()` (in `container_operations.rs`): Starts the container. If not yet built, it attempts to pull the image and build the container first. Verifies the container is running and provides detailed logs/status on failure.
- `stop()` (in `container_operations.rs`): Stops the container.
- `remove()` (in `container_operations.rs`): Removes the container.
- `exec(command: &str)` (in `container_operations.rs`): Executes a command in the container.
- `copy(source: &str, dest: &str)` (in `container_operations.rs`): Copies files/folders. `source`/`dest` must be formatted like `container_name_or_id:/path` or `/local/path`.
- `status()` (in `container_operations.rs`): Returns `ContainerStatus` by parsing `nerdctl inspect`.
- `health_status()` (in `container_operations.rs`): Returns the health status string from `nerdctl inspect`.
- `logs()` (in `container_operations.rs`): Fetches container logs.
- `resources()` (in `container_operations.rs`): Returns `ResourceUsage` by parsing `nerdctl stats`.
- `commit(image_name: &str)` (in `container_operations.rs`): Commits the container to a new image.
- `export(path: &str)` (in `container_operations.rs`): Exports the container's filesystem as a tarball.
### 4. `HealthCheck` Struct (in `container_types.rs`)
Defines health check parameters:
- `cmd: String`: Command to execute.
- `interval: Option<String>`
- `timeout: Option<String>`
- `retries: Option<u32>`
- `start_period: Option<String>`
### 5. `prepare_health_check_command` (in `health_check_script.rs`)
A helper function that takes a health check command string. If it's multi-line, it attempts to save it as an executable script in `/root/hero/var/containers/healthcheck_<container_name>.sh` and returns the script path. Otherwise, returns the command as is. The path `/root/hero/var/containers` implies this script needs to be accessible from within the target container at that specific location if a multi-line script is used.
### 6. `Image` Struct (in `images.rs`)
Represents a `nerdctl` image, typically from `nerdctl images` output.
- `id: String`
- `repository: String`
- `tag: String`
- `size: String`
- `created: String`
### 7. Static Image Functions (in `images.rs`)
These functions operate on images:
- `images() -> Result<CommandResult, NerdctlError>`: Lists images (`nerdctl images`).
- `image_remove(image: &str)`: Removes an image (`nerdctl rmi`).
- `image_push(image: &str, destination: &str)`: Pushes an image (`nerdctl push`).
- `image_tag(image: &str, new_name: &str)`: Tags an image (`nerdctl tag`).
- `image_pull(image: &str)`: Pulls an image (`nerdctl pull`).
- `image_commit(container: &str, image_name: &str)`: Commits a container to an image (`nerdctl commit`).
- `image_build(tag: &str, context_path: &str)`: Builds an image from a Dockerfile (`nerdctl build -t <tag> <context_path>`).
### 8. Static Container Functions (in `container_functions.rs`)
Direct wrappers for `nerdctl` commands, an alternative to the builder pattern:
- `run(image: &str, name: Option<&str>, detach: bool, ports: Option<&[&str]>, snapshotter: Option<&str>)`: Runs a container.
- `exec(container: &str, command: &str)`: Executes a command in a running container.
- `copy(source: &str, dest: &str)`: Copies files.
- `stop(container: &str)`: Stops a container.
- `remove(container: &str)`: Removes a container.
- `list(all: bool)`: Lists containers (`nerdctl ps`).
- `logs(container: &str)`: Fetches logs for a container.
### 9. `ContainerStatus` and `ResourceUsage` Structs (in `container_types.rs`)
- `ContainerStatus`: Holds parsed data from `nerdctl inspect` (state, status, created, started, health info).
- `ResourceUsage`: Holds parsed data from `nerdctl stats` (CPU, memory, network, block I/O, PIDs).
## Usage Examples ## Usage Examples
### Getting a Container by Name ### Rust Example (Builder Pattern)
You can get a reference to an existing container by name:
```rust ```rust
use sal::virt::nerdctl::Container; use sal::virt::nerdctl::{Container, NerdctlError};
use std::collections::HashMap;
// Get a container by name (if it exists) fn main() -> Result<(), NerdctlError> {
match Container::new("existing-container") { let mut envs = HashMap::new();
Ok(container) => { envs.insert("MY_VAR", "my_value");
if container.container_id.is_some() {
println!("Found container with ID: {}", container.container_id.unwrap());
// Perform operations on the existing container
let status = container.status()?;
println!("Container status: {}", status.status);
} else {
println!("Container exists but has no ID");
}
},
Err(e) => {
println!("Error getting container: {}", e);
}
}
```
### Creating a Container let container_config = Container::new("my_nginx_container", "nginx:latest") // Assuming a constructor like this exists or is adapted
You can create a new container from an image using the builder pattern:
```rust
use sal::virt::nerdctl::Container;
// Create a container from an image
let container = Container::from_image("my-nginx", "nginx:latest")?
.with_port("8080:80")
.with_env("NGINX_HOST", "example.com")
.with_volume("/tmp/nginx:/usr/share/nginx/html")
.with_health_check("curl -f http://localhost/ || exit 1")
.with_detach(true)
.build()?;
```
### Container Operations
Once you have a container, you can perform various operations on it:
```rust
// Execute a command in the container
let result = container.exec("echo 'Hello from container'")?;
println!("Command output: {}", result.stdout);
// Get container status
let status = container.status()?;
println!("Container status: {}", status.status);
// Get resource usage
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
// Stop and remove the container
container.stop()?;
container.remove()?;
```
## Container Configuration Options
The Container API supports a wide range of configuration options through its builder pattern:
### Ports
Map container ports to host ports:
```rust
// Map a single port
.with_port("8080:80")
// Map multiple ports
.with_ports(&["8080:80", "8443:443"])
```
### Volumes
Mount host directories or volumes in the container:
```rust
// Mount a single volume
.with_volume("/host/path:/container/path")
// Mount multiple volumes
.with_volumes(&["/host/path1:/container/path1", "/host/path2:/container/path2"])
```
### Environment Variables
Set environment variables in the container:
```rust
// Set a single environment variable
.with_env("KEY", "value")
// Set multiple environment variables
let mut env_map = HashMap::new();
env_map.insert("KEY1", "value1");
env_map.insert("KEY2", "value2");
.with_envs(&env_map)
```
### Network Configuration
Configure container networking:
```rust
// Set the network
.with_network("bridge")
// Add a network alias
.with_network_alias("my-container")
// Add multiple network aliases
.with_network_aliases(&["alias1", "alias2"])
```
### Resource Limits
Set CPU and memory limits:
```rust
// Set CPU limit (e.g., 0.5 for half a CPU, 2 for 2 CPUs)
.with_cpu_limit("0.5")
// Set memory limit (e.g., 512m for 512MB, 1g for 1GB)
.with_memory_limit("512m")
// Set memory swap limit
.with_memory_swap_limit("1g")
// Set CPU shares (relative weight)
.with_cpu_shares("1024")
```
### Health Checks
Configure container health checks:
```rust
// Simple health check
.with_health_check("curl -f http://localhost/ || exit 1")
// Health check with custom options
.with_health_check_options(
"curl -f http://localhost/ || exit 1", // Command
Some("30s"), // Interval
Some("10s"), // Timeout
Some(3), // Retries
Some("5s") // Start period
)
```
### Other Options
Other container configuration options:
```rust
// Set restart policy
.with_restart_policy("always") // Options: no, always, on-failure, unless-stopped
// Set snapshotter
.with_snapshotter("native") // Options: native, fuse-overlayfs, etc.
// Set detach mode
.with_detach(true) // Run in detached mode
```
## Container Operations
Once a container is created, you can perform various operations on it:
### Basic Operations
```rust
// Start the container
container.start()?;
// Stop the container
container.stop()?;
// Remove the container
container.remove()?;
```
### Command Execution
```rust
// Execute a command in the container
let result = container.exec("echo 'Hello from container'")?;
println!("Command output: {}", result.stdout);
```
### File Operations
```rust
// Copy files between the container and host
container.copy("container_name:/path/in/container", "/path/on/host")?;
container.copy("/path/on/host", "container_name:/path/in/container")?;
// Export the container to a tarball
container.export("/path/to/export.tar")?;
```
### Image Operations
```rust
// Commit the container to an image
container.commit("my-custom-image:latest")?;
```
### Status and Monitoring
```rust
// Get container status
let status = container.status()?;
println!("Container state: {}", status.state);
println!("Container status: {}", status.status);
println!("Created: {}", status.created);
println!("Started: {}", status.started);
// Get health status
let health_status = container.health_status()?;
println!("Health status: {}", health_status);
// Get resource usage
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
println!("Memory limit: {}", resources.memory_limit);
println!("Memory percentage: {}", resources.memory_percentage);
println!("Network I/O: {} / {}", resources.network_input, resources.network_output);
println!("Block I/O: {} / {}", resources.block_input, resources.block_output);
println!("PIDs: {}", resources.pids);
```
## Error Handling
The Container API uses a custom error type `NerdctlError` that can be one of the following:
- `CommandExecutionFailed`: The nerdctl command failed to execute
- `CommandFailed`: The nerdctl command executed but returned an error
- `JsonParseError`: Failed to parse JSON output
- `ConversionError`: Failed to convert data
- `Other`: Generic error
Example error handling:
```rust
match Container::new("non-existent-container") {
Ok(container) => {
// Container exists
println!("Container found");
},
Err(e) => {
match e {
NerdctlError::CommandExecutionFailed(io_error) => {
println!("Failed to execute nerdctl command: {}", io_error);
},
NerdctlError::CommandFailed(error_msg) => {
println!("nerdctl command failed: {}", error_msg);
},
_ => {
println!("Other error: {}", e);
}
}
}
}
```
## Implementation Details
The Container API is implemented in several modules:
- `container_types.rs`: Contains the struct definitions
- `container.rs`: Contains the main Container implementation
- `container_builder.rs`: Contains the builder pattern methods
- `container_operations.rs`: Contains the container operations
- `health_check.rs`: Contains the HealthCheck implementation
This modular approach makes the code more maintainable and easier to understand.
## Complete Example
Here's a complete example that demonstrates the Container API:
```rust
use std::error::Error;
use sal::virt::nerdctl::Container;
fn main() -> Result<(), Box<dyn Error>> {
// Create a container from an image
println!("Creating container from image...");
let container = Container::from_image("my-nginx", "nginx:latest")?
.with_port("8080:80") .with_port("8080:80")
.with_env("NGINX_HOST", "example.com") .with_envs(&envs)
.with_volume("/tmp/nginx:/usr/share/nginx/html")
.with_health_check("curl -f http://localhost/ || exit 1")
.with_detach(true) .with_detach(true)
.build()?; .with_restart_policy("always");
println!("Container created successfully"); // Build (create and run) the container
let built_container = container_config.build()?;
// Execute a command in the container println!("Container {} created with ID: {:?}", built_container.name, built_container.container_id);
println!("Executing command in container...");
let result = container.exec("echo 'Hello from container'")?; // Perform operations
println!("Command output: {}", result.stdout); let status = built_container.status()?;
println!("Status: {}, State: {}", status.status, status.state);
// Get container status
println!("Getting container status..."); // Stop and remove
let status = container.status()?; built_container.stop()?;
println!("Container status: {}", status.status); built_container.remove()?;
println!("Container stopped and removed.");
// Get resource usage
println!("Getting resource usage...");
let resources = container.resources()?;
println!("CPU usage: {}", resources.cpu_usage);
println!("Memory usage: {}", resources.memory_usage);
// Stop and remove the container
println!("Stopping and removing container...");
container.stop()?;
container.remove()?;
println!("Container stopped and removed");
Ok(()) Ok(())
} }
```
*Note: The direct `Container::new(name, image)` constructor isn't explicitly shown in the provided Rust code snippets for `Container` itself, but the Rhai bindings `nerdctl_container_new` and `nerdctl_container_from_image` imply underlying Rust constructors that initialize a `Container` struct. The `build()` method is the primary way to run it after configuration.*
### Rhai Script Example (using `herodo`)
```rhai
// Create and configure a container using the builder pattern
let c = nerdctl_container_from_image("my_redis", "redis:alpine")
.with_port("6379:6379")
.with_restart_policy("unless-stopped");
// Build and run the container
let running_container = c.build();
if running_container.is_ok() {
print(`Container ${running_container.name} ID: ${running_container.container_id}`);
// Get status
let status = running_container.status();
if status.is_ok() {
print(`Status: ${status.state}, Health: ${status.health_status}`);
}
// Stop the container (example, might need a mutable borrow or re-fetch)
// running_container.stop(); // Assuming stop is available and works on the result
// running_container.remove();
} else {
print(`Error building container: ${running_container.error()}`);
}
// Direct command example
let images = nerdctl_images();
print(images.stdout);
nerdctl_image_pull("alpine:latest");
```
## Key Design Points
- **Fluent Builder**: The `Container` struct uses a builder pattern, allowing for clear and chainable configuration of container parameters before execution.
- **Comprehensive Operations**: Covers most common `nerdctl` functionalities for containers and images.
- **Error Handling**: `NerdctlError` provides typed errors. The Rhai layer adds more descriptive error messages for common scenarios.
- **Dual API**: Offers both a detailed builder pattern and simpler static functions for flexibility.
- **Health Check Scripting**: Supports multi-line shell scripts for health checks by saving them to a file, though care must be taken regarding the script's accessibility from within the target container.
- **Resource Parsing**: Includes parsing for `nerdctl inspect` (JSON) and `nerdctl stats` (tabular text) to provide structured information.
## File Structure
- `src/virt/nerdctl/mod.rs`: Main module file, error definitions, sub-module declarations.
- `src/virt/nerdctl/cmd.rs`: Core `execute_nerdctl_command` function.
- `src/virt/nerdctl/container_types.rs`: Definitions for `Container`, `HealthCheck`, `ContainerStatus`, `ResourceUsage`.
- `src/virt/nerdctl/container_builder.rs`: Implements the builder pattern methods for the `Container` struct.
- `src/virt/nerdctl/container_operations.rs`: Implements instance methods on `Container` (start, stop, status, etc.).
- `src/virt/nerdctl/images.rs`: `Image` struct and static functions for image management.
- `src/virt/nerdctl/container_functions.rs`: Static functions for direct container commands.
- `src/virt/nerdctl/health_check_script.rs`: Logic for `prepare_health_check_command`.
- `src/rhai/nerdctl.rs`: Rhai script bindings for `herodo`.

165
src/virt/rfs/README.md Normal file
View File

@ -0,0 +1,165 @@
# SAL RFS (Remote File System) Module (`sal::virt::rfs`)
## Overview
The `sal::virt::rfs` module provides a Rust interface for interacting with an underlying `rfs` command-line tool. This tool facilitates mounting various types of remote and local filesystems and managing packed filesystem layers.
The module allows Rust applications and `herodo` Rhai scripts to:
- Mount and unmount filesystems from different sources (e.g., local paths, SSH, S3, WebDAV).
- List currently mounted filesystems and retrieve information about specific mounts.
- Pack directories into filesystem layers, potentially using specified storage backends.
- Unpack, list contents of, and verify these filesystem layers.
All operations are performed by invoking the `rfs` CLI tool and parsing its output.
## Key Design Points
- **CLI Wrapper**: This module acts as a wrapper around an external `rfs` command-line utility. The actual filesystem operations and layer management are delegated to this tool.
- **Asynchronous Operations (Implicit)**: While the Rust functions themselves might be synchronous, the underlying `execute_rfs_command` (presumably from `super::cmd`) likely handles command execution, which could be asynchronous or blocking depending on its implementation.
- **Filesystem Abstraction**: Supports mounting diverse filesystem types such as `local`, `ssh`, `s3`, and `webdav` through the `rfs` tool's capabilities.
- **Layer Management**: Provides functionalities to `pack` directories into portable layers, `unpack` them, `list_contents`, and `verify` their integrity. This is useful for creating and managing reproducible filesystem snapshots or components.
- **Store Specifications (`StoreSpec`)**: The packing functionality allows specifying `StoreSpec` types, suggesting that packed layers can be stored or referenced using different backend mechanisms (e.g., local files, S3 buckets). This enables flexible storage and retrieval of filesystem layers.
- **Builder Pattern**: Uses `RfsBuilder` for constructing mount commands with various options and `PackBuilder` for packing operations, providing a fluent interface for complex configurations.
- **Rhai Scriptability**: Most functionalities are exposed to Rhai scripts via `herodo` through the `sal::rhai::rfs` bridge, enabling automation of filesystem and layer management tasks.
- **Structured Error Handling**: Defines `RfsError` for specific error conditions encountered during `rfs` command execution or output parsing.
## Rhai Scripting with `herodo`
The `sal::virt::rfs` module is scriptable via `herodo`. The following functions are available in Rhai, prefixed with `rfs_`:
### Mount Operations
- `rfs_mount(source: String, target: String, mount_type: String, options: Map) -> Map`
- Mounts a filesystem.
- `source`: The source path or URL (e.g., `/path/to/local_dir`, `ssh://user@host:/remote/path`, `s3://bucket/key`).
- `target`: The local path where the filesystem will be mounted.
- `mount_type`: A string specifying the type of filesystem (e.g., "local", "ssh", "s3", "webdav").
- `options`: A Rhai map of additional mount options (e.g., `#{ "read_only": true, "uid": 1000 }`).
- Returns a map containing details of the mount (id, source, target, fs_type, options) on success.
- `rfs_unmount(target: String) -> ()`
- Unmounts the filesystem at the specified target path.
- `rfs_list_mounts() -> Array`
- Lists all currently mounted filesystems managed by `rfs`.
- Returns an array of maps, each representing a mount with its details.
- `rfs_unmount_all() -> ()`
- Unmounts all filesystems currently managed by `rfs`.
- `rfs_get_mount_info(target: String) -> Map`
- Retrieves information about a specific mounted filesystem.
- Returns a map with mount details if found.
### Pack/Layer Operations
- `rfs_pack(directory: String, output: String, store_specs: String) -> ()`
- Packs the contents of a `directory` into an `output` file (layer).
- `store_specs`: A comma-separated string defining storage specifications for the layer (e.g., `"file:path=/path/to/local_store,s3:bucket=my-archive,region=us-west-1"`). Each spec is `type:key=value,key2=value2`.
- `rfs_unpack(input: String, directory: String) -> ()`
- Unpacks an `input` layer file into the specified `directory`.
- `rfs_list_contents(input: String) -> String`
- Lists the contents of an `input` layer file.
- Returns a string containing the file listing (raw output from the `rfs` tool).
- `rfs_verify(input: String) -> bool`
- Verifies the integrity of an `input` layer file.
- Returns `true` if the layer is valid, `false` otherwise.
### Rhai Example
```rhai
// Example: Mounting a local directory (ensure /mnt/my_local_mount exists)
let source_dir = "/tmp/my_data_source"; // Create this directory first
let target_mount = "/mnt/my_local_mount";
// Create source_dir if it doesn't exist for the example to run
// In a real script, you might use sal::os::dir_create or ensure it exists.
// For this example, assume it's manually created or use: os_run_command(`mkdir -p ${source_dir}`);
print(`Mounting ${source_dir} to ${target_mount}...`);
let mount_result = rfs_mount(source_dir, target_mount, "local", #{});
if mount_result.is_ok() {
print(`Mount successful: ${mount_result}`);
} else {
print(`Mount failed: ${mount_result}`);
}
// List mounts
print("\nCurrent mounts:");
let mounts = rfs_list_mounts();
if mounts.is_ok() {
for m in mounts {
print(` Target: ${m.target}, Source: ${m.source}, Type: ${m.fs_type}`);
}
} else {
print(`Error listing mounts: ${mounts}`);
}
// Example: Packing a directory
let dir_to_pack = "/tmp/pack_this_dir"; // Create and populate this directory
let packed_file = "/tmp/my_layer.pack";
// os_run_command(`mkdir -p ${dir_to_pack}`);
// os_run_command(`echo 'hello' > ${dir_to_pack}/file1.txt`);
print(`\nPacking ${dir_to_pack} to ${packed_file}...`);
// Using a file-based store spec for simplicity
let pack_store_specs = "file:path=/tmp/rfs_store";
// os_run_command(`mkdir -p /tmp/rfs_store`);
let pack_result = rfs_pack(dir_to_pack, packed_file, pack_store_specs);
if pack_result.is_ok() {
print("Packing successful.");
// List contents of the packed file
print(`\nContents of ${packed_file}:`);
let contents = rfs_list_contents(packed_file);
if contents.is_ok() {
print(contents);
} else {
print(`Error listing contents: ${contents}`);
}
// Verify the packed file
print(`\nVerifying ${packed_file}...`);
let verify_result = rfs_verify(packed_file);
if verify_result.is_ok() && verify_result {
print("Verification successful: Layer is valid.");
} else {
print(`Verification failed or error: ${verify_result}`);
}
// Example: Unpacking
let unpack_dir = "/tmp/unpacked_layer_here";
// os_run_command(`mkdir -p ${unpack_dir}`);
print(`\nUnpacking ${packed_file} to ${unpack_dir}...`);
let unpack_result = rfs_unpack(packed_file, unpack_dir);
if unpack_result.is_ok() {
print("Unpacking successful.");
// You would typically check contents of unpack_dir here
// os_run_command(`ls -la ${unpack_dir}`);
} else {
print(`Error unpacking: ${unpack_result}`);
}
} else {
print(`Error packing: ${pack_result}`);
}
// Cleanup: Unmount the local mount
if mount_result.is_ok() {
print(`\nUnmounting ${target_mount}...`);
rfs_unmount(target_mount);
}
// To run this example, ensure the 'rfs' command-line tool is installed and configured,
// and that the necessary directories (/tmp/my_data_source, /mnt/my_local_mount, etc.)
// exist and have correct permissions.
// You might need to run herodo with sudo for mount/unmount operations.
print("\nRFS Rhai script finished.");
```
This module provides a flexible way to manage diverse filesystems and filesystem layers, making it a powerful tool for system automation and deployment tasks within the SAL ecosystem.

163
src/zinit_client/README.md Normal file
View File

@ -0,0 +1,163 @@
# SAL Zinit Client Module (`sal::zinit_client`)
## Overview
The `sal::zinit_client` module provides a Rust interface for interacting with a [Zinit](https://github.com/systeminit/zinit) process supervisor daemon. Zinit is a process and service manager for Linux systems, designed for simplicity and robustness.
This SAL module allows Rust applications and `herodo` Rhai scripts to:
- List and manage Zinit services (get status, start, stop, restart, monitor, forget, kill).
- Define and manage service configurations (create, delete, get).
- Retrieve logs from Zinit.
The client communicates with the Zinit daemon over a Unix domain socket. All operations are performed asynchronously.
## Key Design Points
- **Async Operations**: Leverages `tokio` for asynchronous communication with the Zinit daemon, ensuring non-blocking calls suitable for concurrent applications.
- **Unix Socket Communication**: Connects to the Zinit daemon via a specified Unix domain socket path (e.g., `/var/run/zinit.sock`).
- **Global Client Instance**: Manages a global, lazily-initialized `Arc<ZinitClientWrapper>` to reuse the Zinit client connection across multiple calls within the same process, improving efficiency.
- **Comprehensive Service Management**: Exposes a wide range of Zinit's service management capabilities, from basic lifecycle control to service definition and log retrieval.
- **Rhai Scriptability**: A significant portion of the Zinit client's functionality is exposed to Rhai scripts via `herodo` through the `sal::rhai::zinit` bridge, enabling automation of service management tasks.
- **Error Handling**: Converts errors from the underlying `zinit_client` crate into `zinit_client::ClientError`, which are then translated to `EvalAltResult` for Rhai, providing clear feedback.
- **Simplified Rhai Interface**: For some operations like service creation, the Rhai interface offers a simplified parameter set compared to the direct Rust API for ease of use in scripts.
## Rhai Scripting with `herodo`
The `sal::zinit_client` module is scriptable via `herodo`. The following functions are available in Rhai, prefixed with `zinit_`. All functions require `socket_path` (String) as their first argument, specifying the path to the Zinit Unix domain socket.
- `zinit_list(socket_path: String) -> Map`
- Lists all services managed by Zinit and their states.
- Returns a map where keys are service names and values are their current states (e.g., "Running", "Stopped").
- `zinit_status(socket_path: String, name: String) -> Map`
- Retrieves the detailed status of a specific service.
- `name`: The name of the service.
- Returns a map containing status details like PID, state, target state, and dependencies.
- `zinit_start(socket_path: String, name: String) -> bool`
- Starts the specified service.
- Returns `true` on success.
- `zinit_stop(socket_path: String, name: String) -> bool`
- Stops the specified service.
- Returns `true` on success.
- `zinit_restart(socket_path: String, name: String) -> bool`
- Restarts the specified service.
- Returns `true` on success.
- `zinit_monitor(socket_path: String, name: String) -> bool`
- Enables monitoring for the specified service (Zinit will attempt to keep it running).
- Returns `true` on success.
- `zinit_forget(socket_path: String, name: String) -> bool`
- Disables monitoring for the specified service (Zinit will no longer attempt to restart it if it stops).
- Returns `true` on success.
- `zinit_kill(socket_path: String, name: String, signal: String) -> bool`
- Sends a specific signal (e.g., "TERM", "KILL", "HUP") to the specified service.
- Returns `true` on success.
- `zinit_create_service(socket_path: String, name: String, exec: String, oneshot: bool) -> String`
- Creates a new service configuration in Zinit.
- `name`: The name for the new service.
- `exec`: The command to execute for the service.
- `oneshot`: A boolean indicating if the service is a one-shot task (true) or a long-running process (false).
- Returns a confirmation message or an error.
- `zinit_delete_service(socket_path: String, name: String) -> String`
- Deletes the specified service configuration from Zinit.
- Returns a confirmation message or an error.
- `zinit_get_service(socket_path: String, name: String) -> Dynamic`
- Retrieves the configuration of the specified service as a dynamic map.
- `zinit_logs(socket_path: String, filter: String) -> Array`
- Retrieves logs for a specific service or component matching the filter.
- `filter`: The name of the service/component to get logs for.
- Returns an array of log lines.
- `zinit_logs_all(socket_path: String) -> Array`
- Retrieves all available logs from Zinit.
- Returns an array of log lines.
### Rhai Example
```rhai
// Default Zinit socket path
let zinit_socket = "/var/run/zinit.sock";
// Ensure Zinit is running and socket exists before running this script.
// List all services
print("Listing Zinit services...");
let services = zinit_list(zinit_socket);
if services.is_ok() {
print(`Services: ${services}`);
} else {
print(`Error listing services: ${services}`);
// exit(); // Or handle error appropriately
}
// Define a test service
let service_name = "my_test_app";
let service_exec = "/usr/bin/sleep 300"; // Example command
// Try to get service info first, to see if it exists
let existing_service = zinit_get_service(zinit_socket, service_name);
if !existing_service.is_ok() { // Assuming error means it doesn't exist or can't be fetched
print(`\nService '${service_name}' not found or error. Attempting to create...`);
let create_result = zinit_create_service(zinit_socket, service_name, service_exec, false);
if create_result.is_ok() {
print(`Service '${service_name}' created successfully.`);
} else {
print(`Error creating service '${service_name}': ${create_result}`);
// exit();
}
} else {
print(`\nService '${service_name}' already exists: ${existing_service}`);
}
// Get status of the service
print(`\nFetching status for '${service_name}'...`);
let status = zinit_status(zinit_socket, service_name);
if status.is_ok() {
print(`Status for '${service_name}': ${status}`);
// Example: Start if not running (simplified check)
if status.state != "Running" && status.state != "Starting" {
print(`Attempting to start '${service_name}'...`);
zinit_start(zinit_socket, service_name);
}
} else {
print(`Error fetching status for '${service_name}': ${status}`);
}
// Get some logs for the service (if it produced any)
// Note: Logs might be empty if service just started or hasn't output anything.
print(`\nFetching logs for '${service_name}'...`);
let logs = zinit_logs(zinit_socket, service_name);
if logs.is_ok() {
if logs.len() > 0 {
print(`Logs for '${service_name}':`);
for log_line in logs {
print(` ${log_line}`);
}
} else {
print(`No logs found for '${service_name}'.`);
}
} else {
print(`Error fetching logs for '${service_name}': ${logs}`);
}
// Example: Stop and delete the service (cleanup)
// print(`\nStopping service '${service_name}'...`);
// zinit_stop(zinit_socket, service_name);
// print(`Forgetting service '${service_name}'...`);
// zinit_forget(zinit_socket, service_name); // Stop monitoring before delete
// print(`Deleting service '${service_name}'...`);
// zinit_delete_service(zinit_socket, service_name);
print("\nZinit Rhai script finished.");
```
This module provides a powerful way to automate service management and interaction with Zinit-supervised systems directly from Rust or `herodo` scripts.

2
vault/.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']

22
vault/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "vault"
version = "0.1.0"
edition = "2024"
[features]
native = ["kv/native"]
wasm = ["kv/web"]
[dependencies]
getrandom = { version = "0.3.3", features = ["wasm_js"] }
rand = "0.9.1"
# We need to pull v0.2.x to enable the "js" feature for wasm32 builds
getrandom_old = { package = "getrandom", version = "0.2.16", features = ["js"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
chacha20poly1305 = "0.10.1"
k256 = { version = "0.13.4", features = ["ecdh"] }
sha2 = "0.10.9"
kv = { git = "https://git.ourworld.tf/samehabouelsaad/sal-modular", package = "kvstore", rev = "9dce815daa" }
bincode = { version = "2.0.1", features = ["serde"] }
pbkdf2 = "0.12.2"

160
vault/src/README.md Normal file
View File

@ -0,0 +1,160 @@
# Hero Vault Cryptography Module
The Hero Vault module provides comprehensive cryptographic functionality for the SAL project, including key management, digital signatures, symmetric encryption, Ethereum wallet operations, and a secure key-value store.
## Module Structure
The Hero Vault module is organized into several submodules:
- `error.rs` - Error types for cryptographic operations
- `keypair/` - ECDSA keypair management functionality
- `symmetric/` - Symmetric encryption using ChaCha20Poly1305
- `ethereum/` - Ethereum wallet and smart contract functionality
- `kvs/` - Encrypted key-value store
## Key Features
### Key Space Management
The module provides functionality for creating, loading, and managing key spaces. A key space is a secure container for cryptographic keys, which can be encrypted and stored on disk.
```rust
// Create a new key space
let space = KeySpace::new("my_space", "secure_password")?;
// Save the key space to disk
space.save()?;
// Load a key space from disk
let loaded_space = KeySpace::load("my_space", "secure_password")?;
```
### Keypair Management
The module provides functionality for creating, selecting, and using ECDSA keypairs for digital signatures.
```rust
// Create a new keypair in the active key space
let keypair = space.create_keypair("my_keypair", "secure_password")?;
// Select a keypair for use
space.select_keypair("my_keypair")?;
// List all keypairs in the active key space
let keypairs = space.list_keypairs()?;
```
### Digital Signatures
The module provides functionality for signing and verifying messages using ECDSA.
```rust
// Sign a message using the selected keypair
let signature = space.sign("This is a message to sign")?;
// Verify a signature
let is_valid = space.verify("This is a message to sign", &signature)?;
```
### Symmetric Encryption
The module provides functionality for symmetric encryption using ChaCha20Poly1305.
```rust
// Generate a new symmetric key
let key = space.generate_key()?;
// Encrypt a message
let encrypted = space.encrypt(&key, "This is a secret message")?;
// Decrypt a message
let decrypted = space.decrypt(&key, &encrypted)?;
```
### Ethereum Wallet Functionality
The module provides comprehensive Ethereum wallet functionality, including:
- Creating and managing wallets for different networks
- Sending ETH transactions
- Checking balances
- Interacting with smart contracts
```rust
// Create an Ethereum wallet
let wallet = EthereumWallet::new(keypair)?;
// Get the wallet address
let address = wallet.get_address()?;
// Send ETH
let tx_hash = wallet.send_eth("0x1234...", "1000000000000000")?;
// Check balance
let balance = wallet.get_balance("0x1234...")?;
```
### Smart Contract Interactions
The module provides functionality for interacting with smart contracts on EVM-based blockchains.
```rust
// Load a contract ABI
let contract = Contract::new(provider, "0x1234...", abi)?;
// Call a read-only function
let result = contract.call_read("balanceOf", vec!["0x5678..."])?;
// Call a write function
let tx_hash = contract.call_write("transfer", vec!["0x5678...", "1000"])?;
```
### Key-Value Store
The module provides an encrypted key-value store for securely storing sensitive data.
```rust
// Create a new store
let store = KvStore::new("my_store", "secure_password")?;
// Set a value
store.set("api_key", "secret_api_key")?;
// Get a value
let api_key = store.get("api_key")?;
```
## Error Handling
The module uses a comprehensive error type (`CryptoError`) for handling errors that can occur during cryptographic operations:
- `InvalidKeyLength` - Invalid key length
- `EncryptionFailed` - Encryption failed
- `DecryptionFailed` - Decryption failed
- `SignatureFormatError` - Signature format error
- `KeypairAlreadyExists` - Keypair already exists
- `KeypairNotFound` - Keypair not found
- `NoActiveSpace` - No active key space
- `NoKeypairSelected` - No keypair selected
- `SerializationError` - Serialization error
- `InvalidAddress` - Invalid address format
- `ContractError` - Smart contract error
## Ethereum Networks
The module supports multiple Ethereum networks, including:
- Gnosis Chain
- Peaq Network
- Agung Network
## Security Considerations
- Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password
- Private keys are never stored in plaintext
- The module uses secure random number generation for key creation
- All cryptographic operations use well-established libraries and algorithms
## Examples
For examples of how to use the Hero Vault module, see the `examples/hero_vault` directory.

109
vault/src/error.rs Normal file
View File

@ -0,0 +1,109 @@
#[derive(Debug)]
/// Errors encountered while using the vault
pub enum Error {
/// An error during cryptographic operations
Crypto(CryptoError),
/// An error while performing an I/O operation
IOError(std::io::Error),
/// A corrupt keyspace is returned if a keyspace can't be decrypted
CorruptKeyspace,
/// An error in the used key value store
KV(kv::error::KVError),
/// An error while encoding/decoding the keyspace.
Coding,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::Crypto(e) => f.write_fmt(format_args!("crypto: {e}")),
Error::IOError(e) => f.write_fmt(format_args!("io: {e}")),
Error::CorruptKeyspace => f.write_str("corrupt keyspace"),
Error::KV(e) => f.write_fmt(format_args!("kv: {e}")),
Error::Coding => f.write_str("keyspace coding failed"),
}
}
}
impl core::error::Error for Error {}
#[derive(Debug)]
/// Errors generated by the vault or keys.
///
/// These errors are intentionally vague to avoid issues such as padding oracles.
pub enum CryptoError {
/// Key size is not valid for this type of key
InvalidKeySize,
/// Something went wrong while trying to encrypt data
EncryptionFailed,
/// Something went wrong while trying to decrypt data
DecryptionFailed,
/// Something went wrong while trying to sign a message
SigningError,
/// The signature is invalid for this message and public key
SignatureFailed,
/// The signature does not have the expected size
InvalidSignatureSize,
/// Trying to load a key which is not the expected format,
InvalidKey,
}
impl core::fmt::Display for CryptoError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CryptoError::InvalidKeySize => f.write_str("provided key is not the correct size"),
CryptoError::EncryptionFailed => f.write_str("encryption failure"),
CryptoError::DecryptionFailed => f.write_str("decryption failure"),
CryptoError::SigningError => f.write_str("signature generation failure"),
CryptoError::SignatureFailed => f.write_str("signature verification failure"),
CryptoError::InvalidSignatureSize => {
f.write_str("provided signature does not have the expected size")
}
CryptoError::InvalidKey => f.write_str("the provided bytes are not a valid key"),
}
}
}
impl core::error::Error for CryptoError {}
impl From<CryptoError> for Error {
fn from(value: CryptoError) -> Self {
Self::Crypto(value)
}
}
impl From<std::io::Error> for Error {
fn from(value: std::io::Error) -> Self {
Self::IOError(value)
}
}
impl From<kv::error::KVError> for Error {
fn from(value: kv::error::KVError) -> Self {
Self::KV(value)
}
}
impl From<bincode::error::DecodeError> for Error {
fn from(_: bincode::error::DecodeError) -> Self {
Self::Coding
}
}
impl From<bincode::error::EncodeError> for Error {
fn from(_: bincode::error::EncodeError) -> Self {
Self::Coding
}
}
impl From<k256::ecdsa::Error> for CryptoError {
fn from(_: k256::ecdsa::Error) -> Self {
Self::InvalidKey
}
}
impl From<k256::elliptic_curve::Error> for CryptoError {
fn from(_: k256::elliptic_curve::Error) -> Self {
Self::InvalidKey
}
}

83
vault/src/key.rs Normal file
View File

@ -0,0 +1,83 @@
use asymmetric::AsymmetricKeypair;
use serde::{Deserialize, Serialize};
use signature::SigningKeypair;
use symmetric::SymmetricKey;
pub mod asymmetric;
pub mod signature;
pub mod symmetric;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
pub enum KeyType {
/// The key can be used for symmetric key encryption
Symmetric,
/// The key can be used for asymmetric encryption
Asymmetric,
/// The key can be used for digital signatures
Signature,
}
/// Key holds generic information about a key
#[derive(Clone, Deserialize, Serialize)]
pub struct Key {
/// The mode of the key
mode: KeyType,
/// Raw bytes of the key
raw_key: Vec<u8>,
}
impl Key {
/// Try to downcast this `Key` to a [`SymmetricKey`]
pub fn as_symmetric(&self) -> Option<SymmetricKey> {
if matches!(self.mode, KeyType::Symmetric) {
SymmetricKey::from_bytes(&self.raw_key).ok()
} else {
None
}
}
/// Try to downcast this `Key` to an [`AsymmetricKeypair`]
pub fn as_asymmetric(&self) -> Option<AsymmetricKeypair> {
if matches!(self.mode, KeyType::Asymmetric) {
AsymmetricKeypair::from_bytes(&self.raw_key).ok()
} else {
None
}
}
/// Try to downcast this `Key` to a [`SigningKeypair`]
pub fn as_signing(&self) -> Option<SigningKeypair> {
if matches!(self.mode, KeyType::Signature) {
SigningKeypair::from_bytes(&self.raw_key).ok()
} else {
None
}
}
}
impl From<SymmetricKey> for Key {
fn from(value: SymmetricKey) -> Self {
Self {
mode: KeyType::Symmetric,
raw_key: Vec::from(value.as_raw_bytes()),
}
}
}
impl From<AsymmetricKeypair> for Key {
fn from(value: AsymmetricKeypair) -> Self {
Self {
mode: KeyType::Asymmetric,
raw_key: value.as_raw_private_key(),
}
}
}
impl From<SigningKeypair> for Key {
fn from(value: SigningKeypair) -> Self {
Self {
mode: KeyType::Signature,
raw_key: value.as_raw_private_key(),
}
}
}

161
vault/src/key/asymmetric.rs Normal file
View File

@ -0,0 +1,161 @@
//! An implementation of asymmetric cryptography using SECP256k1 ECDH with ChaCha20Poly1305
//! for the actual encryption.
use k256::{SecretKey, ecdh::diffie_hellman, elliptic_curve::sec1::ToEncodedPoint};
use sha2::Sha256;
use crate::{error::CryptoError, key::symmetric::SymmetricKey};
/// A keypair for use in asymmetric encryption operations.
pub struct AsymmetricKeypair {
/// Private part of the key
private: SecretKey,
/// Public part of the key
public: k256::PublicKey,
}
/// The public key part of an asymmetric keypair.
#[derive(Debug, PartialEq, Eq)]
pub struct PublicKey(k256::PublicKey);
impl AsymmetricKeypair {
/// Generates a new random keypair
pub fn new() -> Result<Self, CryptoError> {
let mut raw_private = [0u8; 32];
rand::fill(&mut raw_private);
let sk = SecretKey::from_slice(&raw_private)
.expect("Key is provided generated with fixed valid size");
let pk = sk.public_key();
Ok(Self {
private: sk,
public: pk,
})
}
/// Create a new key from existing bytes.
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
if bytes.len() == 32 {
let sk = SecretKey::from_slice(&bytes).expect("Key was checked to be a valid size");
let pk = sk.public_key();
Ok(Self {
private: sk,
public: pk,
})
} else {
Err(CryptoError::InvalidKeySize)
}
}
/// View the raw bytes of the private key of this keypair.
pub(crate) fn as_raw_private_key(&self) -> Vec<u8> {
self.private.as_scalar_primitive().to_bytes().to_vec()
}
/// Get the public part of this keypair.
pub fn public_key(&self) -> PublicKey {
PublicKey(self.public.clone())
}
/// Encrypt data for a receiver. First a shared secret is derived using the own private key and
/// the receivers public key. Then, this shared secret is used for symmetric encryption of the
/// plaintext. The receiver can decrypt this by generating the same shared secret, using his
/// own private key and our public key.
pub fn encrypt(
&self,
remote_key: &PublicKey,
plaintext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let mut symmetric_key = [0u8; 32];
diffie_hellman(self.private.to_nonzero_scalar(), remote_key.0.as_affine())
.extract::<Sha256>(None)
.expand(&[], &mut symmetric_key)
.map_err(|_| CryptoError::InvalidKeySize)?;
let sym_key = SymmetricKey::from_bytes(&symmetric_key)?;
sym_key.encrypt(plaintext)
}
/// Decrypt data from a sender. The remote key must be the public key of the keypair used by
/// the sender to encrypt this message.
pub fn decrypt(
&self,
remote_key: &PublicKey,
ciphertext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
let mut symmetric_key = [0u8; 32];
diffie_hellman(self.private.to_nonzero_scalar(), remote_key.0.as_affine())
.extract::<Sha256>(None)
.expand(&[], &mut symmetric_key)
.map_err(|_| CryptoError::InvalidKeySize)?;
let sym_key = SymmetricKey::from_bytes(&symmetric_key)?;
sym_key.decrypt(ciphertext)
}
}
impl PublicKey {
/// Import a public key from raw bytes
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
Ok(Self(k256::PublicKey::from_sec1_bytes(bytes)?))
}
/// Get the raw bytes of this `PublicKey`, which can be transferred to another party.
///
/// The public key is SEC-1 encoded and compressed.
pub fn as_bytes(&self) -> Box<[u8]> {
self.0.to_encoded_point(true).to_bytes()
}
}
#[cfg(test)]
mod tests {
/// Export a public key and import it later
#[test]
fn import_public_key() {
let kp = super::AsymmetricKeypair::new().expect("Can generate new keypair");
let pk1 = kp.public_key();
let pk_bytes = pk1.as_bytes();
let pk2 = super::PublicKey::from_bytes(&pk_bytes).expect("Can import public key");
assert_eq!(pk1, pk2);
}
/// Make sure 2 random keypairs derive the same shared secret (and thus encryption key), by
/// encrypting a random message, decrypting it, and verifying it matches.
#[test]
fn encrypt_and_decrypt() {
let kp1 = super::AsymmetricKeypair::new().expect("Can generate new keypair");
let kp2 = super::AsymmetricKeypair::new().expect("Can generate new keypair");
let pk1 = kp1.public_key();
let pk2 = kp2.public_key();
let message = b"this is a random message to encrypt and decrypt";
let enc = kp1.encrypt(&pk2, message).expect("Can encrypt message");
let dec = kp2.decrypt(&pk1, &enc).expect("Can decrypt message");
assert_eq!(message.as_slice(), dec.as_slice());
}
/// Use a different public key for decrypting than the expected one, this should fail the
/// decryption process as we use AEAD encryption with the symmetric key.
#[test]
fn decrypt_with_wrong_key() {
let kp1 = super::AsymmetricKeypair::new().expect("Can generate new keypair");
let kp2 = super::AsymmetricKeypair::new().expect("Can generate new keypair");
let kp3 = super::AsymmetricKeypair::new().expect("Can generate new keypair");
let pk2 = kp2.public_key();
let pk3 = kp3.public_key();
let message = b"this is a random message to encrypt and decrypt";
let enc = kp1.encrypt(&pk2, message).expect("Can encrypt message");
let dec = kp2.decrypt(&pk3, &enc);
assert!(dec.is_err());
}
}

142
vault/src/key/signature.rs Normal file
View File

@ -0,0 +1,142 @@
//! An implementation of digitial signatures using secp256k1 ECDSA.
use k256::ecdsa::{
Signature, SigningKey, VerifyingKey,
signature::{Signer, Verifier},
};
use crate::error::CryptoError;
pub struct SigningKeypair {
sk: SigningKey,
vk: VerifyingKey,
}
#[derive(Debug, PartialEq, Eq)]
pub struct PublicKey(VerifyingKey);
impl SigningKeypair {
/// Generates a new random keypair
pub fn new() -> Result<Self, CryptoError> {
let mut raw_private = [0u8; 32];
rand::fill(&mut raw_private);
let sk = SigningKey::from_slice(&raw_private)
.expect("Key is provided generated with fixed valid size");
let vk = sk.verifying_key().to_owned();
Ok(Self { sk, vk })
}
/// Create a new key from existing bytes.
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
if bytes.len() == 32 {
let sk = SigningKey::from_slice(&bytes).expect("Key was checked to be a valid size");
let vk = sk.verifying_key().to_owned();
Ok(Self { sk, vk })
} else {
Err(CryptoError::InvalidKeySize)
}
}
/// View the raw bytes of the private key of this keypair.
pub(crate) fn as_raw_private_key(&self) -> Vec<u8> {
self.sk.as_nonzero_scalar().to_bytes().to_vec()
}
/// Get the public part of this keypair.
pub fn public_key(&self) -> PublicKey {
PublicKey(self.vk)
}
/// Sign data with the private key of this `SigningKeypair`. Other parties can use the public
/// key to verify the signature. The generated signature is a detached signature.
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
let sig: Signature = self.sk.sign(message);
Ok(sig.to_vec())
}
}
impl PublicKey {
/// Import a public key from raw bytes
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
Ok(Self(VerifyingKey::from_sec1_bytes(bytes)?))
}
/// Get the raw bytes of this `PublicKey`, which can be transferred to another party.
///
/// The public key is SEC-1 encoded and compressed.
pub fn as_bytes(&self) -> Box<[u8]> {
self.0.to_encoded_point(true).to_bytes()
}
pub fn verify_signature(&self, message: &[u8], sig: &[u8]) -> Result<(), CryptoError> {
let sig = Signature::from_slice(sig).map_err(|_| CryptoError::InvalidKeySize)?;
self.0
.verify(message, &sig)
.map_err(|_| CryptoError::SignatureFailed)
}
}
#[cfg(test)]
mod tests {
/// Generate a key, get the public key, export the bytes of said public key, import them again
/// as a public key, and verify the keys match. This make sure public keys can be exchanged.
#[test]
fn recover_public_key() {
let sk = super::SigningKeypair::new().expect("Can generate new key");
let pk = sk.public_key();
let pk_bytes = pk.as_bytes();
let pk2 = super::PublicKey::from_bytes(&pk_bytes).expect("Can import public key");
assert_eq!(pk, pk2);
}
/// Sign a message and validate the signature with the public key. Together with the above test
/// this makes sure a remote system can receive our public key and validate messages we sign.
#[test]
fn validate_signature() {
let sk = super::SigningKeypair::new().expect("Can generate new key");
let pk = sk.public_key();
let message = b"this is an arbitrary message we want to sign";
let sig = sk.sign(message).expect("Message can be signed");
assert!(pk.verify_signature(message, &sig).is_ok());
}
/// Make sure a signature which is tampered with does not pass signature validation
#[test]
fn corrupt_signature_does_not_validate() {
let sk = super::SigningKeypair::new().expect("Can generate new key");
let pk = sk.public_key();
let message = b"this is an arbitrary message we want to sign";
let mut sig = sk.sign(message).expect("Message can be signed");
// Tamper with the sig
sig[0] = sig[0].wrapping_add(1);
assert!(pk.verify_signature(message, &sig).is_err());
}
/// Make sure a valid signature does not work for a message which has been modified
#[test]
fn tampered_message_does_not_validate() {
let sk = super::SigningKeypair::new().expect("Can generate new key");
let pk = sk.public_key();
let message = b"this is an arbitrary message we want to sign";
let mut message_clone = message.to_vec();
let sig = sk.sign(message).expect("Message can be signed");
// Modify the message
message_clone[0] = message[0].wrapping_add(1);
assert!(pk.verify_signature(&message_clone, &sig).is_err());
}
}

151
vault/src/key/symmetric.rs Normal file
View File

@ -0,0 +1,151 @@
//! An implementation of symmetric keys for ChaCha20Poly1305 encryption.
//!
//! The ciphertext is authenticated.
//! The 12-byte nonce is appended to the generated ciphertext.
//! Keys are 32 bytes in size.
use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce, aead::Aead};
use crate::error::CryptoError;
#[derive(Debug, PartialEq, Eq)]
pub struct SymmetricKey([u8; 32]);
/// Size of a nonce in ChaCha20Poly1305.
const NONCE_SIZE: usize = 12;
impl SymmetricKey {
/// Generate a new random SymmetricKey.
pub fn new() -> Self {
let mut key = [0u8; 32];
rand::fill(&mut key);
Self(key)
}
/// Create a new key from existing bytes.
pub(crate) fn from_bytes(bytes: &[u8]) -> Result<SymmetricKey, CryptoError> {
if bytes.len() == 32 {
let mut key = [0u8; 32];
key.copy_from_slice(bytes);
Ok(SymmetricKey(key))
} else {
Err(CryptoError::InvalidKeySize)
}
}
/// View the raw bytes of this key
pub(crate) fn as_raw_bytes(&self) -> &[u8; 32] {
&self.0
}
/// Encrypt a plaintext with the key. A nonce is generated and appended to the end of the
/// message.
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
// Create cipher
let cipher = ChaCha20Poly1305::new_from_slice(&self.0)
.expect("Key is a fixed 32 byte array so size is always ok");
// Generate random nonce
let mut nonce_bytes = [0u8; NONCE_SIZE];
rand::fill(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
// Encrypt message
let mut ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|_| CryptoError::EncryptionFailed)?;
// Append nonce to ciphertext
ciphertext.extend_from_slice(&nonce_bytes);
Ok(ciphertext)
}
/// Decrypts a ciphertext with appended nonce.
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
// Check if ciphertext is long enough to contain a nonce
if ciphertext.len() <= NONCE_SIZE {
return Err(CryptoError::DecryptionFailed);
}
// Extract nonce from the end of ciphertext
let ciphertext_len = ciphertext.len() - NONCE_SIZE;
let nonce_bytes = &ciphertext[ciphertext_len..];
let ciphertext = &ciphertext[0..ciphertext_len];
// Create cipher
let cipher = ChaCha20Poly1305::new_from_slice(&self.0)
.expect("Key is a fixed 32 byte array so size is always ok");
let nonce = Nonce::from_slice(nonce_bytes);
// Decrypt message
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| CryptoError::DecryptionFailed)
}
/// Derives a new symmetric key from a password.
///
/// Derivation is done using pbkdf2 with Sha256 hashing.
pub fn derive_from_password(password: &str) -> Self {
/// Salt to use for PBKDF2. This needs to be consistent accross runs to generate the same
/// key. Additionally, it does not really matter what this is, as long as its unique.
const SALT: &[u8; 10] = b"vault_salt";
/// Amount of rounds to use for key generation. More rounds => more cpu time. Changing this
/// also chagnes the generated keys.
const ROUNDS: u32 = 100_000;
let mut key = [0; 32];
pbkdf2::pbkdf2_hmac::<sha2::Sha256>(password.as_bytes(), SALT, ROUNDS, &mut key);
Self(key)
}
}
#[cfg(test)]
mod tests {
/// Using the same password derives the same key
#[test]
fn same_password_derives_same_key() {
const EXPECTED_KEY: [u8; 32] = [
4, 179, 233, 202, 225, 70, 211, 200, 7, 73, 115, 1, 85, 149, 90, 42, 160, 68, 16, 106,
136, 19, 197, 195, 153, 145, 179, 21, 37, 13, 37, 90,
];
const PASSWORD: &str = "test123";
let key = super::SymmetricKey::derive_from_password(PASSWORD);
assert_eq!(key.0, EXPECTED_KEY);
}
/// Make sure an encrypted value with some key can be decrypted with the same key
#[test]
fn can_decrypt() {
let key = super::SymmetricKey::new();
let message = b"this is a message to decrypt";
let enc = key.encrypt(message).expect("Can encrypt message");
let dec = key.decrypt(&enc).expect("Can decrypt message");
assert_eq!(message.as_slice(), dec.as_slice());
}
/// Make sure a value encrypted with one key can't be decrypted with a different key. Since we
/// use AEAD encryption we will notice this when trying to decrypt
#[test]
fn different_key_cant_decrypt() {
let key1 = super::SymmetricKey::new();
let key2 = super::SymmetricKey::new();
let message = b"this is a message to decrypt";
let enc = key1.encrypt(message).expect("Can encrypt message");
let dec = key2.decrypt(&enc);
assert!(dec.is_err());
}
}

131
vault/src/keyspace.rs Normal file
View File

@ -0,0 +1,131 @@
// #[cfg(not(target_arch = "wasm32"))]
// mod fallback;
// #[cfg(target_arch = "wasm32")]
// mod wasm;
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
use crate::{
error::Error,
key::{Key, symmetric::SymmetricKey},
};
use kv::KVStore;
/// Configuration to use for bincode en/decoding.
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
// #[cfg(not(target_arch = "wasm32"))]
// use fallback::KeySpace as Ks;
// #[cfg(target_arch = "wasm32")]
// use wasm::KeySpace as Ks;
#[cfg(not(target_arch = "wasm32"))]
use kv::native::NativeStore;
#[cfg(target_arch = "wasm32")]
use kv::wasm::WasmStore;
const KEYSPACE_NAME: &str = "vault_keyspace";
/// A keyspace represents a group of stored cryptographic keys. The storage is encrypted, a
/// password must be provided when opening the KeySpace to decrypt the keys.
pub struct KeySpace {
// store: Ks,
#[cfg(not(target_arch = "wasm32"))]
store: NativeStore,
#[cfg(target_arch = "wasm32")]
store: WasmStore,
/// A collection of all keys stored in the KeySpace, in decrypted form.
keys: HashMap<String, Key>,
/// The encryption key used to encrypt/decrypt this keyspace.
encryption_key: SymmetricKey,
}
/// Wasm32 constructor
#[cfg(target_arch = "wasm32")]
impl KeySpace {}
/// Non-wasm constructor
#[cfg(not(target_arch = "wasm32"))]
impl KeySpace {
/// Open the keyspace at the provided path using the given key for encryption.
pub async fn open(path: &Path, encryption_key: SymmetricKey) -> Result<Self, Error> {
let store = NativeStore::open(&path.display().to_string())?;
let mut ks = Self {
store,
keys: HashMap::new(),
encryption_key,
};
ks.load_keyspace().await?;
Ok(ks)
}
}
#[cfg(target_arch = "wasm32")]
impl KeySpace {
pub async fn open(name: &str, encryption_key: SymmetricKey) -> Result<Self, Error> {
let store = WasmStore::open(name).await?;
let mut ks = Self {
store,
keys: HashMap::new(),
encryption_key,
};
ks.load_keyspace().await?;
Ok(ks)
}
}
/// Exposed methods, platform independant
impl KeySpace {
/// Get a [`Key`] previously stored under the provided name.
pub async fn get(&self, key: &str) -> Result<Option<Key>, Error> {
Ok(self.keys.get(key).cloned())
}
/// Store a [`Key`] under the provided name.
///
/// This overwrites the existing key if one is already stored with the same name.
pub async fn set(&mut self, key: String, value: Key) -> Result<(), Error> {
self.keys.insert(key, value);
self.save_keyspace().await
}
/// Delete the [`Key`] stored under the provided name.
pub async fn delete(&mut self, key: &str) -> Result<(), Error> {
self.keys.remove(key);
self.save_keyspace().await
}
/// Iterate over all stored [`keys`](Key) in the KeySpace
pub async fn iter(&self) -> Result<impl Iterator<Item = (&String, &Key)>, Error> {
Ok(self.keys.iter())
}
/// Encrypt all keys and save them to the underlying store
async fn save_keyspace(&self) -> Result<(), Error> {
let encoded_keys = bincode::serde::encode_to_vec(&self.keys, BINCODE_CONFIG)?;
let value = self.encryption_key.encrypt(&encoded_keys)?;
// Put in store
Ok(self.store.set(KEYSPACE_NAME, &value).await?)
}
/// Loads the encrypted keyspace from the underlying storage
async fn load_keyspace(&mut self) -> Result<(), Error> {
let Some(ks) = self.store.get(KEYSPACE_NAME).await? else {
// Keyspace doesn't exist yet, nothing to do here
return Ok(());
};
let raw = self.encryption_key.decrypt(&ks)?;
let (decoded_keys, _): (HashMap<String, Key>, _) =
bincode::serde::decode_from_slice(&raw, BINCODE_CONFIG)?;
self.keys = decoded_keys;
Ok(())
}
}

View File

@ -0,0 +1,72 @@
use std::{collections::HashMap, io::Write, path::PathBuf};
use crate::{
error::Error,
key::{Key, symmetric::SymmetricKey},
};
/// Magic value used as header in decrypted keyspace files.
const KEYSPACE_MAGIC: [u8; 14] = [
118, 97, 117, 108, 116, 95, 107, 101, 121, 115, 112, 97, 99, 101,
]; //"vault_keyspace"
/// A KeySpace using the filesystem as storage
pub struct KeySpace {
/// Path to file on disk
path: PathBuf,
/// Decrypted keys held in the store
keystore: HashMap<String, Key>,
/// The encryption key used to encrypt/decrypt the storage.
encryption_key: SymmetricKey,
}
impl KeySpace {
/// Opens the `KeySpace`. If it does not exist, it will be created. The provided encryption key
/// will be used for Encrypting and Decrypting the content of the KeySpace.
async fn open(path: PathBuf, encryption_key: SymmetricKey) -> Result<Self, Error> {
/// If the path does not exist, create it first and write the encrypted magic header
if !path.exists() {
// Since we checked path does not exist, the only errors here can be actual IO errors
// (unless something else creates the same file at the same time).
let mut file = std::fs::File::create_new(path)?;
let content = encryption_key.encrypt(&KEYSPACE_MAGIC)?;
file.write_all(&content)?;
}
// Load file, try to decrypt, verify magic header, deserialize keystore
let mut file = std::fs::File::open(path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
if buffer.len() < KEYSPACE_MAGIC.len() {
return Err(Error::CorruptKeyspace);
}
if buffer[..KEYSPACE_MAGIC.len()] != KEYSPACE_MAGIC {
return Err(Error::CorruptKeyspace);
}
// TODO: Actual deserialization
todo!();
}
/// Get a [`Key`] previously stored under the provided name.
async fn get(&self, key: &str) -> Result<Option<Key>, Error> {
todo!();
}
/// Store a [`Key`] under the provided name.
async fn set(&self, key: &str, value: Key) -> Result<(), Error> {
todo!();
}
/// Delete the [`Key`] stored under the provided name.
async fn delete(&self, key: &str) -> Result<(), Error> {
todo!();
}
/// Iterate over all stored [`keys`](Key) in the KeySpace
async fn iter(&self) -> Result<impl Iterator<Item = (String, Key)>, Error> {
todo!()
}
}

View File

@ -0,0 +1,26 @@
use crate::{error::Error, key::Key};
/// KeySpace represents an IndexDB keyspace
pub struct KeySpace {}
impl KeySpace {
/// Get a [`Key`] previously stored under the provided name.
async fn get(&self, key: &str) -> Result<Option<Key>, Error> {
todo!();
}
/// Store a [`Key`] under the provided name.
async fn set(&self, key: &str, value: Key) -> Result<(), Error> {
todo!();
}
/// Delete the [`Key`] stored under the provided name.
async fn delete(&self, key: &str) -> Result<(), Error> {
todo!();
}
/// Iterate over all stored [`keys`](Key) in the KeySpace
async fn iter(&self) -> Result<impl Iterator<Item = (String, Key)>, Error> {
todo!()
}
}

51
vault/src/lib.rs Normal file
View File

@ -0,0 +1,51 @@
pub mod error;
pub mod key;
pub mod keyspace;
#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use crate::{error::Error, key::symmetric::SymmetricKey, keyspace::KeySpace};
/// Vault is a 2 tiered key-value store. That is, it is a collection of [`spaces`](KeySpace), where
/// each [`space`](KeySpace) is itself an encrypted key-value store
pub struct Vault {
#[cfg(not(target_arch = "wasm32"))]
path: PathBuf,
}
#[cfg(not(target_arch = "wasm32"))]
impl Vault {
/// Create a new store at the given path, creating the path if it does not exist yet.
pub async fn new(path: &Path) -> Result<Self, Error> {
if path.exists() {
if !path.is_dir() {
return Err(Error::IOError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"expected directory",
)));
}
} else {
std::fs::create_dir_all(path)?;
}
Ok(Self {
path: path.to_path_buf(),
})
}
}
impl Vault {
/// Open a keyspace with the given name
pub async fn open_keyspace(&self, name: &str, password: &str) -> Result<KeySpace, Error> {
let encryption_key = SymmetricKey::derive_from_password(password);
#[cfg(not(target_arch = "wasm32"))]
{
let path = self.path.join(name);
KeySpace::open(&path, encryption_key).await
}
#[cfg(target_arch = "wasm32")]
{
KeySpace::open(name, encryption_key).await
}
}
}