encrypted-json-kv 0.2.1

Easily store encrypted JSON blobs
Documentation
/********************************************************************************
 *   Encrypted KV store for json blobs based on sled                            *
 *   Copyright (C) 2020 Famedly GmbH                                            *
 *                                                                              *
 *   This program is free software: you can redistribute it and/or modify       *
 *   it under the terms of the GNU Affero General Public License as             *
 *   published by the Free Software Foundation, either version 3 of the         *
 *   License, or (at your option) any later version.                            *
 *                                                                              *
 *   This program is distributed in the hope that it will be useful,            *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of             *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the               *
 *   GNU Affero General Public License for more details.                        *
 *                                                                              *
 *   You should have received a copy of the GNU Affero General Public License   *
 *   along with this program.  If not, see <https://www.gnu.org/licenses/>.     *
 ********************************************************************************/
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sled;
use sodiumoxide::crypto::{pwhash, secretbox};

use thiserror::Error;

use std::{fs, path::PathBuf};

use crate::{crypto::derive_key, EncryptedValue, EncryptionError};

pub(crate) const CURRENT_DATABASE_VERSION: usize = 0;

/// A KV store based on sled, storing serde_json::Value values in an encrypted fashion
pub struct Database {
    path: Option<PathBuf>,
    db: sled::Db,
    key: secretbox::Key,
}

