use std::collections::HashSet;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use zeroize::Zeroizing;
use super::{ProtectedLocalKeyStoreErrorKind, StoreResult, error};
pub const KEYRING_ITEM: &str = "keyring";
pub struct ProtectedStorageLock {
_guard: Box<dyn Send + Sync>,
}
impl ProtectedStorageLock {
pub fn new(guard: impl Send + Sync + 'static) -> Self {
Self {
_guard: Box::new(guard),
}
}
}
pub trait ProtectedStorage: Send + Sync {
fn database_location(&self, database_path: &Path) -> StoreResult<Vec<u8>> {
canonical_location(database_path)
}
fn prepare(&self, namespace: &str) -> StoreResult<()>;
fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock>;
fn load_secret(
&self,
namespace: &str,
item: &str,
expected_len: usize,
) -> StoreResult<Option<Zeroizing<Vec<u8>>>>;
fn create_secret(&self, namespace: &str, item: &str, bytes: &[u8]) -> StoreResult<()>;
fn delete_secret(&self, namespace: &str, item: &str) -> StoreResult<()>;
fn read_record(
&self,
namespace: &str,
name: &str,
expected_len: usize,
) -> StoreResult<Option<Vec<u8>>>;
fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()>;
fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()>;
fn list_secrets(&self, namespace: &str) -> StoreResult<HashSet<String>>;
fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>>;
}
pub fn canonical_location(database_path: &Path) -> StoreResult<Vec<u8>> {
let canonical = database_path
.canonicalize()
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Ok(canonical.as_os_str().as_bytes().to_vec())
}
#[cfg(not(unix))]
{
Ok(canonical.to_string_lossy().as_bytes().to_vec())
}
}
pub struct FileProtectedStorage {
directory: PathBuf,
}
impl FileProtectedStorage {
pub fn new(directory: PathBuf) -> Self {
Self { directory }
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn path(&self, namespace: &str, name: &str) -> PathBuf {
self.directory.join(format!("{namespace}.{name}"))
}
}
impl ProtectedStorage for FileProtectedStorage {
fn prepare(&self, _namespace: &str) -> StoreResult<()> {
prepare_directory(&self.directory)
}
fn lock(&self, namespace: &str) -> StoreResult<ProtectedStorageLock> {
let path = self.path(namespace, "lock");
let mut options = OpenOptions::new();
options.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let file = options
.open(path)
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
file.lock()
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
Ok(ProtectedStorageLock::new(file))
}
fn load_secret(
&self,
namespace: &str,
item: &str,
expected_len: usize,
) -> StoreResult<Option<Zeroizing<Vec<u8>>>> {
Ok(read_restricted_file(&self.path(namespace, item), expected_len)?.map(Zeroizing::new))
}
fn create_secret(&self, namespace: &str, item: &str, bytes: &[u8]) -> StoreResult<()> {
write_restricted_new(&self.path(namespace, item), bytes)
}
fn delete_secret(&self, namespace: &str, item: &str) -> StoreResult<()> {
remove_restricted_file(&self.path(namespace, item))
}
fn read_record(
&self,
namespace: &str,
name: &str,
expected_len: usize,
) -> StoreResult<Option<Vec<u8>>> {
read_restricted_file(&self.path(namespace, name), expected_len)
}
fn create_record(&self, namespace: &str, name: &str, bytes: &[u8]) -> StoreResult<()> {
write_restricted_new(&self.path(namespace, name), bytes)
}
fn remove_record(&self, namespace: &str, name: &str) -> StoreResult<()> {
remove_restricted_file(&self.path(namespace, name))
}
fn list_secrets(&self, namespace: &str) -> StoreResult<HashSet<String>> {
self.list(namespace)
}
fn list_records(&self, namespace: &str) -> StoreResult<HashSet<String>> {
self.list(namespace)
}
}
impl FileProtectedStorage {
fn list(&self, namespace: &str) -> StoreResult<HashSet<String>> {
let prefix = format!("{namespace}.");
let entries = match fs::read_dir(&self.directory) {
Ok(entries) => entries,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
return Ok(HashSet::new());
}
Err(_) => return Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
};
let mut names = HashSet::new();
for entry in entries {
let entry = entry.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
if let Some(name) = entry
.file_name()
.to_str()
.and_then(|name| name.strip_prefix(&prefix))
.filter(|name| *name != "lock")
{
names.insert(name.to_string());
}
}
Ok(names)
}
}
fn prepare_directory(path: &Path) -> StoreResult<()> {
fs::create_dir_all(path).map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
}
Ok(())
}
fn read_restricted_file(path: &Path, expected_len: usize) -> StoreResult<Option<Vec<u8>>> {
let mut file = match File::open(path) {
Ok(file) => file,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(_) => return Err(error(ProtectedLocalKeyStoreErrorKind::Unavailable)),
};
let metadata = file
.metadata()
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Unavailable))?;
if !metadata.is_file() || metadata.len() != expected_len as u64 {
return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o077 != 0 {
return Err(error(ProtectedLocalKeyStoreErrorKind::Corrupt));
}
}
let mut bytes = vec![0_u8; expected_len];
file.read_exact(&mut bytes)
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::Corrupt))?;
Ok(Some(bytes))
}
fn sync_parent(path: &Path) -> StoreResult<()> {
let parent = path
.parent()
.ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
File::open(parent)
.and_then(|directory| directory.sync_all())
.map_err(|_| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))
}
fn remove_restricted_file(path: &Path) -> StoreResult<()> {
match fs::remove_file(path) {
Ok(()) => sync_parent(path),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(_) => Err(super::error(ProtectedLocalKeyStoreErrorKind::WriteFailed)),
}
}
fn write_failure(
phase: &'static str,
source: std::io::Error,
) -> super::ProtectedLocalKeyStoreError {
tracing::warn!(
phase,
errno = source.raw_os_error(),
"protected storage write failed"
);
error(ProtectedLocalKeyStoreErrorKind::WriteFailed)
}
fn write_restricted_new(path: &Path, bytes: &[u8]) -> StoreResult<()> {
let parent = path
.parent()
.ok_or_else(|| error(ProtectedLocalKeyStoreErrorKind::WriteFailed))?;
let mut staged = tempfile::NamedTempFile::new_in(parent)
.map_err(|source| write_failure("stage_create", source))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
staged
.as_file()
.set_permissions(fs::Permissions::from_mode(0o600))
.map_err(|source| write_failure("stage_permissions", source))?;
}
staged
.as_file_mut()
.write_all(bytes)
.map_err(|source| write_failure("stage_write", source))?;
staged
.as_file()
.sync_all()
.map_err(|source| write_failure("stage_sync", source))?;
staged
.persist_noclobber(path)
.map_err(|source| write_failure("stage_persist", source.error))?;
File::open(parent)
.map_err(|source| write_failure("parent_open", source))?
.sync_all()
.map_err(|source| write_failure("parent_sync", source))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn failed_create_preserves_existing_bytes_and_error_contract() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("record");
write_restricted_new(&path, b"first").unwrap();
assert_eq!(
write_restricted_new(&path, b"other").unwrap_err().kind(),
ProtectedLocalKeyStoreErrorKind::WriteFailed
);
assert_eq!(read_restricted_file(&path, 5).unwrap().unwrap(), b"first");
assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 1);
}
#[test]
fn failed_staging_keeps_stable_private_error() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("missing/record");
let failure = write_restricted_new(&path, b"private").unwrap_err();
assert_eq!(failure.kind(), ProtectedLocalKeyStoreErrorKind::WriteFailed);
assert!(!failure.to_string().contains("missing"));
assert!(!failure.to_string().contains("private"));
}
}