microkv 0.2.9

a minimal and persistent key-value store designed with security in mind.
Documentation
//! Defines the foundational structure and API for the key-value store implementation.
//! The `kv` module should be used to spin up localized instances of the key-value store.
//!
//! ## Features
//!
//! * Database interaction operations, with sorted-key iteration possible
//! * Serialization to persistent storage
//! * Symmetric authenticated cryptography
//! * Mutual exclusion with RWlocks and mutexes
//! * Secure memory wiping
//!
//! ## Example
//!
//! ```rust
//! use microkv::MicroKV;
//!
//! let kv: MicroKV = MicroKV::new("example").with_pwd_clear("p@ssw0rd".to_string());
//!
//! // put
//! let value = 123;
//! kv.put("keyname", &value);
//!
//! // get
//! let res: i32 = kv.get_unwrap("keyname").expect("cannot retrieve value");
//! println!("{}", res);
//!
//! // delete
//! kv.delete("keyname").expect("cannot delete key");
//! ```
//!
//! width namespace
//!
//! ```rust
//! use microkv::MicroKV;
//!
//! let kv: MicroKV = MicroKV::new("example").with_pwd_clear("p@ssw0rd".to_string());
//! let namespace_custom = kv.namespace("custom");
//!
//! // put
//! let value = 123;
//! namespace_custom.put("keyname", &value);
//!
//! // get
//! let res: i32 = namespace_custom.get_unwrap("keyname").expect("cannot retrieve value");
//! println!("{}", res);
//!
//! // delete
//! namespace_custom.delete("keyname").expect("cannot delete key");
//! ```
#![allow(clippy::result_map_unit_fn)]

use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};

use indexmap::IndexMap;
use secstr::{SecStr, SecVec};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use sodiumoxide::crypto::hash::sha256;
use sodiumoxide::crypto::secretbox::{self, Nonce};

use crate::errors::{ErrorType, KVError, Result};
use crate::namespace::NamespaceMicrokv;

/// Defines the directory path where a key-value store
/// (or multiple) can be interacted with.
const DEFAULT_WORKSPACE_PATH: &str = ".microkv/";

/// An alias to a base data structure that supports storing
/// associated types. An `IndexMap` is a strong choice due to
/// strong asymptotic performance with sorted key iteration.
type KV = IndexMap<String, SecVec<u8>>;

/// Defines the main interface structure to represent the most
/// recent state of the data store.
#[derive(Clone, Serialize, Deserialize)]
pub struct MicroKV {
    path: PathBuf,

    /// stores the actual key-value store encapsulated with a RwLock
    storage: Arc<RwLock<KV>>,

    /// pseudorandom nonce that can be publicly known
    nonce: Nonce,

    /// memory-guarded hashed password
    #[serde(skip_serializing, skip_deserializing)]
    pwd: Option<SecStr>,

    /// is auto commit
    is_auto_commit: bool,
}

impl MicroKV {
    /// New MicroKV store with store to base path
    pub fn new_with_base_path<S: AsRef<str>>(dbname: S, base_path: PathBuf) -> Self {
        let storage = Arc::new(RwLock::new(KV::new()));

        // no password, until set by `with_pwd_*` methods
        let pwd: Option<SecStr> = None;

        // initialize a new public nonce for symmetric AEAD
        let nonce: Nonce = secretbox::gen_nonce();

        // get abspath to dbname to write to.
        let path = MicroKV::get_db_path_with_base_path(dbname, base_path);

        Self {
            path,
            storage,
            nonce,
            pwd,
            is_auto_commit: false,
        }
    }

    /// Initializes a new empty and unencrypted MicroKV store with
    /// an identifying database name. This is the bare minimum that can operate as a
    /// key-value store, and can be configured using other builder methods.
    pub fn new<S: AsRef<str>>(dbname: S) -> Self {
        let mut path = MicroKV::get_home_dir();
        path.push(DEFAULT_WORKSPACE_PATH);
        Self::new_with_base_path(dbname, path)
    }

    /// Open with base path
    pub fn open_with_base_path<S: AsRef<str>>(dbname: S, base_path: PathBuf) -> Result<Self> {
        // initialize abspath to persistent db
        let path = MicroKV::get_db_path_with_base_path(dbname.as_ref(), base_path.clone());

        if path.is_file() {
            // read kv raw serialized structure to kv_raw
            let mut kv_raw: Vec<u8> = Vec::new();
            File::open(path)?.read_to_end(&mut kv_raw)?;

            // deserialize with bincode and return
            let kv: Self = bincode::deserialize(&kv_raw).unwrap();
            Ok(kv)
        } else {
            Ok(Self::new_with_base_path(dbname, base_path))
        }
    }

