use crate::{
atomic_file::{mask, AtomicFile, TryDefault},
crypto::{
AsymKeyPair, AsymKeyPub, ConcreteCreds, CredStore, CredStoreMut, DerivationParams, Encrypt,
Envelope, Error, Scheme, TaggedCiphertext,
},
error::DisplayErr,
Result,
};
use btserde::field_helpers;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{path::PathBuf, sync::Arc};
use zeroize::{Zeroize, Zeroizing};
pub use private::FileCredStore;
mod private {
use super::*;
fn serialize_optional_inner<S: Serializer, T: Serialize>(
value: &Option<Arc<T>>,
ser: S,
) -> std::result::Result<S::Ok, S::Error> {
value.as_ref().map(|inner| inner.as_ref()).serialize(ser)
}
fn deserialize_optional_inner<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
de: D,
) -> std::result::Result<Option<Arc<T>>, D::Error> {
let opt: Option<T> = Deserialize::deserialize(de)?;
Ok(opt.map(Arc::new))
}
fn serialize_optional_zeroizing<S: Serializer, T: Serialize + Zeroize>(
value: &Option<Zeroizing<T>>,
ser: S,
) -> std::result::Result<S::Ok, S::Error> {
value
.as_ref()
.map(|inner| {
let reference: &T = inner;
reference
})
.serialize(ser)
}
fn deserialize_optional_zeroizing<'de, D: Deserializer<'de>, T: Deserialize<'de> + Zeroize>(
de: D,
) -> std::result::Result<Option<Zeroizing<T>>, D::Error> {
let opt: Option<T> = Deserialize::deserialize(de)?;
Ok(opt.map(Zeroizing::new))
}
#[derive(Serialize, Deserialize)]
struct State {
#[serde(with = "field_helpers::smart_ptr")]
node_creds: Arc<ConcreteCreds>,
#[serde(serialize_with = "serialize_optional_inner")]
#[serde(deserialize_with = "deserialize_optional_inner")]
root_creds: Option<Arc<ConcreteCreds>>,
storage_key: AsymKeyPair<Encrypt>,
derivation_params: DerivationParams,
#[serde(serialize_with = "serialize_optional_zeroizing")]
#[serde(deserialize_with = "deserialize_optional_zeroizing")]
root_password_hash: Option<Zeroizing<[u8; DerivationParams::EXPORT_KEY_KIND.key_len()]>>,
}
impl State {
fn new() -> Result<State> {
let node_creds = ConcreteCreds::generate()?;
let storage_key = Encrypt::RSA_OAEP_3072_SHA_256.generate()?;
Ok(State {
node_creds: Arc::new(node_creds),
root_creds: None,
storage_key,
derivation_params: DerivationParams::new()?,
root_password_hash: None,
})
}
fn root_password_valid(&self, password: &str) -> Result<()> {
let expected = if let Some(ref expected) = self.root_password_hash {
expected
} else {
return Err(Error::NoRootCreds.into());
};
let actual = self.derivation_params.hmac(password)?;
if expected != &actual {
return Err(Error::WrongRootPassword.into());
}
Ok(())
}
fn set_root_creds(&mut self, root_creds: Arc<ConcreteCreds>, password: &str) -> Result<()> {
self.root_creds = Some(root_creds);
self.root_password_hash = Some(self.derivation_params.hmac(password)?);
Ok(())
}
}
impl TryDefault for State {
fn try_default() -> Result<Self> {
State::new()
}
}
pub struct FileCredStore {
state: AtomicFile<State>,
}
impl FileCredStore {
pub fn new(path: PathBuf) -> Result<Self> {
Ok(FileCredStore {
state: AtomicFile::new(path, mask::OWNER_ONLY)?,
})
}
}
impl CredStore for FileCredStore {
type CredHandle = Arc<ConcreteCreds>;
type ExportedCreds = TaggedCiphertext<Envelope<ConcreteCreds>, DerivationParams>;
fn node_creds(&self) -> Result<Self::CredHandle> {
let state = self.state.read().display_err()?;
Ok(state.node_creds.clone())
}
fn root_creds(&self, password: &str) -> Result<Self::CredHandle> {
let state = self.state.read().display_err()?;
state.root_password_valid(password)?;
let creds = state.root_creds.as_ref().cloned();
if let Some(creds) = creds {
Ok(creds)
} else {
Err(Error::NoRootCreds.into())
}
}
fn storage_key(&self) -> Result<AsymKeyPub<Encrypt>> {
let guard = self.state.read().display_err()?;
Ok(guard.storage_key.public.clone())
}
fn export_root_creds(
&self,
root_creds: &Self::CredHandle,
password: &str,
new_parent: &AsymKeyPub<Encrypt>,
) -> Result<Self::ExportedCreds> {
let envelope = Envelope::new(root_creds.as_ref(), new_parent)?;
let params = DerivationParams::new()?;
let aead_key = params.derive_key(password)?;
let export = aead_key.encrypt(params, &envelope)?;
Ok(export)
}
}
impl CredStoreMut for FileCredStore {
fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
{
let state = self.state.read().display_err()?;
if let Some(ref root_creds) = state.root_creds {
state.root_password_valid(password)?;
return Ok(root_creds.clone());
}
}
let mut state = self.state.write().display_err()?;
let root_creds = Arc::new(ConcreteCreds::generate()?);
state.set_root_creds(root_creds.clone(), password)?;
state.save()?;
Ok(root_creds)
}
fn import_root_creds(
&self,
password: &str,
exported: Self::ExportedCreds,
) -> Result<Self::CredHandle> {
let aead_key = exported.aad.derive_key(password)?;
let envelope = aead_key.decrypt(&exported)?;
let mut state = self.state.write().display_err()?;
let root_creds = Arc::new(envelope.open(&state.storage_key)?);
state.set_root_creds(root_creds.clone(), password)?;
state.save()?;
Ok(root_creds)
}
fn assign_node_writecap(
&self,
handle: &mut Self::CredHandle,
writecap: crate::Writecap,
) -> Result<()> {
let node_creds = Arc::make_mut(handle);
node_creds.set_writecap(writecap)?;
let mut state = self.state.write().display_err()?;
state.node_creds = handle.clone();
state.save()?;
Ok(())
}
fn assign_root_writecap(
&self,
handle: &mut Self::CredHandle,
writecap: crate::Writecap,
) -> Result<()> {
let root_creds = Arc::make_mut(handle);
root_creds.set_writecap(writecap)?;
let mut state = self.state.write().display_err()?;
state.root_creds = Some(handle.clone());
state.save()?;
Ok(())
}
}
}
#[cfg(test)]
mod test {
use crate::{
crypto::{Creds, CredsPriv},
Epoch, Principaled,
};
use super::*;
use btserde::to_vec;
use std::{ops::Deref, path::Path, time::Duration};
use tempdir::TempDir;
struct TestCase {
store: FileCredStore,
dir: TempDir,
}
impl TestCase {
fn new() -> TestCase {
let dir = TempDir::new("FileCredStore").unwrap();
Self::from_dir(dir)
}
fn from_dir(dir: TempDir) -> TestCase {
let file_path = Self::file_path(dir.path());
let store = FileCredStore::new(file_path).unwrap();
TestCase { store, dir }
}
fn file_path(dir_path: &Path) -> PathBuf {
let mut file_path = dir_path.to_owned();
file_path.push("cred_store");
file_path
}
}
impl Deref for TestCase {
type Target = FileCredStore;
fn deref(&self) -> &Self::Target {
&self.store
}
}
#[test]
fn create_new() {
let _ = TestCase::new();
}
#[test]
fn node_creds() {
let case = TestCase::new();
let result = case.node_creds();
assert!(result.is_ok());
}
#[test]
fn root_creds_returns_same_as_gen_root_creds() {
const PASSWORD: &str = "MaximalIrony";
let case = TestCase::new();
let expected = case.gen_root_creds(PASSWORD).unwrap();
let actual = case.root_creds(PASSWORD).unwrap();
assert!(std::ptr::eq(expected.as_ref(), actual.as_ref()));
}
#[test]
fn root_creds_wrong_password_is_error() {
let case = TestCase::new();
case.gen_root_creds("right").unwrap();
let result = case.root_creds("wrong");
let passed = if let Some(err) = result.err() {
if let Some(Error::WrongRootPassword) = err.downcast_ref::<Error>() {
true
} else {
false
}
} else {
false
};
assert!(passed);
}
#[test]
fn storage_key() {
let case = TestCase::new();
let result = case.storage_key();
assert!(result.is_ok());
}
#[test]
fn export_import_root_creds() {
const SRC_PASSWORD: &str = "FALLING_MAN";
const DST_PASSWORD: &str = "RUNNING_MAN";
let src = TestCase::new();
let dst = TestCase::new();
let expected = src.gen_root_creds(SRC_PASSWORD).unwrap();
let previous = dst.gen_root_creds(DST_PASSWORD).unwrap();
let storage_key = dst.storage_key().unwrap();
let exported = src
.export_root_creds(&expected, SRC_PASSWORD, &storage_key)
.unwrap();
let actual = dst.import_root_creds(SRC_PASSWORD, exported).unwrap();
assert!(!std::ptr::eq(previous.as_ref(), actual.as_ref()));
assert!(to_vec(expected.as_ref()).unwrap() == to_vec(actual.as_ref()).unwrap());
}
#[test]
fn import_root_creds_wrong_password_is_error() {
const RIGHT_PW: &str = "right";
const WRONG_PW: &str = "wrong";
let src = TestCase::new();
let dst = TestCase::new();
let root_creds = src.gen_root_creds("right").unwrap();
let storage_key = dst.storage_key().unwrap();
let exported = src
.export_root_creds(&root_creds, RIGHT_PW, &storage_key)
.unwrap();
let result = dst.import_root_creds(WRONG_PW, exported);
assert!(result.is_err());
}
#[test]
fn assign_node_writecap() {
let case = TestCase::new();
let mut node_creds = case.node_creds().unwrap();
let root_creds = case.gen_root_creds("password").unwrap();
let expires = Epoch::now() + Duration::from_secs(3600);
let expected = root_creds
.issue_writecap(node_creds.principal(), &mut std::iter::empty(), expires)
.unwrap();
case.assign_node_writecap(&mut node_creds, expected.clone())
.unwrap();
let actual = node_creds.writecap().unwrap();
assert_eq!(&expected, actual);
}
fn persistence_test<R: PartialEq, F: Fn(&FileCredStore) -> R>(sample: F) {
let case = TestCase::new();
let expected = sample(&case);
let case = TestCase::from_dir(case.dir);
let actual = sample(&case);
assert!(expected == actual);
}
#[test]
fn node_creds_persisted() {
persistence_test(|store| to_vec(store.node_creds().unwrap().as_ref()).unwrap())
}
#[test]
fn root_creds_persisted() {
persistence_test(|store| {
to_vec(store.gen_root_creds("password").unwrap().as_ref()).unwrap()
})
}
#[test]
fn storage_key_persisted() {
persistence_test(|store| store.storage_key().unwrap());
}
#[test]
fn node_writecap_persisted() {
let case = TestCase::new();
let expected = {
let mut node_creds = case.node_creds().unwrap();
let root_creds = case.gen_root_creds("password").unwrap();
let expires = Epoch::now() + Duration::from_secs(3600);
let expected = root_creds
.issue_writecap(node_creds.principal(), &mut std::iter::empty(), expires)
.unwrap();
case.assign_node_writecap(&mut node_creds, expected.clone())
.unwrap();
assert_eq!(&expected, node_creds.writecap().unwrap());
expected
};
let case = TestCase::from_dir(case.dir);
let node_creds = case.node_creds().unwrap();
let actual = node_creds.writecap().unwrap();
assert_eq!(&expected, actual);
}
#[test]
fn root_writecap_persisted() {
const PASSWORD: &str = "Intransigent";
let case = TestCase::new();
let expected = {
let mut root_creds = case.gen_root_creds(PASSWORD).unwrap();
let expires = Epoch::now() + Duration::from_secs(3600);
let expected = root_creds
.issue_writecap(root_creds.principal(), &mut std::iter::empty(), expires)
.unwrap();
case.assign_root_writecap(&mut root_creds, expected.clone())
.unwrap();
assert_eq!(&expected, root_creds.writecap().unwrap());
expected
};
let case = TestCase::from_dir(case.dir);
let root_creds = case.root_creds(PASSWORD).unwrap();
let actual = root_creds.writecap().unwrap();
assert_eq!(&expected, actual);
}
}