|
@@ -0,0 +1,499 @@
|
|
|
+use crate::{
|
|
|
+ crypto::{
|
|
|
+ AsymKeyPair, AsymKeyPub, ConcreteCreds, CredStore, DerivationParams, Encrypt, Envelope,
|
|
|
+ Scheme, TaggedCiphertext,
|
|
|
+ },
|
|
|
+ error::DisplayErr,
|
|
|
+ Result,
|
|
|
+};
|
|
|
+use btserde::{read_from, write_to};
|
|
|
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
|
+use std::{
|
|
|
+ fmt::Display,
|
|
|
+ fs::{File, Metadata, OpenOptions},
|
|
|
+ io::{self, BufReader, BufWriter},
|
|
|
+ os::unix::fs::PermissionsExt,
|
|
|
+ path::{Path, PathBuf},
|
|
|
+ sync::{Arc, RwLock},
|
|
|
+};
|
|
|
+use zeroize::{Zeroize, Zeroizing};
|
|
|
+
|
|
|
+pub use private::{Error, FileCredStore};
|
|
|
+
|
|
|
+mod private {
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ fn serialize_inner<S: Serializer, T: Serialize>(
|
|
|
+ value: &Arc<T>,
|
|
|
+ ser: S,
|
|
|
+ ) -> std::result::Result<S::Ok, S::Error> {
|
|
|
+ value.serialize(ser)
|
|
|
+ }
|
|
|
+
|
|
|
+ fn deserialize_inner<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
|
|
|
+ de: D,
|
|
|
+ ) -> std::result::Result<Arc<T>, D::Error> {
|
|
|
+ let inner: T = Deserialize::deserialize(de)?;
|
|
|
+ Ok(Arc::new(inner))
|
|
|
+ }
|
|
|
+
|
|
|
+ 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))
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Errors which can occur when using a [FileCredStore].
|
|
|
+ #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
|
|
+ pub enum Error {
|
|
|
+ /// Indicates that no root credentials were present in a [CredStore].
|
|
|
+ NoRootCreds,
|
|
|
+ /// Indicates that the wrong root password was given when attempting to access the root
|
|
|
+ /// credentials.
|
|
|
+ WrongRootPassword,
|
|
|
+ }
|
|
|
+
|
|
|
+ impl Display for Error {
|
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
+ match self {
|
|
|
+ Error::NoRootCreds => write!(f, "root creds are not present"),
|
|
|
+ Error::WrongRootPassword => write!(f, "incorrect root password"),
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ impl std::error::Error for Error {}
|
|
|
+
|
|
|
+ #[derive(Serialize, Deserialize)]
|
|
|
+ struct State {
|
|
|
+ #[serde(serialize_with = "serialize_inner")]
|
|
|
+ #[serde(deserialize_with = "deserialize_inner")]
|
|
|
+ /// The credentials of this node.
|
|
|
+ node_creds: Arc<ConcreteCreds>,
|
|
|
+ #[serde(serialize_with = "serialize_optional_inner")]
|
|
|
+ #[serde(deserialize_with = "deserialize_optional_inner")]
|
|
|
+ /// The root credentials of the blocktree this node is part of.
|
|
|
+ root_creds: Option<Arc<ConcreteCreds>>,
|
|
|
+ /// The key used to protect exported root credentials.
|
|
|
+ storage_key: AsymKeyPair<Encrypt>,
|
|
|
+ /// Derivation parameters for turning the root password into a key.
|
|
|
+ derivation_params: DerivationParams,
|
|
|
+ #[serde(serialize_with = "serialize_optional_zeroizing")]
|
|
|
+ #[serde(deserialize_with = "deserialize_optional_zeroizing")]
|
|
|
+ /// The hash generated from the root password.
|
|
|
+ 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 open_file(file_path: &Path) -> Result<(File, Metadata)> {
|
|
|
+ let result = OpenOptions::new()
|
|
|
+ .read(true)
|
|
|
+ .write(true)
|
|
|
+ .create(false)
|
|
|
+ .open(file_path);
|
|
|
+ let file = match result {
|
|
|
+ Ok(file) => file,
|
|
|
+ Err(err) => {
|
|
|
+ if io::ErrorKind::NotFound == err.kind() {
|
|
|
+ OpenOptions::new()
|
|
|
+ .read(true)
|
|
|
+ .write(true)
|
|
|
+ .create_new(true)
|
|
|
+ .open(file_path)?
|
|
|
+ } else {
|
|
|
+ return Err(err.into());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ let metadata = file.metadata()?;
|
|
|
+ let mut permissions = metadata.permissions();
|
|
|
+ const MASK: u32 = 0o077;
|
|
|
+ if permissions.mode() & MASK != 0 {
|
|
|
+ permissions.set_mode(permissions.mode() & !MASK);
|
|
|
+ file.set_permissions(permissions)?;
|
|
|
+ }
|
|
|
+ Ok((file, metadata))
|
|
|
+ }
|
|
|
+
|
|
|
+ fn save(&mut self, file_path: &Path) -> Result<()> {
|
|
|
+ let (file, ..) = Self::open_file(file_path)?;
|
|
|
+ let mut writer = BufWriter::new(file);
|
|
|
+ write_to(self, &mut writer)?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ 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(())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// An implementation of [CredStore] which uses a file to store credentials.
|
|
|
+ ///
|
|
|
+ /// This struct relies on the security of the underlying filesystem to protect the
|
|
|
+ /// credentials stored in it. Even with this protection, all processes that run with the same
|
|
|
+ /// UID as the process which creates the credential file will be able to read them.
|
|
|
+ /// In addition, the private keys associated with these credentials
|
|
|
+ /// are loaded into main memory as long as this struct is alive. Thus, this struct does not
|
|
|
+ /// provide anywhere near the level of protection that [crate::crypto::tpm::TpmCredStore] does.
|
|
|
+ pub struct FileCredStore {
|
|
|
+ state: RwLock<State>,
|
|
|
+ file_path: PathBuf,
|
|
|
+ }
|
|
|
+
|
|
|
+ impl FileCredStore {
|
|
|
+ /// Returns a new [FileCredStore] which is stored at the given path.
|
|
|
+ ///
|
|
|
+ /// If no file is present at `file_path`, then a new one will be created containing
|
|
|
+ /// freshly generated credentials.
|
|
|
+ pub fn new(file_path: PathBuf) -> Result<Self> {
|
|
|
+ let (file, metadata) = State::open_file(&file_path)?;
|
|
|
+ let state = if metadata.len() > 0 {
|
|
|
+ let mut reader = BufReader::new(file);
|
|
|
+ read_from::<State, _>(&mut reader)?
|
|
|
+ } else {
|
|
|
+ // If the file is zero length then we must have just created it.
|
|
|
+ let state = State::new()?;
|
|
|
+ let mut writer = BufWriter::new(file);
|
|
|
+ write_to(&state, &mut writer)?;
|
|
|
+ state
|
|
|
+ };
|
|
|
+ Ok(FileCredStore {
|
|
|
+ state: RwLock::new(state),
|
|
|
+ file_path,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 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(&self.file_path)?;
|
|
|
+ Ok(root_creds)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+
|
|
|
+ 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(&self.file_path)?;
|
|
|
+ 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(&self.file_path)?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[cfg(test)]
|
|
|
+mod test {
|
|
|
+ use crate::{
|
|
|
+ crypto::{Creds, CredsPriv},
|
|
|
+ Epoch, Principaled,
|
|
|
+ };
|
|
|
+
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ use btserde::to_vec;
|
|
|
+ use std::{ops::Deref, 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 gen_root_creds_and_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(), vec![], 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 writecap_persisted() {
|
|
|
+ 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(), vec![], expires)
|
|
|
+ .unwrap();
|
|
|
+ case.assign_node_writecap(&mut node_creds, expected.clone())
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ let case = TestCase::from_dir(case.dir);
|
|
|
+ let node_creds = case.node_creds().unwrap();
|
|
|
+ let actual = node_creds.writecap().unwrap();
|
|
|
+
|
|
|
+ assert_eq!(&expected, actual);
|
|
|
+ }
|
|
|
+}
|