use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use windows_sys::Win32::Foundation::{LocalFree, ERROR_SUCCESS, HLOCAL};
use windows_sys::Win32::Security::Authorization::{
ConvertSecurityDescriptorToStringSecurityDescriptorW, ConvertSidToStringSidW,
ConvertStringSecurityDescriptorToSecurityDescriptorW, ConvertStringSidToSidW,
GetNamedSecurityInfoW, SetNamedSecurityInfoW, SDDL_REVISION_1, SE_FILE_OBJECT,
};
use windows_sys::Win32::Security::{
EqualSid, GetSecurityDescriptorDacl, GetTokenInformation, TokenUser, ACL,
DACL_SECURITY_INFORMATION, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
SECURITY_ATTRIBUTES, TOKEN_QUERY, TOKEN_USER,
};
use windows_sys::Win32::Storage::FileSystem::CreateDirectoryW;
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
const ACE_INHERIT_FLAGS: &str = "OICI";
const ACE_RIGHTS: &str = "FA";
const ALLOWED_TRUSTEES: &[&str] = &[
"SY", "S-1-5-18", "BA", "S-1-5-32-544", ];
const ACE_FIELDS: usize = 6;
pub fn ensure_dir_private(path: &Path) -> io::Result<bool> {
let _ = std::fs::metadata(path)?;
let user_sid = current_user_sid()?;
let current = dacl_sddl(path)?;
if is_owner_only(¤t, &user_sid) {
return Ok(false);
}
apply_dacl(path, &owner_only_sddl(&user_sid))?;
let after = dacl_sddl(path)?;
if !is_owner_only(&after, &user_sid) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"{} is writable by other local users (DACL {after}) and could not be tightened",
path.display()
),
));
}
Ok(true)
}
pub fn create_dir_all_private(path: &Path) -> io::Result<()> {
if path.is_dir() {
return Ok(());
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
create_dir_all_private(parent)?;
}
}
let user_sid = current_user_sid()?;
match create_dir_with_dacl(path, &owner_only_sddl(&user_sid)) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => Ok(()),
Err(error) => Err(error),
}
}
fn create_dir_with_dacl(path: &Path, sddl: &str) -> io::Result<()> {
let wide_path = wide(path)?;
let wide_sddl: 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_sddl.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
std::ptr::null_mut(),
)
};
if ok == 0 || descriptor.is_null() {
return Err(io::Error::last_os_error());
}
let attributes = SECURITY_ATTRIBUTES {
nLength: u32::try_from(std::mem::size_of::<SECURITY_ATTRIBUTES>()).unwrap_or(0),
lpSecurityDescriptor: descriptor.cast(),
bInheritHandle: 0,
};
let created = unsafe { CreateDirectoryW(wide_path.as_ptr(), &attributes) };
let result = if created == 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
};
unsafe { LocalFree(descriptor as HLOCAL) };
result
}
fn owner_only_sddl(user_sid: &str) -> String {
format!("D:P(A;{ACE_INHERIT_FLAGS};{ACE_RIGHTS};;;{user_sid})(A;{ACE_INHERIT_FLAGS};{ACE_RIGHTS};;;SY)")
}
fn is_owner_only(sddl: &str, user_sid: &str) -> bool {
let Some(rest) = sddl.strip_prefix("D:") else {
return false;
};
let flags = rest.split('(').next().unwrap_or_default();
if !flags.contains('P') {
return false;
}
if rest.contains("NO_ACCESS_CONTROL") {
return false;
}
aces(rest).all(|trustee| trustee_is_self_or_admin(trustee, user_sid))
}
fn trustee_is_self_or_admin(trustee: &str, user_sid: &str) -> bool {
if trustee == user_sid || ALLOWED_TRUSTEES.contains(&trustee) {
return true;
}
match (native_sid(trustee), native_sid(user_sid)) {
(Some(lhs), Some(rhs)) => {
unsafe { EqualSid(lhs.0, rhs.0) != 0 }
}
_ => false,
}
}
struct NativeSid(windows_sys::Win32::Security::PSID);
impl Drop for NativeSid {
fn drop(&mut self) {
unsafe { LocalFree(self.0.cast::<std::ffi::c_void>()) };
}
}
fn native_sid(trustee: &str) -> Option<NativeSid> {
if trustee.contains('\0') {
return None;
}
let wide: Vec<u16> = trustee.encode_utf16().chain(std::iter::once(0)).collect();
let mut psid: windows_sys::Win32::Security::PSID = std::ptr::null_mut();
if unsafe { ConvertStringSidToSidW(wide.as_ptr(), &mut psid) } == 0 || psid.is_null() {
return None;
}
Some(NativeSid(psid))
}
fn aces(body: &str) -> impl Iterator<Item = &str> {
body.split('(').skip(1).filter_map(|ace| {
let ace = ace.split(')').next()?;
let fields: Vec<&str> = ace.split(';').collect();
(fields.len() >= ACE_FIELDS).then(|| fields[ACE_FIELDS - 1])
})
}
fn wide(path: &Path) -> io::Result<Vec<u16>> {
let mut value: Vec<u16> = path.as_os_str().encode_wide().collect();
if value.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"path contains NUL",
));
}
value.push(0);
Ok(value)
}
fn current_user_sid() -> io::Result<String> {
let mut token: windows_sys::Win32::Foundation::HANDLE = std::ptr::null_mut();
let ok = unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) };
if ok == 0 {
return Err(io::Error::last_os_error());
}
let result = token_user_sid(token);
unsafe { windows_sys::Win32::Foundation::CloseHandle(token) };
result
}
fn token_user_sid(token: windows_sys::Win32::Foundation::HANDLE) -> io::Result<String> {
let mut needed: u32 = 0;
unsafe {
GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &mut needed);
}
if needed == 0 {
return Err(io::Error::last_os_error());
}
let words = (needed as usize).div_ceil(std::mem::size_of::<usize>());
let mut buffer = vec![0usize; words];
let ok = unsafe {
GetTokenInformation(
token,
TokenUser,
buffer.as_mut_ptr().cast(),
needed,
&mut needed,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
let sid = unsafe {
std::ptr::read_unaligned(buffer.as_ptr().cast::<TOKEN_USER>())
.User
.Sid
};
let mut text: *mut u16 = std::ptr::null_mut();
let ok = unsafe { ConvertSidToStringSidW(sid, &mut text) };
if ok == 0 || text.is_null() {
return Err(io::Error::last_os_error());
}
let sid_string = wide_to_string(text);
unsafe { LocalFree(text as HLOCAL) };
Ok(sid_string)
}
fn dacl_sddl(path: &Path) -> io::Result<String> {
let wide_path = wide(path)?;
let mut descriptor: PSECURITY_DESCRIPTOR = std::ptr::null_mut();
let mut dacl: *mut ACL = std::ptr::null_mut();
let status = unsafe {
GetNamedSecurityInfoW(
wide_path.as_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut dacl,
std::ptr::null_mut(),
&mut descriptor,
)
};
if status != ERROR_SUCCESS {
return Err(io::Error::from_raw_os_error(status as i32));
}
let mut text: *mut u16 = std::ptr::null_mut();
let ok = unsafe {
ConvertSecurityDescriptorToStringSecurityDescriptorW(
descriptor,
SDDL_REVISION_1,
DACL_SECURITY_INFORMATION,
&mut text,
std::ptr::null_mut(),
)
};
if ok == 0 || text.is_null() {
let err = io::Error::last_os_error();
unsafe { LocalFree(descriptor as HLOCAL) };
return Err(err);
}
let sddl = wide_to_string(text);
unsafe {
LocalFree(text as HLOCAL);
LocalFree(descriptor as HLOCAL);
}
Ok(sddl)
}
fn apply_dacl(path: &Path, sddl: &str) -> io::Result<()> {
let mut wide_path = wide(path)?;
let wide_sddl: 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_sddl.as_ptr(),
SDDL_REVISION_1,
&mut descriptor,
std::ptr::null_mut(),
)
};
if ok == 0 || descriptor.is_null() {
return Err(io::Error::last_os_error());
}
let mut dacl: *mut ACL = std::ptr::null_mut();
let mut present: i32 = 0;
let mut defaulted: i32 = 0;
let ok =
unsafe { GetSecurityDescriptorDacl(descriptor, &mut present, &mut dacl, &mut defaulted) };
let result = if ok == 0 || present == 0 {
Err(io::Error::last_os_error())
} else {
let status = unsafe {
SetNamedSecurityInfoW(
wide_path.as_mut_ptr(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
std::ptr::null_mut(),
dacl,
std::ptr::null_mut(),
)
};
if status == ERROR_SUCCESS {
Ok(())
} else {
Err(io::Error::from_raw_os_error(status as i32))
}
};
unsafe { LocalFree(descriptor as HLOCAL) };
result
}
fn wide_to_string(text: *const u16) -> String {
let mut len = 0usize;
while unsafe { *text.add(len) } != 0 {
len += 1;
}
let slice = unsafe { std::slice::from_raw_parts(text, len) };
String::from_utf16_lossy(slice)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_nul_is_rejected_without_touching_the_prefix_path() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("prefix\0suffix");
let user = current_user_sid().unwrap();
let error = create_dir_with_dacl(&path, &owner_only_sddl(&user)).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
assert!(!temp.path().join("prefix").exists());
}
#[test]
fn protected_trustee_policy_rejects_inheritance_and_everyone() {
let user = "S-1-5-21-1-2-3-1001";
assert!(is_owner_only(&owner_only_sddl(user), user));
assert!(is_owner_only("D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", user));
assert!(!is_owner_only("D:(A;OICI;FA;;;SY)", user));
assert!(!is_owner_only("D:P(A;OICI;FA;;;WD)", user));
assert!(!is_owner_only("D:PNO_ACCESS_CONTROL", user));
}
#[test]
fn system_alias_and_raw_sid_compare_as_the_same_trustee() {
assert!(trustee_is_self_or_admin("SY", "S-1-5-18"));
assert!(trustee_is_self_or_admin("S-1-5-18", "SY"));
assert!(!trustee_is_self_or_admin("WD", "S-1-5-18"));
}
#[test]
fn native_sid_normalization_retains_valid_aligned_allocations() {
let alias = native_sid("SY").expect("SYSTEM alias");
let raw = native_sid("S-1-5-18").expect("SYSTEM SID");
assert_ne!(unsafe { EqualSid(alias.0, raw.0) }, 0);
assert!(native_sid("not-a-sid").is_none());
assert!(native_sid("SY\0WD").is_none());
}
#[test]
fn creation_persists_protected_dacl_and_tightening_is_verified() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("private");
create_dir_all_private(&path).unwrap();
let user = current_user_sid().unwrap();
assert!(is_owner_only(&dacl_sddl(&path).unwrap(), &user));
assert!(!ensure_dir_private(&path).unwrap());
let exposed = format!("{}(A;OICI;FA;;;WD)", owner_only_sddl(&user));
apply_dacl(&path, &exposed).unwrap();
let observed = dacl_sddl(&path).unwrap();
let tightened = ensure_dir_private(&path).unwrap();
assert!(!is_owner_only(&observed, &user));
assert!(tightened);
assert!(is_owner_only(&dacl_sddl(&path).unwrap(), &user));
}
}