From 2adda10664a0a8abef5d0d2e320009df7b93a98c Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Thu, 15 May 2025 13:53:16 +0200 Subject: [PATCH] Basic API Signed-off-by: Lee Smet --- vault/Cargo.toml | 2 ++ vault/src/error.rs | 15 +++++++++++++ vault/src/key/symmetric.rs | 18 +++++++++++++++ vault/src/keyspace.rs | 41 ++++++++++++++++++++++++--------- vault/src/kvs.rs | 46 +++++++++++++++++++++++++++++++++++++- 5 files changed, 110 insertions(+), 12 deletions(-) diff --git a/vault/Cargo.toml b/vault/Cargo.toml index e847f9f..5285312 100644 --- a/vault/Cargo.toml +++ b/vault/Cargo.toml @@ -18,3 +18,5 @@ 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/error.rs b/vault/src/error.rs index 5613f7e..4f4f502 100644 --- a/vault/src/error.rs +++ b/vault/src/error.rs @@ -9,6 +9,8 @@ pub enum Error { 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 { @@ -18,6 +20,7 @@ impl core::fmt::Display for Error { 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"), } } } @@ -77,3 +80,15 @@ impl From for Error { 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 + } +} diff --git a/vault/src/key/symmetric.rs b/vault/src/key/symmetric.rs index ec11a77..c73a31b 100644 --- a/vault/src/key/symmetric.rs +++ b/vault/src/key/symmetric.rs @@ -83,4 +83,22 @@ impl SymmetricKey { .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) + } } diff --git a/vault/src/keyspace.rs b/vault/src/keyspace.rs index 775b400..112be5e 100644 --- a/vault/src/keyspace.rs +++ b/vault/src/keyspace.rs @@ -3,13 +3,21 @@ // #[cfg(target_arch = "wasm32")] // mod wasm; -use std::{collections::HashMap, path::Path}; +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")] @@ -44,14 +52,14 @@ impl KeySpace {} #[cfg(not(target_arch = "wasm32"))] impl KeySpace { /// Open the keyspace at the provided path using the given key for encryption. - pub fn open(path: &Path, encryption_key: SymmetricKey) -> Result { + 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()?; + ks.load_keyspace().await?; Ok(ks) } } @@ -60,8 +68,13 @@ impl KeySpace { impl KeySpace { pub async fn open(name: &str, encryption_key: SymmetricKey) -> Result { let store = WasmStore::open(name).await?; - todo!(); - // Ok(Self { store }) + let mut ks = Self { + store, + keys: HashMap::new(), + encryption_key, + }; + ks.load_keyspace().await?; + Ok(ks) } } @@ -77,13 +90,13 @@ impl KeySpace { /// 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() + 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() + self.save_keyspace().await } /// Iterate over all stored [`keys`](Key) in the KeySpace @@ -93,9 +106,10 @@ impl KeySpace { /// Encrypt all keys and save them to the underlying store async fn save_keyspace(&self) -> Result<(), Error> { - // Bincode encode keys - // + 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 @@ -105,8 +119,13 @@ impl KeySpace { return Ok(()); }; - // TODO: bincode decode + let raw = self.encryption_key.decrypt(&ks)?; - todo!(); + let (decoded_keys, _): (HashMap, _) = + bincode::serde::decode_from_slice(&raw, BINCODE_CONFIG)?; + + self.keys = decoded_keys; + + Ok(()) } } diff --git a/vault/src/kvs.rs b/vault/src/kvs.rs index 4ff117e..2052afa 100644 --- a/vault/src/kvs.rs +++ b/vault/src/kvs.rs @@ -1,3 +1,47 @@ +#[cfg(not(target_arch = "wasm32"))] +use std::path::{Path, PathBuf}; + +use crate::{error::Error, key::symmetric::SymmetricKey, keyspace::KeySpace}; + /// Store 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 Store {} +pub struct Store { + #[cfg(not(target_arch = "wasm32"))] + path: PathBuf, +} + +#[cfg(not(target_arch = "wasm32"))] +impl Store { + /// 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 Store { + /// 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 + } + } +}