refactor and add peaq support
This commit is contained in:
50
src/hero_vault/error.rs
Normal file
50
src/hero_vault/error.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
//! Error types for cryptographic operations
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur during cryptographic operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CryptoError {
|
||||
/// Invalid key length
|
||||
#[error("Invalid key length")]
|
||||
InvalidKeyLength,
|
||||
|
||||
/// Encryption failed
|
||||
#[error("Encryption failed: {0}")]
|
||||
EncryptionFailed(String),
|
||||
|
||||
/// Decryption failed
|
||||
#[error("Decryption failed: {0}")]
|
||||
DecryptionFailed(String),
|
||||
|
||||
/// Signature format error
|
||||
#[error("Signature format error: {0}")]
|
||||
SignatureFormatError(String),
|
||||
|
||||
/// Keypair already exists
|
||||
#[error("Keypair already exists: {0}")]
|
||||
KeypairAlreadyExists(String),
|
||||
|
||||
/// Keypair not found
|
||||
#[error("Keypair not found: {0}")]
|
||||
KeypairNotFound(String),
|
||||
|
||||
/// No active key space
|
||||
#[error("No active key space")]
|
||||
NoActiveSpace,
|
||||
|
||||
/// No keypair selected
|
||||
#[error("No keypair selected")]
|
||||
NoKeypairSelected,
|
||||
|
||||
/// Serialization error
|
||||
#[error("Serialization error: {0}")]
|
||||
SerializationError(String),
|
||||
}
|
||||
|
||||
/// Convert CryptoError to SAL's Error type
|
||||
impl From<CryptoError> for crate::Error {
|
||||
fn from(err: CryptoError) -> Self {
|
||||
crate::Error::Sal(err.to_string())
|
||||
}
|
||||
}
|
||||
66
src/hero_vault/ethereum/mod.rs
Normal file
66
src/hero_vault/ethereum/mod.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Ethereum wallet functionality
|
||||
//!
|
||||
//! This module provides functionality for creating and managing Ethereum wallets.
|
||||
//!
|
||||
//! The module is organized into several components:
|
||||
//! - `wallet.rs`: Core Ethereum wallet implementation
|
||||
//! - `networks.rs`: Network registry and configuration
|
||||
//! - `provider.rs`: Provider creation and management
|
||||
//! - `transaction.rs`: Transaction-related functionality
|
||||
//! - `storage.rs`: Wallet storage functionality
|
||||
|
||||
mod wallet;
|
||||
mod provider;
|
||||
mod transaction;
|
||||
mod storage;
|
||||
pub mod networks;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use wallet::EthereumWallet;
|
||||
pub use networks::NetworkConfig;
|
||||
|
||||
// Re-export wallet creation functions
|
||||
pub use storage::{
|
||||
create_ethereum_wallet_for_network,
|
||||
create_peaq_wallet,
|
||||
create_agung_wallet,
|
||||
create_ethereum_wallet_from_name_for_network,
|
||||
create_ethereum_wallet_from_name,
|
||||
create_ethereum_wallet_from_private_key_for_network,
|
||||
create_ethereum_wallet_from_private_key,
|
||||
};
|
||||
|
||||
// Re-export wallet management functions
|
||||
pub use storage::{
|
||||
get_current_ethereum_wallet_for_network,
|
||||
get_current_peaq_wallet,
|
||||
get_current_agung_wallet,
|
||||
clear_ethereum_wallets,
|
||||
clear_ethereum_wallets_for_network,
|
||||
};
|
||||
|
||||
// Re-export provider functions
|
||||
pub use provider::{
|
||||
create_provider,
|
||||
create_gnosis_provider,
|
||||
create_peaq_provider,
|
||||
create_agung_provider,
|
||||
};
|
||||
|
||||
// Re-export transaction functions
|
||||
pub use transaction::{
|
||||
get_balance,
|
||||
send_eth,
|
||||
format_balance,
|
||||
};
|
||||
|
||||
// Re-export network registry functions
|
||||
pub use networks::{
|
||||
get_network_by_name,
|
||||
get_proper_network_name,
|
||||
list_network_names,
|
||||
get_all_networks,
|
||||
names,
|
||||
};
|
||||
101
src/hero_vault/ethereum/networks.rs
Normal file
101
src/hero_vault/ethereum/networks.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! Ethereum network registry
|
||||
//!
|
||||
//! This module provides a centralized registry of Ethereum networks and utilities
|
||||
//! to work with them.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Configuration for an EVM-compatible network
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkConfig {
|
||||
pub name: String,
|
||||
pub chain_id: u64,
|
||||
pub rpc_url: String,
|
||||
pub explorer_url: String,
|
||||
pub token_symbol: String,
|
||||
pub decimals: u8,
|
||||
}
|
||||
|
||||
/// Network name constants
|
||||
pub mod names {
|
||||
pub const GNOSIS: &str = "Gnosis";
|
||||
pub const PEAQ: &str = "Peaq";
|
||||
pub const AGUNG: &str = "Agung";
|
||||
}
|
||||
|
||||
/// Get the Gnosis Chain network configuration
|
||||
pub fn gnosis() -> NetworkConfig {
|
||||
NetworkConfig {
|
||||
name: names::GNOSIS.to_string(),
|
||||
chain_id: 100,
|
||||
rpc_url: "https://rpc.gnosischain.com".to_string(),
|
||||
explorer_url: "https://gnosisscan.io".to_string(),
|
||||
token_symbol: "xDAI".to_string(),
|
||||
decimals: 18,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Peaq Network configuration
|
||||
pub fn peaq() -> NetworkConfig {
|
||||
NetworkConfig {
|
||||
name: names::PEAQ.to_string(),
|
||||
chain_id: 1701,
|
||||
rpc_url: "https://peaq.api.onfinality.io/public".to_string(),
|
||||
explorer_url: "https://peaq.subscan.io/".to_string(),
|
||||
token_symbol: "PEAQ".to_string(),
|
||||
decimals: 18,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Agung Testnet configuration
|
||||
pub fn agung() -> NetworkConfig {
|
||||
NetworkConfig {
|
||||
name: names::AGUNG.to_string(),
|
||||
chain_id: 9990,
|
||||
rpc_url: "https://wss-async.agung.peaq.network".to_string(),
|
||||
explorer_url: "https://agung-testnet.subscan.io/".to_string(),
|
||||
token_symbol: "AGNG".to_string(),
|
||||
decimals: 18,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a network by its name (case-insensitive)
|
||||
pub fn get_network_by_name(name: &str) -> Option<NetworkConfig> {
|
||||
let name_lower = name.to_lowercase();
|
||||
match name_lower.as_str() {
|
||||
"gnosis" => Some(gnosis()),
|
||||
"peaq" => Some(peaq()),
|
||||
"agung" => Some(agung()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the proper capitalization of a network name
|
||||
pub fn get_proper_network_name(name: &str) -> Option<&'static str> {
|
||||
let name_lower = name.to_lowercase();
|
||||
match name_lower.as_str() {
|
||||
"gnosis" => Some(names::GNOSIS),
|
||||
"peaq" => Some(names::PEAQ),
|
||||
"agung" => Some(names::AGUNG),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of all supported network names
|
||||
pub fn list_network_names() -> Vec<&'static str> {
|
||||
vec![names::GNOSIS, names::PEAQ, names::AGUNG]
|
||||
}
|
||||
|
||||
/// Get a map of all networks
|
||||
pub fn get_all_networks() -> &'static HashMap<&'static str, NetworkConfig> {
|
||||
static NETWORKS: OnceLock<HashMap<&'static str, NetworkConfig>> = OnceLock::new();
|
||||
|
||||
NETWORKS.get_or_init(|| {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(names::GNOSIS, gnosis());
|
||||
map.insert(names::PEAQ, peaq());
|
||||
map.insert(names::AGUNG, agung());
|
||||
map
|
||||
})
|
||||
}
|
||||
27
src/hero_vault/ethereum/provider.rs
Normal file
27
src/hero_vault/ethereum/provider.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Ethereum provider functionality.
|
||||
|
||||
use ethers::prelude::*;
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
use super::networks::{self, NetworkConfig};
|
||||
|
||||
/// Creates a provider for a specific network.
|
||||
pub fn create_provider(network: &NetworkConfig) -> Result<Provider<Http>, CryptoError> {
|
||||
Provider::<Http>::try_from(network.rpc_url.as_str())
|
||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to create provider for {}: {}", network.name, e)))
|
||||
}
|
||||
|
||||
/// Creates a provider for the Gnosis Chain.
|
||||
pub fn create_gnosis_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
create_provider(&networks::gnosis())
|
||||
}
|
||||
|
||||
/// Creates a provider for the Peaq network.
|
||||
pub fn create_peaq_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
create_provider(&networks::peaq())
|
||||
}
|
||||
|
||||
/// Creates a provider for the Agung testnet.
|
||||
pub fn create_agung_provider() -> Result<Provider<Http>, CryptoError> {
|
||||
create_provider(&networks::agung())
|
||||
}
|
||||
114
src/hero_vault/ethereum/storage.rs
Normal file
114
src/hero_vault/ethereum/storage.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
//! Ethereum wallet storage functionality.
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
use super::wallet::EthereumWallet;
|
||||
use super::networks::{self, NetworkConfig};
|
||||
|
||||
/// Global storage for Ethereum wallets.
|
||||
static ETH_WALLETS: Lazy<Mutex<HashMap<String, Vec<EthereumWallet>>>> = Lazy::new(|| {
|
||||
Mutex::new(HashMap::new())
|
||||
});
|
||||
|
||||
/// 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> {
|
||||
// Get the currently selected keypair
|
||||
let keypair = crate::hero_vault::keypair::get_selected_keypair()?;
|
||||
|
||||
// Create an Ethereum wallet from the keypair
|
||||
let wallet = EthereumWallet::from_keypair(&keypair, network)?;
|
||||
|
||||
// Store the wallet
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
|
||||
network_wallets.push(wallet.clone());
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from the currently selected keypair for the Peaq network.
|
||||
pub fn create_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||
create_ethereum_wallet_for_network(networks::peaq())
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from the currently selected keypair for the Agung testnet.
|
||||
pub fn create_agung_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||
create_ethereum_wallet_for_network(networks::agung())
|
||||
}
|
||||
|
||||
/// Gets the current Ethereum wallet for a specific network.
|
||||
pub fn get_current_ethereum_wallet_for_network(network_name: &str) -> Result<EthereumWallet, CryptoError> {
|
||||
let wallets = ETH_WALLETS.lock().unwrap();
|
||||
|
||||
let network_wallets = wallets.get(network_name).ok_or(CryptoError::NoKeypairSelected)?;
|
||||
|
||||
if network_wallets.is_empty() {
|
||||
return Err(CryptoError::NoKeypairSelected);
|
||||
}
|
||||
|
||||
Ok(network_wallets.last().unwrap().clone())
|
||||
}
|
||||
|
||||
/// Gets the current Ethereum wallet for the Peaq network.
|
||||
pub fn get_current_peaq_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||
get_current_ethereum_wallet_for_network("Peaq")
|
||||
}
|
||||
|
||||
/// Gets the current Ethereum wallet for the Agung testnet.
|
||||
pub fn get_current_agung_wallet() -> Result<EthereumWallet, CryptoError> {
|
||||
get_current_ethereum_wallet_for_network("Agung")
|
||||
}
|
||||
|
||||
/// Clears all Ethereum wallets.
|
||||
pub fn clear_ethereum_wallets() {
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.clear();
|
||||
}
|
||||
|
||||
/// Clears Ethereum wallets for a specific network.
|
||||
pub fn clear_ethereum_wallets_for_network(network_name: &str) {
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
wallets.remove(network_name);
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// Get the currently selected keypair
|
||||
let keypair = crate::hero_vault::keypair::get_selected_keypair()?;
|
||||
|
||||
// Create an Ethereum wallet from the name and keypair
|
||||
let wallet = EthereumWallet::from_name_and_keypair(name, &keypair, network)?;
|
||||
|
||||
// Store the wallet
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
|
||||
network_wallets.push(wallet.clone());
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a name and the currently selected keypair for the Gnosis network.
|
||||
pub fn create_ethereum_wallet_from_name(name: &str) -> Result<EthereumWallet, CryptoError> {
|
||||
create_ethereum_wallet_from_name_for_network(name, networks::gnosis())
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a private key for a specific network.
|
||||
pub fn create_ethereum_wallet_from_private_key_for_network(private_key: &str, network: NetworkConfig) -> Result<EthereumWallet, CryptoError> {
|
||||
// Create an Ethereum wallet from the private key
|
||||
let wallet = EthereumWallet::from_private_key(private_key, network)?;
|
||||
|
||||
// Store the wallet
|
||||
let mut wallets = ETH_WALLETS.lock().unwrap();
|
||||
let network_wallets = wallets.entry(wallet.network.name.clone()).or_insert_with(Vec::new);
|
||||
network_wallets.push(wallet.clone());
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
/// Creates an Ethereum wallet from a private key for the Gnosis network.
|
||||
pub fn create_ethereum_wallet_from_private_key(private_key: &str) -> Result<EthereumWallet, CryptoError> {
|
||||
create_ethereum_wallet_from_private_key_for_network(private_key, networks::gnosis())
|
||||
}
|
||||
5
src/hero_vault/ethereum/tests/mod.rs
Normal file
5
src/hero_vault/ethereum/tests/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
//! Tests for Ethereum functionality.
|
||||
|
||||
mod wallet_tests;
|
||||
mod network_tests;
|
||||
mod transaction_tests;
|
||||
74
src/hero_vault/ethereum/tests/network_tests.rs
Normal file
74
src/hero_vault/ethereum/tests/network_tests.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Tests for Ethereum network functionality.
|
||||
|
||||
use crate::hero_vault::ethereum::*;
|
||||
|
||||
#[test]
|
||||
fn test_network_config() {
|
||||
let gnosis = networks::gnosis();
|
||||
assert_eq!(gnosis.name, "Gnosis");
|
||||
assert_eq!(gnosis.chain_id, 100);
|
||||
assert_eq!(gnosis.token_symbol, "xDAI");
|
||||
|
||||
let peaq = networks::peaq();
|
||||
assert_eq!(peaq.name, "Peaq");
|
||||
assert_eq!(peaq.chain_id, 1701);
|
||||
assert_eq!(peaq.token_symbol, "PEAQ");
|
||||
|
||||
let agung = networks::agung();
|
||||
assert_eq!(agung.name, "Agung");
|
||||
assert_eq!(agung.chain_id, 9990);
|
||||
assert_eq!(agung.token_symbol, "AGNG");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_registry() {
|
||||
let network_names = networks::list_network_names();
|
||||
assert!(network_names.iter().any(|&name| name == "Gnosis"));
|
||||
assert!(network_names.iter().any(|&name| name == "Peaq"));
|
||||
assert!(network_names.iter().any(|&name| name == "Agung"));
|
||||
|
||||
let gnosis_proper = networks::get_proper_network_name("gnosis");
|
||||
assert_eq!(gnosis_proper, Some("Gnosis"));
|
||||
|
||||
let peaq_proper = networks::get_proper_network_name("peaq");
|
||||
assert_eq!(peaq_proper, Some("Peaq"));
|
||||
|
||||
let agung_proper = networks::get_proper_network_name("agung");
|
||||
assert_eq!(agung_proper, Some("Agung"));
|
||||
|
||||
let unknown = networks::get_proper_network_name("unknown");
|
||||
assert_eq!(unknown, None);
|
||||
|
||||
let gnosis_config = networks::get_network_by_name("Gnosis");
|
||||
assert!(gnosis_config.is_some());
|
||||
assert_eq!(gnosis_config.unwrap().chain_id, 100);
|
||||
|
||||
let unknown_config = networks::get_network_by_name("Unknown");
|
||||
assert!(unknown_config.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_provider() {
|
||||
let gnosis = networks::gnosis();
|
||||
let peaq = networks::peaq();
|
||||
let agung = networks::agung();
|
||||
|
||||
// Create providers
|
||||
let gnosis_provider = create_provider(&gnosis);
|
||||
let peaq_provider = create_provider(&peaq);
|
||||
let agung_provider = create_provider(&agung);
|
||||
|
||||
// They should all succeed
|
||||
assert!(gnosis_provider.is_ok());
|
||||
assert!(peaq_provider.is_ok());
|
||||
assert!(agung_provider.is_ok());
|
||||
|
||||
// The convenience functions should also work
|
||||
let gnosis_provider2 = create_gnosis_provider();
|
||||
let peaq_provider2 = create_peaq_provider();
|
||||
let agung_provider2 = create_agung_provider();
|
||||
|
||||
assert!(gnosis_provider2.is_ok());
|
||||
assert!(peaq_provider2.is_ok());
|
||||
assert!(agung_provider2.is_ok());
|
||||
}
|
||||
70
src/hero_vault/ethereum/tests/transaction_tests.rs
Normal file
70
src/hero_vault/ethereum/tests/transaction_tests.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Tests for Ethereum transaction functionality.
|
||||
|
||||
use crate::hero_vault::ethereum::*;
|
||||
use crate::hero_vault::keypair::KeyPair;
|
||||
use ethers::types::U256;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_format_balance() {
|
||||
let network = networks::gnosis();
|
||||
|
||||
// Test with 0
|
||||
let balance = U256::from(0);
|
||||
let formatted = format_balance(balance, &network);
|
||||
assert_eq!(formatted, "0.000000 xDAI");
|
||||
|
||||
// Test with 1 wei
|
||||
let balance = U256::from(1);
|
||||
let formatted = format_balance(balance, &network);
|
||||
assert_eq!(formatted, "0.000000 xDAI");
|
||||
|
||||
// Test with 1 gwei (10^9 wei)
|
||||
let balance = U256::from(1_000_000_000u64);
|
||||
let formatted = format_balance(balance, &network);
|
||||
assert_eq!(formatted, "0.000000 xDAI");
|
||||
|
||||
// Test with 1 ETH (10^18 wei)
|
||||
let balance = U256::from_dec_str("1000000000000000000").unwrap();
|
||||
let formatted = format_balance(balance, &network);
|
||||
assert_eq!(formatted, "1.000000 xDAI");
|
||||
|
||||
// Test with a larger amount
|
||||
let balance = U256::from_dec_str("123456789000000000000").unwrap();
|
||||
let formatted = format_balance(balance, &network);
|
||||
assert_eq!(formatted, "123.456789 xDAI");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_balance() {
|
||||
// This is a mock test since we can't actually query the blockchain in a unit test
|
||||
// In a real test, we would use a local blockchain or mock the provider
|
||||
|
||||
// Create a provider
|
||||
let network = networks::gnosis();
|
||||
let provider_result = create_provider(&network);
|
||||
|
||||
// The provider creation should succeed
|
||||
assert!(provider_result.is_ok());
|
||||
|
||||
// We can't actually test get_balance without a blockchain
|
||||
// In a real test, we would mock the provider and test the function
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_eth() {
|
||||
// This is a mock test since we can't actually send transactions in a unit test
|
||||
// In a real test, we would use a local blockchain or mock the provider
|
||||
|
||||
// Create a wallet
|
||||
let keypair = KeyPair::new("test_keypair6");
|
||||
let network = networks::gnosis();
|
||||
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||
|
||||
// Create a provider
|
||||
let provider_result = create_provider(&network);
|
||||
assert!(provider_result.is_ok());
|
||||
|
||||
// We can't actually test send_eth without a blockchain
|
||||
// In a real test, we would mock the provider and test the function
|
||||
}
|
||||
143
src/hero_vault/ethereum/tests/wallet_tests.rs
Normal file
143
src/hero_vault/ethereum/tests/wallet_tests.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! Tests for Ethereum wallet functionality.
|
||||
|
||||
use crate::hero_vault::ethereum::*;
|
||||
use crate::hero_vault::keypair::KeyPair;
|
||||
use ethers::utils::hex;
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_wallet_from_keypair() {
|
||||
let keypair = KeyPair::new("test_keypair");
|
||||
let network = networks::gnosis();
|
||||
|
||||
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||
|
||||
assert_eq!(wallet.network.name, "Gnosis");
|
||||
assert_eq!(wallet.network.chain_id, 100);
|
||||
|
||||
// The address should be a valid Ethereum address
|
||||
assert!(wallet.address_string().starts_with("0x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_wallet_from_name_and_keypair() {
|
||||
let keypair = KeyPair::new("test_keypair2");
|
||||
let network = networks::gnosis();
|
||||
|
||||
let wallet = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
|
||||
|
||||
assert_eq!(wallet.network.name, "Gnosis");
|
||||
assert_eq!(wallet.network.chain_id, 100);
|
||||
|
||||
// The address should be a valid Ethereum address
|
||||
assert!(wallet.address_string().starts_with("0x"));
|
||||
|
||||
// Creating another wallet with the same name and keypair should yield the same address
|
||||
let wallet2 = EthereumWallet::from_name_and_keypair("test", &keypair, network.clone()).unwrap();
|
||||
assert_eq!(wallet.address, wallet2.address);
|
||||
|
||||
// Creating a wallet with a different name should yield a different address
|
||||
let wallet3 = EthereumWallet::from_name_and_keypair("test2", &keypair, network.clone()).unwrap();
|
||||
assert_ne!(wallet.address, wallet3.address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ethereum_wallet_from_private_key() {
|
||||
let private_key = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
let network = networks::gnosis();
|
||||
|
||||
let wallet = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
|
||||
|
||||
assert_eq!(wallet.network.name, "Gnosis");
|
||||
assert_eq!(wallet.network.chain_id, 100);
|
||||
|
||||
// The address should be a valid Ethereum address
|
||||
assert!(wallet.address_string().starts_with("0x"));
|
||||
|
||||
// The address should be deterministic based on the private key
|
||||
let wallet2 = EthereumWallet::from_private_key(private_key, network.clone()).unwrap();
|
||||
assert_eq!(wallet.address, wallet2.address);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wallet_management() {
|
||||
// Clear any existing wallets
|
||||
clear_ethereum_wallets();
|
||||
|
||||
// Create a key space and keypair
|
||||
crate::hero_vault::keypair::create_space("test_space").unwrap();
|
||||
crate::hero_vault::keypair::create_keypair("test_keypair3").unwrap();
|
||||
|
||||
// Create wallets for different networks
|
||||
let gnosis_wallet = create_ethereum_wallet_for_network(networks::gnosis()).unwrap();
|
||||
let peaq_wallet = create_ethereum_wallet_for_network(networks::peaq()).unwrap();
|
||||
let agung_wallet = create_ethereum_wallet_for_network(networks::agung()).unwrap();
|
||||
|
||||
// Get the current wallets
|
||||
let current_gnosis = get_current_ethereum_wallet_for_network("Gnosis").unwrap();
|
||||
let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap();
|
||||
let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap();
|
||||
|
||||
// Check that they match
|
||||
assert_eq!(gnosis_wallet.address, current_gnosis.address);
|
||||
assert_eq!(peaq_wallet.address, current_peaq.address);
|
||||
assert_eq!(agung_wallet.address, current_agung.address);
|
||||
|
||||
// Clear wallets for a specific network
|
||||
clear_ethereum_wallets_for_network("Gnosis");
|
||||
|
||||
// Check that the wallet is gone
|
||||
let result = get_current_ethereum_wallet_for_network("Gnosis");
|
||||
assert!(result.is_err());
|
||||
|
||||
// But the others should still be there
|
||||
let current_peaq = get_current_ethereum_wallet_for_network("Peaq").unwrap();
|
||||
let current_agung = get_current_ethereum_wallet_for_network("Agung").unwrap();
|
||||
assert_eq!(peaq_wallet.address, current_peaq.address);
|
||||
assert_eq!(agung_wallet.address, current_agung.address);
|
||||
|
||||
// Clear all wallets
|
||||
clear_ethereum_wallets();
|
||||
|
||||
// Check that all wallets are gone
|
||||
let result1 = get_current_ethereum_wallet_for_network("Gnosis");
|
||||
let result2 = get_current_ethereum_wallet_for_network("Peaq");
|
||||
let result3 = get_current_ethereum_wallet_for_network("Agung");
|
||||
assert!(result1.is_err());
|
||||
assert!(result2.is_err());
|
||||
assert!(result3.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_message() {
|
||||
let keypair = KeyPair::new("test_keypair4");
|
||||
let network = networks::gnosis();
|
||||
|
||||
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||
|
||||
// Create a tokio runtime for the async test
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
// Sign a message
|
||||
let message = b"Hello, world!";
|
||||
let signature = rt.block_on(wallet.sign_message(message)).unwrap();
|
||||
|
||||
// The signature should be a non-empty string
|
||||
assert!(!signature.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_hex() {
|
||||
let keypair = KeyPair::new("test_keypair5");
|
||||
let network = networks::gnosis();
|
||||
|
||||
let wallet = EthereumWallet::from_keypair(&keypair, network.clone()).unwrap();
|
||||
|
||||
// Get the private key as hex
|
||||
let private_key_hex = wallet.private_key_hex();
|
||||
|
||||
// The private key should be a 64-character hex string (32 bytes)
|
||||
assert_eq!(private_key_hex.len(), 64);
|
||||
|
||||
// It should be possible to parse it as hex
|
||||
let _bytes = hex::decode(private_key_hex).unwrap();
|
||||
}
|
||||
54
src/hero_vault/ethereum/transaction.rs
Normal file
54
src/hero_vault/ethereum/transaction.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! Ethereum transaction functionality.
|
||||
|
||||
use ethers::prelude::*;
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
use super::wallet::EthereumWallet;
|
||||
use super::networks::NetworkConfig;
|
||||
|
||||
/// Formats a token balance for display.
|
||||
pub fn format_balance(balance: U256, network: &NetworkConfig) -> String {
|
||||
let wei = balance.as_u128();
|
||||
let divisor = 10u128.pow(network.decimals as u32) as f64;
|
||||
let token = wei as f64 / divisor;
|
||||
|
||||
// Display with the appropriate number of decimal places
|
||||
let display_decimals = std::cmp::min(6, network.decimals);
|
||||
|
||||
format!("{:.*} {}", display_decimals as usize, token, network.token_symbol)
|
||||
}
|
||||
|
||||
/// Gets the balance of an Ethereum address.
|
||||
pub async fn get_balance(provider: &Provider<Http>, address: Address) -> Result<U256, CryptoError> {
|
||||
provider.get_balance(address, None)
|
||||
.await
|
||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to get balance: {}", e)))
|
||||
}
|
||||
|
||||
/// Sends Ethereum from one address to another.
|
||||
pub async fn send_eth(
|
||||
wallet: &EthereumWallet,
|
||||
provider: &Provider<Http>,
|
||||
to: Address,
|
||||
amount: U256,
|
||||
) -> Result<H256, CryptoError> {
|
||||
// Create a client with the wallet
|
||||
let client = SignerMiddleware::new(
|
||||
provider.clone(),
|
||||
wallet.wallet.clone(),
|
||||
);
|
||||
|
||||
// Create the transaction
|
||||
let tx = TransactionRequest::new()
|
||||
.to(to)
|
||||
.value(amount)
|
||||
.gas(21000);
|
||||
|
||||
// Send the transaction
|
||||
let pending_tx = client.send_transaction(tx, None)
|
||||
.await
|
||||
.map_err(|e| CryptoError::SerializationError(format!("Failed to send transaction: {}", e)))?;
|
||||
|
||||
// Return the transaction hash instead of waiting for the receipt
|
||||
Ok(pending_tx.tx_hash())
|
||||
}
|
||||
114
src/hero_vault/ethereum/wallet.rs
Normal file
114
src/hero_vault/ethereum/wallet.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
//! Ethereum wallet implementation.
|
||||
|
||||
use ethers::prelude::*;
|
||||
use ethers::signers::{LocalWallet, Signer, Wallet};
|
||||
use ethers::utils::hex;
|
||||
use k256::ecdsa::SigningKey;
|
||||
use std::str::FromStr;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
use crate::hero_vault::keypair::KeyPair;
|
||||
use super::networks::NetworkConfig;
|
||||
|
||||
/// An Ethereum wallet derived from a keypair.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EthereumWallet {
|
||||
pub address: Address,
|
||||
pub wallet: Wallet<SigningKey>,
|
||||
pub network: NetworkConfig,
|
||||
}
|
||||
|
||||
impl EthereumWallet {
|
||||
/// Creates a new Ethereum wallet from a keypair for a specific network.
|
||||
pub fn from_keypair(keypair: &KeyPair, network: NetworkConfig) -> Result<Self, CryptoError> {
|
||||
// Get the private key bytes from the keypair
|
||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||
|
||||
// Convert to a hex string (without 0x prefix)
|
||||
let private_key_hex = hex::encode(private_key_bytes);
|
||||
|
||||
// Create an Ethereum wallet from the private key
|
||||
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(network.chain_id);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
wallet,
|
||||
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> {
|
||||
// Get the private key bytes from the keypair
|
||||
let private_key_bytes = keypair.signing_key.to_bytes();
|
||||
|
||||
// Create a deterministic seed by combining name and private key
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(name.as_bytes());
|
||||
hasher.update(&private_key_bytes);
|
||||
let seed = hasher.finalize();
|
||||
|
||||
// Use the seed as a private key
|
||||
let private_key_hex = hex::encode(seed);
|
||||
|
||||
// Create an Ethereum wallet from the derived private key
|
||||
let wallet = LocalWallet::from_str(&private_key_hex)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(network.chain_id);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
wallet,
|
||||
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> {
|
||||
// Remove 0x prefix if present
|
||||
let private_key_clean = private_key.trim_start_matches("0x");
|
||||
|
||||
// Create an Ethereum wallet from the private key
|
||||
let wallet = LocalWallet::from_str(private_key_clean)
|
||||
.map_err(|_e| CryptoError::InvalidKeyLength)?
|
||||
.with_chain_id(network.chain_id);
|
||||
|
||||
// Get the Ethereum address
|
||||
let address = wallet.address();
|
||||
|
||||
Ok(EthereumWallet {
|
||||
address,
|
||||
wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the Ethereum address as a string.
|
||||
pub fn address_string(&self) -> String {
|
||||
format!("{:?}", self.address)
|
||||
}
|
||||
|
||||
/// Signs a message with the Ethereum wallet.
|
||||
pub async fn sign_message(&self, message: &[u8]) -> Result<String, CryptoError> {
|
||||
let signature = self.wallet.sign_message(message)
|
||||
.await
|
||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||
|
||||
Ok(signature.to_string())
|
||||
}
|
||||
|
||||
/// Gets the private key as a hex string.
|
||||
pub fn private_key_hex(&self) -> String {
|
||||
let bytes = self.wallet.signer().to_bytes();
|
||||
hex::encode(bytes)
|
||||
}
|
||||
}
|
||||
467
src/hero_vault/keypair/implementation.rs
Normal file
467
src/hero_vault/keypair/implementation.rs
Normal file
@@ -0,0 +1,467 @@
|
||||
//! Implementation of keypair functionality.
|
||||
|
||||
use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature};
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
|
||||
/// A keypair for signing and verifying messages.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KeyPair {
|
||||
pub name: String,
|
||||
#[serde(with = "verifying_key_serde")]
|
||||
pub verifying_key: VerifyingKey,
|
||||
#[serde(with = "signing_key_serde")]
|
||||
pub signing_key: SigningKey,
|
||||
}
|
||||
|
||||
// Serialization helpers for VerifyingKey
|
||||
mod verifying_key_serde {
|
||||
use super::*;
|
||||
use serde::{Serializer, Deserializer};
|
||||
use serde::de::{self, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
pub fn serialize<S>(key: &VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let bytes = key.to_sec1_bytes();
|
||||
// Convert bytes to a Vec<u8> and serialize that instead
|
||||
serializer.collect_seq(bytes)
|
||||
}
|
||||
|
||||
struct VerifyingKeyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for VerifyingKeyVisitor {
|
||||
type Value = VerifyingKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte array representing a verifying key")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
VerifyingKey::from_sec1_bytes(v).map_err(|e| {
|
||||
log::error!("Error deserializing verifying key: {:?}", e);
|
||||
E::custom(format!("invalid verifying key: {:?}", e))
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
// Collect all bytes from the sequence
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(byte) = seq.next_element()? {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
VerifyingKey::from_sec1_bytes(&bytes).map_err(|e| {
|
||||
log::error!("Error deserializing verifying key from seq: {:?}", e);
|
||||
de::Error::custom(format!("invalid verifying key from seq: {:?}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<VerifyingKey, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Try to deserialize as bytes first, then as a sequence
|
||||
deserializer.deserialize_any(VerifyingKeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization helpers for SigningKey
|
||||
mod signing_key_serde {
|
||||
use super::*;
|
||||
use serde::{Serializer, Deserializer};
|
||||
use serde::de::{self, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
pub fn serialize<S>(key: &SigningKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let bytes = key.to_bytes();
|
||||
// Convert bytes to a Vec<u8> and serialize that instead
|
||||
serializer.collect_seq(bytes)
|
||||
}
|
||||
|
||||
struct SigningKeyVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for SigningKeyVisitor {
|
||||
type Value = SigningKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a byte array representing a signing key")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
SigningKey::from_bytes(v.into()).map_err(|e| {
|
||||
log::error!("Error deserializing signing key: {:?}", e);
|
||||
E::custom(format!("invalid signing key: {:?}", e))
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: de::SeqAccess<'de>,
|
||||
{
|
||||
// Collect all bytes from the sequence
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(byte) = seq.next_element()? {
|
||||
bytes.push(byte);
|
||||
}
|
||||
|
||||
SigningKey::from_bytes(bytes.as_slice().into()).map_err(|e| {
|
||||
log::error!("Error deserializing signing key from seq: {:?}", e);
|
||||
de::Error::custom(format!("invalid signing key from seq: {:?}", e))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<SigningKey, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// Try to deserialize as bytes first, then as a sequence
|
||||
deserializer.deserialize_any(SigningKeyVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
/// Creates a new keypair with the given name.
|
||||
pub fn new(name: &str) -> Self {
|
||||
let signing_key = SigningKey::random(&mut OsRng);
|
||||
let verifying_key = VerifyingKey::from(&signing_key);
|
||||
|
||||
KeyPair {
|
||||
name: name.to_string(),
|
||||
verifying_key,
|
||||
signing_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the public key bytes.
|
||||
pub fn pub_key(&self) -> Vec<u8> {
|
||||
self.verifying_key.to_sec1_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Derives a public key from a private key.
|
||||
pub fn pub_key_from_private(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let signing_key = SigningKey::from_bytes(private_key.into())
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
let verifying_key = VerifyingKey::from(&signing_key);
|
||||
Ok(verifying_key.to_sec1_bytes().to_vec())
|
||||
}
|
||||
|
||||
/// Signs a message.
|
||||
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
|
||||
let signature: Signature = self.signing_key.sign(message);
|
||||
signature.to_bytes().to_vec()
|
||||
}
|
||||
|
||||
/// Verifies a message signature.
|
||||
pub fn verify(&self, message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||
let signature = Signature::from_bytes(signature_bytes.into())
|
||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||
|
||||
match self.verifying_key.verify(message, &signature) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false), // Verification failed, but operation was successful
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies a message signature using only a public key.
|
||||
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||
let verifying_key = VerifyingKey::from_sec1_bytes(public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
let signature = Signature::from_bytes(signature_bytes.into())
|
||||
.map_err(|e| CryptoError::SignatureFormatError(e.to_string()))?;
|
||||
|
||||
match verifying_key.verify(message, &signature) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false), // Verification failed, but operation was successful
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypts a message using the recipient's public key.
|
||||
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme):
|
||||
/// 1. Generate an ephemeral keypair
|
||||
/// 2. Derive a shared secret using ECDH
|
||||
/// 3. Derive encryption key from the shared secret
|
||||
/// 4. Encrypt the message using symmetric encryption
|
||||
/// 5. Return the ephemeral public key and the ciphertext
|
||||
pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
// Parse recipient's public key
|
||||
let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
// Generate ephemeral keypair
|
||||
let ephemeral_signing_key = SigningKey::random(&mut OsRng);
|
||||
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
|
||||
|
||||
// Derive shared secret (this is a simplified ECDH)
|
||||
// In a real implementation, we would use proper ECDH, but for this example:
|
||||
let shared_point = recipient_key.to_encoded_point(false);
|
||||
let shared_secret = {
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(ephemeral_signing_key.to_bytes());
|
||||
hasher.update(shared_point.as_bytes());
|
||||
hasher.finalize().to_vec()
|
||||
};
|
||||
|
||||
// Encrypt the message using the derived key
|
||||
let ciphertext = crate::hero_vault::symmetric::encrypt_with_key(&shared_secret, message)
|
||||
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
||||
|
||||
// Format: ephemeral_public_key || ciphertext
|
||||
let mut result = ephemeral_public_key.to_sec1_bytes().to_vec();
|
||||
result.extend_from_slice(&ciphertext);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Decrypts a message using the recipient's private key.
|
||||
/// This is the counterpart to encrypt_asymmetric.
|
||||
pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
// The first 33 or 65 bytes (depending on compression) are the ephemeral public key
|
||||
// For simplicity, we'll assume uncompressed keys (65 bytes)
|
||||
if ciphertext.len() <= 65 {
|
||||
return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string()));
|
||||
}
|
||||
|
||||
// Extract ephemeral public key and actual ciphertext
|
||||
let ephemeral_public_key = &ciphertext[..65];
|
||||
let actual_ciphertext = &ciphertext[65..];
|
||||
|
||||
// Parse ephemeral public key
|
||||
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
// Derive shared secret (simplified ECDH)
|
||||
let shared_point = sender_key.to_encoded_point(false);
|
||||
let shared_secret = {
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(self.signing_key.to_bytes());
|
||||
hasher.update(shared_point.as_bytes());
|
||||
hasher.finalize().to_vec()
|
||||
};
|
||||
|
||||
// Decrypt the message using the derived key
|
||||
crate::hero_vault::symmetric::decrypt_with_key(&shared_secret, actual_ciphertext)
|
||||
.map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of keypairs.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct KeySpace {
|
||||
pub name: String,
|
||||
pub keypairs: HashMap<String, KeyPair>,
|
||||
}
|
||||
|
||||
impl KeySpace {
|
||||
/// Creates a new key space with the given name.
|
||||
pub fn new(name: &str) -> Self {
|
||||
KeySpace {
|
||||
name: name.to_string(),
|
||||
keypairs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new keypair to the space.
|
||||
pub fn add_keypair(&mut self, name: &str) -> Result<(), CryptoError> {
|
||||
if self.keypairs.contains_key(name) {
|
||||
return Err(CryptoError::KeypairAlreadyExists(name.to_string()));
|
||||
}
|
||||
|
||||
let keypair = KeyPair::new(name);
|
||||
self.keypairs.insert(name.to_string(), keypair);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets a keypair by name.
|
||||
pub fn get_keypair(&self, name: &str) -> Result<&KeyPair, CryptoError> {
|
||||
self.keypairs.get(name).ok_or(CryptoError::KeypairNotFound(name.to_string()))
|
||||
}
|
||||
|
||||
/// Lists all keypair names in the space.
|
||||
pub fn list_keypairs(&self) -> Vec<String> {
|
||||
self.keypairs.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Session state for the current key space and selected keypair.
|
||||
pub struct Session {
|
||||
pub current_space: Option<KeySpace>,
|
||||
pub selected_keypair: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
fn default() -> Self {
|
||||
Session {
|
||||
current_space: None,
|
||||
selected_keypair: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global session state.
|
||||
static SESSION: Lazy<Mutex<Session>> = Lazy::new(|| {
|
||||
Mutex::new(Session::default())
|
||||
});
|
||||
|
||||
/// Creates a new key space with the given name.
|
||||
pub fn create_space(name: &str) -> Result<(), CryptoError> {
|
||||
let mut session = SESSION.lock().unwrap();
|
||||
|
||||
// Create a new space
|
||||
let space = KeySpace::new(name);
|
||||
|
||||
// Set as current space
|
||||
session.current_space = Some(space);
|
||||
session.selected_keypair = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the current key space.
|
||||
pub fn set_current_space(space: KeySpace) -> Result<(), CryptoError> {
|
||||
let mut session = SESSION.lock().unwrap();
|
||||
session.current_space = Some(space);
|
||||
session.selected_keypair = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the current key space.
|
||||
pub fn get_current_space() -> Result<KeySpace, CryptoError> {
|
||||
let session = SESSION.lock().unwrap();
|
||||
session.current_space.clone().ok_or(CryptoError::NoActiveSpace)
|
||||
}
|
||||
|
||||
/// Clears the current session (logout).
|
||||
pub fn clear_session() {
|
||||
let mut session = SESSION.lock().unwrap();
|
||||
session.current_space = None;
|
||||
session.selected_keypair = None;
|
||||
}
|
||||
|
||||
/// Creates a new keypair in the current space.
|
||||
pub fn create_keypair(name: &str) -> Result<(), CryptoError> {
|
||||
let mut session = SESSION.lock().unwrap();
|
||||
|
||||
if let Some(ref mut space) = session.current_space {
|
||||
if space.keypairs.contains_key(name) {
|
||||
return Err(CryptoError::KeypairAlreadyExists(name.to_string()));
|
||||
}
|
||||
|
||||
let keypair = KeyPair::new(name);
|
||||
space.keypairs.insert(name.to_string(), keypair);
|
||||
|
||||
// Automatically select the new keypair
|
||||
session.selected_keypair = Some(name.to_string());
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CryptoError::NoActiveSpace)
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects a keypair for use.
|
||||
pub fn select_keypair(name: &str) -> Result<(), CryptoError> {
|
||||
let mut session = SESSION.lock().unwrap();
|
||||
|
||||
if let Some(ref space) = session.current_space {
|
||||
if !space.keypairs.contains_key(name) {
|
||||
return Err(CryptoError::KeypairNotFound(name.to_string()));
|
||||
}
|
||||
|
||||
session.selected_keypair = Some(name.to_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CryptoError::NoActiveSpace)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the currently selected keypair.
|
||||
pub fn get_selected_keypair() -> Result<KeyPair, CryptoError> {
|
||||
let session = SESSION.lock().unwrap();
|
||||
|
||||
if let Some(ref space) = session.current_space {
|
||||
if let Some(ref keypair_name) = session.selected_keypair {
|
||||
if let Some(keypair) = space.keypairs.get(keypair_name) {
|
||||
return Ok(keypair.clone());
|
||||
}
|
||||
return Err(CryptoError::KeypairNotFound(keypair_name.clone()));
|
||||
}
|
||||
return Err(CryptoError::NoKeypairSelected);
|
||||
}
|
||||
|
||||
Err(CryptoError::NoActiveSpace)
|
||||
}
|
||||
|
||||
/// Lists all keypair names in the current space.
|
||||
pub fn list_keypairs() -> Result<Vec<String>, CryptoError> {
|
||||
let session = SESSION.lock().unwrap();
|
||||
|
||||
if let Some(ref space) = session.current_space {
|
||||
Ok(space.keypairs.keys().cloned().collect())
|
||||
} else {
|
||||
Err(CryptoError::NoActiveSpace)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the public key of the selected keypair.
|
||||
pub fn keypair_pub_key() -> Result<Vec<u8>, CryptoError> {
|
||||
let keypair = get_selected_keypair()?;
|
||||
Ok(keypair.pub_key())
|
||||
}
|
||||
|
||||
/// Derives a public key from a private key.
|
||||
pub fn derive_public_key(private_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
KeyPair::pub_key_from_private(private_key)
|
||||
}
|
||||
|
||||
/// Signs a message with the selected keypair.
|
||||
pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let keypair = get_selected_keypair()?;
|
||||
Ok(keypair.sign(message))
|
||||
}
|
||||
|
||||
/// Verifies a message signature with the selected keypair.
|
||||
pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||
let keypair = get_selected_keypair()?;
|
||||
keypair.verify(message, signature_bytes)
|
||||
}
|
||||
|
||||
/// Verifies a message signature with a public key.
|
||||
pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
|
||||
KeyPair::verify_with_public_key(public_key, message, signature_bytes)
|
||||
}
|
||||
|
||||
/// Encrypts a message for a recipient using their public key.
|
||||
pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let keypair = get_selected_keypair()?;
|
||||
keypair.encrypt_asymmetric(recipient_public_key, message)
|
||||
}
|
||||
|
||||
/// Decrypts a message that was encrypted with the current keypair's public key.
|
||||
pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
let keypair = get_selected_keypair()?;
|
||||
keypair.decrypt_asymmetric(ciphertext)
|
||||
}
|
||||
14
src/hero_vault/keypair/mod.rs
Normal file
14
src/hero_vault/keypair/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! Key pair management functionality
|
||||
//!
|
||||
//! This module provides functionality for creating and managing ECDSA key pairs.
|
||||
|
||||
mod implementation;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use implementation::{
|
||||
KeyPair, KeySpace,
|
||||
create_space, set_current_space, get_current_space, clear_session,
|
||||
create_keypair, select_keypair, get_selected_keypair, list_keypairs,
|
||||
keypair_pub_key, derive_public_key, keypair_sign, keypair_verify,
|
||||
verify_with_public_key, encrypt_asymmetric, decrypt_asymmetric
|
||||
};
|
||||
66
src/hero_vault/kvs/error.rs
Normal file
66
src/hero_vault/kvs/error.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Error types for the key-value store.
|
||||
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur when using the key-value store.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KvsError {
|
||||
/// I/O error
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Key not found
|
||||
#[error("Key not found: {0}")]
|
||||
KeyNotFound(String),
|
||||
|
||||
/// Store not found
|
||||
#[error("Store not found: {0}")]
|
||||
StoreNotFound(String),
|
||||
|
||||
/// Serialization error
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
/// Deserialization error
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(String),
|
||||
|
||||
/// Encryption error
|
||||
#[error("Encryption error: {0}")]
|
||||
Encryption(String),
|
||||
|
||||
/// Decryption error
|
||||
#[error("Decryption error: {0}")]
|
||||
Decryption(String),
|
||||
|
||||
/// Other error
|
||||
#[error("Error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for KvsError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
KvsError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KvsError> for crate::hero_vault::error::CryptoError {
|
||||
fn from(err: KvsError) -> Self {
|
||||
crate::hero_vault::error::CryptoError::SerializationError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::hero_vault::error::CryptoError> for KvsError {
|
||||
fn from(err: crate::hero_vault::error::CryptoError) -> Self {
|
||||
match err {
|
||||
crate::hero_vault::error::CryptoError::EncryptionFailed(msg) => KvsError::Encryption(msg),
|
||||
crate::hero_vault::error::CryptoError::DecryptionFailed(msg) => KvsError::Decryption(msg),
|
||||
crate::hero_vault::error::CryptoError::SerializationError(msg) => KvsError::Serialization(msg),
|
||||
_ => KvsError::Other(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result type for key-value store operations.
|
||||
pub type Result<T> = std::result::Result<T, KvsError>;
|
||||
14
src/hero_vault/kvs/mod.rs
Normal file
14
src/hero_vault/kvs/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
//! Key-Value Store functionality
|
||||
//!
|
||||
//! This module provides a simple key-value store with encryption support.
|
||||
|
||||
mod error;
|
||||
mod store;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use error::KvsError;
|
||||
pub use store::{
|
||||
KvStore, KvPair,
|
||||
create_store, open_store, delete_store,
|
||||
list_stores, get_store_path
|
||||
};
|
||||
362
src/hero_vault/kvs/store.rs
Normal file
362
src/hero_vault/kvs/store.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
//! Implementation of a simple key-value store using the filesystem.
|
||||
|
||||
use crate::hero_vault::kvs::error::{KvsError, Result};
|
||||
use crate::hero_vault::symmetric;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// A key-value pair.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KvPair {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// A simple key-value store.
|
||||
///
|
||||
/// This implementation uses the filesystem to store key-value pairs.
|
||||
#[derive(Clone)]
|
||||
pub struct KvStore {
|
||||
/// The name of the store
|
||||
name: String,
|
||||
/// The path to the store file
|
||||
path: PathBuf,
|
||||
/// In-memory cache of the store data
|
||||
data: Arc<Mutex<HashMap<String, String>>>,
|
||||
/// Whether the store is encrypted
|
||||
encrypted: bool,
|
||||
/// The password for encryption (if encrypted)
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
/// Gets the path to the key-value store directory.
|
||||
pub fn get_store_path() -> PathBuf {
|
||||
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
home_dir.join(".hero-vault").join("kvs")
|
||||
}
|
||||
|
||||
/// Creates a new key-value store with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the store
|
||||
/// * `encrypted` - Whether to encrypt the store
|
||||
/// * `password` - The password for encryption (required if encrypted is true)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new `KvStore` instance
|
||||
pub fn create_store(name: &str, encrypted: bool, password: Option<&str>) -> Result<KvStore> {
|
||||
// Check if password is provided when encryption is enabled
|
||||
if encrypted && password.is_none() {
|
||||
return Err(KvsError::Other("Password required for encrypted store".to_string()));
|
||||
}
|
||||
|
||||
// Create the store directory if it doesn't exist
|
||||
let store_dir = get_store_path();
|
||||
if !store_dir.exists() {
|
||||
fs::create_dir_all(&store_dir)?;
|
||||
}
|
||||
|
||||
// Create the store file path
|
||||
let store_path = store_dir.join(format!("{}.json", name));
|
||||
|
||||
// Create an empty store
|
||||
let store = KvStore {
|
||||
name: name.to_string(),
|
||||
path: store_path,
|
||||
data: Arc::new(Mutex::new(HashMap::new())),
|
||||
encrypted,
|
||||
password: password.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
// Save the empty store
|
||||
store.save()?;
|
||||
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Opens an existing key-value store with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the store
|
||||
/// * `password` - The password for decryption (required if the store is encrypted)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The opened `KvStore` instance
|
||||
pub fn open_store(name: &str, password: Option<&str>) -> Result<KvStore> {
|
||||
// Get the store file path
|
||||
let store_dir = get_store_path();
|
||||
let store_path = store_dir.join(format!("{}.json", name));
|
||||
|
||||
// Check if the store exists
|
||||
if !store_path.exists() {
|
||||
return Err(KvsError::StoreNotFound(name.to_string()));
|
||||
}
|
||||
|
||||
// Read the store file
|
||||
let file_content = fs::read_to_string(&store_path)?;
|
||||
|
||||
// Check if the file is encrypted (simple heuristic)
|
||||
let is_encrypted = !file_content.starts_with('{');
|
||||
|
||||
// If encrypted, we need a password
|
||||
if is_encrypted && password.is_none() {
|
||||
return Err(KvsError::Other("Password required for encrypted store".to_string()));
|
||||
}
|
||||
|
||||
// Parse the store data
|
||||
let data: HashMap<String, String> = if is_encrypted {
|
||||
// Decrypt the file content
|
||||
let password = password.unwrap();
|
||||
let encrypted_data: Vec<u8> = serde_json::from_str(&file_content)?;
|
||||
let key = symmetric::derive_key_from_password(password);
|
||||
let decrypted_data = symmetric::decrypt_symmetric(&key, &encrypted_data)?;
|
||||
let decrypted_str = String::from_utf8(decrypted_data)
|
||||
.map_err(|e| KvsError::Deserialization(e.to_string()))?;
|
||||
serde_json::from_str(&decrypted_str)?
|
||||
} else {
|
||||
serde_json::from_str(&file_content)?
|
||||
};
|
||||
|
||||
// Create the store
|
||||
let store = KvStore {
|
||||
name: name.to_string(),
|
||||
path: store_path,
|
||||
data: Arc::new(Mutex::new(data)),
|
||||
encrypted: is_encrypted,
|
||||
password: password.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
Ok(store)
|
||||
}
|
||||
|
||||
/// Deletes a key-value store with the given name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the store to delete
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn delete_store(name: &str) -> Result<()> {
|
||||
// Get the store file path
|
||||
let store_dir = get_store_path();
|
||||
let store_path = store_dir.join(format!("{}.json", name));
|
||||
|
||||
// Check if the store exists
|
||||
if !store_path.exists() {
|
||||
return Err(KvsError::StoreNotFound(name.to_string()));
|
||||
}
|
||||
|
||||
// Delete the store file
|
||||
fs::remove_file(store_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists all available key-value stores.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of store names
|
||||
pub fn list_stores() -> Result<Vec<String>> {
|
||||
// Get the store directory
|
||||
let store_dir = get_store_path();
|
||||
if !store_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// List all JSON files in the directory
|
||||
let mut stores = Vec::new();
|
||||
for entry in fs::read_dir(store_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.extension().map_or(false, |ext| ext == "json") {
|
||||
if let Some(name) = path.file_stem() {
|
||||
if let Some(name_str) = name.to_str() {
|
||||
stores.push(name_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stores)
|
||||
}
|
||||
|
||||
impl KvStore {
|
||||
/// Saves the store to disk.
|
||||
fn save(&self) -> Result<()> {
|
||||
// Get the store data
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
// Serialize the data
|
||||
let serialized = serde_json::to_string(&*data)?;
|
||||
|
||||
// Write to file
|
||||
if self.encrypted {
|
||||
if let Some(password) = &self.password {
|
||||
// Encrypt the data
|
||||
let key = symmetric::derive_key_from_password(password);
|
||||
let encrypted_data = symmetric::encrypt_symmetric(&key, serialized.as_bytes())?;
|
||||
let encrypted_json = serde_json::to_string(&encrypted_data)?;
|
||||
fs::write(&self.path, encrypted_json)?;
|
||||
} else {
|
||||
return Err(KvsError::Other("Password required for encrypted store".to_string()));
|
||||
}
|
||||
} else {
|
||||
fs::write(&self.path, serialized)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores a value with the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to store the value under
|
||||
/// * `value` - The value to store
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn set<K, V>(&self, key: K, value: &V) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
V: Serialize,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let serialized = serde_json::to_string(value)?;
|
||||
|
||||
// Update in-memory data
|
||||
{
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.insert(key_str, serialized);
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
self.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves a value for the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to retrieve the value for
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The value if found, or `Err(KvsError::KeyNotFound)` if not found
|
||||
pub fn get<K, V>(&self, key: K) -> Result<V>
|
||||
where
|
||||
K: ToString,
|
||||
V: DeserializeOwned,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
match data.get(&key_str) {
|
||||
Some(serialized) => {
|
||||
let value = serde_json::from_str(serialized)?;
|
||||
Ok(value)
|
||||
},
|
||||
None => Err(KvsError::KeyNotFound(key_str)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a value for the given key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to delete
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn delete<K>(&self, key: K) -> Result<()>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
|
||||
// Update in-memory data
|
||||
{
|
||||
let mut data = self.data.lock().unwrap();
|
||||
if data.remove(&key_str).is_none() {
|
||||
return Err(KvsError::KeyNotFound(key_str));
|
||||
}
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
self.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a key exists in the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to check
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the key exists, `false` otherwise
|
||||
pub fn contains<K>(&self, key: K) -> Result<bool>
|
||||
where
|
||||
K: ToString,
|
||||
{
|
||||
let key_str = key.to_string();
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.contains_key(&key_str))
|
||||
}
|
||||
|
||||
/// Lists all keys in the store.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A vector of keys as strings
|
||||
pub fn keys(&self) -> Result<Vec<String>> {
|
||||
let data = self.data.lock().unwrap();
|
||||
|
||||
Ok(data.keys().cloned().collect())
|
||||
}
|
||||
|
||||
/// Clears all key-value pairs from the store.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if the operation was successful
|
||||
pub fn clear(&self) -> Result<()> {
|
||||
// Update in-memory data
|
||||
{
|
||||
let mut data = self.data.lock().unwrap();
|
||||
data.clear();
|
||||
}
|
||||
|
||||
// Save to disk
|
||||
self.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the name of the store.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Gets whether the store is encrypted.
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.encrypted
|
||||
}
|
||||
}
|
||||
19
src/hero_vault/mod.rs
Normal file
19
src/hero_vault/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Hero Vault: Cryptographic functionality for SAL
|
||||
//!
|
||||
//! This module provides cryptographic operations including:
|
||||
//! - Key space management (creation, loading, encryption, decryption)
|
||||
//! - Key pair management (ECDSA)
|
||||
//! - Digital signatures (signing and verification)
|
||||
//! - Symmetric encryption (ChaCha20Poly1305)
|
||||
//! - Ethereum wallet functionality
|
||||
//! - Key-value store with encryption
|
||||
|
||||
pub mod error;
|
||||
pub mod keypair;
|
||||
pub mod symmetric;
|
||||
pub mod ethereum;
|
||||
pub mod kvs;
|
||||
|
||||
// Re-export common types for convenience
|
||||
pub use error::CryptoError;
|
||||
pub use keypair::{KeyPair, KeySpace};
|
||||
266
src/hero_vault/symmetric/implementation.rs
Normal file
266
src/hero_vault/symmetric/implementation.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
//! Implementation of symmetric encryption functionality.
|
||||
|
||||
use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce};
|
||||
use chacha20poly1305::aead::Aead;
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use crate::hero_vault::error::CryptoError;
|
||||
use crate::hero_vault::keypair::KeySpace;
|
||||
|
||||
/// The size of the nonce in bytes.
|
||||
const NONCE_SIZE: usize = 12;
|
||||
|
||||
/// Generates a random 32-byte symmetric key.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A 32-byte array containing the random key.
|
||||
pub fn generate_symmetric_key() -> [u8; 32] {
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut key);
|
||||
key
|
||||
}
|
||||
|
||||
/// Derives a 32-byte key from a password.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `password` - The password to derive the key from.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A 32-byte array containing the derived key.
|
||||
pub fn derive_key_from_password(password: &str) -> [u8; 32] {
|
||||
let mut hasher = Sha256::default();
|
||||
hasher.update(password.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&result);
|
||||
key
|
||||
}
|
||||
|
||||
/// Encrypts data using ChaCha20Poly1305 with an internally generated nonce.
|
||||
///
|
||||
/// The nonce is appended to the ciphertext so it can be extracted during decryption.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The encryption key (should be 32 bytes).
|
||||
/// * `message` - The message to encrypt.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the ciphertext with the nonce appended.
|
||||
/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid.
|
||||
/// * `Err(CryptoError::EncryptionFailed)` if encryption fails.
|
||||
pub fn encrypt_symmetric(key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
// Create cipher
|
||||
let cipher = ChaCha20Poly1305::new_from_slice(key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
// Generate random nonce
|
||||
let mut nonce_bytes = [0u8; NONCE_SIZE];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
// Encrypt message
|
||||
let ciphertext = cipher.encrypt(nonce, message)
|
||||
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
||||
|
||||
// Append nonce to ciphertext
|
||||
let mut result = ciphertext;
|
||||
result.extend_from_slice(&nonce_bytes);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Decrypts data using ChaCha20Poly1305, extracting the nonce from the ciphertext.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The decryption key (should be 32 bytes).
|
||||
/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the decrypted message.
|
||||
/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid.
|
||||
/// * `Err(CryptoError::DecryptionFailed)` if decryption fails or the ciphertext is too short.
|
||||
pub fn decrypt_symmetric(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
// Check if ciphertext is long enough to contain a nonce
|
||||
if ciphertext_with_nonce.len() <= NONCE_SIZE {
|
||||
return Err(CryptoError::DecryptionFailed("Ciphertext too short".to_string()));
|
||||
}
|
||||
|
||||
// Extract nonce from the end of ciphertext
|
||||
let ciphertext_len = ciphertext_with_nonce.len() - NONCE_SIZE;
|
||||
let ciphertext = &ciphertext_with_nonce[0..ciphertext_len];
|
||||
let nonce_bytes = &ciphertext_with_nonce[ciphertext_len..];
|
||||
|
||||
// Create cipher
|
||||
let cipher = ChaCha20Poly1305::new_from_slice(key)
|
||||
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
|
||||
// Decrypt message
|
||||
cipher.decrypt(nonce, ciphertext)
|
||||
.map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
|
||||
}
|
||||
|
||||
/// Encrypts data using a key directly (for internal use).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The encryption key.
|
||||
/// * `message` - The message to encrypt.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the ciphertext with the nonce appended.
|
||||
/// * `Err(CryptoError)` if encryption fails.
|
||||
pub fn encrypt_with_key(key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
encrypt_symmetric(key, message)
|
||||
}
|
||||
|
||||
/// Decrypts data using a key directly (for internal use).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The decryption key.
|
||||
/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<u8>)` containing the decrypted message.
|
||||
/// * `Err(CryptoError)` if decryption fails.
|
||||
pub fn decrypt_with_key(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec<u8>, CryptoError> {
|
||||
decrypt_symmetric(key, ciphertext_with_nonce)
|
||||
}
|
||||
|
||||
/// Metadata for an encrypted key space.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EncryptedKeySpaceMetadata {
|
||||
pub name: String,
|
||||
pub created_at: u64,
|
||||
pub last_accessed: u64,
|
||||
}
|
||||
|
||||
/// An encrypted key space with metadata.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EncryptedKeySpace {
|
||||
pub metadata: EncryptedKeySpaceMetadata,
|
||||
pub encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Encrypts a key space using a password.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `space` - The key space to encrypt.
|
||||
/// * `password` - The password to encrypt with.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(EncryptedKeySpace)` containing the encrypted key space.
|
||||
/// * `Err(CryptoError)` if encryption fails.
|
||||
pub fn encrypt_key_space(space: &KeySpace, password: &str) -> Result<EncryptedKeySpace, CryptoError> {
|
||||
// Serialize the key space
|
||||
let serialized = match serde_json::to_vec(space) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
log::error!("Serialization error during encryption: {}", e);
|
||||
return Err(CryptoError::SerializationError(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
// Derive key from password
|
||||
let key = derive_key_from_password(password);
|
||||
|
||||
// Encrypt the serialized data
|
||||
let encrypted_data = encrypt_symmetric(&key, &serialized)?;
|
||||
|
||||
// Create metadata
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64;
|
||||
let metadata = EncryptedKeySpaceMetadata {
|
||||
name: space.name.clone(),
|
||||
created_at: now,
|
||||
last_accessed: now,
|
||||
};
|
||||
|
||||
Ok(EncryptedKeySpace {
|
||||
metadata,
|
||||
encrypted_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decrypts a key space using a password.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `encrypted_space` - The encrypted key space.
|
||||
/// * `password` - The password to decrypt with.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(KeySpace)` containing the decrypted key space.
|
||||
/// * `Err(CryptoError)` if decryption fails.
|
||||
pub fn decrypt_key_space(encrypted_space: &EncryptedKeySpace, password: &str) -> Result<KeySpace, CryptoError> {
|
||||
// Derive key from password
|
||||
let key = derive_key_from_password(password);
|
||||
|
||||
// Decrypt the data
|
||||
let decrypted_data = decrypt_symmetric(&key, &encrypted_space.encrypted_data)?;
|
||||
|
||||
// Deserialize the key space
|
||||
let space: KeySpace = match serde_json::from_slice(&decrypted_data) {
|
||||
Ok(space) => space,
|
||||
Err(e) => {
|
||||
log::error!("Deserialization error: {}", e);
|
||||
return Err(CryptoError::SerializationError(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(space)
|
||||
}
|
||||
|
||||
/// Serializes an encrypted key space to a JSON string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `encrypted_space` - The encrypted key space to serialize.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(String)` containing the serialized encrypted key space.
|
||||
/// * `Err(CryptoError)` if serialization fails.
|
||||
pub fn serialize_encrypted_space(encrypted_space: &EncryptedKeySpace) -> Result<String, CryptoError> {
|
||||
serde_json::to_string(encrypted_space)
|
||||
.map_err(|e| CryptoError::SerializationError(e.to_string()))
|
||||
}
|
||||
|
||||
/// Deserializes an encrypted key space from a JSON string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `serialized` - The serialized encrypted key space.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(EncryptedKeySpace)` containing the deserialized encrypted key space.
|
||||
/// * `Err(CryptoError)` if deserialization fails.
|
||||
pub fn deserialize_encrypted_space(serialized: &str) -> Result<EncryptedKeySpace, CryptoError> {
|
||||
match serde_json::from_str(serialized) {
|
||||
Ok(space) => Ok(space),
|
||||
Err(e) => {
|
||||
log::error!("Error deserializing encrypted space: {}", e);
|
||||
Err(CryptoError::SerializationError(e.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/hero_vault/symmetric/mod.rs
Normal file
15
src/hero_vault/symmetric/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
//! Symmetric encryption functionality
|
||||
//!
|
||||
//! This module provides functionality for symmetric encryption using ChaCha20Poly1305.
|
||||
|
||||
mod implementation;
|
||||
|
||||
// Re-export public types and functions
|
||||
pub use implementation::{
|
||||
generate_symmetric_key, derive_key_from_password,
|
||||
encrypt_symmetric, decrypt_symmetric,
|
||||
encrypt_with_key, decrypt_with_key,
|
||||
encrypt_key_space, decrypt_key_space,
|
||||
serialize_encrypted_space, deserialize_encrypted_space,
|
||||
EncryptedKeySpace, EncryptedKeySpaceMetadata
|
||||
};
|
||||
Reference in New Issue
Block a user