    /// Opens a previously instantiated and encrypted MicroKV, given a db name.
    /// The public nonce generated from a previous session is also retrieved in order to
    /// do authenticated encryption later on.
    pub fn open<S: AsRef<str>>(dbname: S) -> Result<Self> {
        let mut path = MicroKV::get_home_dir();
        path.push(DEFAULT_WORKSPACE_PATH);
        Self::open_with_base_path(dbname, path)
    }

    /// Helper that retrieves the home directory by resolving $HOME
    #[inline]
    fn get_home_dir() -> PathBuf {
        dirs::home_dir().unwrap()
    }

    /// Helper that forms an absolute path from a given database name and the default workspace path.
    #[inline]
    pub fn get_db_path<S: AsRef<str>>(name: S) -> PathBuf {
        let mut path = MicroKV::get_home_dir();
        path.push(DEFAULT_WORKSPACE_PATH);
        Self::get_db_path_with_base_path(name, path)
    }

    /// with base path
    #[inline]
    pub fn get_db_path_with_base_path<S: AsRef<str>>(name: S, mut base_path: PathBuf) -> PathBuf {
        base_path.push(name.as_ref());
        base_path.set_extension("kv");
        base_path
    }

    /*
    /// `override_path()` changes the default path for persisting the store, rather than
    /// writing/reading from the default workspace directory.
    pub fn override_path(mut self, path: PathBuf) -> io::Result<Self> {
        self.path = fs::canonicalize(Path::new(&path))?;
        Ok(self)
    }
    */

    /// Builds up the MicroKV with a cleartext password, which is hashed using
    /// the defaultly supported SHA-256 by `sodiumoxide`, in order to instantiate a 32-byte hash.
    ///
    /// Use if the password to encrypt is not naturally pseudorandom and secured in-memory,
    /// and is instead read elsewhere, like a file or stdin (developer should guarantee security when
    /// implementing such methods, as MicroKV only guarantees hashing and secure storage).
    pub fn with_pwd_clear<S: AsRef<str>>(mut self, unsafe_pwd: S) -> Self {
        let pwd: SecStr = SecVec::new(sha256::hash(unsafe_pwd.as_ref().as_bytes()).0.to_vec());
        self.pwd = Some(pwd);
        self
    }

    /// Builds up the MicroKV with a hashed buffer, which is then locked securely `for later use.
    ///
    /// Use if the password to encrypt is generated as a pseudorandom value, or previously hashed by
    /// another preferred one-way function within or outside the application.
    pub fn with_pwd_hash(mut self, _pwd: [u8; 32]) -> Self {
        let pwd: SecStr = SecVec::new(_pwd.to_vec());
        self.pwd = Some(pwd);
        self
    }

    /// Set is auto commit
    pub fn set_auto_commit(mut self, enable: bool) -> Self {
        self.is_auto_commit = enable;
        self
    }

    ///////////////////////////////////////
    // extended
    ///////////////////////////////////////

    pub(crate) fn pwd(&self) -> &Option<SecStr> {
        &self.pwd
    }

    pub(crate) fn nonce(&self) -> &Nonce {
        &self.nonce
    }

    pub fn namespace(&self, namespace: impl AsRef<str>) -> NamespaceMicrokv {
        NamespaceMicrokv::new(namespace, self)
    }

    pub fn namespace_default(&self) -> NamespaceMicrokv {
        self.namespace("")
    }

    ///////////////////////////////////////
    // Primitive key-value store operations
    ///////////////////////////////////////

    /// unsafe get, may this api can change name to get_unwrap
    pub fn get_unwrap<V>(&self, key: impl AsRef<str>) -> Result<V>
    where
        V: DeserializeOwned + 'static,
    {
        self.namespace_default().get_unwrap(key)
    }

    /// Decrypts and retrieves a value. Can return errors if lock is poisoned,
    /// ciphertext decryption doesn't work, and if parsing bytes fail.
    pub fn get<V>(&self, key: impl AsRef<str>) -> Result<Option<V>>
    where
        V: DeserializeOwned + 'static,
    {
        self.namespace_default().get(key)
    }

    /// Encrypts and adds a new key-value pair to storage.
    pub fn put<V>(&self, key: impl AsRef<str>, value: &V) -> Result<()>
    where
        V: Serialize,
    {
        self.namespace_default().put(key, value)
    }

    /// Delete removes an entry in the key value store.
    pub fn delete(&self, key: impl AsRef<str>) -> Result<()> {
        self.namespace_default().delete(key)
    }

    //////////////////////////////////////////
    // Other key-value store helper operations
    //////////////////////////////////////////

