#![doc = include_str!("../README.md")]
#![warn(
clippy::all,
clippy::pedantic,
rust_2018_idioms,
missing_docs,
clippy::missing_docs_in_private_items
)]
#![allow(
clippy::option_if_let_else,
clippy::module_name_repetitions,
clippy::shadow_unrelated,
clippy::must_use_candidate,
clippy::implicit_hasher
)]
#![doc(
html_logo_url = "https://gitlab.com/rust-community-matrix/snapper/-/raw/trunk/static/snapper.png"
)]
use std::{
collections::HashMap,
fs::{create_dir_all, File},
io::Write,
path::{Path, PathBuf},
sync::Arc,
};
use serde::{de::DeserializeOwned, Serialize};
use snafu::{ensure, OptionExt, ResultExt};
use tracing::{info, instrument};
use crate::{
crypto::{DerivedKey, EncryptedRootKey, RootKey},
entries::{Control, Namespace, Namespaces, Settings},
error::{
CryptoBoxError, DirectoryAlreadyExists, DirectoryDoesNotExist, FailedCreatingDirectory,
Fetch, MissingConfiguration, MissingNamespaceDirectory, NamespaceOpen, RootKeyDecryption,
RootKeyEncryption, RootKeyIO, RootKeySerial, RootNamespaceInit, RootNamespaceOpen, Store,
},
file::LsmFile,
};
#[cfg(feature = "experimental-async")]
pub mod async_wrapper;
pub mod crypto;
mod entries;
pub mod error;
pub mod file;
pub struct CryptoBox {
path: PathBuf,
root_key: Arc<RootKey>,
root_namespace: LsmFile<File, RootKey>,
compression: Option<i32>,
max_cache_entries: Option<usize>,
namespaces: HashMap<String, (Namespace, LsmFile<File, DerivedKey>)>,
}
impl CryptoBox {
#[instrument(skip(path, password), err)]
pub fn init(
path: impl AsRef<Path>,
compression: Option<i32>,
max_cache_entries: Option<usize>,
password: impl AsRef<[u8]>,
) -> Result<Self, CryptoBoxError> {
let path = path.as_ref();
info!(?path, "Creating CryptoBox");
let password = password.as_ref();
ensure!(
!path.exists(),
DirectoryAlreadyExists {
directory: format!("{:?}", path)
}
);
create_dir_all(path).context(FailedCreatingDirectory {
directory: format!("{:?}", path),
})?;
let namespaces_path = path.join("namespaces");
create_dir_all(&namespaces_path).context(FailedCreatingDirectory {
directory: format!("{:?}", namespaces_path),
})?;
let root_key = Arc::new(RootKey::random());
let encrypted_root_key = root_key.encrypt(password).context(RootKeyEncryption)?;
let root_key_path = path.join("KEY");
let mut key_file = File::create(&root_key_path).context(RootKeyIO {
path: format!("{:?}", root_key_path),
})?;
serde_cbor::to_writer(&mut key_file, &encrypted_root_key).context(RootKeySerial)?;
key_file.flush().context(RootKeyIO {
path: format!("{:?}", root_key_path),
})?;
std::mem::drop(key_file);
let root_namespace_path = path.join("root");
let mut root_namespace =
LsmFile::create(&root_namespace_path, None, root_key.clone(), Some(0)).context(
RootNamespaceInit {
path: format!("{:?}", root_namespace_path),
},
)?;
root_namespace
.insert(
&"",
&Control::Settings(Settings {
compression,
max_cache_entries,
}),
)
.context(RootNamespaceInit {
path: format!("{:?}", root_namespace_path),
})?;
root_namespace
.insert(
&"namespaces",
&Control::Namespaces(Namespaces { namespaces: vec![] }),
)
.context(RootNamespaceInit {
path: format!("{:?}", root_namespace_path),
})?;
root_namespace.flush().context(RootNamespaceInit {
path: format!("{:?}", root_namespace_path),
})?;
Ok(CryptoBox {
path: path.to_path_buf(),
root_key,
root_namespace,
compression,
namespaces: HashMap::new(),
max_cache_entries,
})
}
#[instrument(skip(path, password), err)]
pub fn open(
path: impl AsRef<Path>,
password: impl AsRef<[u8]>,
) -> Result<Self, CryptoBoxError> {
let path = path.as_ref().to_path_buf();
let password = password.as_ref();
info!(?path, "Opening CryptoBox");
ensure!(
path.exists() && path.is_dir(),
DirectoryDoesNotExist {
path: format!("{:?}", path)
}
);
let root_key_path = path.join("KEY");
let mut root_key_file = File::open(&root_key_path).context(RootKeyIO {
path: format!("{:?}", root_key_path),
})?;
let enc_root_key: EncryptedRootKey =
serde_cbor::from_reader(&mut root_key_file).context(RootKeySerial)?;
let root_key = Arc::new(enc_root_key.decrypt(password).context(RootKeyDecryption)?);
std::mem::drop(root_key_file);
let root_namespace_path = path.join("root");
let mut root_namespace =
LsmFile::open(&root_namespace_path, None, root_key.clone(), Some(0)).context(
RootNamespaceOpen {
path: format!("{:?}", root_namespace_path),
},
)?;
ensure!(path.join("namespaces").exists(), MissingNamespaceDirectory);
if let Control::Settings(settings) = root_namespace
.get(&"")
.ok()
.context(MissingConfiguration)?
.context(MissingConfiguration)?
{
if let Control::Namespaces(namespaces_raw) = root_namespace
.get(&"namespaces")
.ok()
.context(MissingConfiguration)?
.context(MissingConfiguration)?
{
let mut namespaces = HashMap::new();
for namespace in namespaces_raw.namespaces {
let name = namespace.name.clone();
let path = path.join("namespaces").join(namespace.uuid.to_string());
let lsm_file = LsmFile::open(
&path,
settings.compression,
namespace.key.clone(),
settings.max_cache_entries,
)
.context(NamespaceOpen { name: name.clone() })?;
namespaces.insert(name, (namespace, lsm_file));
}
Ok(CryptoBox {
path,
root_key,
root_namespace,
compression: settings.compression,
namespaces,
max_cache_entries: settings.max_cache_entries,
})
} else {
Err(CryptoBoxError::MissingConfiguration)
}
} else {
Err(CryptoBoxError::MissingConfiguration)
}
}
pub fn namespace_exists(&self, name: &str) -> bool {
self.namespaces.contains_key(name)
}
pub fn namespaces(&self) -> Vec<String> {
self.namespaces.keys().cloned().collect()
}
#[instrument(skip(self), err)]
pub fn create_namespace(&mut self, name: String) -> Result<(), CryptoBoxError> {
if self.namespace_exists(&name) {
Ok(())
} else {
info!("Creating namespace");
let derived_key = Arc::new(self.root_key.derive(&name));
let uuid = uuid::Uuid::new_v4();
let path = self.path.join("namespaces").join(uuid.to_string());
let lsm_file = LsmFile::create(
&path,
self.compression,
derived_key.clone(),
self.max_cache_entries,
)
.context(NamespaceOpen { name: name.clone() })?;
let namespace = Namespace {
name: name.clone(),
key: derived_key,
uuid,
};
self.namespaces.insert(name.clone(), (namespace, lsm_file));
let namespaces: Vec<_> = self.namespaces.values().map(|(x, _)| x.clone()).collect();
let namespaces = Control::Namespaces(Namespaces { namespaces });
self.root_namespace
.insert(&"namespaces", &namespaces)
.context(NamespaceOpen { name: name.clone() })?;
self.root_namespace
.flush()
.context(NamespaceOpen { name })?;
Ok(())
}
}
#[instrument(skip(self, key, namespace), err)]
pub fn get<K, V>(&mut self, key: &K, namespace: &str) -> Result<Option<V>, CryptoBoxError>
where
K: Serialize,
V: DeserializeOwned,
{
if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
lsm.get(key).context(Fetch)
} else {
Err(CryptoBoxError::NoSuchNamespace {
name: namespace.to_string(),
})
}
}
#[instrument(skip(self, key), err)]
pub fn get_root<K, V>(&mut self, key: &K) -> Result<Option<V>, CryptoBoxError>
where
K: Serialize,
V: DeserializeOwned,
{
self.root_namespace.get(key).context(Fetch)
}
#[instrument(skip(self, key, value, namespace), err)]
pub fn insert<K, V>(
&mut self,
key: &K,
value: &V,
namespace: &str,
) -> Result<(), CryptoBoxError>
where
K: Serialize,
V: Serialize,
{
if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
lsm.insert(key, value).context(Store)
} else {
Err(CryptoBoxError::NoSuchNamespace {
name: namespace.to_string(),
})
}
}
#[instrument(skip(self, key, value), err)]
pub fn insert_root<K, V>(&mut self, key: &K, value: &V) -> Result<(), CryptoBoxError>
where
K: Serialize,
V: Serialize,
{
self.root_namespace.insert(key, value).context(Store)
}
#[instrument(skip(self, key, namespace), err)]
pub fn contains_key<K, V>(&mut self, key: &K, namespace: &str) -> Result<bool, CryptoBoxError>
where
K: Serialize,
V: DeserializeOwned,
{
let res = self.get::<K, V>(key, namespace)?;
Ok(res.is_some())
}
#[instrument(skip(self))]
pub fn flush(&mut self) -> Result<(), CryptoBoxError> {
let mut errors = vec![];
let res = self.root_namespace.flush();
if let Err(e) = res {
errors.push((None, e));
}
for (name, (_, lsm)) in &mut self.namespaces {
if let Err(e) = lsm.flush() {
errors.push((Some(name.clone()), e));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(CryptoBoxError::Flush { sources: errors })
}
}
pub fn to_hashmap<K, V>(&mut self, namespace: &str) -> Result<HashMap<K, V>, CryptoBoxError>
where
K: DeserializeOwned + Serialize + std::hash::Hash + Eq,
V: DeserializeOwned,
{
if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
lsm.to_hashmap().context(Fetch)
} else {
Err(CryptoBoxError::NoSuchNamespace {
name: namespace.to_string(),
})
}
}
pub fn to_pairs<K, V>(&mut self, namespace: &str) -> Result<Vec<(K, V)>, CryptoBoxError>
where
K: DeserializeOwned + Serialize + Eq + Clone,
V: DeserializeOwned + Clone,
{
if let Some((_, lsm)) = self.namespaces.get_mut(namespace) {
lsm.to_pairs().context(Fetch)
} else {
Err(CryptoBoxError::NoSuchNamespace {
name: namespace.to_string(),
})
}
}
pub fn root_to_pairs<K, V>(&mut self) -> Result<Vec<(K, V)>, CryptoBoxError>
where
K: DeserializeOwned + Serialize + Eq + Clone,
V: DeserializeOwned + Clone,
{
self.root_namespace.to_pairs().context(Fetch)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use tempfile::tempdir;
mod init {
use super::*;
#[test]
fn directory_layout() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let _crypto_box = CryptoBox::init(&path, None, None, "testing")?;
assert!(path.join("KEY").exists());
assert!(path.join("root").exists());
assert!(path.join("namespaces").exists());
Ok(())
}
#[test]
fn init_open() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let crypto_box = CryptoBox::init(&path, None, None, "testing")?;
std::mem::drop(crypto_box);
let _crypto_box = CryptoBox::open(&path, "testing")?;
Ok(())
}
#[test]
fn namespaces() -> Result<(), CryptoBoxError> {
let namespace_names = ["one", "two", "three"]
.into_iter()
.map(std::string::ToString::to_string)
.collect::<HashSet<_>>();
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
for namespace in &namespace_names {
crypto_box.create_namespace(namespace.to_string())?;
}
std::mem::drop(crypto_box);
let crypto_box = CryptoBox::open(&path, "testing")?;
assert_eq!(
namespace_names,
crypto_box.namespaces().into_iter().collect::<HashSet<_>>()
);
Ok(())
}
}
mod box_smoke {
use super::*;
#[test]
fn basic_insertions() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let pairs = [(1, 2), (3, 4), (5, 6)];
let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
crypto_box.create_namespace("".to_string())?;
let namespace = "";
for (key, value) in &pairs {
crypto_box.insert(key, value, namespace)?;
}
for (key, value) in &pairs {
let res = crypto_box.get(key, namespace)?;
if Some(*value) != res {
panic!("Unable to retrieve pair k: {} v: {}", key, value);
}
}
Ok(())
}
#[test]
fn basic_insertions_flush() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let pairs = [(1, 2), (3, 4), (5, 6)];
let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
crypto_box.create_namespace("".to_string())?;
let namespace = "";
for (key, value) in &pairs {
crypto_box.insert(key, value, namespace)?;
}
crypto_box.flush()?;
std::mem::drop(crypto_box);
let mut crypto_box = CryptoBox::open(&path, "testing")?;
for (key, value) in &pairs {
let res = crypto_box.get(key, namespace)?;
if Some(*value) != res {
panic!("Unable to retrieve pair k: {} v: {}", key, value);
}
}
Ok(())
}
#[test]
fn basic_insertions_hashmap() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let pairs = [(1, 2), (3, 4), (5, 6)];
let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
crypto_box.create_namespace("".to_string())?;
let namespace = "";
for (key, value) in &pairs {
crypto_box.insert(key, value, namespace)?;
}
assert_eq!(
crypto_box.to_hashmap("")?,
pairs.into_iter().collect::<HashMap<_, _>>()
);
Ok(())
}
#[test]
fn basic_insertions_pairs() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let pairs = [(1, 2), (3, 4), (5, 6)];
let mut crypto_box = CryptoBox::init(&path, None, None, "testing")?;
crypto_box.create_namespace("".to_string())?;
let namespace = "";
for (key, value) in &pairs {
crypto_box.insert(key, value, namespace)?;
}
let comparison = pairs.into_iter().collect::<HashMap<i32, i32>>();
assert_eq!(crypto_box.to_hashmap("")?, comparison,);
assert_eq!(
crypto_box
.to_pairs::<i32, i32>("")?
.into_iter()
.collect::<HashMap<_, _>>(),
comparison
);
Ok(())
}
}
mod failures {
use super::*;
#[test]
fn bad_password() -> Result<(), CryptoBoxError> {
let tempdir = tempdir().context(FailedCreatingDirectory {
directory: "tempdir".to_string(),
})?;
let path = tempdir.path().join("box");
let crypto_box = CryptoBox::init(&path, None, None, "testing")?;
std::mem::drop(crypto_box);
let crypto_box = CryptoBox::open(&path, "testing 2");
assert!(matches!(
crypto_box,
Err(CryptoBoxError::RootKeyDecryption { .. })
));
Ok(())
}
}
}