Merge branch 'development_lee'
This commit is contained in:
commit
fedf957079
21
Cargo.toml
21
Cargo.toml
@ -10,18 +10,24 @@ 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]
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
base64 = "0.22.1" # 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.34.0" # Command-line argument parsing
|
clap = "2.34.0" # Command-line argument parsing
|
||||||
dirs = "6.0.0" # Directory paths
|
dirs = "6.0.0" # Directory paths
|
||||||
env_logger = "0.11.8" # 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.4", features = ["ecdsa", "ecdh"] } # 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
|
||||||
@ -38,7 +44,7 @@ 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 = "2.0.12" # For error handling
|
thiserror = "2.0.12" # For error handling
|
||||||
@ -63,8 +69,11 @@ windows = { version = "0.61.1", features = [
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockall = "0.13.1" # 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"
|
||||||
|
@ -11,8 +11,8 @@ use std::str::FromStr;
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
use crate::vault::ethereum;
|
use crate::vault::ethereum::contract_utils::{convert_token_to_rhai, prepare_function_arguments};
|
||||||
use crate::vault::keyspace::session_manager as keypair;
|
use crate::vault::{ethereum, keyspace};
|
||||||
|
|
||||||
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 +73,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 +83,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::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 +151,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 +207,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 +232,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,35 +252,35 @@ 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) {
|
match keyspace::select_keypair(name) {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error selecting keypair: {}", e);
|
log::error!("Error selecting keyspace: {}", e);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_keypairs() -> Vec<String> {
|
fn list_keyspaces() -> Vec<String> {
|
||||||
match keypair::list_keypairs() {
|
match keyspace::list_keypairs() {
|
||||||
Ok(keypairs) => keypairs,
|
Ok(keyspaces) => keyspaces,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error listing keypairs: {}", e);
|
log::error!("Error listing keyspaces: {}", e);
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,7 +289,7 @@ fn list_keypairs() -> Vec<String> {
|
|||||||
// 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::keypair_sign(message_bytes) {
|
||||||
Ok(signature) => BASE64.encode(signature),
|
Ok(signature) => BASE64.encode(signature),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Error signing message: {}", e);
|
log::error!("Error signing message: {}", e);
|
||||||
@ -301,7 +301,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);
|
||||||
@ -881,10 +881,10 @@ 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", list_keyspaces);
|
||||||
|
|
||||||
// Register signing/verification functions
|
// Register signing/verification functions
|
||||||
engine.register_fn("sign", sign);
|
engine.register_fn("sign", sign);
|
||||||
|
@ -8,8 +8,8 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::networks::NetworkConfig;
|
use super::networks::NetworkConfig;
|
||||||
use crate::vault;
|
|
||||||
use crate::vault::error::CryptoError;
|
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)]
|
||||||
@ -49,7 +49,7 @@ impl EthereumWallet {
|
|||||||
/// 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(
|
pub fn from_name_and_keypair(
|
||||||
name: &str,
|
name: &str,
|
||||||
keypair: &vault::keyspace::keypair_types::KeyPair,
|
keypair: &KeyPair,
|
||||||
network: NetworkConfig,
|
network: NetworkConfig,
|
||||||
) -> Result<Self, CryptoError> {
|
) -> Result<Self, CryptoError> {
|
||||||
// Get the private key bytes from the keypair
|
// Get the private key bytes from the keypair
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use k256::ecdh::EphemeralSecret;
|
||||||
/// Implementation of keypair functionality.
|
/// Implementation of keypair functionality.
|
||||||
use k256::ecdsa::{
|
use k256::ecdsa::{
|
||||||
signature::{Signer, Verifier},
|
signature::{Signer, Verifier},
|
||||||
@ -205,31 +206,32 @@ impl KeyPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypts a message using the recipient's public key.
|
/// Encrypts a message using the recipient's public key.
|
||||||
/// This implements a simplified version of ECIES (Elliptic Curve Integrated Encryption Scheme):
|
/// This implements ECIES (Elliptic Curve Integrated Encryption Scheme):
|
||||||
/// 1. Generate a random symmetric key
|
/// 1. Generate an ephemeral keypair
|
||||||
/// 2. Encrypt the message with the symmetric key
|
/// 2. Derive a shared secret using ECDH
|
||||||
/// 3. Encrypt the symmetric key with the recipient's public key
|
/// 3. Derive encryption key from the shared secret
|
||||||
/// 4. Return the encrypted key and the ciphertext
|
/// 4. Encrypt the message using symmetric encryption
|
||||||
|
/// 5. Return the ephemeral public key and the ciphertext
|
||||||
pub fn encrypt_asymmetric(
|
pub fn encrypt_asymmetric(
|
||||||
&self,
|
&self,
|
||||||
recipient_public_key: &[u8],
|
recipient_public_key: &[u8],
|
||||||
message: &[u8],
|
message: &[u8],
|
||||||
) -> Result<Vec<u8>, CryptoError> {
|
) -> Result<Vec<u8>, CryptoError> {
|
||||||
// Validate recipient's public key format
|
// Parse recipient's public 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 a random symmetric key
|
// Generate ephemeral keypair
|
||||||
let symmetric_key = implementation::generate_symmetric_key();
|
let ephemeral_signing_key = SigningKey::random(&mut OsRng);
|
||||||
|
let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key);
|
||||||
|
|
||||||
// Encrypt the message with the symmetric key
|
// Derive shared secret using ECDH
|
||||||
let encrypted_message = implementation::encrypt_with_key(&symmetric_key, message)
|
let ephemeral_secret = EphemeralSecret::random(&mut OsRng);
|
||||||
.map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?;
|
let shared_secret = ephemeral_secret.diffie_hellman(&recipient_key.into());
|
||||||
|
|
||||||
// Encrypt the symmetric key with the recipient's public key
|
// Derive encryption key from the shared secret (e.g., using HKDF or hashing)
|
||||||
// For simplicity, we'll just use the recipient's public key to derive an encryption key
|
// For simplicity, we'll hash the shared secret here
|
||||||
// This is not secure for production use, but works for our test
|
let encryption_key = {
|
||||||
let key_encryption_key = {
|
|
||||||
let mut hasher = Sha256::default();
|
let mut hasher = Sha256::default();
|
||||||
hasher.update(recipient_public_key);
|
hasher.update(recipient_public_key);
|
||||||
// Use a fixed salt for testing purposes
|
// Use a fixed salt for testing purposes
|
||||||
@ -237,16 +239,16 @@ impl KeyPair {
|
|||||||
hasher.finalize().to_vec()
|
hasher.finalize().to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Encrypt the symmetric key
|
// Encrypt the message using the derived key
|
||||||
let encrypted_key = implementation::encrypt_with_key(&key_encryption_key, &symmetric_key)
|
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: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
|
// Format: ephemeral_public_key || ciphertext
|
||||||
let mut result = Vec::new();
|
let mut result = ephemeral_public_key
|
||||||
let key_len = encrypted_key.len() as u32;
|
.to_encoded_point(false)
|
||||||
result.extend_from_slice(&key_len.to_be_bytes());
|
.as_bytes()
|
||||||
result.extend_from_slice(&encrypted_key);
|
.to_vec();
|
||||||
result.extend_from_slice(&encrypted_message);
|
result.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@ -254,32 +256,28 @@ impl KeyPair {
|
|||||||
/// 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 format is: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message
|
// The first 33 or 65 bytes (depending on compression) are the ephemeral public key
|
||||||
if ciphertext.len() <= 4 {
|
// For simplicity, we'll assume uncompressed keys (65 bytes)
|
||||||
|
if ciphertext.len() <= 65 {
|
||||||
return Err(CryptoError::DecryptionFailed(
|
return Err(CryptoError::DecryptionFailed(
|
||||||
"Ciphertext too short".to_string(),
|
"Ciphertext too short".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the encrypted key length
|
// Extract ephemeral public key and actual ciphertext
|
||||||
let mut key_len_bytes = [0u8; 4];
|
let ephemeral_public_key = &ciphertext[..65];
|
||||||
key_len_bytes.copy_from_slice(&ciphertext[0..4]);
|
let actual_ciphertext = &ciphertext[65..];
|
||||||
let key_len = u32::from_be_bytes(key_len_bytes) as usize;
|
|
||||||
|
|
||||||
// Check if the ciphertext is long enough
|
// Parse ephemeral public key
|
||||||
if ciphertext.len() <= 4 + key_len {
|
let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key)
|
||||||
return Err(CryptoError::DecryptionFailed(
|
.map_err(|_| CryptoError::InvalidKeyLength)?;
|
||||||
"Ciphertext too short".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the encrypted key and the encrypted message
|
// Derive shared secret using ECDH
|
||||||
let encrypted_key = &ciphertext[4..4 + key_len];
|
let recipient_secret = EphemeralSecret::random(&mut OsRng);
|
||||||
let encrypted_message = &ciphertext[4 + key_len..];
|
let shared_secret = recipient_secret.diffie_hellman(&sender_key.into());
|
||||||
|
|
||||||
// Decrypt the symmetric key
|
// Derive decryption key from the shared secret (using the same method as encryption)
|
||||||
// Use the same key derivation as in encryption
|
let decryption_key = {
|
||||||
let key_encryption_key = {
|
|
||||||
let mut hasher = Sha256::default();
|
let mut hasher = Sha256::default();
|
||||||
hasher.update(self.verifying_key.to_sec1_bytes());
|
hasher.update(self.verifying_key.to_sec1_bytes());
|
||||||
// Use the same fixed salt as in encryption
|
// Use the same fixed salt as in encryption
|
||||||
@ -287,13 +285,9 @@ impl KeyPair {
|
|||||||
hasher.finalize().to_vec()
|
hasher.finalize().to_vec()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Decrypt the symmetric key
|
// Decrypt the message using the derived key
|
||||||
let symmetric_key = implementation::decrypt_with_key(&key_encryption_key, encrypted_key)
|
implementation::decrypt_with_key(&decryption_key, actual_ciphertext)
|
||||||
.map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt key: {}", e)))?;
|
.map_err(|e| CryptoError::DecryptionFailed(e.to_string()))
|
||||||
|
|
||||||
// Decrypt the message with the symmetric key
|
|
||||||
implementation::decrypt_with_key(&symmetric_key, encrypted_message)
|
|
||||||
.map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt message: {}", e)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,7 +355,7 @@ impl KvStore {
|
|||||||
// Save to disk
|
// Save to disk
|
||||||
self.save()?;
|
self.save()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the name of the store.
|
/// Gets the name of the store.
|
||||||
|
2
vault/.cargo/config.toml
Normal file
2
vault/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[target.wasm32-unknown-unknown]
|
||||||
|
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
22
vault/Cargo.toml
Normal file
22
vault/Cargo.toml
Normal 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
160
vault/src/README.md
Normal 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
109
vault/src/error.rs
Normal 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
83
vault/src/key.rs
Normal 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
161
vault/src/key/asymmetric.rs
Normal 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
142
vault/src/key/signature.rs
Normal 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
151
vault/src/key/symmetric.rs
Normal 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
131
vault/src/keyspace.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
72
vault/src/keyspace/fallback.rs
Normal file
72
vault/src/keyspace/fallback.rs
Normal 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!()
|
||||||
|
}
|
||||||
|
}
|
26
vault/src/keyspace/wasm.rs
Normal file
26
vault/src/keyspace/wasm.rs
Normal 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
51
vault/src/lib.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user