use std::path::Path;
pub fn restrict_file_to_owner(path: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(path)?.permissions();
perm.set_mode(0o600);
std::fs::set_permissions(path, perm)?;
}
#[cfg(windows)]
{
apply_windows_user_only_dacl(path)?;
}
#[cfg(not(any(unix, windows)))]
{
let _ = path;
}
Ok(())
}
pub fn restrict_dir_to_owner(path: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = std::fs::metadata(path)?.permissions();
perm.set_mode(0o700);
std::fs::set_permissions(path, perm)?;
}
#[cfg(windows)]
{
apply_windows_user_only_dacl(path)?;
}
#[cfg(not(any(unix, windows)))]
{
let _ = path;
}
Ok(())
}
pub fn write_secret_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let file = opts.open(path)?;
if let Err(e) = write_and_harden(file, path, bytes) {
let _ = std::fs::remove_file(path);
return Err(e);
}
Ok(())
}
fn write_and_harden(mut file: std::fs::File, path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
file.write_all(bytes)?;
file.sync_all()?;
drop(file);
#[cfg(windows)]
restrict_file_to_owner(path)?;
#[cfg(not(windows))]
let _ = path;
Ok(())
}
#[cfg(windows)]
fn apply_windows_user_only_dacl(path: &Path) -> std::io::Result<()> {
use std::process::Command;
let user = std::env::var("USERNAME").map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"USERNAME env var not set — cannot apply Windows user-only DACL",
)
})?;
let user_trimmed = user.trim();
if user_trimmed.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"USERNAME is empty — cannot apply Windows user-only DACL",
));
}
let path_str = path.to_str().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"path is not valid UTF-8 — cannot pass to icacls",
)
})?;
let output = Command::new("icacls")
.arg(path_str)
.arg("/inheritance:r")
.arg("/grant:r")
.arg(format!("{user_trimmed}:(F)"))
.output()?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"icacls failed ({}): {}",
output.status,
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn restrict_file_sets_0600_on_unix() {
use std::os::unix::fs::PermissionsExt;
let tmp = std::env::temp_dir().join(format!("vta-test-secure-{}", rand::random::<u32>()));
std::fs::create_dir_all(&tmp).unwrap();
let f = tmp.join("secret.bin");
std::fs::write(&f, b"sensitive").unwrap();
let mut perm = std::fs::metadata(&f).unwrap().permissions();
perm.set_mode(0o644);
std::fs::set_permissions(&f, perm).unwrap();
restrict_file_to_owner(&f).expect("restrict_file_to_owner succeeds");
let mode = std::fs::metadata(&f).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn restrict_dir_sets_0700_on_unix() {
use std::os::unix::fs::PermissionsExt;
let tmp = std::env::temp_dir().join(format!("vta-test-secure-{}", rand::random::<u32>()));
std::fs::create_dir_all(&tmp).unwrap();
let mut perm = std::fs::metadata(&tmp).unwrap().permissions();
perm.set_mode(0o755);
std::fs::set_permissions(&tmp, perm).unwrap();
restrict_dir_to_owner(&tmp).expect("restrict_dir_to_owner succeeds");
let mode = std::fs::metadata(&tmp).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o700);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn write_secret_file_is_0600_under_a_022_umask() {
use std::os::unix::fs::PermissionsExt;
let tmp = std::env::temp_dir().join(format!("vta-test-secure-{}", rand::random::<u32>()));
std::fs::create_dir_all(&tmp).unwrap();
let f = tmp.join("export.vtcbak");
let previous = unsafe { libc::umask(0o022) };
let result = write_secret_file(&f, b"sensitive");
unsafe { libc::umask(previous) };
result.expect("write_secret_file succeeds");
let mode = std::fs::metadata(&f).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600, "got {:o}", mode & 0o777);
assert_eq!(std::fs::read(&f).unwrap(), b"sensitive");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn write_secret_file_refuses_an_existing_path() {
let tmp = std::env::temp_dir().join(format!("vta-test-secure-{}", rand::random::<u32>()));
std::fs::create_dir_all(&tmp).unwrap();
let f = tmp.join("export.vtcbak");
std::fs::write(&f, b"original").unwrap();
let err = write_secret_file(&f, b"replacement").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
assert_eq!(std::fs::read(&f).unwrap(), b"original");
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn write_secret_file_does_not_follow_a_symlink() {
let tmp = std::env::temp_dir().join(format!("vta-test-secure-{}", rand::random::<u32>()));
std::fs::create_dir_all(&tmp).unwrap();
let target = tmp.join("elsewhere");
let link = tmp.join("export.vtcbak");
std::os::unix::fs::symlink(&target, &link).unwrap();
let err = write_secret_file(&link, b"sensitive").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
assert!(!target.exists(), "the symlink target must not be created");
let _ = std::fs::remove_dir_all(&tmp);
}
}