#[derive(Error, Debug)]
pub enum DatabaseOpenError {
    #[error("file io broke")]
    Io(#[from] std::io::Error),
    #[error("the database isn't configured properly")]
    DbConfig(#[from] DbConfigError),
    #[error("the version on disk is not supported by the current version")]
    UnsupportedVersion,
    #[error("error from sled")]
    Sled(#[from] sled::Error),
    #[error("internal config file couldn't be parsed")]
    TomlRead(#[from] toml::de::Error),
    #[error("couldn't serialize config file")]
    TomlWrite(#[from] toml::ser::Error),
}

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("error from sled")]
    Sled(#[from] sled::Error),
    #[error("")]
    Encryption(#[from] EncryptionError),
    #[error("")]
    Serde(#[from] serde_json::Error),
}

#[derive(Error, Debug)]
pub enum DatabaseSetPassphraseError {
    #[error("toml")]
    Toml(#[from] toml::ser::Error),
    #[error("io")]
    Io(#[from] std::io::Error),
    #[error("database is temporary")]
    Temporary,
}

impl Database {
    /// Opens a new Database instance.
    pub fn new(path: PathBuf, passphrase: &[u8]) -> Result<Self, DatabaseOpenError> {
        // ensure that the passed path exists
        fs::create_dir_all(&path)?;

        let mut is_new_db = false;

        // get config if exists, create new one otherwise
        let mut db_config_path = path.clone();
        db_config_path.push("encrypted_json_kv.toml");
        let db_config = match fs::read_to_string(&db_config_path) {
            Ok(db_config_string) => toml::from_str(&db_config_string)?,
            Err(error) => match error.kind() {
                // File not found, creating a new one
                std::io::ErrorKind::NotFound => {
                    let config = DbConfig::new(passphrase, None);
                    fs::write(db_config_path, toml::to_string(&config)?)?;
                    is_new_db = true;
                    config
                }
                // Other error -> pass it to the caller
                _ => Err(error)?,
            },
        };

        // check db version
        if db_config.version != CURRENT_DATABASE_VERSION {
            return Err(DatabaseOpenError::UnsupportedVersion);
        }

        // decrypt key
        let key = db_config.get_key(passphrase)?;

        // open sled database
        let mut db_path = path.clone();
        db_path.push("sled");
        let db = sled::Config::new()
            .path(db_path)
            .create_new(is_new_db)
            .open()?;

        // return database
        Ok(Database { path: Some(path), db, key })
    }

    /// Opens a new temporary Database instance
    pub fn temporary() -> Result<Self, DatabaseOpenError> {
        let key = secretbox::gen_key();
        let db = sled::Config::new()
            .temporary(true)
            .open()?;
        Ok(Database { path: None, db, key })
    }

    pub fn set_passphrase(&self, passphrase: &[u8]) -> Result<(), DatabaseSetPassphraseError> {
        match &self.path {
            Some(path) => {
                let mut db_config_path = path.clone();
                db_config_path.push("encrypted_json_kv.toml");
        let config = DbConfig::new(passphrase, None);
        fs::write(db_config_path, toml::to_string(&config)?)?;
        Ok(())
            },
            None => Err(DatabaseSetPassphraseError::Temporary)
        }
    }

    /// Get a serde_json::Value from the database. The wrapping sled::Result will only
    /// throw an error when something goes really wrong. The Option inside that Result
    /// indicates whether the requested item is present
    pub fn get(&self, key: &str) -> Result<Option<Value>, DatabaseError> {
        match self.db.get(key)? {
            Some(value) => Ok(Some(serde_json::from_slice::<EncryptedValue>(&value)?.decrypt(&self.key)?)),
            None => Ok(None),
        }
    }

    /// Insert a serde_json::Value into the database. The wrapping sled::Result will only
    /// throw an error when something goes really wrong. The Option inside contains the
    /// ciphertext of the just inserted value, if this insertion was successful.
    pub fn insert(&self, key: &str, value: &Value) -> Result<(), DatabaseError> {
        let value = EncryptedValue::encrypt(value, &self.key);
        let value = serde_json::to_vec(&value).unwrap();
        self.db.insert(key, value)?;
        Ok(())
    }

    /// Remove a value from the database. The wrapping sled::Result will only throw an error when
    /// something goes really wrong. The Option inside contains the old value, if one was present.
    pub fn remove(&self, key: &str) -> Result<Option<Value>, DatabaseError> {
        match self.db.remove(key)? {
            Some(value) => Ok(Some(serde_json::from_slice::<EncryptedValue>(&value)?.decrypt(&self.key)?)),
            None => Ok(None),
        }
    }

    /// Iterate over all keys in the database
    pub fn keys(&self) -> impl DoubleEndedIterator<Item = sled::Result<sled::IVec>> {
        self.db.iter().keys()
    }

    /// Get encryption key
    pub fn encryption_key(&self) -> &secretbox::Key {
        &self.key
    }
}

#[derive(Serialize, Deserialize)]
pub(crate) struct DbConfig {
    version: usize,
    salt: pwhash::Salt,
    encrypted_key: EncryptedValue,
}

impl DbConfig {
    pub(crate) fn new(passphrase: &[u8], key: Option<secretbox::Key>) -> DbConfig {
        let salt = pwhash::gen_salt();
        let outer_key = derive_key(passphrase, salt);
        let inner_key = key.unwrap_or_else(|| secretbox::gen_key());
        let encrypted_key =
            EncryptedValue::encrypt(&Value::String(base64::encode(inner_key)), &outer_key);
        DbConfig {
            version: CURRENT_DATABASE_VERSION,
            salt,
            encrypted_key,
        }
    }

    pub(crate) fn get_key(&self, passphrase: &[u8]) -> Result<secretbox::Key, DbConfigError> {
        let outer_key = derive_key(passphrase, self.salt);
        let inner_key_value = self.encrypted_key.decrypt(&outer_key)?;
        let inner_key_encoded = inner_key_value.as_str().ok_or_else(|| DbConfigError::WrongKeyType)?;
        let inner_key_bytes = base64::decode(inner_key_encoded)?;
        Ok(secretbox::Key::from_slice(&inner_key_bytes).ok_or_else(|| DbConfigError::WrongKeySize)?)
    }
}

#[derive(Error, Debug)]
pub enum DbConfigError {
    #[error("couldn't decrypt database key")]
    Encryption(#[from] EncryptionError),
    #[error("base64 decoding of the key didn't work")]
    Encoding(#[from] base64::DecodeError),
    #[error("key was not a string after decrypting and parsing")]
    WrongKeyType,
    #[error("key has the wrong size")]
    WrongKeySize,
}