...
 
Commits (232)
......@@ -3,9 +3,9 @@ stages:
- build
- publish
# Build the Rust root on nightly
build:beta:
image: instrumentisto/rust:1.39.0-beta
# Build the Rust root on a pinned stable
build:
image: rust:1.42.0-buster
script:
- cargo test
- cargo build
......@@ -13,7 +13,7 @@ build:beta:
# Run test suite
test_webgui:
stage: test
image: circleci/node:10-browsers
image: circleci/node:12-browsers
cache:
key: yarn
paths:
......@@ -27,7 +27,7 @@ test_webgui:
build_webgui:
stage: build
image: node:10
image: node:12
cache:
key: yarn
paths:
......
This source diff could not be displayed because it is too large. You can view the blob instead.
[workspace]
members = [
# Main storage database
"alexandria",
# qaul.net service library
"libqaul",
......@@ -18,6 +20,7 @@ members = [
# decentralised routing protocol
"ratman",
"ratman/harness",
"ratman/identity",
"ratman/netmod",
......@@ -35,10 +38,13 @@ members = [
# Client specific targets
"clients/linux",
"clients/linux-cli",
"clients/linux-daemon",
# Android specifics
"librobot",
# test binaries
"clients/linux-http-test"
"clients/linux-http-test",
"clients/linux-voice-test",
# "clients/http-test"
]
/target
**/*.rs.bk
Cargo.lock
\ No newline at end of file
stages:
- build
- test
build:nightly:
image: rustlang/rust:nightly
script:
- cargo build
test:stable:
image: rustlang/rust:nightly
script:
- cargo test --all
\ No newline at end of file
[package]
name = "alexandria"
description = "An encrypted document-oriented database with tag based query support"
version = "0.2.0"
authors = ["Katharina Fey <kookie@spacekookie.de>"]
repository = "https://git.open-communication.net/qaul/alexandria"
documentation = "https://docs.rs/alexandria"
license = "GPL-3.0-or-later"
edition = "2018"
[dependencies]
id = { version = "0.4", path = "../ratman/identity", features = ["digest", "random", "aligned"], package = "ratman-identity" }
async-std = { version = "1.0", features = ["unstable", "attributes"] }
bincode = "1.0"
failure = "0.1"
hex = "0.4"
keybob = "0.3"
serde = { version = "1.0", features = ["derive", "rc"] }
sodiumoxide = "0.2.5"
tracing = "0.1"
tracing-futures = "0.2"
[dev-dependencies]
bincode = "1.0"
ed25519-dalek = "1.0.0-pre.3"
rand = "0.7"
serde_json = "1.0"
tempfile = "3.0"
This diff is collapsed.
# alexandria 📚 [![][irc-badge]][irc-url]
[irc-badge]: https://img.shields.io/badge/IRC-%23qaul.net-1e72ff.svg
[irc-url]: https://www.irccloud.com/invite?channel=%23qaul.net&hostname=irc.freenode.org&port=6697&ssl=1
Strongly typed, embedded record database with seemless encryption at
rest storage. Supports key-value Diff transactions, as well as
externally loaded binary payloads. Supports encrypted metadata
without extra configuration.
Alexandria has the following features:
- Store data on internal db path
- Query the database by path or dynamic search tags
- Subscribe to events based on query
- Iterate over query dynamically
- Store data in session or global namespaces
**Notice:** alexandria should be considered experimental and not used
in production systems where data loss is unacceptable.
## How to use
Alexandria requires `rustc` 1.42 to compile.
```rust
use alexandria::{Library, Builder};
use tempfile::tempdir();
let dir = tempdir().unwrap();
let lib = Builder::new()
.offset(dir.path())
.root_sec("car horse battery staple")
.build()?
```
Alexandria is developed as part of [qaul.net][website]. We have a
[mailing list][list] and an [IRC channel][irc]! Please come by and ask
us questions! (the issue tracker is a bad place to ask questions)
[website]: https://qaul.net
[list]: https://lists.sr.ht/~qaul/community/
[irc]: https://irccloud.com/freenode/#qaul.net
## License
Alexandria is free software and part of [qaul.net][qaul.net]. You
are free to use, modify and redistribute the source code under the
terms of the GNU General Public License 3.0 or (at your choice) any
later version. For a full copy of the license, see `LICENSE` in the
source directory attached.
**Additional Permissions:** For Submission to the Apple App Store:
Provided that you are otherwise in compliance with the GPLv3 for each
covered work you convey (including without limitation making the
Corresponding Source available in compliance with Section 6 of the
GPLv3), the qaul.net developers also grant you the additional
permission to convey through the Apple App Store non-source executable
versions of the Program as incorporated into each applicable covered
work as Executable Versions only under the Mozilla Public License
version 2.0.
A copy of both the GPL-3.0 and MPL-2.0 license texts are included in
this repository.
//! Alexandria internal caching system
//!
//! The caches are divided into a hot cache, which is in active
//! rotation and <user-id>-<zone>-<record> indexed, and a cold cache
//! which can be used to pre-validate a set of changes, which is
//! <delta-id> indexed. Because each transaction is assigned a new
//! delta id,
use crate::{
crypto::{asym::KeyPair, DetachedKey, EncryptedMap},
notify::{Lock, LockNotify, Notify},
Id,
};
use async_std::{sync::Arc, task};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
cmp::{Ord, Ordering as Order /* slurred yelling */, PartialOrd},
hash::Hash,
path::PathBuf,
};
/// A key that expresses an (id, zone) tuple
#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)]
pub(crate) struct CombKey {
pub(crate) id: Id,
pub(crate) zone: String,
}
impl Ord for CombKey {
fn cmp(&self, other: &Self) -> Order {
self.id.cmp(&other.id).then(self.zone.cmp(&other.zone))
}
}
impl PartialOrd for CombKey {
fn partial_cmp(&self, other: &Self) -> Option<Order> {
self.id.partial_cmp(&other.id).and_then(|id| {
self.zone
.partial_cmp(&other.zone)
.and_then(|zone| Some(id.then(zone)))
})
}
}
impl ToString for CombKey {
fn to_string(&self) -> String {
format!("{}/{}", self.id, self.zone)
}
}
/// An Arc reference to a cache
pub(crate) type CacheRef<K, V> = Arc<Cache<K, V>>;
pub(crate) struct Cache<K, V>
where
K: Serialize + DeserializeOwned + Ord + PartialOrd + Hash + ToString,
V: DetachedKey<KeyPair> + Serialize + DeserializeOwned,
{
/// Cache from K -> V with an asymmetric encryption key
cache: LockNotify<EncryptedMap<K, Notify<V>, KeyPair>>,
/// The path the cache is written to
path: Option<PathBuf>,
}
impl<K, V> Cache<K, V>
where
K: Serialize + DeserializeOwned + Ord + PartialOrd + Hash + ToString,
V: DetachedKey<KeyPair> + Serialize + DeserializeOwned,
{
/// Create a new in-memory cache
pub(crate) fn new<P>(path: P) -> CacheRef<K, V>
where
P: Into<Option<PathBuf>>,
{
Arc::new(Self {
cache: Notify::new(Lock::new(EncryptedMap::new())),
path: path.into(),
})
}
/// Set the cache to hot, enabling write-through caching
///
/// Reversing this option is not possible. A hot cache does
/// write-through caching to disk, meaning that changes are
/// mirrored to disk immediately to avoid data loss when crashing.
/// By default the cache can be used to improve in-memory lookups,
/// but will not be persistent across reboots.
pub(crate) fn hot(self: Arc<Self>) {
let path = self.path.as_ref().expect("Can't set cache without path to 'hot'");
task::spawn(async move {});
}
}
This diff is collapsed.
use crate::{
dir::Dirs,
error::Result,
meta::{tags::TagCache, users::UserTable},
query::SubHub,
store::Store,
Library,
};
use async_std::sync::{Arc, RwLock};
use std::path::Path;
/// A utility to configure and initialise an alexandria database
///
/// To load an existing database from disk, look at
/// [`Library::load()`][load]!
///
/// [load]: struct.Library.html#load
///
/// ```
/// # use alexandria::{Builder, Library, error::Result};
/// # use tempfile::tempdir;
/// # fn test() -> Result<()> {
/// let dir = tempdir().unwrap();
/// let lib = Builder::new()
/// .offset(dir.path())
/// .root_sec("car horse battery staple")
/// .build()?;
/// # drop(lib);
/// # Ok(()) }
/// ```
#[derive(Default)]
pub struct Builder {
/// The main offset path
offset: Option<String>,
}
impl Builder {
pub fn new() -> Self {
Self::default()
}
/// Specify a normal path offset
///
/// This will act as the root metadata store. On multi-user
/// devices it needs to be a directory that's accessibly from the
/// daemon that owns the alexandria scope.
pub fn offset<'tmp, P: Into<&'tmp Path>>(self, offset: P) -> Self {
let p: &Path = offset.into();
let offset = p.to_str().map(|s| s.to_string());
Self { offset, ..self }
}
/// Some secret that will be used for the root namespace
///
/// When loading a library from disk in a future session, this
/// secret will have to be provided to [`Library::load()`][load]
///
/// [load]: struct.Library.html#load
pub fn root_sec<S: Into<String>>(self, _: S) -> Self {
self
}
/// Consume the builder and create a Library
pub fn build(self) -> Result<Arc<Library>> {
let root = Dirs::new(
self.offset
.expect("Builder without `offset` cannot be built"),
);
let users = RwLock::new(UserTable::new());
let tag_cache = RwLock::new(TagCache::new());
let store = RwLock::new(Store::new());
let subs = SubHub::new();
Library {
root,
users,
tag_cache,
store,
subs,
}
.init()
.map(|l| Arc::new(l))
}
}
//! Fundamental API types
mod sessions;
pub use sessions::{Session, SessionsApi, GLOBAL};
mod builder;
pub use builder::Builder;
mod api;
pub use api::Library;
//! User management API scope
use crate::{core::Library, error::Result, utils::Id};
use serde::{Deserialize, Serialize};
/// Represents a database session
///
/// A session is either bound to the global scope (meaning the
/// lifetime of the database in memory), or a specific id, which you
/// can yield via `id()`. To learn more about sessions, have a look
/// at the [`SessionsApi`][api]!
///
/// [api]: struct.SessionsApi.html
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum Session {
/// The global session, accessed on `load()`
Global,
/// A user-specific session with separate key tree
Id(Id),
}
impl Session {
/// Get the inner Id, if applicable
pub(crate) fn id(&self) -> Option<Id> {
match self {
Self::Id(id) => Some(*id),
Self::Global => None,
}
}
}
impl From<Id> for Session {
fn from(id: Id) -> Self {
Self::Id(id)
}
}
/// Convenience type to represent the global namespace
pub const GLOBAL: Session = Session::Global;
/// Api scope wrapper for database sessions
///
/// A session is some random Id which is used as a namespace
/// identifier. Each session namespace is encrypted independently,
/// with a unique key, and record tree. This means that two paths
/// (say `/foo:bar`) can point to two separate records in the
/// database, if they belong to different sessions.
///
/// An important distiction to make here: a session is not ephemeral
/// but a long living namespace Id, similar to a user in a traditional
/// database.
pub struct SessionsApi<'alex> {
pub(crate) inner: &'alex Library,
}
impl<'alex> SessionsApi<'alex> {
/// List available sessions in this database
pub async fn list(&self) -> Vec<Id> {
vec![]
}
/// Open a previously created session
pub async fn open(&self, id: Id, pw: &str) -> Result<Session> {
let ref mut u = self.inner.users.write().await;
u.open(id, pw).map(|_| Session::Id(id))
}
/// Close an active session
pub async fn close(&self, id: Session) -> Result<()> {
if let Some(id) = id.id() {
let ref mut u = self.inner.users.write().await;
u.close(id)
} else {
Ok(())
}
}
/// Create a new session with a unique encryption key
pub async fn create(&self, id: Id, pw: &str) -> Result<Session> {
let ref mut u = self.inner.users.write().await;
u.insert(id, pw).map(|_| Session::Id(id))
}
/// Remove a session Id and corresponding data from the database
pub async fn destroy(&self, id: Session) -> Result<()> {
if let Some(id) = id.id() {
let ref mut u = self.inner.users.write().await;
u.delete(id)
} else {
Ok(())
}
}
}
//! Symmetric cipher utilities
//!
//! These functions are only used to bootstrap the unlocking process
//! for the database user table. For all other cryptographic
//! operations, see the `asym` module instead.
use crate::{
crypto::{CipherText, Encrypter},
error::{Error, Result},
wire::Encoder,
};
use keybob::{Key as KeyBuilder, KeyType};
use serde::{de::DeserializeOwned, Serialize};
use sodiumoxide::crypto::secretbox::{gen_nonce, open, seal, Nonce};
// Make it easier for alexandria internals to use this type
pub(crate) use sodiumoxide::crypto::secretbox::Key;
pub(crate) trait Constructor {
/// Create an AES symmetric key from a user password and salt
fn from_pw(pw: &str, salt: &str) -> Self;
}
impl Constructor for Key {
fn from_pw(pw: &str, salt: &str) -> Self {
let kb = KeyBuilder::from_pw(KeyType::Aes128, pw, salt);
Self::from_slice(kb.as_slice()).unwrap()
}
}
impl<T> Encrypter<T> for Key
where
T: Encoder<T> + Serialize + DeserializeOwned,
{
fn seal(&self, data: &T) -> Result<CipherText> {
let nonce = gen_nonce();
let encoded = data.encode()?;
let data = seal(&encoded, &nonce, self);
Ok(CipherText {
nonce: nonce.0.iter().cloned().collect(),
data,
})
}
fn open(&self, data: &CipherText) -> Result<T> {
let CipherText {
ref nonce,
ref data,
} = data;
let nonce = Nonce::from_slice(nonce.as_slice()).ok_or(Error::InternalError {
msg: "Failed to read nonce!".into(),
})?;
let clear = open(data.as_slice(), &nonce, self).map_err(|_| Error::InternalError {
msg: "Failed to decrypt data".into(),
})?;
Ok(T::decode(&clear)?)
}
}
#[test]
fn key_is_key() {
let k1 = KeyBuilder::from_pw(KeyType::Aes128, "password", "salt");
let k2 = KeyBuilder::from_pw(KeyType::Aes128, "password", "salt");
assert_eq!(k1, k2);
}
//! Asymmetric cryto utilities
use crate::{
crypto::{CipherText, Encrypter},
error::{Error, Result},
wire::Encoder,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sodiumoxide::crypto::box_::{self, Nonce, PublicKey, SecretKey};
pub(crate) type SharedKey = KeyPair;
/// Both public and private keys for a user
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct KeyPair {
pub_: PublicKey,
sec: SecretKey,
}
impl KeyPair {
/// Create a new tree of keys
pub(crate) fn new() -> Self {
let (pub_, sec) = box_::gen_keypair();
Self { pub_, sec }
}
}
impl<T> Encrypter<T> for KeyPair
where
T: Encoder<T> + Serialize + DeserializeOwned,
{
fn seal(&self, data: &T) -> Result<CipherText> {
let non = box_::gen_nonce();
let enc = data.encode()?;
let data = box_::seal(&enc, &non, &self.pub_, &self.sec);
let nonce = non.0.iter().cloned().collect();
Ok(CipherText { nonce, data })
}
fn open(&self, data: &CipherText) -> Result<T> {
let CipherText {
ref nonce,
ref data,
} = data;
let nonce = Nonce::from_slice(nonce.as_slice()).ok_or(Error::InternalError {
msg: "Failed to read nonce!".into(),
})?;
let clear = box_::open(data.as_slice(), &nonce, &self.pub_, &self.sec).map_err(|_| {
Error::InternalError {
msg: "Failed to decrypt data".into(),
}
})?;
Ok(T::decode(&clear)?)
}
}
#[test]
fn sign_and_encrypt() {
use ed25519_dalek::Keypair as DKP;
use rand::rngs::OsRng;
let mut rng = OsRng {};
let DKP { secret, public } = DKP::generate(&mut rng);
let nacl_pair = KeyPair {
sec: SecretKey::from_slice(secret.as_bytes()).unwrap(),
pub_: PublicKey::from_slice(public.as_bytes()).unwrap(),
};
let dalek_pair = DKP { secret, public };
let message = "this can be signed and encrypted!";
// Try to sign data
let sign = dalek_pair.sign(message.as_bytes());
// Encrypt the message
let ctext = nacl_pair
.seal(&message.as_bytes().iter().cloned().collect::<Vec<u8>>())
.unwrap();
// Verify signature
dalek_pair.verify(message.as_bytes(), &sign).unwrap();
// Decrypt secret
nacl_pair.open(&ctext).unwrap()
}
//! An encrypted map datastructure
use crate::{
crypto::{DetachedKey, Encrypted, Encrypter},
error::{Error, Result},
};
use async_std::sync::Arc;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
collections::BTreeMap,
fmt::Debug,
hash::Hash,
ops::{Deref, DerefMut},
};
/// A mapper around encrypted data in a map
#[derive(Serialize, Deserialize)]
pub(crate) struct EncryptedMap<K, V, Q>
where
K: Serialize + DeserializeOwned + Ord + PartialOrd + Hash + ToString,
V: Clone + DetachedKey<Q> + Serialize + DeserializeOwned,
Q: Encrypter<V>,
{
#[serde(bound(deserialize = "V: DeserializeOwned"))]
inner: BTreeMap<K, Encrypted<V, Q>>,
}
impl<K, V, Q> EncryptedMap<K, V, Q>
where
K: Serialize + DeserializeOwned + Ord + PartialOrd + Hash + ToString,
V: Clone + DetachedKey<Q> + Serialize + DeserializeOwned,
Q: Encrypter<V>,
{
/// Create a new encrypted map
pub(crate) fn new() -> Self {
Self {
inner: Default::default(),
}
}
/// Open an entry in the map with a key
pub(crate) fn open(&mut self, id: K, key: &Q) -> Result<()> {
match self.inner.get_mut(&id) {
Some(entry) => Ok(entry.open(key)?),
None => Err(Error::UnlockFailed { id: id.to_string() }),
}
}
/// Close an entry in the map
pub(crate) fn close<P>(&mut self, id: K, key: P) -> Result<()>
where
P: Into<Option<Arc<Q>>>,
{
let key = key.into();
match self.inner.get_mut(&id) {
Some(entry) => {
let key = entry
.key()
.or(key)
.expect("No key provided for `open` operation");
Ok(entry.close(key)?)
}
None => Err(Error::UnlockFailed { id: id.to_string() }),
}
}
/// Get a reference to the mapped value, if opened
pub(crate) fn get(&self, id: K) -> Result<&V> {
match self.inner.get(&id) {
Some(Encrypted::Open(ref data)) => Ok(data),
Some(Encrypted::Closed(_)) => Err(Error::InternalError {
msg: "Tried reading ::Closed variant".into(),
}),
Some(Encrypted::Never(_)) => unreachable!(),
None => Err(Error::InternalError {
msg: "No data for key".into(),
}),
}
}
/// Get a mutable reference to the mapped value, if opened
pub(crate) fn get_mut(&mut self, id: K) -> Result<&mut V> {
match self.inner.get_mut(&id) {
Some(Encrypted::Open(ref mut data)) => Ok(data),
Some(Encrypted::Closed(_)) => Err(Error::InternalError {
msg: "Tried reading ::Closed variant".into(),
}),
Some(Encrypted::Never(_)) => unreachable!(),
None => Err(Error::InternalError {
msg: "No data for key".into(),
}),
}
}
}
impl<K, V, Q> Deref for EncryptedMap<K, V, Q>
where
K: Serialize + DeserializeOwned + Ord + PartialOrd + Hash + ToString,
V: Clone + DetachedKey<Q> + Serialize + DeserializeOwned,
Q: Encrypter<V> + Debug,
{
type Target = BTreeMap<K, Encrypted<V, Q>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K, V, Q> DerefMut for EncryptedMap<K, V, Q>
where
K: Serialize + DeserializeOwned + Ord + PartialOrd + Hash + ToString,
V: Clone + DetachedKey<Q> + Serialize + DeserializeOwned,
Q: Encrypter<V> + Debug,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
//! Provides more convenient crypto wrappers
#![allow(unused)]
pub(crate) mod aes;
pub(crate) mod asym;
mod map;
pub(crate) use map::EncryptedMap;
use crate::{
error::{Error, Result},
utils::Id,
wire::Encoder,
};
use async_std::sync::Arc;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fmt::Debug, marker::PhantomData};
/// An encrypted piece of data
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub(crate) struct CipherText {
/// Number only used once
nonce: Vec<u8>,
/// Data buffer
data: Vec<u8>,
}
/// A trait that encrypts data on an associated key
pub(crate) trait Encrypter<T>
where
T: Encoder<T>,
{
fn seal(&self, data: &T) -> Result<CipherText>;
fn open(&self, data: &CipherText) -> Result<T>;
}
/// A type that can provide an out-of-band key
///
/// Sometimes a type that is stored inside the `Encrypted` can bring
/// it's own key, to avoid having to have a second control-structure
/// for the keys.
pub(crate) trait DetachedKey<K> {
fn key(&self) -> Option<Arc<K>> {
None
}
}
// Ids are special and should just impl this
impl<K> DetachedKey<K> for Id {}
/// A generic wrapper around the unlock state of data
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) enum Encrypted<T, K>
where
T: Clone + Encoder<T> + DetachedKey<K>,
K: Encrypter<T>,
{
/// An in-use data variant
#[serde(skip_serializing)]
#[serde(bound(deserialize = "T: DeserializeOwned"))]
Open(T),
/// An encrypted value
Closed(CipherText),
/// Purely here to make rustc happy about the generic bounds
#[doc(hidden)]
#[serde(skip)]
Never(Option<PhantomData<K>>),
}
impl<T, K> Encrypted<T, K>
where
T: Clone + Encoder<T> + DetachedKey<K>,
K: Encrypter<T>,
{
pub(crate) fn new(init: T) -> Self {
Self::Open(init)
}
/// Check if the value is encrypted
pub(crate) fn encrypted(&self) -> bool {
match self {
Self::Closed(_) => true,
_ => false,
}
}
/// Attempt to deref the entry
pub(crate) fn deref<'s>(&'s self) -> Result<&'s T> {
match self {
Self::Open(ref t) => Ok(t),
_ => Err(Error::LockedState {
msg: "Encrypted::Closed(_) can't be derefed".into(),
}),
}
}
/// Swap the underlying data in place
pub(crate) fn swap(&mut self, new: &mut T) {
match self {
Self::Open(ref mut t) => std::mem::swap(t, new),
_ => {}
}
}
/// Attempt to deref the entry
pub(crate) fn deref_mut<'s>(&'s mut self) -> Result<&'s mut T> {
match self {
Self::Open(ref mut t) => Ok(t),
_ => Err(Error::LockedState {
msg: "Encrypted::Closed(_) can't be derefed".into(),
}),
}
}
/// Call to the inner unlocked `key()` if the entry is open
pub(crate) fn key(&self) -> Option<Arc<K>> {
match self {
Self::Open(t) => t.key(),
_ => None,
}
}
/// Perform the open operation in place with a key
pub(crate) fn open(&mut self, key: &K) -> Result<()> {
match self {
Self::Open(_) => Err(Error::InternalError {
msg: "tried to open ::Open(_) variant".into(),
}),
Self::Closed(enc) => {
*self = Self::Open(key.open(enc)?);
Ok(())
}
_ => unreachable!(),
}
}
/// Perform the close operation in place with a key
pub(crate) fn close(&mut self, key: Arc<K>) -> Result<()> {
match self {
Self::Closed(_) => Err(Error::InternalError {
msg: "tried to close ::Closed(_) variant".into(),
}),
Self::Open(data) => {
let key = data.key().unwrap_or(key);
*self = Self::Closed(key.seal(data)?);
Ok(())
}
_ => unreachable!(),
}
}
/// Performs the close operation, purely with an detached key
///
/// This function can panic and shouldn't be used unless _really_
/// neccessary.
pub(crate) fn close_detached(&mut self) -> Result<()> {
match self {
Self::Closed(_) => Err(Error::InternalError {
msg: "tried to close ::Closed(_) variant".into(),
}),
Self::Open(data) => {
let key = data.key().unwrap();
*self = Self::Closed(key.seal(data)?);
Ok(())
}
_ => unreachable!(),
}
}
/// Consume the `Encrypted<T>` type into the inner value
///
/// Pancis if the value is encrypted
#[cfg(test)]
pub(crate) fn consume(self) -> T {
match self {
Self::Open(data) => data,
_ => panic!("Couldn't consume encrypted value!"),
}
}
}
#[test]
fn aes_encrypt_decrypt() {
use aes::{Constructor, Key};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct Data {
num: i32,
};
impl DetachedKey<Key> for Data {
fn key(&self) -> Option<Arc<Key>> {
None
}
}
let key = Arc::new(Key::from_pw("fuck", "cops"));
let data = Data { num: 1312 };
// Encrypted data wrapper
let mut enc = Encrypted::new(data.clone());
// Close the entry
enc.close(Arc::clone(&key)).unwrap();
assert!(enc.encrypted());
// Re-open the entry
enc.open(&*key).unwrap();
assert_eq!(enc.encrypted(), false);
let data2 = enc.consume();
assert_eq!(data, data2);
}
#[test]
fn asym_encrypt_decrypt() {
use asym::KeyPair;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
struct Data {
num: i32,
};
impl DetachedKey<KeyPair> for Data {
fn key(&self) -> Option<Arc<KeyPair>> {
None
}
}
let key = Arc::new(KeyPair::new());
let data = Data { num: 1312 };
// Encrypted data wrapper
let mut enc = Encrypted::new(data.clone());
// Close the entry
enc.close(Arc::clone(&key)).unwrap();
assert!(enc.encrypted());
// Re-open the entry
enc.open(&*key).unwrap();
assert_eq!(enc.encrypted(), false);
let data2 = enc.consume();
assert_eq!(data, data2);
}
use crate::{
utils::{Id, Path, TagSet},
Session,
};
pub(crate) struct DeltaBuilder {
user: Session,
path: Option<Path>,
rec_id: Option<Id>,
tags: Option<TagSet>,
action: DeltaType,
}
impl DeltaBuilder {
pub(crate) fn new(user: Session, action: DeltaType) -> Self {
Self {
action,
user,
path: None,
rec_id: None,
tags: Some(TagSet::empty()),
}
}
pub(crate) fn path(&mut self, path: &Path) {
self.path = Some(path.clone());
}
pub(crate) fn rec_id(&mut self, rec_id: Id) {
self.rec_id = Some(rec_id);
}
pub(crate) fn tags(&mut self, tags: &TagSet) {
self.tags = Some(tags.clone());
}
pub(crate) fn make(self) -> Delta {
Delta {
user: self.user,
rec_id: self.rec_id,
action: self.action,
tags: self.tags.unwrap_or_else(|| TagSet::empty()),
path: self.path.unwrap(),
}
}
}
/// A transaction to the active dataset of a library
///
/// A delta is atomic, touches one field of one record, and can reside in the hot
/// cache before being fully commited. It is authenticated against an
/// active user before being cached.
///
/// The `path` segment is constructed via the
#[derive(Clone, Debug)]
pub(crate) struct Delta {
pub(crate) user: Session,
pub(crate) rec_id: Option<Id>,
pub(crate) path: Path,
pub(crate) tags: TagSet,
pub(crate) action: DeltaType,
}
#[derive(Clone, Debug)]
pub(crate) enum DeltaType {
Insert,
Update,
Delete,
}
//! Directory helper to create and manage Alexandria instances
use crate::error::Result;
use std::{fs, path::PathBuf};
/// Metadata for where things are stored
pub(crate) struct Dirs {
/// The root path, contains metadata
root: PathBuf,
}
impl Dirs {
pub(crate) fn new<P: Into<PathBuf>>(root: P) -> Self {
Self { root: root.into() }
}
pub(crate) fn scaffold(&self) -> Result<()> {
fs::create_dir_all(&self.root)?;
fs::create_dir(self.records())?;
fs::create_dir(self.meta())?;
fs::create_dir(self.cache())?;
Ok(())
}
/// Return the records directory in the library
pub(crate) fn records(&self) -> PathBuf {
self.root.join("records")
}
/// Return the meta directory in the library
pub(crate) fn meta(&self) -> PathBuf {
self.root.join("meta")
}
/// Return the cache directory in the library
pub(crate) fn cache(&self) -> PathBuf {
self.root.join("cache")
}
}
#[test]
fn scaffold_lib() -> Result<()> {
use std::path::Path;
use tempfile::tempdir;
let root = tempdir().unwrap();
let mut offset = root.path().to_path_buf();
offset.push("library");
let d = Dirs::new(offset.clone());
d.scaffold()?;
assert!(Path::new(dbg!(&offset.join("records"))).exists());
assert!(Path::new(dbg!(&offset.join("meta"))).exists());
assert!(Path::new(dbg!(&offset.join("cache"))).exists());
Ok(())
}
//! Alexandria specific error handling
//!
//! Generally, not all errors can be expressed as one, and many of the
//! errors that happen internally are filtered and repacked into a set
//! of common errors that users of the library will have to deal with.
//! They are most commonly related to user mistakes, scheduling
//! problems, etc.
//!
//! However there are some errors that the database itself can't
//! handle, and so it has to bubble up via the `IternalError` variant
//! on `Error`. These can be bugs in Alexandria itself, or some
//! runtime constraint like having run out of memory or disk space.
use failure::Fail;
use std::fmt::{self, Display, Formatter};
/// Common alexandria error fascade
#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "failed to add a user that already exits")]
UserAlreadyExists,
#[fail(display = "operation failed because user `{}` doesn't exist", id)]
NoSuchUser { id: String },
#[fail(display = "failed to initialise library at offset `{}`", offset)]
InitFailed { offset: String },
#[fail(display = "failed to perform action because user `{}` is locked", id)]
UserNotOpen { id: String },
#[fail(display = "bad unlock token (password?) for id `{}`", id)]
UnlockFailed { id: String },
#[fail(display = "tried to operate on locked encrypted state: {}", msg)]
LockedState { msg: String },
#[fail(display = "tried to unlock user Id `{}` twice", id)]
AlreadyUnlocked { id: String },
#[fail(display = "no such path: `{}`", path)]
NoSuchPath { path: String },
#[fail(display = "path exists already: {}", path)]
PathExists { path: String },
#[fail(display = "failed to load data: `{}`", msg)]
LoadFailed { msg: String },
#[fail(display = "failed to sync data: `{}`", msg)]
SyncFailed { msg: String },
#[fail(display = "tried to apply Diff of incompatible type")]
BadDiffType,
#[fail(display = "a Diff failed to apply: \n{}", msgs)]
BadDiff { msgs: DiffErrors },
#[fail(
display = "can't merge two iterators with different underlying queries: a: '{}', b: '{}'",
q1, q2
)]
IncompatibleQuery { q1: String, q2: String },
#[doc(hidden)]
#[fail(display = "An alexandria internal error occured: `{}`", msg)]
InternalError { msg: String },
}
/// A convenience alias to contain a common alexandria error
pub type Result<T> = std::result::Result<T, Error>;
/// Span info errors that can occur while applying a diff to a record
#[derive(Debug)]
pub struct DiffErrors(Vec<(usize, String)>);
impl DiffErrors {
pub(crate) fn add(mut self, new: Self) -> Self {
let mut ctr = self.0.len();
new.0.into_iter().for_each(|(_, e)| {
self.0.push((ctr, e));
ctr += 1;
});
self
}
/// Helper function to apply text replacements to nested messages
pub(crate) fn replace_text<'n, 'o>(self, old: &'o str, new: &'n str) -> Self {
Self(
self.0
.into_iter()
.map(|(i, s)| (i, s.as_str().replace(old, new).into()))
.collect(),
)
}
}
impl Display for DiffErrors {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.0.iter().fold(Ok(()), |res, (num, msg)| {
res.and_then(|_| write!(f, r#"{}: "{}""#, num, msg))
})
}
}
impl From<Vec<(usize, String)>> for DiffErrors {
fn from(vec: Vec<(usize, String)>) -> Self {
Self(vec)
}
}
impl From<(usize, String)> for DiffErrors {
fn from(tup: (usize, String)) -> Self {
Self(vec![tup])
}
}
impl From<DiffErrors> for Error {
fn from(msgs: DiffErrors) -> Self {
Error::BadDiff { msgs }
}
}
impl From<bincode::Error> for Error {
fn from(be: bincode::Error) -> Self {
use bincode::ErrorKind::*;
// FIXME: this isn't great but like... whatevs
let msg = match *be {
Io(e) => format!("I/O error: '{}'", e),
SizeLimit => "Payload too large!".into(),
SequenceMustHaveLength => "Internal sequencing error".into(),