    /// Arbitrary read-lock that encapsulates a read-only closure. Multiple concurrent readers
    /// can hold a lock and parse out data.
    /// ```rust
    /// use microkv::MicroKV;
    /// use microkv::namespace::ExtendedIndexMap;
    ///
    /// let kv = MicroKV::new("example").with_pwd_clear("p@ssw0rd".to_string());
    /// let value = String::from("my value");
    /// kv.namespace("a").put("user", &value).expect("cannot insert user");
    /// kv.namespace("b").put("user", &value).expect("cannot insert user");
    ///
    /// kv.lock_read(|c| {
    ///     let user_namespace_1: String = c.kv_get(&kv, "a", "user").expect("cannot read user").expect("key not found");
    ///     let user_namespace_2: String = c.kv_get(&kv, "b", "user").expect("cannot read user").expect("key not found");
    ///     assert_eq!(user_namespace_1, user_namespace_2);
    /// }).expect("cannot get lock")
    /// ```
    pub fn lock_read<C, R>(&self, callback: C) -> Result<R>
    where
        C: FnOnce(&KV) -> R,
    {
        let data = self.storage.read().map_err(|_| KVError {
            error: ErrorType::PoisonError,
            msg: None,
        })?;
        Ok(callback(&data))
    }

    /// Arbitrary write-lock that encapsulates a write-only closure Single writer can hold a
    /// lock and mutate data, blocking any other readers/writers before the lock is released.
    /// ```rust
    /// use microkv::MicroKV;
    /// use microkv::namespace::ExtendedIndexMap;
    ///
    /// let kv = MicroKV::new("example").with_pwd_clear("p@ssw0rd".to_string());
    /// let value: u32 = 123;
    /// kv.put("number", &value).expect("cannot insert number");
    ///
    /// kv.lock_write(|c| {
    ///     let current_value: u32 = c.kv_get_unwrap(&kv, "", "number").expect("cannot read number");
    ///     println!("Current value is: {current_value}");
    ///     c.kv_put(&kv, "", "number", &(current_value + 1));
    ///     let current_value: u32 = c.kv_get_unwrap(&kv, "", "number").expect("cannot read number");
    ///     println!("Now the value is: {current_value}");
    /// }).expect("cannot get lock")
    /// ```    
    pub fn lock_write<C, R>(&self, callback: C) -> Result<R>
    where
        C: FnOnce(&mut KV) -> R,
    {
        let mut data = self.storage.write().map_err(|_| KVError {
            error: ErrorType::PoisonError,
            msg: None,
        })?;

        let result = callback(&mut data);
        drop(data);

        if self.is_auto_commit {
            self.commit()?;
        }

        Ok(result)
    }

    /// Helper routine that acquires a reader lock and checks if a key exists.
    pub fn exists(&self, key: impl AsRef<str>) -> Result<bool> {
        self.namespace_default().exists(key)
    }

    /// Safely consumes an iterator over the keys in the `IndexMap` and returns a
    /// `Vec<String>` for further use.
    ///
    /// Note that key iteration, not value iteration, is only supported in order to preserve
    /// security guarantees.
    pub fn keys(&self) -> Result<Vec<String>> {
        self.namespace_default().keys()
    }

    /// Safely consumes an iterator over a copy of in-place sorted keys in the
    /// `IndexMap` and returns a `Vec<String>` for further use.
    ///
    /// Note that key iteration, not value iteration, is only supported in order to preserve
    /// security guarantees.
    pub fn sorted_keys(&self) -> Result<Vec<String>> {
        self.namespace_default().sorted_keys()
    }

    /// Empties out the entire underlying `IndexMap` in O(n) time, but does
    /// not delete the persistent storage file from disk. The `IndexMap` remains,
    /// and its capacity is kept the same.
    pub fn clear(&self) -> Result<()> {
        self.namespace_default().clear()
    }

    ///////////////////
    // I/O Operations
    ///////////////////

    /// Writes the IndexMap to persistent storage after encrypting with secure crypto construction.
    pub fn commit(&self) -> Result<()> {
        // initialize workspace directory if not exists
        // let mut workspace_dir = MicroKV::get_home_dir();
        // workspace_dir.push(DEFAULT_WORKSPACE_PATH);
        match self.path.parent() {
            Some(path) => {
                if !path.is_dir() {
                    fs::create_dir_all(path)?;
                }
            }
            None => {
                return Err(KVError {
                    error: ErrorType::FileError,
                    msg: Some("The store file parent path isn't sound".to_string()),
                });
            }
        }

        // check if path to db exists, if not create it
        let path = Path::new(&self.path);
        let mut file: File = OpenOptions::new().write(true).create(true).open(path)?;

        // acquire a file lock that unlocks at the end of scope
        // let _file_lock = Arc::new(Mutex::new(0));
        let ser = bincode::serialize(self).unwrap();
        file.write_all(&ser)?;
        Ok(())
    }

    /// Clears the underlying data structure for the key-value store, and deletes the database file to remove all traces.
    pub fn destruct(&self) -> Result<()> {
        unimplemented!();
    }
}

// coerce a secure zero wipe
impl Drop for MicroKV {
    fn drop(&mut self) {
        if let Some(ref mut pwd) = self.pwd {
            pwd.zero_out()
        }
    }
}