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;
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 {
pub fn new(path: PathBuf, passphrase: &[u8]) -> Result<Self, DatabaseOpenError> {
fs::create_dir_all(&path)?;
let mut is_new_db = false;
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() {
std::io::ErrorKind::NotFound => {
let config = DbConfig::new(passphrase, None);
fs::write(db_config_path, toml::to_string(&config)?)?;
is_new_db = true;
config
}
_ => Err(error)?,
},
};
if db_config.version != CURRENT_DATABASE_VERSION {
return Err(DatabaseOpenError::UnsupportedVersion);
}
let key = db_config.get_key(passphrase)?;
let mut db_path = path.clone();
db_path.push("sled");
let db = sled::Config::new()
.path(db_path)
.create_new(is_new_db)
.open()?;
Ok(Database { path: Some(path), db, key })
}
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)
}
}
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),
}
}
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(())
}
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),
}
}
pub fn keys(&self) -> impl DoubleEndedIterator<Item = sled::Result<sled::IVec>> {
self.db.iter().keys()
}
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,
}