diff --git a/Cargo.toml b/Cargo.toml index 207ac8b..50801a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,24 @@ keywords = ["system", "os", "abstraction", "platform", "filesystem"] categories = ["os", "filesystem", "api-bindings"] readme = "README.md" +[workspace] +members = [".", "vault"] + [dependencies] anyhow = "1.0.98" -base64 = "0.22.1" # Base64 encoding/decoding +base64 = "0.22.1" # Base64 encoding/decoding cfg-if = "1.0" chacha20poly1305 = "0.10.1" # ChaCha20Poly1305 AEAD cipher 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 ethers = { version = "2.0.7", features = ["legacy"] } # Ethereum library glob = "0.3.1" # For file pattern matching 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 libc = "0.2" log = "0.4" # Logging facade @@ -38,7 +44,7 @@ serde = { version = "1.0", features = [ "derive", ] } # For serialization/deserialization 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 tera = "1.19.0" # Template engine for text rendering thiserror = "2.0.12" # For error handling @@ -63,8 +69,11 @@ windows = { version = "0.61.1", features = [ [dev-dependencies] mockall = "0.13.1" # For mocking in tests -tempfile = "3.5" # For tests that need temporary files/directories -tokio = { version = "1.28", features = ["full", "test-util"] } # For async testing +tempfile = "3.5" # For tests that need temporary files/directories +tokio = { version = "1.28", features = [ + "full", + "test-util", +] } # For async testing [[bin]] name = "herodo" diff --git a/src/rhai/vault.rs b/src/rhai/vault.rs index 726979e..5dfbdac 100644 --- a/src/rhai/vault.rs +++ b/src/rhai/vault.rs @@ -11,8 +11,8 @@ use std::str::FromStr; use std::sync::Mutex; use tokio::runtime::Runtime; -use crate::vault::ethereum; -use crate::vault::keyspace::session_manager as keypair; +use crate::vault::ethereum::contract_utils::{convert_token_to_rhai, prepare_function_arguments}; +use crate::vault::{ethereum, keyspace}; use crate::vault::symmetric::implementation as symmetric_impl; // Global Tokio runtime for blocking async operations @@ -73,7 +73,7 @@ fn load_key_space(name: &str, password: &str) -> bool { }; // Set as current space - match keypair::set_current_space(space) { + match keyspace::set_current_space(space) { Ok(_) => true, Err(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 { - match keypair::create_space(name) { + match keyspace::session_manager::create_space(name) { Ok(_) => { // Get the current space - match keypair::get_current_space() { + match keyspace::get_current_space() { Ok(space) => { // Encrypt the key space 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 fn auto_save_key_space(password: &str) -> bool { - match keypair::get_current_space() { + match keyspace::get_current_space() { Ok(space) => { // Encrypt the key space 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 { - match keypair::get_current_space() { + match keyspace::get_current_space() { Ok(space) => match symmetric_impl::encrypt_key_space(&space, password) { Ok(encrypted_space) => match serde_json::to_string(&encrypted_space) { Ok(json) => json, @@ -232,7 +232,7 @@ fn decrypt_key_space(encrypted: &str, password: &str) -> bool { match serde_json::from_str(encrypted) { Ok(encrypted_space) => { 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, Err(e) => { log::error!("Error setting current space: {}", e); @@ -252,35 +252,35 @@ fn decrypt_key_space(encrypted: &str, password: &str) -> bool { } } -// Keypair management functions -fn create_keypair(name: &str, password: &str) -> bool { - match keypair::create_keypair(name) { +// keyspace management functions +fn create_keyspace(name: &str, password: &str) -> bool { + match keyspace::create_keypair(name) { Ok(_) => { - // Auto-save the key space after creating a keypair + // Auto-save the key space after creating a keyspace auto_save_key_space(password) } Err(e) => { - log::error!("Error creating keypair: {}", e); + log::error!("Error creating keyspace: {}", e); false } } } -fn select_keypair(name: &str) -> bool { - match keypair::select_keypair(name) { +fn select_keyspace(name: &str) -> bool { + match keyspace::select_keypair(name) { Ok(_) => true, Err(e) => { - log::error!("Error selecting keypair: {}", e); + log::error!("Error selecting keyspace: {}", e); false } } } -fn list_keypairs() -> Vec { - match keypair::list_keypairs() { - Ok(keypairs) => keypairs, +fn list_keyspaces() -> Vec { + match keyspace::list_keypairs() { + Ok(keyspaces) => keyspaces, Err(e) => { - log::error!("Error listing keypairs: {}", e); + log::error!("Error listing keyspaces: {}", e); Vec::new() } } @@ -289,7 +289,7 @@ fn list_keypairs() -> Vec { // Cryptographic operations fn sign(message: &str) -> String { let message_bytes = message.as_bytes(); - match keypair::keypair_sign(message_bytes) { + match keyspace::keypair_sign(message_bytes) { Ok(signature) => BASE64.encode(signature), Err(e) => { log::error!("Error signing message: {}", e); @@ -301,7 +301,7 @@ fn sign(message: &str) -> String { fn verify(message: &str, signature: &str) -> bool { let message_bytes = message.as_bytes(); match BASE64.decode(signature) { - Ok(signature_bytes) => match keypair::keypair_verify(message_bytes, &signature_bytes) { + Ok(signature_bytes) => match keyspace::keypair_verify(message_bytes, &signature_bytes) { Ok(is_valid) => is_valid, Err(e) => { log::error!("Error verifying signature: {}", e); @@ -881,10 +881,10 @@ pub fn register_crypto_module(engine: &mut Engine) -> Result<(), Box Result { // Get the private key bytes from the keypair diff --git a/src/vault/keyspace/keypair_types.rs b/src/vault/keyspace/keypair_types.rs index a6cac49..4d4b9ca 100644 --- a/src/vault/keyspace/keypair_types.rs +++ b/src/vault/keyspace/keypair_types.rs @@ -1,3 +1,4 @@ +use k256::ecdh::EphemeralSecret; /// Implementation of keypair functionality. use k256::ecdsa::{ signature::{Signer, Verifier}, @@ -205,31 +206,32 @@ impl KeyPair { } /// Encrypts a message using the recipient's public key. - /// This implements a simplified version of ECIES (Elliptic Curve Integrated Encryption Scheme): - /// 1. Generate a random symmetric key - /// 2. Encrypt the message with the symmetric key - /// 3. Encrypt the symmetric key with the recipient's public key - /// 4. Return the encrypted key and the ciphertext + /// 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, CryptoError> { - // Validate recipient's public key format - VerifyingKey::from_sec1_bytes(recipient_public_key) + // Parse recipient's public key + let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key) .map_err(|_| CryptoError::InvalidKeyLength)?; - // Generate a random symmetric key - let symmetric_key = implementation::generate_symmetric_key(); + // Generate ephemeral keypair + let ephemeral_signing_key = SigningKey::random(&mut OsRng); + let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key); - // Encrypt the message with the symmetric key - let encrypted_message = implementation::encrypt_with_key(&symmetric_key, message) - .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; + // Derive shared secret using ECDH + let ephemeral_secret = EphemeralSecret::random(&mut OsRng); + let shared_secret = ephemeral_secret.diffie_hellman(&recipient_key.into()); - // Encrypt the symmetric key with the recipient's public key - // For simplicity, we'll just use the recipient's public key to derive an encryption key - // This is not secure for production use, but works for our test - let key_encryption_key = { + // Derive encryption key from the shared secret (e.g., using HKDF or hashing) + // For simplicity, we'll hash the shared secret here + let encryption_key = { let mut hasher = Sha256::default(); hasher.update(recipient_public_key); // Use a fixed salt for testing purposes @@ -237,16 +239,16 @@ impl KeyPair { hasher.finalize().to_vec() }; - // Encrypt the symmetric key - let encrypted_key = implementation::encrypt_with_key(&key_encryption_key, &symmetric_key) + // Encrypt the message using the derived key + let ciphertext = implementation::encrypt_with_key(&encryption_key, message) .map_err(|e| CryptoError::EncryptionFailed(e.to_string()))?; - // Format: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message - let mut result = Vec::new(); - let key_len = encrypted_key.len() as u32; - result.extend_from_slice(&key_len.to_be_bytes()); - result.extend_from_slice(&encrypted_key); - result.extend_from_slice(&encrypted_message); + // Format: ephemeral_public_key || ciphertext + let mut result = ephemeral_public_key + .to_encoded_point(false) + .as_bytes() + .to_vec(); + result.extend_from_slice(&ciphertext); Ok(result) } @@ -254,32 +256,28 @@ impl KeyPair { /// Decrypts a message using the recipient's private key. /// This is the counterpart to encrypt_asymmetric. pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result, CryptoError> { - // The format is: encrypted_key_length (4 bytes) || encrypted_key || encrypted_message - if ciphertext.len() <= 4 { + // 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 the encrypted key length - let mut key_len_bytes = [0u8; 4]; - key_len_bytes.copy_from_slice(&ciphertext[0..4]); - let key_len = u32::from_be_bytes(key_len_bytes) as usize; + // Extract ephemeral public key and actual ciphertext + let ephemeral_public_key = &ciphertext[..65]; + let actual_ciphertext = &ciphertext[65..]; - // Check if the ciphertext is long enough - if ciphertext.len() <= 4 + key_len { - return Err(CryptoError::DecryptionFailed( - "Ciphertext too short".to_string(), - )); - } + // Parse ephemeral public key + let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; - // Extract the encrypted key and the encrypted message - let encrypted_key = &ciphertext[4..4 + key_len]; - let encrypted_message = &ciphertext[4 + key_len..]; + // Derive shared secret using ECDH + let recipient_secret = EphemeralSecret::random(&mut OsRng); + let shared_secret = recipient_secret.diffie_hellman(&sender_key.into()); - // Decrypt the symmetric key - // Use the same key derivation as in encryption - let key_encryption_key = { + // Derive decryption key from the shared secret (using the same method as encryption) + let decryption_key = { let mut hasher = Sha256::default(); hasher.update(self.verifying_key.to_sec1_bytes()); // Use the same fixed salt as in encryption @@ -287,13 +285,9 @@ impl KeyPair { hasher.finalize().to_vec() }; - // Decrypt the symmetric key - let symmetric_key = implementation::decrypt_with_key(&key_encryption_key, encrypted_key) - .map_err(|e| CryptoError::DecryptionFailed(format!("Failed to decrypt key: {}", e)))?; - - // 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))) + // Decrypt the message using the derived key + implementation::decrypt_with_key(&decryption_key, actual_ciphertext) + .map_err(|e| CryptoError::DecryptionFailed(e.to_string())) } } diff --git a/src/vault/kvs/store.rs b/src/vault/kvs/store.rs index f30ab85..74c9c6f 100644 --- a/src/vault/kvs/store.rs +++ b/src/vault/kvs/store.rs @@ -355,7 +355,7 @@ impl KvStore { // Save to disk self.save()?; - Ok(()) + Ok(()) } /// Gets the name of the store. diff --git a/vault/.cargo/config.toml b/vault/.cargo/config.toml new file mode 100644 index 0000000..2e07606 --- /dev/null +++ b/vault/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/vault/Cargo.toml b/vault/Cargo.toml new file mode 100644 index 0000000..5285312 --- /dev/null +++ b/vault/Cargo.toml @@ -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" diff --git a/vault/src/README.md b/vault/src/README.md new file mode 100644 index 0000000..28a3f1b --- /dev/null +++ b/vault/src/README.md @@ -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. diff --git a/vault/src/error.rs b/vault/src/error.rs new file mode 100644 index 0000000..87ff0d4 --- /dev/null +++ b/vault/src/error.rs @@ -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 for Error { + fn from(value: CryptoError) -> Self { + Self::Crypto(value) + } +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::IOError(value) + } +} + +impl From for Error { + fn from(value: kv::error::KVError) -> Self { + Self::KV(value) + } +} + +impl From for Error { + fn from(_: bincode::error::DecodeError) -> Self { + Self::Coding + } +} + +impl From for Error { + fn from(_: bincode::error::EncodeError) -> Self { + Self::Coding + } +} + +impl From for CryptoError { + fn from(_: k256::ecdsa::Error) -> Self { + Self::InvalidKey + } +} + +impl From for CryptoError { + fn from(_: k256::elliptic_curve::Error) -> Self { + Self::InvalidKey + } +} diff --git a/vault/src/key.rs b/vault/src/key.rs new file mode 100644 index 0000000..42d2529 --- /dev/null +++ b/vault/src/key.rs @@ -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, +} + +impl Key { + /// Try to downcast this `Key` to a [`SymmetricKey`] + pub fn as_symmetric(&self) -> Option { + 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 { + 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 { + if matches!(self.mode, KeyType::Signature) { + SigningKeypair::from_bytes(&self.raw_key).ok() + } else { + None + } + } +} + +impl From for Key { + fn from(value: SymmetricKey) -> Self { + Self { + mode: KeyType::Symmetric, + raw_key: Vec::from(value.as_raw_bytes()), + } + } +} + +impl From for Key { + fn from(value: AsymmetricKeypair) -> Self { + Self { + mode: KeyType::Asymmetric, + raw_key: value.as_raw_private_key(), + } + } +} + +impl From for Key { + fn from(value: SigningKeypair) -> Self { + Self { + mode: KeyType::Signature, + raw_key: value.as_raw_private_key(), + } + } +} diff --git a/vault/src/key/asymmetric.rs b/vault/src/key/asymmetric.rs new file mode 100644 index 0000000..ea89740 --- /dev/null +++ b/vault/src/key/asymmetric.rs @@ -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 { + 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 { + 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 { + 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, CryptoError> { + let mut symmetric_key = [0u8; 32]; + diffie_hellman(self.private.to_nonzero_scalar(), remote_key.0.as_affine()) + .extract::(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, CryptoError> { + let mut symmetric_key = [0u8; 32]; + diffie_hellman(self.private.to_nonzero_scalar(), remote_key.0.as_affine()) + .extract::(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 { + 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()); + } +} diff --git a/vault/src/key/signature.rs b/vault/src/key/signature.rs new file mode 100644 index 0000000..e83d364 --- /dev/null +++ b/vault/src/key/signature.rs @@ -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 { + 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 { + 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 { + 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, 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 { + 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()); + } +} diff --git a/vault/src/key/symmetric.rs b/vault/src/key/symmetric.rs new file mode 100644 index 0000000..00aaa96 --- /dev/null +++ b/vault/src/key/symmetric.rs @@ -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 { + 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, 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, 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::(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()); + } +} diff --git a/vault/src/keyspace.rs b/vault/src/keyspace.rs new file mode 100644 index 0000000..112be5e --- /dev/null +++ b/vault/src/keyspace.rs @@ -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, + /// 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 { + 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 { + 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, 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, 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, _) = + bincode::serde::decode_from_slice(&raw, BINCODE_CONFIG)?; + + self.keys = decoded_keys; + + Ok(()) + } +} diff --git a/vault/src/keyspace/fallback.rs b/vault/src/keyspace/fallback.rs new file mode 100644 index 0000000..cd8cca7 --- /dev/null +++ b/vault/src/keyspace/fallback.rs @@ -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, + /// 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 { + /// 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, 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, Error> { + todo!() + } +} diff --git a/vault/src/keyspace/wasm.rs b/vault/src/keyspace/wasm.rs new file mode 100644 index 0000000..5c60ddf --- /dev/null +++ b/vault/src/keyspace/wasm.rs @@ -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, 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, Error> { + todo!() + } +} diff --git a/vault/src/lib.rs b/vault/src/lib.rs new file mode 100644 index 0000000..1f9e834 --- /dev/null +++ b/vault/src/lib.rs @@ -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 { + 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 { + 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 + } + } +}