mod crypto;
mod errors;
mod serial;
mod shared;
#[cfg(test)]
mod tests;
#[doc(hidden)]
pub const BACKEND: &'static str = crypto_backend();
#[doc(hidden)]
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
use self::shared::{CryptoKeys, EncryptedBlob, Vault};
use crate::crypto::{pbkdf2_hmac_sha1, rand_bytes};
pub use crate::errors::{Error, ErrorKind};
pub use crate::serial::{BinaryDeserializable, BinarySerializable};
use std::fs::File;
use std::path::{Path, PathBuf};
#[non_exhaustive]
#[derive(Clone)]
pub enum KeySource<'a> {
Path(&'a Path),
Buffer(&'a [u8]),
Password(&'a str),
Csprng,
}
pub trait GenericKeySource {
fn key_source(&self) -> KeySource<'_>;
}
impl<'a> KeySource<'a> {
#[doc(hidden)]
#[allow(non_snake_case)]
pub fn File<P: AsRef<Path>>(path: P) -> impl GenericKeySource {
path
}
pub fn from_file<P: AsRef<Path>>(path: P) -> impl GenericKeySource {
path
}
}
impl<P: AsRef<Path>> GenericKeySource for P {
fn key_source(&self) -> KeySource<'_> {
KeySource::Path(self.as_ref())
}
}
impl<'a> GenericKeySource for KeySource<'_> {
fn key_source(&self) -> KeySource<'_> {
match self {
Self::Csprng => KeySource::Csprng,
Self::Path(p) => KeySource::Path(p),
Self::Buffer(b) => KeySource::Buffer(*b),
Self::Password(p) => KeySource::Password(p),
}
}
}
impl GenericKeySource for &KeySource<'_> {
fn key_source(&self) -> KeySource<'_> {
(*self).key_source()
}
}
pub struct SecretsManager {
vault: Vault,
path: Option<PathBuf>,
cryptokeys: CryptoKeys,
}
const _: () = {
fn assert_send<T: Send>() {}
let _ = assert_send::<SecretsManager>;
fn assert_sync<T: Sync>() {}
let _ = assert_sync::<SecretsManager>;
};
impl SecretsManager {
fn create_sentinel(keys: &CryptoKeys) -> EncryptedBlob {
let mut random = [0u8; shared::IV_SIZE * 2];
rand_bytes(&mut random);
EncryptedBlob::encrypt(&keys, &random)
}
pub fn new<K: GenericKeySource>(key_source: K) -> Result<SecretsManager, Error> {
let key_source = key_source.key_source();
let mut vault = Vault::new();
let keys = key_source.extract_keys(&vault.iv)?;
vault.sentinel = Some(Self::create_sentinel(&keys));
Ok(SecretsManager {
cryptokeys: keys,
path: None,
vault,
})
}
pub fn load<'a, P: AsRef<Path> + 'a, K: GenericKeySource>(
path: P,
key_source: K,
) -> Result<SecretsManager, Error> {
let path = path.as_ref();
let file = File::open(path)?;
let mut sman = Self::load_from(file, key_source)?;
sman.path = Some(PathBuf::from(path));
Ok(sman)
}
pub fn load_from<R: std::io::Read, K: GenericKeySource>(
mut vault_source: R,
key_source: K,
) -> Result<SecretsManager, Error> {
let key_source = key_source.key_source();
if matches!(key_source, KeySource::Csprng) {
debug_assert!(
false,
concat!(
"It is incorrect to call SecretsManager::load() ",
"except with an existing key source!"
)
);
}
let mut vault = Vault::load(&mut vault_source)?;
let keys = key_source.extract_keys(&vault.iv)?;
if let Some(ref sentinel) = vault.sentinel {
sentinel.decrypt(&keys)?;
} else {
vault.sentinel = Some(Self::create_sentinel(&keys));
}
let sman = SecretsManager {
cryptokeys: keys,
path: None,
vault,
};
Ok(sman)
}
pub fn save(&self) -> Result<(), Error> {
match self.path.as_ref() {
Some(path) => self.save_as(path),
None => panic!(concat!(
"Cannot call save() on a newly-created store without a path. ",
"Use SecretsManager::save_as(&path) instead!"
)),
}
}
pub fn save_as<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
let path = path.as_ref();
let file = File::options()
.create(true)
.truncate(true)
.write(true)
.open(path)?;
self.vault.save(file)
}
pub fn export_key<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
self.cryptokeys.export(path)
}
#[doc(hidden)]
#[inline]
pub fn export_keyfile<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
self.export_key(path)
}
pub fn get(&self, name: &str) -> Result<String, Error> {
self.get_as::<String>(name)
}
pub fn get_as<T: BinaryDeserializable>(&self, name: &str) -> Result<T, Error> {
match self.vault.secrets.get(name) {
None => ErrorKind::SecretNotFound.into(),
Some(blob) => {
let decrypted = blob.decrypt(&self.cryptokeys)?;
T::deserialize(decrypted)
.map_err(|e| Error::from_inner(ErrorKind::DeserializationError, e))
}
}
}
pub fn set<T: BinarySerializable>(&mut self, name: &str, value: T) {
let encrypted = EncryptedBlob::encrypt(&self.cryptokeys, T::serialize(&value));
self.vault.secrets.insert(name.to_string(), encrypted);
}
pub fn remove(&mut self, name: &str) -> Result<(), Error> {
self.vault
.secrets
.remove(name)
.ok_or_else(|| ErrorKind::SecretNotFound.into())
.map(|_| ())
}
pub fn keys<'a>(&'a self) -> impl Iterator<Item = &'a str> {
self.vault.secrets.keys().map(|s| s.as_str())
}
}
impl<'a> KeySource<'a> {
fn extract_keys(&self, iv: &[u8; shared::IV_SIZE]) -> Result<CryptoKeys, Error> {
match &self {
KeySource::Csprng => {
let mut buffer = [0u8; shared::KEY_COUNT * shared::KEY_LENGTH];
rand_bytes(&mut buffer);
CryptoKeys::import(&buffer[..])
}
KeySource::Path(path) => {
let attr = std::fs::metadata(path)?;
if (attr.len() as usize) < (shared::KEY_COUNT * shared::KEY_LENGTH) {
return ErrorKind::InvalidKeyfile.into();
}
let file = File::open(path)?;
CryptoKeys::import(file)
}
KeySource::Buffer(buf) => CryptoKeys::import(&buf[..]),
KeySource::Password(password) => {
let mut key_data = [0u8; shared::KEY_COUNT * shared::KEY_LENGTH];
pbkdf2_hmac_sha1(
password.as_bytes(),
iv,
shared::PBKDF2_ROUNDS as u32,
&mut key_data,
);
CryptoKeys::import(&key_data[..])
}
}
}
}
const fn crypto_backend() -> &'static str {
let variant = if cfg!(feature = "openssl-vendored") {
"openssl-static"
} else if cfg!(feature = "openssl") {
"openssl"
} else if cfg!(feature = "rustls") {
"rustls"
} else {
panic!("Unknown build variant!");
};
variant
}
#[doc(hidden)]
#[cfg(feature = "openssl")]
pub fn openssl_version() -> &'static str {
openssl::version::version()
}