use std::io::{self, Write};
use std::path::Path;
pub struct SecretFile;
impl SecretFile {
pub fn write(path: impl AsRef<Path>, data: &[u8]) -> io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
SecretFile::write_raw(path, data)?;
SecretFile::restrict(path)?;
Ok(())
}
pub fn write_str(path: impl AsRef<Path>, s: &str) -> io::Result<()> {
SecretFile::write(path, s.as_bytes())
}
pub fn restrict(path: impl AsRef<Path>) -> io::Result<()> {
let path = path.as_ref();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
}
#[cfg(windows)]
{
restrict_dacl(path)?;
}
Ok(())
}
fn write_raw(path: &Path, data: &[u8]) -> io::Result<()> {
let dir = path.parent().unwrap_or(std::path::Path::new("."));
let temp = dir.join(format!("{}.tmp.{}", path.file_name().unwrap_or_default().to_string_lossy(), std::process::id()));
{
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(&temp)?;
f.write_all(data)?;
f.sync_all()?;
}
std::fs::rename(&temp, path)?;
if let Some(parent) = path.parent() {
if let Ok(dir) = std::fs::OpenOptions::new().read(true).open(parent) {
let _ = dir.sync_all();
}
}
Ok(())
}
}
#[cfg(windows)]
fn current_user_sid() -> io::Result<String> {
let out = std::process::Command::new("whoami")
.args(["/user", "/fo", "csv", "/nh"])
.output()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("failed to run whoami: {e}")))?;
if !out.status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("whoami /user failed with exit code {:?}", out.status.code()),
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
let sid = stdout
.split(|c: char| c == ',' || c == '"' || c.is_whitespace())
.find(|tok| tok.starts_with("S-1-"));
match sid {
Some(s) if !s.is_empty() => Ok(s.to_string()),
_ => Err(io::Error::new(
io::ErrorKind::Other,
format!("could not parse a user SID from whoami output: {:?}", stdout.trim()),
)),
}
}
#[cfg(windows)]
fn restrict_dacl(path: &Path) -> io::Result<()> {
let sid = current_user_sid()?;
let path_str = path.display().to_string();
let out = std::process::Command::new("icacls")
.args([&path_str, "/inheritance:r", "/grant:r", &format!("*{sid}:(F)"), "/Q"])
.output()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("failed to run icacls: {e}")))?;
if !out.status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"icacls failed with exit code {:?}: {}{}",
out.status.code(),
String::from_utf8_lossy(&out.stdout).trim(),
String::from_utf8_lossy(&out.stderr).trim(),
),
));
}
Ok(())
}