use std::path::Path;
use windows::core::{HSTRING, PWSTR};
use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
use windows::Win32::Security::Authorization::{
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
SetNamedSecurityInfoW, SDDL_REVISION_1, SE_FILE_OBJECT,
};
use windows::Win32::Security::{
GetSecurityDescriptorDacl, TokenUser, ACL, DACL_SECURITY_INFORMATION,
PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, TOKEN_QUERY, TOKEN_USER,
};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
pub(crate) fn current_user_sid() -> Result<String, String> {
unsafe {
let mut token = HANDLE::default();
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
.map_err(|e| format!("OpenProcessToken: {e}"))?;
let mut len = 0u32;
let _ = windows::Win32::Security::GetTokenInformation(token, TokenUser, None, 0, &mut len);
if len == 0 {
let _ = CloseHandle(token);
return Err("GetTokenInformation reported a zero-length TOKEN_USER".into());
}
let mut buf = vec![0u8; len as usize];
let res = windows::Win32::Security::GetTokenInformation(
token,
TokenUser,
Some(buf.as_mut_ptr() as *mut core::ffi::c_void),
len,
&mut len,
);
let _ = CloseHandle(token);
res.map_err(|e| format!("GetTokenInformation(TokenUser): {e}"))?;
let tu = &*(buf.as_ptr() as *const TOKEN_USER);
let mut s = PWSTR::null();
ConvertSidToStringSidW(tu.User.Sid, &mut s)
.map_err(|e| format!("ConvertSidToStringSid: {e}"))?;
let out = s.to_string().map_err(|e| format!("SID string: {e}"))?;
let _ = LocalFree(Some(HLOCAL(s.0 as *mut core::ffi::c_void)));
Ok(out)
}
}
pub fn restrict_to_owner(path: &Path) -> Result<(), String> {
let sid = current_user_sid()?;
let sddl = HSTRING::from(format!("D:P(A;OICI;FA;;;{sid})"));
unsafe {
let mut psd = PSECURITY_DESCRIPTOR::default();
ConvertStringSecurityDescriptorToSecurityDescriptorW(
&sddl,
SDDL_REVISION_1,
&mut psd,
None,
)
.map_err(|e| format!("building security descriptor: {e}"))?;
let mut dacl: *mut ACL = std::ptr::null_mut();
let mut present = false.into();
let mut defaulted = false.into();
let got = GetSecurityDescriptorDacl(psd, &mut present, &mut dacl, &mut defaulted);
if got.is_err() || dacl.is_null() {
let _ = LocalFree(Some(HLOCAL(psd.0)));
return Err("could not extract the DACL from the security descriptor".into());
}
let rc = SetNamedSecurityInfoW(
&HSTRING::from(path.as_os_str()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
None,
None,
Some(dacl),
None,
);
let _ = LocalFree(Some(HLOCAL(psd.0)));
if rc.is_ok() {
Ok(())
} else {
Err(format!("SetNamedSecurityInfo failed (WIN32_ERROR {})", rc.0))
}
}
}
pub fn restrict_to_owner_warn(path: &Path) {
if let Err(e) = restrict_to_owner(path) {
eprintln!(
"(warning: could not restrict {} to owner-only: {e} — falling back to the \
inherited profile ACL)",
path.display()
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sid_looks_like_a_sid() {
let sid = current_user_sid().expect("current user SID");
assert!(sid.starts_with("S-1-"), "unexpected SID form: {sid}");
}
#[test]
fn restricts_a_real_file() {
let dir = std::env::temp_dir().join("secrets-winacl-test");
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("probe.txt");
std::fs::write(&f, b"x").unwrap();
restrict_to_owner(&f).expect("restrict a file we own");
assert_eq!(std::fs::read(&f).unwrap(), b"x");
let _ = std::fs::remove_file(&f);
let _ = std::fs::remove_dir(&dir);
}
}