use crate::Result;
use crate::error::{
Error, FileOperationError, SecurityError, SecurityUnsupportedError, WindowsApiError,
};
use crate::security::{AccessMask, Ace, AceType, Dacl, InheritanceFlags, SecurityDescriptor, Sid};
use crate::utils::{pwstr_to_string_len, to_utf16_nul};
use windows::Win32::Foundation::{HLOCAL, LocalFree};
use windows::Win32::Security::Authorization::{
ConvertSecurityDescriptorToStringSecurityDescriptorW,
ConvertStringSecurityDescriptorToSecurityDescriptorW, GetNamedSecurityInfoW, SDDL_REVISION_1,
SE_FILE_OBJECT, SetNamedSecurityInfoW,
};
use windows::Win32::Security::{
ACL, DACL_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, GetSecurityDescriptorDacl,
GetSecurityDescriptorGroup, GetSecurityDescriptorOwner, OWNER_SECURITY_INFORMATION,
PSECURITY_DESCRIPTOR, PSID,
};
use windows::core::{PCWSTR, PWSTR};
pub(crate) fn read_descriptor(path: &str) -> Result<SecurityDescriptor> {
let path_wide = to_utf16_nul(path);
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
let get_result = unsafe {
GetNamedSecurityInfoW(
PCWSTR(path_wide.as_ptr()),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
None,
None,
None,
None,
&mut security_descriptor,
)
};
if get_result.0 != 0 {
return Err(Error::FileOperation(FileOperationError::with_code(
path.to_string(),
"GetNamedSecurityInfoW",
get_result.0 as i32,
)));
}
let mut sddl = PWSTR::null();
let mut sddl_len = 0u32;
unsafe {
ConvertSecurityDescriptorToStringSecurityDescriptorW(
security_descriptor,
SDDL_REVISION_1,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&mut sddl,
Some(&mut sddl_len),
)
.map_err(|e| {
let _ = LocalFree(HLOCAL(security_descriptor.0));
Error::WindowsApi(WindowsApiError::with_context(
e,
"ConvertSecurityDescriptorToStringSecurityDescriptorW",
))
})?;
}
let sddl_str = pwstr_to_string_len(sddl, sddl_len as usize);
unsafe {
let _ = LocalFree(HLOCAL(sddl.0 as *mut core::ffi::c_void));
let _ = LocalFree(HLOCAL(security_descriptor.0));
}
sddl_to_descriptor(path, &sddl_str)
}
pub(crate) fn write_descriptor(path: &str, descriptor: &SecurityDescriptor) -> Result<()> {
let sddl = descriptor_to_sddl(descriptor);
let sddl_wide = to_utf16_nul(&sddl);
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
let mut security_descriptor_size = 0u32;
unsafe {
ConvertStringSecurityDescriptorToSecurityDescriptorW(
PCWSTR(sddl_wide.as_ptr()),
SDDL_REVISION_1,
&mut security_descriptor as *mut _,
Some(&mut security_descriptor_size),
)
.map_err(|e| {
Error::WindowsApi(WindowsApiError::with_context(
e,
"ConvertStringSecurityDescriptorToSecurityDescriptorW",
))
})?;
}
let mut dacl_present = false.into();
let mut dacl: *mut ACL = std::ptr::null_mut();
let mut dacl_defaulted = false.into();
unsafe {
GetSecurityDescriptorDacl(
security_descriptor,
&mut dacl_present,
&mut dacl,
&mut dacl_defaulted,
)
}
.map_err(|e| {
Error::WindowsApi(WindowsApiError::with_context(
e,
"GetSecurityDescriptorDacl",
))
})?;
if !dacl_present.as_bool() || dacl.is_null() {
unsafe {
let _ = LocalFree(HLOCAL(security_descriptor.0));
}
return Err(Error::Security(SecurityError::Unsupported(
SecurityUnsupportedError::with_reason(
path.to_string(),
"write_descriptor",
"generated security descriptor has no DACL",
),
)));
}
let mut owner_sid = PSID::default();
let mut owner_defaulted = false.into();
unsafe {
GetSecurityDescriptorOwner(security_descriptor, &mut owner_sid, &mut owner_defaulted)
}
.map_err(|e| {
Error::WindowsApi(WindowsApiError::with_context(
e,
"GetSecurityDescriptorOwner",
))
})?;
let mut group_sid = PSID::default();
let mut group_defaulted = false.into();
unsafe {
GetSecurityDescriptorGroup(security_descriptor, &mut group_sid, &mut group_defaulted)
}
.map_err(|e| {
Error::WindowsApi(WindowsApiError::with_context(
e,
"GetSecurityDescriptorGroup",
))
})?;
let mut security_information = DACL_SECURITY_INFORMATION;
let mut owner_for_set = PSID::default();
if descriptor.owner().is_some() {
if owner_sid.0.is_null() {
unsafe {
let _ = LocalFree(HLOCAL(security_descriptor.0));
}
return Err(Error::Security(SecurityError::Unsupported(
SecurityUnsupportedError::with_reason(
path.to_string(),
"write_descriptor",
"generated security descriptor has no owner SID",
),
)));
}
security_information |= OWNER_SECURITY_INFORMATION;
owner_for_set = owner_sid;
}
let mut group_for_set = PSID::default();
if descriptor.group().is_some() {
if group_sid.0.is_null() {
unsafe {
let _ = LocalFree(HLOCAL(security_descriptor.0));
}
return Err(Error::Security(SecurityError::Unsupported(
SecurityUnsupportedError::with_reason(
path.to_string(),
"write_descriptor",
"generated security descriptor has no group SID",
),
)));
}
security_information |= GROUP_SECURITY_INFORMATION;
group_for_set = group_sid;
}
let path_wide = to_utf16_nul(path);
let result = unsafe {
SetNamedSecurityInfoW(
PWSTR(path_wide.as_ptr() as *mut u16),
SE_FILE_OBJECT,
security_information,
owner_for_set,
group_for_set,
Some(dacl as *const ACL),
None,
)
};
unsafe {
let _ = LocalFree(HLOCAL(security_descriptor.0));
}
if result.0 == 0 {
Ok(())
} else {
map_file_security_status(path, "SetNamedSecurityInfoW", result.0 as i32)
}
}
fn map_file_security_status<T>(resource: &str, operation: &str, code: i32) -> Result<T> {
if code == 5 {
return Err(Error::AccessDenied(
crate::error::AccessDeniedError::with_reason(
resource.to_string(),
operation.to_string(),
"access denied",
),
));
}
if code == 1314 {
return Err(Error::AccessDenied(
crate::error::AccessDeniedError::with_reason(
resource.to_string(),
operation.to_string(),
"required privilege is not held (likely SeRestorePrivilege or SeTakeOwnershipPrivilege)",
),
));
}
Err(Error::FileOperation(FileOperationError::with_code(
resource.to_string(),
operation.to_string(),
code,
)))
}
fn dacl_to_sddl(dacl: &Dacl) -> String {
let mut sddl = String::from("D:");
for ace in dacl.entries() {
let ace_type = match ace.ace_type {
AceType::Allow => "A",
AceType::Deny => "D",
};
let mut flags = String::new();
if ace.inheritance.object_inherit {
flags.push_str("OI");
}
if ace.inheritance.container_inherit {
flags.push_str("CI");
}
if ace.inheritance.inherit_only {
flags.push_str("IO");
}
if ace.inheritance.no_propagate_inherit {
flags.push_str("NP");
}
let sid = ace.trustee.as_str();
let rights = format!("0x{:X}", ace.access_mask.bits());
sddl.push_str(&format!("({};{};{};;;{})", ace_type, flags, rights, sid));
}
sddl
}
fn descriptor_to_sddl(descriptor: &SecurityDescriptor) -> String {
let mut sddl = String::new();
if let Some(owner) = descriptor.owner() {
sddl.push_str("O:");
sddl.push_str(owner.as_str());
}
if let Some(group) = descriptor.group() {
sddl.push_str("G:");
sddl.push_str(group.as_str());
}
sddl.push_str(&dacl_to_sddl(descriptor.dacl()));
sddl
}
fn sddl_to_descriptor(path: &str, sddl: &str) -> Result<SecurityDescriptor> {
let owner = extract_sddl_section(sddl, 'O').and_then(|v| sid_or_alias_to_sid(v).ok());
let group = extract_sddl_section(sddl, 'G').and_then(|v| sid_or_alias_to_sid(v).ok());
let dacl = parse_dacl_from_sddl(sddl)?;
let mut descriptor = SecurityDescriptor::for_file_path(path.to_string()).with_dacl(dacl);
if let Some(owner) = owner {
descriptor = descriptor.with_owner(owner);
}
if let Some(group) = group {
descriptor = descriptor.with_group(group);
}
Ok(descriptor)
}
fn parse_dacl_from_sddl(sddl: &str) -> Result<Dacl> {
let Some(dacl_section) = extract_sddl_section(sddl, 'D') else {
return Ok(Dacl::new());
};
let mut entries = Vec::new();
let mut index = 0usize;
while let Some(start_rel) = dacl_section[index..].find('(') {
let start = index + start_rel;
let Some(end_rel) = dacl_section[start..].find(')') else {
break;
};
let end = start + end_rel;
let ace_body = &dacl_section[start + 1..end];
if let Some(ace) = parse_ace(ace_body)? {
entries.push(ace);
}
index = end + 1;
}
let mut dacl = Dacl::from_entries(entries);
dacl.canonicalize();
Ok(dacl)
}
fn parse_ace(ace_body: &str) -> Result<Option<Ace>> {
let fields: Vec<&str> = ace_body.split(';').collect();
if fields.len() < 6 {
return Ok(None);
}
let ace_type = match fields[0] {
"A" => AceType::Allow,
"D" => AceType::Deny,
_ => return Ok(None),
};
let inheritance = parse_inheritance_flags(fields[1]);
let inherited = fields[1].contains("ID");
let access_mask = parse_sddl_rights(fields[2]).ok_or_else(|| {
Error::FileOperation(FileOperationError::new(
"sddl_dacl",
"parse ACE access mask",
))
})?;
let trustee = match sid_or_alias_to_sid(fields[5]) {
Ok(v) => v,
Err(_) => return Ok(None),
};
Ok(Some(
Ace::new(trustee, ace_type, access_mask)
.with_inheritance(inheritance)
.inherited(inherited),
))
}
fn parse_inheritance_flags(value: &str) -> InheritanceFlags {
InheritanceFlags {
object_inherit: value.contains("OI"),
container_inherit: value.contains("CI"),
inherit_only: value.contains("IO"),
no_propagate_inherit: value.contains("NP"),
}
}
fn parse_sddl_rights(value: &str) -> Option<AccessMask> {
if let Some(hex) = value.strip_prefix("0x") {
let bits = u32::from_str_radix(hex, 16).ok()?;
return Some(AccessMask::from_bits(bits));
}
let mut mask = AccessMask::from_bits(0);
let mut i = 0usize;
while i + 1 < value.len() {
let token = &value[i..i + 2];
let token_mask = match token {
"FA" => AccessMask::from_bits(0x1F01FF),
"FR" => AccessMask::from_bits(0x120089),
"FW" => AccessMask::from_bits(0x120116),
"FX" => AccessMask::from_bits(0x1200A0),
"SD" => AccessMask::from_bits(0x00010000),
"RC" => AccessMask::from_bits(0x00020000),
"WD" => AccessMask::from_bits(0x00040000),
"WO" => AccessMask::from_bits(0x00080000),
_ => AccessMask::from_bits(0),
};
if token_mask.bits() != 0 {
mask |= token_mask;
}
i += 2;
}
if mask.bits() == 0 { None } else { Some(mask) }
}
fn sid_or_alias_to_sid(value: &str) -> Result<Sid> {
Sid::from_sddl_trustee(value)
.map_err(|_| Error::FileOperation(FileOperationError::new("sddl_sid", "map SID alias")))
}
fn extract_sddl_section(sddl: &str, section: char) -> Option<&str> {
let marker = format!("{}:", section);
let start = sddl.find(&marker)? + marker.len();
let mut end = sddl.len();
for candidate in ['O', 'G', 'D', 'S'] {
if candidate == section {
continue;
}
let candidate_marker = format!("{}:", candidate);
if let Some(pos) = sddl[start..].find(&candidate_marker) {
end = end.min(start + pos);
}
}
Some(sddl[start..end].trim())
}
#[cfg(test)]
mod tests {
use super::{
dacl_to_sddl, descriptor_to_sddl, map_file_security_status, parse_dacl_from_sddl,
sddl_to_descriptor,
};
use crate::security::{
AccessMask, Ace, AceType, Dacl, InheritanceFlags, SecurityDescriptor, Sid,
};
#[test]
fn dacl_to_sddl_generates_expected_ace_entries() {
let sid = Sid::parse("S-1-5-32-545").expect("valid sid");
let dacl = Dacl::from_entries(vec![
Ace::new(sid.clone(), AceType::Allow, AccessMask::from_bits(0x120089)),
Ace::new(sid, AceType::Deny, AccessMask::from_bits(0x2)).with_inheritance(
InheritanceFlags {
object_inherit: true,
container_inherit: true,
inherit_only: false,
no_propagate_inherit: false,
},
),
]);
let sddl = dacl_to_sddl(&dacl);
assert!(sddl.starts_with("D:"));
assert!(sddl.contains("(A;;0x120089;;;S-1-5-32-545)"));
assert!(sddl.contains("(D;OICI;0x2;;;S-1-5-32-545)"));
}
#[test]
fn descriptor_to_sddl_includes_owner_and_group_when_present() {
let owner = Sid::parse("S-1-5-18").expect("valid owner sid");
let group = Sid::parse("S-1-5-32-544").expect("valid group sid");
let descriptor = SecurityDescriptor::for_file_path("C:\\temp\\x.txt")
.with_owner(owner)
.with_group(group)
.with_dacl(Dacl::new());
let sddl = descriptor_to_sddl(&descriptor);
assert!(sddl.contains("O:S-1-5-18"));
assert!(sddl.contains("G:S-1-5-32-544"));
assert!(sddl.contains("D:"));
}
#[test]
fn parse_dacl_from_sddl_round_trips_basic_aces() {
let dacl =
parse_dacl_from_sddl("D:(A;;0x120089;;;S-1-5-32-545)(D;OICI;0x2;;;S-1-5-32-545)")
.expect("parse dacl");
assert_eq!(dacl.entries().len(), 2);
}
#[test]
fn sddl_to_descriptor_parses_owner_group_and_dacl() {
let descriptor = sddl_to_descriptor("C:\\temp\\x.txt", "O:SYG:BAD:(A;;0x120089;;;BU)")
.expect("descriptor parse");
assert!(descriptor.owner().is_some());
assert!(descriptor.group().is_some());
assert_eq!(descriptor.dacl().entries().len(), 1);
}
#[test]
fn parse_dacl_marks_inherited_entries() {
let dacl =
parse_dacl_from_sddl("D:(A;OICIID;0x120089;;;S-1-5-32-545)").expect("parse dacl");
assert_eq!(dacl.entries().len(), 1);
assert!(dacl.entries()[0].inherited);
assert!(dacl.entries()[0].inheritance.object_inherit);
assert!(dacl.entries()[0].inheritance.container_inherit);
}
#[test]
fn parse_dacl_skips_unknown_trustee_alias() {
let dacl =
parse_dacl_from_sddl("D:(A;;0x120089;;;BU)(A;;0x120089;;;ZZ)").expect("parse dacl");
assert_eq!(dacl.entries().len(), 1);
}
#[test]
fn map_file_security_status_reports_privilege_not_held_as_access_denied() {
let err = map_file_security_status::<()>("C:\\temp\\x.txt", "SetNamedSecurityInfoW", 1314)
.expect_err("expected privilege error");
let message = err.to_string();
assert!(message.contains("required privilege is not held"));
}
}