use std::io;
use std::path::{Path, PathBuf};
pub(crate) fn set_mode(path: &Path, _mode: u32) -> io::Result<()> {
restrict_to_owner(path)
}
pub(crate) fn write_with_mode(path: &Path, contents: &[u8], _mode: u32) -> io::Result<()> {
std::fs::write(path, contents)?;
restrict_to_owner(path)
}
pub(crate) fn ensure_private(path: &Path, _mode: u32) -> io::Result<Option<u32>> {
if !path.exists() {
return Ok(None);
}
restrict_to_owner(path)?;
Ok(None)
}
pub(crate) fn configure_detached(_cmd: &mut std::process::Command) {}
pub(crate) fn current_uid() -> u32 {
0
}
pub(crate) fn kill_process_group(_pgid: u32) -> io::Result<()> {
Ok(())
}
fn restrict_to_owner(path: &Path) -> io::Result<()> {
let user = resolve_user(std::env::var("USERNAME").ok())?;
let output = std::process::Command::new(icacls_program())
.args(icacls_args(path, &user))
.output()?;
interpret_icacls(output.status.success(), &output.stderr, path)
}
fn icacls_program() -> PathBuf {
match std::env::var("SystemRoot") {
Ok(root) => PathBuf::from(root).join("System32").join("icacls.exe"),
Err(_) => PathBuf::from("icacls.exe"),
}
}
fn icacls_args(path: &Path, user: &str) -> Vec<String> {
vec![
path.display().to_string(),
"/inheritance:r".to_string(),
"/grant:r".to_string(),
format!("{user}:(F)"),
]
}
fn resolve_user(username: Option<String>) -> io::Result<String> {
match username {
Some(user) if !user.trim().is_empty() => Ok(user),
_ => Err(io::Error::new(
io::ErrorKind::NotFound,
"cannot restrict a file to its owner: USERNAME is not set, so there is \
no account to grant. Leviath's secrets would be left readable by other \
users on this machine.",
)),
}
}
fn interpret_icacls(success: bool, stderr: &[u8], path: &Path) -> io::Result<()> {
if success {
return Ok(());
}
Err(io::Error::other(format!(
"failed to restrict '{}' to its owner: {}",
path.display(),
String::from_utf8_lossy(stderr).trim()
)))
}
#[cfg(test)]
pub(crate) static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_arguments_drop_inheritance_and_replace_the_grant() {
let args = icacls_args(Path::new(r"C:\Users\u\.leviath\config.toml"), "u");
assert_eq!(
args,
vec![
r"C:\Users\u\.leviath\config.toml".to_string(),
"/inheritance:r".to_string(),
"/grant:r".to_string(),
"u:(F)".to_string(),
]
);
}
#[test]
fn icacls_comes_from_the_system_directory() {
let _env = ENV_LOCK.lock().expect("env lock");
temp_env::with_var("SystemRoot", Some(r"C:\Windows"), || {
assert_eq!(
icacls_program(),
PathBuf::from(r"C:\Windows\System32\icacls.exe")
);
});
temp_env::with_var_unset("SystemRoot", || {
assert_eq!(icacls_program(), PathBuf::from("icacls.exe"));
});
}
#[test]
fn a_missing_username_is_an_explained_error() {
assert_eq!(resolve_user(Some("gerald".into())).unwrap(), "gerald");
for absent in [None, Some(String::new()), Some(" ".to_string())] {
let err = resolve_user(absent).expect_err("no account, no grant");
assert_eq!(err.kind(), io::ErrorKind::NotFound);
assert!(err.to_string().contains("USERNAME"), "{err}");
}
}
#[test]
fn a_failed_icacls_names_the_file_and_the_reason() {
assert!(interpret_icacls(true, b"", Path::new("x")).is_ok());
let err = interpret_icacls(false, b"Access is denied.\r\n", Path::new(r"C:\secret"))
.expect_err("a non-zero exit is a failure");
assert!(err.to_string().contains(r"C:\secret"), "{err}");
assert!(err.to_string().contains("Access is denied."), "{err}");
}
#[test]
fn restricting_a_real_file_keeps_other_users_out() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
std::fs::write(&path, b"the key").unwrap();
restrict_to_owner(&path).expect("restricting a file we own succeeds");
assert_eq!(
std::fs::read(&path).unwrap(),
b"the key",
"still ours to read"
);
let shown = std::process::Command::new(icacls_program())
.arg(path.display().to_string())
.output()
.expect("icacls runs");
let acl = String::from_utf8_lossy(&shown.stdout).into_owned();
let user = std::env::var("USERNAME").unwrap();
assert!(acl.contains(&user), "the owner keeps access:\n{acl}");
for broad in ["\\Users:", "Everyone:", "Authenticated Users:"] {
assert!(
!acl.contains(broad),
"'{broad}' must not be granted after restricting:\n{acl}"
);
}
}
#[test]
fn restricting_fails_when_there_is_no_account_to_grant() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
std::fs::write(&path, b"x").unwrap();
temp_env::with_var_unset("USERNAME", || {
assert!(restrict_to_owner(&path).is_err(), "no USERNAME, no grant");
assert!(ensure_private(&path, 0o600).is_err());
assert!(write_with_mode(&path, b"x", 0o600).is_err());
});
}
#[test]
fn a_missing_icacls_is_reported() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
std::fs::write(&path, b"x").unwrap();
temp_env::with_var("SystemRoot", Some(r"C:\definitely-not-windows"), || {
assert!(
restrict_to_owner(&path).is_err(),
"a system root with no icacls.exe is an error"
);
});
}
#[test]
fn write_with_mode_writes_and_restricts() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret");
write_with_mode(&path, b"the key", 0o600).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"the key");
write_with_mode(&path, b"rotated", 0o600).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"rotated");
}
#[test]
fn ensure_private_restricts_an_existing_file_and_skips_a_missing_one() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cfg");
std::fs::write(&path, b"x").unwrap();
assert_eq!(ensure_private(&path, 0o600).unwrap(), None);
assert_eq!(
ensure_private(&dir.path().join("nope"), 0o600).unwrap(),
None
);
}
#[test]
fn set_mode_restricts_a_directory() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("d");
std::fs::create_dir(&sub).unwrap();
set_mode(&sub, 0o700).unwrap();
std::fs::write(sub.join("f"), b"x").unwrap();
}
#[test]
fn set_mode_reports_a_missing_path() {
let _env = ENV_LOCK.lock().expect("env lock");
let dir = tempfile::tempdir().unwrap();
assert!(set_mode(&dir.path().join("nope"), 0o600).is_err());
}
#[test]
fn the_no_op_shims_answer() {
let mut cmd = std::process::Command::new("cmd");
configure_detached(&mut cmd);
assert_eq!(current_uid(), 0);
assert!(kill_process_group(1).is_ok());
}
}