use std::fs;
use std::io;
use std::path::Path;
#[cfg(unix)]
pub fn current_uid() -> u32 {
unsafe extern "C" {
fn getuid() -> u32;
}
unsafe { getuid() }
}
#[cfg(unix)]
pub fn ensure_private_dir(dir: &Path) -> io::Result<()> {
use std::os::unix::fs::PermissionsExt;
match fs::symlink_metadata(dir) {
Ok(metadata) => validate_private_dir(dir, &metadata),
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(dir)?;
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))?;
validate_private_dir(dir, &fs::symlink_metadata(dir)?)
}
Err(err) => Err(err),
}
}
#[cfg(unix)]
pub fn write_private_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::fs::PermissionsExt;
if let Some(parent) = path.parent() {
ensure_private_dir(parent)?;
}
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(path)?;
file.set_permissions(fs::Permissions::from_mode(0o600))?;
file.write_all(bytes)?;
file.sync_all()
}
#[cfg(unix)]
pub fn validate_private_dir(dir: &Path, metadata: &fs::Metadata) -> io::Result<()> {
use std::os::unix::fs::MetadataExt;
if !metadata.file_type().is_dir() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is not a directory", dir.display()),
));
}
if metadata.uid() != current_uid() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is not owned by the current user", dir.display()),
));
}
if metadata.mode() & 0o077 != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"{} permissions must not allow group/other access",
dir.display()
),
));
}
Ok(())
}
#[cfg(windows)]
mod windows_impl {
use super::{Path, fs, io};
use windows_sys::Win32::Foundation::{HANDLE, HLOCAL, LocalFree};
use windows_sys::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
SDDL_REVISION_1,
};
use windows_sys::Win32::Security::{
GetTokenInformation, PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
TokenUser,
};
use windows_sys::Win32::Storage::FileSystem::CreateDirectoryW;
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
pub struct PrivateSecurityDescriptor(PSECURITY_DESCRIPTOR);
impl PrivateSecurityDescriptor {
pub fn attributes(&self) -> SECURITY_ATTRIBUTES {
SECURITY_ATTRIBUTES {
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: self.0,
bInheritHandle: 0,
}
}
}
impl Drop for PrivateSecurityDescriptor {
fn drop(&mut self) {
unsafe { LocalFree(self.0 as HLOCAL) };
}
}
pub fn current_user_sid() -> io::Result<String> {
unsafe {
let mut token: HANDLE = std::ptr::null_mut();
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return Err(io::Error::last_os_error());
}
let token = OwnedHandle(token);
let mut needed: u32 = 0;
GetTokenInformation(token.0, TokenUser, std::ptr::null_mut(), 0, &mut needed);
if needed == 0 {
return Err(io::Error::last_os_error());
}
let mut buffer = vec![0u8; needed as usize];
if GetTokenInformation(
token.0,
TokenUser,
buffer.as_mut_ptr().cast(),
needed,
&mut needed,
) == 0
{
return Err(io::Error::last_os_error());
}
let token_user = &*buffer.as_ptr().cast::<TOKEN_USER>();
let mut sid_string: *mut u16 = std::ptr::null_mut();
if ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string) == 0 {
return Err(io::Error::last_os_error());
}
let sid = wide_to_string(sid_string);
LocalFree(sid_string as HLOCAL);
Ok(sid)
}
}
pub fn private_security_descriptor() -> io::Result<PrivateSecurityDescriptor> {
let sid = current_user_sid()?;
let sddl = format!("D:P(A;OICI;GA;;;{sid})");
let wide: Vec<u16> = sddl.encode_utf16().chain(std::iter::once(0)).collect();
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let ok = unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
wide.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
std::ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(PrivateSecurityDescriptor(descriptor))
}
pub fn ensure_private_dir(dir: &Path) -> io::Result<()> {
match fs::symlink_metadata(dir) {
Ok(metadata) => validate_private_dir(dir, &metadata),
Err(err) if err.kind() == io::ErrorKind::NotFound => {
if let Some(parent) = dir.parent() {
fs::create_dir_all(parent)?;
}
let descriptor = private_security_descriptor()?;
let attributes = descriptor.attributes();
let wide = wide_path(dir);
if unsafe { CreateDirectoryW(wide.as_ptr(), &attributes) } == 0 {
return Err(io::Error::last_os_error());
}
validate_private_dir(dir, &fs::symlink_metadata(dir)?)
}
Err(err) => Err(err),
}
}
pub fn write_private_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
use std::io::Write;
if let Some(parent) = path.parent() {
ensure_private_dir(parent)?;
}
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
file.write_all(bytes)?;
file.sync_all()
}
fn validate_private_dir(dir: &Path, metadata: &fs::Metadata) -> io::Result<()> {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
if !metadata.file_type().is_dir() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is not a directory", dir.display()),
));
}
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is a reparse point, not a real directory", dir.display()),
));
}
Ok(())
}
pub fn wide_path(path: &Path) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
unsafe fn wide_to_string(ptr: *const u16) -> String {
let mut len = 0;
while unsafe { *ptr.add(len) } != 0 {
len += 1;
}
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(ptr, len) })
}
struct OwnedHandle(HANDLE);
impl Drop for OwnedHandle {
fn drop(&mut self) {
unsafe { windows_sys::Win32::Foundation::CloseHandle(self.0) };
}
}
}
#[cfg(windows)]
pub use windows_impl::{
current_user_sid, ensure_private_dir, private_security_descriptor, write_private_file,
};
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn temp_base(name: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"hyprmux-fs-security-test-{name}-{}",
std::process::id()
))
}
#[test]
fn creates_missing_directory_with_private_mode() {
let dir = temp_base("create");
let _ = fs::remove_dir_all(&dir);
ensure_private_dir(&dir).expect("create");
let mode = fs::symlink_metadata(&dir).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_private_file_creates_0600_file() {
let dir = temp_base("private-file");
let _ = fs::remove_dir_all(&dir);
let path = dir.join("dump.txt");
write_private_file(&path, b"scrollback\n").expect("write");
let mode = fs::symlink_metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
assert_eq!(fs::read_to_string(&path).unwrap(), "scrollback\n");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn rejects_group_or_other_accessible_directory() {
let dir = temp_base("perms");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
let err = ensure_private_dir(&dir).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn rejects_symlink_standing_in_for_the_directory() {
let dir = temp_base("symlink");
let target = temp_base("symlink-target");
let _ = fs::remove_dir_all(&dir);
let _ = fs::remove_dir_all(&target);
fs::create_dir_all(&target).unwrap();
std::os::unix::fs::symlink(&target, &dir).unwrap();
let err = ensure_private_dir(&dir).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
let _ = fs::remove_dir_all(&dir);
let _ = fs::remove_dir_all(&target);
}
}