use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum SecretFileError {
#[error("path has no parent directory")]
NoParent,
#[error("path has no basename")]
NoBasename,
#[error("parent directory is a symlink")]
SymlinkParent,
#[error("target file already exists")]
FileExists,
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid filename (contains null byte)")]
InvalidFilename,
}
#[cfg(unix)]
pub fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), SecretFileError> {
use std::ffi::CString;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::fs::OpenOptionsExt;
let parent = path.parent().ok_or(SecretFileError::NoParent)?;
if parent.as_os_str().is_empty() {
return Err(SecretFileError::NoParent);
}
let basename = path.file_name().ok_or(SecretFileError::NoBasename)?;
if std::fs::symlink_metadata(parent).is_ok_and(|m| m.file_type().is_symlink()) {
return Err(SecretFileError::SymlinkParent);
}
let parent_fd = OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(parent)
.map_err(|e| match e.raw_os_error() {
Some(libc::ELOOP) => SecretFileError::SymlinkParent,
_ => SecretFileError::Io(e),
})?;
#[allow(unsafe_code)]
{
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
#[allow(clippy::borrow_as_ptr)]
let rc = unsafe { libc::fstat(parent_fd.as_raw_fd(), &mut stat) };
if rc != 0 {
return Err(SecretFileError::Io(std::io::Error::last_os_error()));
}
if (stat.st_mode & libc::S_IFMT) != libc::S_IFDIR {
return Err(SecretFileError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"parent fd does not refer to a directory",
)));
}
}
let c_name =
CString::new(basename.as_encoded_bytes()).map_err(|_| SecretFileError::InvalidFilename)?;
let mode: std::ffi::c_uint = 0o600;
#[allow(unsafe_code)]
let raw_fd = {
unsafe {
libc::openat(
parent_fd.as_raw_fd(),
c_name.as_ptr(),
libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
mode,
)
}
};
if raw_fd < 0 {
let err = std::io::Error::last_os_error();
return Err(match err.raw_os_error() {
Some(libc::EEXIST) => SecretFileError::FileExists,
_ => SecretFileError::Io(err),
});
}
#[allow(unsafe_code)]
let mut file = unsafe { File::from_raw_fd(raw_fd) };
file.write_all(contents)?;
file.sync_all()?;
drop(file);
drop(parent_fd);
Ok(())
}
#[cfg(not(unix))]
pub fn write_secret_file(_path: &Path, _contents: &[u8]) -> Result<(), SecretFileError> {
Err(SecretFileError::Io(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"write_secret_file is not yet implemented on this platform",
)))
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
#[test]
fn happy_path_writes_at_0600_with_contents() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("secret.auth");
write_secret_file(&target, b"user\npass\n").expect("write should succeed");
let meta = std::fs::metadata(&target).unwrap();
assert_eq!(
meta.permissions().mode() & 0o777,
0o600,
"file must be created at 0o600 in one step"
);
let body = std::fs::read(&target).unwrap();
assert_eq!(body, b"user\npass\n");
}
#[test]
fn symlinked_parent_directory_is_rejected() {
let tmp = tempfile::tempdir().unwrap();
let real_dir = tmp.path().join("real");
std::fs::create_dir(&real_dir).unwrap();
let link_dir = tmp.path().join("link");
std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
let via_symlink = link_dir.join("secret.auth");
let result = write_secret_file(&via_symlink, b"x");
assert!(
matches!(result, Err(SecretFileError::SymlinkParent)),
"expected SymlinkParent, got {result:?}"
);
assert!(!real_dir.join("secret.auth").exists());
}
#[test]
fn existing_target_returns_file_exists() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("already.auth");
std::fs::write(&target, b"pre-existing").unwrap();
let result = write_secret_file(&target, b"new");
assert!(
matches!(result, Err(SecretFileError::FileExists)),
"expected FileExists, got {result:?}"
);
assert_eq!(std::fs::read(&target).unwrap(), b"pre-existing");
}
#[test]
fn target_is_symlink_returns_io_eloop() {
let tmp = tempfile::tempdir().unwrap();
let decoy = tmp.path().join("decoy.txt");
std::fs::write(&decoy, b"decoy").unwrap();
let link = tmp.path().join("link.auth");
std::os::unix::fs::symlink(&decoy, &link).unwrap();
let result = write_secret_file(&link, b"payload");
match result {
Err(SecretFileError::FileExists) => {
}
Err(SecretFileError::Io(e)) => {
assert_eq!(e.raw_os_error(), Some(libc::ELOOP), "expected ELOOP");
}
other => panic!("expected FileExists or Io(ELOOP), got {other:?}"),
}
assert_eq!(std::fs::read(&decoy).unwrap(), b"decoy");
}
#[test]
fn missing_parent_returns_io_enoent() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("nope").join("secret.auth");
let result = write_secret_file(&target, b"x");
match result {
Err(SecretFileError::Io(e)) => {
assert_eq!(e.raw_os_error(), Some(libc::ENOENT));
}
other => panic!("expected Io(ENOENT), got {other:?}"),
}
}
#[test]
fn basename_with_null_byte_returns_invalid_filename() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let tmp = tempfile::tempdir().unwrap();
let bad_name = OsString::from_vec(b"bad\0name".to_vec());
let target = tmp.path().join(bad_name);
let result = write_secret_file(&target, b"x");
assert!(
matches!(result, Err(SecretFileError::InvalidFilename)),
"expected InvalidFilename, got {result:?}"
);
}
#[test]
fn permissions_are_0600_immediately_under_loose_umask() {
#[allow(unsafe_code)]
let prev = unsafe { libc::umask(0o022) };
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("perm.auth");
write_secret_file(&target, b"x").unwrap();
let mode = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
#[allow(unsafe_code)]
unsafe {
libc::umask(prev);
}
assert_eq!(mode, 0o600, "umask must not loosen our explicit 0o600");
}
}