use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use crate::backend::{BackendKey, Exclusivity, KeychainBackend};
use crate::error::{KeystoreError, Result};
const EXT: &str = "dks";
const GROUP_AND_OTHER_BITS: u32 = 0o077;
#[cfg_attr(not(unix), allow(dead_code))]
fn is_owner_only(mode: u32) -> bool {
mode & GROUP_AND_OTHER_BITS == 0
}
#[allow(unused_variables)]
fn enforce_owner_only(path: &Path, requested: u32) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(path, fs::Permissions::from_mode(requested));
let mode = fs::metadata(path)?.permissions().mode() & 0o777;
if !is_owner_only(mode) {
return Err(KeystoreError::InsecurePermissions {
path: path.display().to_string(),
mode,
});
}
}
Ok(())
}
fn create_owner_only(path: &Path) -> Result<fs::File> {
let mut opts = fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
Ok(opts.open(path)?)
}
pub struct FileBackend {
root: PathBuf,
}
impl FileBackend {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn root(&self) -> &Path {
&self.root
}
fn path_for(&self, key: &BackendKey) -> PathBuf {
let mut p = self.root.clone();
p.push(format!("{}.{}", key.as_str(), EXT));
p
}
fn ensure_root(&self) -> Result<()> {
match fs::symlink_metadata(&self.root) {
Ok(meta) if meta.file_type().is_symlink() => Err(KeystoreError::UnsafeRoot {
path: self.root.display().to_string(),
reason: "it is a symbolic link; pass the resolved target if that is intended",
}),
Ok(meta) if !meta.is_dir() => Err(KeystoreError::UnsafeRoot {
path: self.root.display().to_string(),
reason: "it exists and is not a directory",
}),
Ok(_) => enforce_owner_only(&self.root, 0o700),
Err(e) if e.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(&self.root)?;
enforce_owner_only(&self.root, 0o700)
}
Err(e) => Err(KeystoreError::from(e)),
}
}
}
impl KeychainBackend for FileBackend {
fn read(&self, key: &BackendKey) -> Result<Vec<u8>> {
let path = self.path_for(key);
let mut f = fs::File::open(&path)?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(buf)
}
fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()> {
self.ensure_root()?;
let final_path = self.path_for(key);
let mut tmp_path = final_path.clone();
let rand_suffix: u64 = fastrand_suffix();
tmp_path.set_extension(format!("{EXT}.tmp.{rand_suffix:016x}"));
let staged = (|| -> Result<()> {
let mut f = create_owner_only(&tmp_path)?;
enforce_owner_only(&tmp_path, 0o600)?;
f.write_all(data)?;
f.sync_all()?;
Ok(())
})();
if let Err(e) = staged {
let _ = fs::remove_file(&tmp_path);
return Err(e);
}
fs::rename(&tmp_path, &final_path).map_err(|e| {
let _ = fs::remove_file(&tmp_path);
KeystoreError::from(e)
})?;
#[cfg(unix)]
{
if let Ok(dir) = fs::File::open(&self.root) {
let _ = dir.sync_all();
}
}
Ok(())
}
fn delete(&self, key: &BackendKey) -> Result<()> {
let path = self.path_for(key);
match fs::symlink_metadata(&path) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e.into()),
}
if let Ok(metadata) = fs::metadata(&path) {
let len = metadata.len();
if let Ok(mut f) = fs::OpenOptions::new().write(true).open(&path) {
let zeros = vec![0u8; 4096];
let mut remaining = len as usize;
while remaining > 0 {
let n = remaining.min(zeros.len());
if f.write_all(&zeros[..n]).is_err() {
break;
}
remaining -= n;
}
let _ = f.sync_all();
}
}
fs::remove_file(&path)?;
Ok(())
}
fn list(&self, prefix: &str) -> Result<Vec<BackendKey>> {
match fs::symlink_metadata(&self.root) {
Ok(_) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
}
let mut out = Vec::new();
for entry in fs::read_dir(&self.root)? {
let entry = entry?;
let name = entry.file_name();
let name = match name.to_str() {
Some(s) => s,
None => continue,
};
let Some(stem) = name.strip_suffix(&format!(".{EXT}")) else {
continue;
};
if stem.starts_with(prefix) {
out.push(BackendKey::new(stem.to_string()));
}
}
Ok(out)
}
fn exists(&self, key: &BackendKey) -> Result<bool> {
match fs::symlink_metadata(self.path_for(key)) {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e.into()),
}
}
fn write_new(&self, key: &BackendKey, data: &[u8]) -> Result<()> {
self.ensure_root()?;
let path = self.path_for(key);
let f = match create_owner_only(&path) {
Ok(f) => f,
Err(KeystoreError::Backend(e)) if e.kind() == io::ErrorKind::AlreadyExists => {
return Err(KeystoreError::AlreadyExists(key.as_str().to_string()))
}
Err(e) => return Err(e),
};
let staged = (|mut f: fs::File| -> Result<()> {
enforce_owner_only(&path, 0o600)?;
f.write_all(data)?;
f.sync_all()?;
Ok(())
})(f);
if let Err(e) = staged {
let _ = fs::remove_file(&path);
return Err(e);
}
#[cfg(unix)]
{
if let Ok(dir) = fs::File::open(&self.root) {
let _ = dir.sync_all();
}
}
Ok(())
}
fn write_new_exclusivity(&self) -> Exclusivity {
Exclusivity::Atomic
}
}
fn fastrand_suffix() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let pid = std::process::id() as u64;
ns.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(pid)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::KeystoreError;
use tempfile::TempDir;
fn undeterminable_key() -> BackendKey {
let mut name = String::from("un");
name.push('\u{0}');
name.push_str("determinable");
BackendKey::new(name)
}
#[test]
fn exists_refuses_rather_than_reporting_absent_when_it_cannot_tell() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
assert!(
!be.exists(&BackendKey::new("genuinely-absent")).unwrap(),
"a determinable absence must still be reported as absent"
);
let err = be
.exists(&undeterminable_key())
.expect_err("an unanswerable stat must not be reported as absent");
assert!(
matches!(err, KeystoreError::Backend(_)),
"the refusal must carry the underlying I/O cause"
);
}
#[test]
fn an_unanswerable_read_does_not_reach_a_write() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let key = undeterminable_key();
assert!(be.exists(&key).is_err());
assert!(be.write(&key, b"payload").is_err());
assert!(
be.list("").unwrap().is_empty(),
"a refused write must leave no residue"
);
}
#[cfg(unix)]
#[test]
fn exists_refuses_when_the_parent_cannot_be_read() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let root = dir.path().join("locked");
fs::create_dir(&root).unwrap();
let be = FileBackend::new(root.clone());
let key = BackendKey::new("sealed");
be.write(&key, b"payload").unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o000)).unwrap();
let answer = be.exists(&key);
fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).unwrap();
if running_as_root() {
assert!(
answer.unwrap(),
"root can read the directory, so the blob must be reported present"
);
} else {
assert!(
answer.is_err(),
"an unreadable parent must refuse, not report the blob absent"
);
}
}
#[cfg(unix)]
fn running_as_root() -> bool {
use std::os::unix::fs::PermissionsExt;
let probe = TempDir::new().unwrap();
let d = probe.path().join("probe");
fs::create_dir(&d).unwrap();
fs::set_permissions(&d, fs::Permissions::from_mode(0o000)).unwrap();
let readable = fs::read_dir(&d).is_ok();
fs::set_permissions(&d, fs::Permissions::from_mode(0o700)).unwrap();
readable
}
#[test]
fn write_new_refuses_an_existing_key_without_touching_it() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let key = BackendKey::new("coupled");
be.write_new(&key, b"established").unwrap();
let err = be
.write_new(&key, b"usurper")
.expect_err("write_new must refuse an established key");
assert!(
matches!(err, KeystoreError::AlreadyExists(ref k) if k == "coupled"),
"the collision must be adoptable, not a generic I/O error: {err:?}"
);
assert_eq!(
be.read(&key).unwrap(),
b"established",
"a refused write_new must not replace the established bytes"
);
}
#[test]
fn write_new_establishes_an_absent_key() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let key = BackendKey::new("fresh");
be.write_new(&key, b"payload").unwrap();
assert_eq!(be.read(&key).unwrap(), b"payload");
be.write(&key, b"replaced").unwrap();
assert_eq!(be.read(&key).unwrap(), b"replaced");
}
#[test]
fn file_backend_claims_exclusive_creation() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
assert_eq!(be.write_new_exclusivity(), Exclusivity::Atomic);
}
#[test]
fn only_one_concurrent_write_new_can_win() {
use std::sync::{Arc, Barrier};
const RACERS: usize = 16;
let dir = TempDir::new().unwrap();
let be = Arc::new(FileBackend::new(dir.path().to_path_buf()));
be.write(&BackendKey::new("warmup"), b"x").unwrap();
let key = BackendKey::new("contended");
let gate = Arc::new(Barrier::new(RACERS));
let winners: usize = std::thread::scope(|scope| {
let handles: Vec<_> = (0..RACERS)
.map(|i| {
let (be, gate, key) = (Arc::clone(&be), Arc::clone(&gate), key.clone());
scope.spawn(move || {
let payload = [i as u8; 8];
gate.wait();
be.write_new(&key, &payload).is_ok()
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap())
.filter(|won| *won)
.count()
});
assert_eq!(
winners, 1,
"exactly one racer may establish a key; {winners} did"
);
let stored = be.read(&key).unwrap();
assert_eq!(stored.len(), 8);
assert!(
stored.iter().all(|b| *b == stored[0]),
"the survivor's payload must be intact, not a mix of two writers"
);
}
#[test]
fn write_then_read_roundtrip() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let key = BackendKey::new("test");
be.write(&key, b"hello").unwrap();
let out = be.read(&key).unwrap();
assert_eq!(out, b"hello");
}
#[test]
fn write_is_atomic_on_rename_failure() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let key = BackendKey::new("atomic");
be.write(&key, b"first").unwrap();
be.write(&key, b"second").unwrap();
assert_eq!(be.read(&key).unwrap(), b"second");
let entries: Vec<_> = fs::read_dir(dir.path()).unwrap().collect();
for e in entries {
let name = e.unwrap().file_name();
let s = name.to_string_lossy().into_owned();
assert!(!s.contains(".tmp."), "leftover tmp file: {s}");
}
}
#[test]
fn delete_removes_file() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let key = BackendKey::new("delete_me");
be.write(&key, b"bye").unwrap();
assert!(be.exists(&key).unwrap());
be.delete(&key).unwrap();
assert!(!be.exists(&key).unwrap());
}
#[test]
fn delete_is_idempotent() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
be.delete(&BackendKey::new("never_existed")).unwrap();
}
#[test]
fn delete_refuses_rather_than_reporting_success_when_it_cannot_tell() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
be.delete(&BackendKey::new("genuinely-absent"))
.expect("a determinable absence must still delete Ok");
let err = be
.delete(&undeterminable_key())
.expect_err("an unanswerable stat must not be reported as a completed delete");
assert!(
matches!(err, KeystoreError::Backend(_)),
"the refusal must carry the underlying I/O cause"
);
}
#[test]
fn list_separates_an_absent_root_from_an_uninspectable_one() {
let dir = TempDir::new().unwrap();
let absent_root = FileBackend::new(dir.path().join("not-created-yet"));
assert!(
absent_root.list("").unwrap().is_empty(),
"a root that is genuinely absent must still list as empty"
);
let empty_root = FileBackend::new(dir.path().to_path_buf());
assert!(
empty_root.list("").unwrap().is_empty(),
"an existing empty root must still list as empty"
);
let unreadable_root = FileBackend::new(PathBuf::from("un\u{0}determinable"));
let err = unreadable_root
.list("")
.expect_err("an uninspectable root must not be reported as holding no keys");
assert!(
matches!(err, KeystoreError::Backend(_)),
"the refusal must carry the underlying I/O cause"
);
}
#[cfg(unix)]
#[test]
fn delete_and_list_refuse_when_the_root_cannot_be_stat_ed() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let parent = dir.path().join("locked-parent");
fs::create_dir(&parent).unwrap();
let root = parent.join("keys");
let be = FileBackend::new(root.clone());
let key = BackendKey::new("sealed");
be.write(&key, b"payload").unwrap();
fs::set_permissions(&parent, fs::Permissions::from_mode(0o000)).unwrap();
let listed = be.list("");
let deleted = be.delete(&key);
fs::set_permissions(&parent, fs::Permissions::from_mode(0o700)).unwrap();
if running_as_root() {
assert_eq!(
listed.unwrap().len(),
1,
"root can read the parent, so the blob must be enumerated"
);
deleted.expect("root can read the parent, so the delete must complete");
} else {
assert!(
listed.is_err(),
"an uninspectable root must refuse, not report an empty keystore"
);
assert!(
deleted.is_err(),
"an uninspectable blob must refuse, not report a completed delete"
);
}
}
#[test]
fn list_with_prefix() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
be.write(&BackendKey::new("alpha"), b"a").unwrap();
be.write(&BackendKey::new("alpha2"), b"a").unwrap();
be.write(&BackendKey::new("beta"), b"b").unwrap();
let mut keys = be.list("alph").unwrap();
keys.sort_by_key(|k| k.0.clone());
assert_eq!(
keys,
vec![BackendKey::new("alpha"), BackendKey::new("alpha2")]
);
}
#[test]
fn read_nonexistent_returns_error() {
let dir = TempDir::new().unwrap();
let be = FileBackend::new(dir.path().to_path_buf());
let err = be.read(&BackendKey::new("missing")).unwrap_err();
let is_not_found = match &err {
KeystoreError::Backend(io) => io.kind() == std::io::ErrorKind::NotFound,
_ => false,
};
assert!(is_not_found);
}
#[test]
fn creates_root_dir() {
let dir = TempDir::new().unwrap();
let sub = dir.path().join("nested/keys");
let be = FileBackend::new(sub.clone());
assert!(!sub.exists());
be.write(&BackendKey::new("k"), b"x").unwrap();
assert!(sub.exists());
}
#[test]
fn owner_only_predicate_rejects_every_non_owner_bit() {
for mode in [0o000, 0o400, 0o600, 0o700] {
assert!(
is_owner_only(mode),
"{mode:04o} grants nobody but the owner"
);
}
for mode in [0o640, 0o604, 0o644, 0o060, 0o006, 0o660, 0o777] {
assert!(!is_owner_only(mode), "{mode:04o} reaches beyond the owner");
}
}
#[cfg(unix)]
#[test]
fn written_blob_and_root_are_owner_only_on_disk() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let root = dir.path().join("keys");
let be = FileBackend::new(root.clone());
be.write(&BackendKey::new("seed"), b"sealed").unwrap();
let root_mode = fs::metadata(&root).unwrap().permissions().mode() & 0o777;
assert_eq!(root_mode, 0o700, "root dir mode");
let blob_mode = fs::metadata(root.join("seed.dks"))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(blob_mode, 0o600, "blob mode");
}
#[cfg(unix)]
#[test]
fn existing_permissive_root_is_tightened_on_write() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let root = dir.path().join("keys");
fs::create_dir_all(&root).unwrap();
fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(
fs::metadata(&root).unwrap().permissions().mode() & 0o777,
0o755,
"fixture must start group/other-accessible, or it proves nothing"
);
let be = FileBackend::new(root.clone());
be.write(&BackendKey::new("seed"), b"sealed").unwrap();
assert_eq!(
fs::metadata(&root).unwrap().permissions().mode() & 0o777,
0o700,
"an existing root must be brought to the floor, not skipped"
);
}
#[cfg(unix)]
#[test]
fn enforce_owner_only_refuses_a_mode_it_could_not_bring_to_the_floor() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let root = dir.path().join("permissive");
fs::create_dir_all(&root).unwrap();
let err = enforce_owner_only(&root, 0o755)
.expect_err("a mode granting group and other access must be refused, not accepted");
match err {
KeystoreError::InsecurePermissions { path, mode } => {
assert_eq!(mode, 0o755, "the reported mode must be the one on disk");
assert_eq!(path, root.display().to_string(), "reported path");
}
other => panic!("expected InsecurePermissions, got {other:?}"),
}
assert_eq!(
fs::metadata(&root).unwrap().permissions().mode() & 0o777,
0o755,
"fixture must remain group/other-accessible, or it proves nothing"
);
}
#[cfg(unix)]
#[test]
fn symlinked_root_is_refused_and_its_target_is_untouched() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let victim = dir.path().join("victim");
fs::create_dir_all(&victim).unwrap();
fs::set_permissions(&victim, fs::Permissions::from_mode(0o755)).unwrap();
let root = dir.path().join("keys");
std::os::unix::fs::symlink(&victim, &root).unwrap();
let result = FileBackend::new(root.clone()).write(&BackendKey::new("seed"), b"sealed");
assert_eq!(
fs::metadata(&victim).unwrap().permissions().mode() & 0o777,
0o755,
"the link's target must not be chmodded through the link"
);
assert!(
!victim.join("seed.dks").exists(),
"the sealed blob must not land in the link's target"
);
assert!(
matches!(result, Err(KeystoreError::UnsafeRoot { .. })),
"a symlinked root must be refused, got {result:?}"
);
}
}