Files
Elydora-Python-SDK/elydora/plugins/_windows_security.py
T
雷电芽衣 ed34831c20 chore(python): audit plugins shared modules
_managed_files owns the file snapshot reader and public metadata helpers used by pinned directories, durable copies, and opaque snapshots; transaction staging owns read_target and same_identity_and_contents; dead helpers removed; hook_template no longer re-exports the guard generator; mypy targets win32 so _windows_security type-checks.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-02 23:16:15 -04:00

492 lines
17 KiB
Python

"""Windows owner-only security boundary for durable transaction state."""
from __future__ import annotations
import ctypes
from ctypes import wintypes
from typing import Any, Tuple
_kernel32: Any = ctypes.WinDLL("kernel32", use_last_error=True)
_advapi32: Any = ctypes.WinDLL("advapi32", use_last_error=True)
_INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
_ERROR_ALREADY_EXISTS = 183
_TOKEN_QUERY = 0x0008
_TOKEN_USER = 1
_SE_FILE_OBJECT = 1
_OWNER_SECURITY_INFORMATION = 0x00000001
_DACL_SECURITY_INFORMATION = 0x00000004
_PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000
_SE_DACL_PROTECTED = 0x1000
_ACL_SIZE_INFORMATION_CLASS = 2
_ACCESS_ALLOWED_ACE_TYPE = 0
_OBJECT_INHERIT_ACE = 0x01
_CONTAINER_INHERIT_ACE = 0x02
_FILE_ALL_ACCESS = 0x001F01FF
_SDDL_REVISION_1 = 1
_READ_CONTROL = 0x00020000
_WRITE_DAC = 0x00040000
_FILE_READ_ATTRIBUTES = 0x0080
_GENERIC_READ = 0x80000000
_GENERIC_WRITE = 0x40000000
_FILE_SHARE_READ = 0x00000001
_FILE_SHARE_WRITE = 0x00000002
_OPEN_EXISTING = 3
_OPEN_ALWAYS = 4
_FILE_ATTRIBUTE_DIRECTORY = 0x00000010
_FILE_ATTRIBUTE_NORMAL = 0x00000080
_FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
_FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
_FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
_FILE_TYPE_DISK = 1
class _SecurityAttributes(ctypes.Structure):
_fields_ = [
("nLength", wintypes.DWORD),
("lpSecurityDescriptor", wintypes.LPVOID),
("bInheritHandle", wintypes.BOOL),
]
class _SidAndAttributes(ctypes.Structure):
_fields_ = [("Sid", wintypes.LPVOID), ("Attributes", wintypes.DWORD)]
class _TokenUser(ctypes.Structure):
_fields_ = [("User", _SidAndAttributes)]
class _AclSizeInformation(ctypes.Structure):
_fields_ = [
("AceCount", wintypes.DWORD),
("AclBytesInUse", wintypes.DWORD),
("AclBytesFree", wintypes.DWORD),
]
class _AceHeader(ctypes.Structure):
_fields_ = [
("AceType", wintypes.BYTE),
("AceFlags", wintypes.BYTE),
("AceSize", wintypes.WORD),
]
class _AccessAllowedAce(ctypes.Structure):
_fields_ = [
("Header", _AceHeader),
("Mask", wintypes.DWORD),
("SidStart", wintypes.DWORD),
]
class _ByHandleFileInformation(ctypes.Structure):
_fields_ = [
("dwFileAttributes", wintypes.DWORD),
("ftCreationTime", wintypes.FILETIME),
("ftLastAccessTime", wintypes.FILETIME),
("ftLastWriteTime", wintypes.FILETIME),
("dwVolumeSerialNumber", wintypes.DWORD),
("nFileSizeHigh", wintypes.DWORD),
("nFileSizeLow", wintypes.DWORD),
("nNumberOfLinks", wintypes.DWORD),
("nFileIndexHigh", wintypes.DWORD),
("nFileIndexLow", wintypes.DWORD),
]
def _declare(function: Any, arguments: list[Any], result: Any) -> None:
function.argtypes = arguments
function.restype = result
_declare(_kernel32.GetCurrentProcess, [], wintypes.HANDLE)
_declare(_advapi32.OpenProcessToken, [wintypes.HANDLE, wintypes.DWORD,
ctypes.POINTER(wintypes.HANDLE)], wintypes.BOOL)
_declare(_advapi32.GetTokenInformation,
[wintypes.HANDLE, ctypes.c_int, wintypes.LPVOID, wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD)], wintypes.BOOL)
_declare(_advapi32.GetLengthSid, [wintypes.LPVOID], wintypes.DWORD)
_declare(_advapi32.CopySid,
[wintypes.DWORD, wintypes.LPVOID, wintypes.LPVOID], wintypes.BOOL)
_declare(_advapi32.ConvertSidToStringSidW,
[wintypes.LPVOID, ctypes.POINTER(wintypes.LPWSTR)], wintypes.BOOL)
_declare(_advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW,
[wintypes.LPCWSTR, wintypes.DWORD, ctypes.POINTER(wintypes.LPVOID),
ctypes.POINTER(wintypes.DWORD)], wintypes.BOOL)
_declare(_advapi32.GetSecurityDescriptorDacl,
[wintypes.LPVOID, ctypes.POINTER(wintypes.BOOL),
ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.BOOL)],
wintypes.BOOL)
_declare(_advapi32.GetSecurityDescriptorControl,
[wintypes.LPVOID, ctypes.POINTER(wintypes.WORD),
ctypes.POINTER(wintypes.DWORD)], wintypes.BOOL)
_declare(
_advapi32.GetSecurityInfo,
[wintypes.HANDLE, ctypes.c_int, wintypes.DWORD,
ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.LPVOID),
ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.LPVOID),
ctypes.POINTER(wintypes.LPVOID)],
wintypes.DWORD,
)
_declare(
_advapi32.SetSecurityInfo,
[wintypes.HANDLE, ctypes.c_int, wintypes.DWORD, wintypes.LPVOID,
wintypes.LPVOID, wintypes.LPVOID, wintypes.LPVOID],
wintypes.DWORD,
)
_declare(_advapi32.GetAclInformation,
[wintypes.LPVOID, wintypes.LPVOID, wintypes.DWORD, ctypes.c_int],
wintypes.BOOL)
_declare(_advapi32.GetAce,
[wintypes.LPVOID, wintypes.DWORD, ctypes.POINTER(wintypes.LPVOID)],
wintypes.BOOL)
_declare(_advapi32.EqualSid, [wintypes.LPVOID, wintypes.LPVOID], wintypes.BOOL)
_declare(_kernel32.LocalFree, [wintypes.HLOCAL], wintypes.HLOCAL)
_declare(_kernel32.CloseHandle, [wintypes.HANDLE], wintypes.BOOL)
_declare(_kernel32.CreateDirectoryW,
[wintypes.LPCWSTR, ctypes.POINTER(_SecurityAttributes)], wintypes.BOOL)
_declare(_kernel32.CreateFileW,
[wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
ctypes.POINTER(_SecurityAttributes), wintypes.DWORD, wintypes.DWORD,
wintypes.HANDLE], wintypes.HANDLE)
_declare(_kernel32.GetFileType, [wintypes.HANDLE], wintypes.DWORD)
_declare(_kernel32.GetFileInformationByHandle,
[wintypes.HANDLE, ctypes.POINTER(_ByHandleFileInformation)], wintypes.BOOL)
def _winerror(action: str, code: int | None = None) -> OSError:
error = ctypes.get_last_error() if code is None else code
return OSError(f"{action}: {ctypes.WinError(error)}")
def close_handle(handle: int) -> None:
if not _kernel32.CloseHandle(handle):
raise _winerror("Close transaction state handle")
def _current_user_sid() -> Tuple[Any, str]:
token = wintypes.HANDLE()
if not _advapi32.OpenProcessToken(
_kernel32.GetCurrentProcess(), _TOKEN_QUERY, ctypes.byref(token)
):
raise _winerror("Open current process token")
try:
required = wintypes.DWORD()
_advapi32.GetTokenInformation(
token, _TOKEN_USER, None, 0, ctypes.byref(required)
)
token_data = ctypes.create_string_buffer(required.value)
if not _advapi32.GetTokenInformation(
token,
_TOKEN_USER,
token_data,
required,
ctypes.byref(required),
):
raise _winerror("Read current process token")
sid = ctypes.cast(token_data, ctypes.POINTER(_TokenUser)).contents.User.Sid
sid_length = _advapi32.GetLengthSid(sid)
if not sid_length:
raise _winerror("Measure current user SID")
sid_copy = ctypes.create_string_buffer(sid_length)
if not _advapi32.CopySid(sid_length, sid_copy, sid):
raise _winerror("Copy current user SID")
sid_text = wintypes.LPWSTR()
if not _advapi32.ConvertSidToStringSidW(sid_copy, ctypes.byref(sid_text)):
raise _winerror("Format current user SID")
try:
sid_string = sid_text.value
if sid_string is None:
raise OSError("Current user SID string is unavailable")
return sid_copy, sid_string
finally:
_kernel32.LocalFree(sid_text)
finally:
token_value = token.value
if token_value is not None:
close_handle(int(token_value))
def _security_descriptor(directory: bool) -> Tuple[int, int]:
_sid, sid_text = _current_user_sid()
inheritance = "OICI" if directory else ""
descriptor = wintypes.LPVOID()
sddl = f"O:{sid_text}D:P(A;{inheritance};FA;;;{sid_text})"
if not _advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl,
_SDDL_REVISION_1,
ctypes.byref(descriptor),
None,
):
raise _winerror("Build transaction security descriptor")
present = wintypes.BOOL()
defaulted = wintypes.BOOL()
dacl = wintypes.LPVOID()
if not _advapi32.GetSecurityDescriptorDacl(
descriptor,
ctypes.byref(present),
ctypes.byref(dacl),
ctypes.byref(defaulted),
):
_kernel32.LocalFree(descriptor)
raise _winerror("Read transaction security descriptor")
if not present.value or not dacl.value:
_kernel32.LocalFree(descriptor)
raise OSError("Transaction security descriptor has no DACL")
descriptor_value = descriptor.value
dacl_value = dacl.value
if descriptor_value is None or dacl_value is None:
_kernel32.LocalFree(descriptor)
raise OSError("Transaction security descriptor has no DACL")
return int(descriptor_value), int(dacl_value)
def _file_information(handle: int, directory: bool) -> _ByHandleFileInformation:
if _kernel32.GetFileType(handle) != _FILE_TYPE_DISK:
raise OSError("Transaction state path is not a disk file")
information = _ByHandleFileInformation()
if not _kernel32.GetFileInformationByHandle(handle, ctypes.byref(information)):
raise _winerror("Inspect transaction state path")
attributes = information.dwFileAttributes
expected_directory = bool(attributes & _FILE_ATTRIBUTE_DIRECTORY)
if attributes & _FILE_ATTRIBUTE_REPARSE_POINT or expected_directory != directory:
kind = "directory" if directory else "file"
raise OSError(f"Transaction state path is not a physical {kind}")
return information
def _identity(information: _ByHandleFileInformation) -> Tuple[int, int, int]:
return (
int(information.dwVolumeSerialNumber),
int(information.nFileIndexHigh),
int(information.nFileIndexLow),
)
def _open_path(
path: str,
directory: bool,
access: int,
disposition: int,
security: _SecurityAttributes | None = None,
) -> int:
flags = _FILE_FLAG_OPEN_REPARSE_POINT
if directory:
flags |= _FILE_FLAG_BACKUP_SEMANTICS
else:
flags |= _FILE_ATTRIBUTE_NORMAL
security_pointer = None if security is None else ctypes.byref(security)
handle = _kernel32.CreateFileW(
path,
access,
_FILE_SHARE_READ | _FILE_SHARE_WRITE,
security_pointer,
disposition,
flags,
None,
)
if int(handle) == _INVALID_HANDLE_VALUE:
raise _winerror("Open transaction state path")
return int(handle)
def _verify_owner_and_dacl(handle: int, directory: bool) -> None:
current_sid, _sid_text = _current_user_sid()
owner = wintypes.LPVOID()
dacl = wintypes.LPVOID()
descriptor = wintypes.LPVOID()
result = _advapi32.GetSecurityInfo(
handle,
_SE_FILE_OBJECT,
_OWNER_SECURITY_INFORMATION | _DACL_SECURITY_INFORMATION,
ctypes.byref(owner),
None,
ctypes.byref(dacl),
None,
ctypes.byref(descriptor),
)
if result:
raise _winerror("Read transaction state security", int(result))
try:
if not owner.value or not _advapi32.EqualSid(owner, current_sid):
raise PermissionError("Transaction state path is not owned by current user")
control = wintypes.WORD()
revision = wintypes.DWORD()
if not _advapi32.GetSecurityDescriptorControl(
descriptor, ctypes.byref(control), ctypes.byref(revision)
):
raise _winerror("Read transaction DACL control")
if not control.value & _SE_DACL_PROTECTED or not dacl.value:
raise PermissionError("Transaction state DACL is not protected owner-only")
size = _AclSizeInformation()
if not _advapi32.GetAclInformation(
dacl,
ctypes.byref(size),
ctypes.sizeof(size),
_ACL_SIZE_INFORMATION_CLASS,
):
raise _winerror("Inspect transaction DACL")
if size.AceCount != 1:
raise PermissionError("Transaction state DACL is not protected owner-only")
ace_pointer = wintypes.LPVOID()
if not _advapi32.GetAce(dacl, 0, ctypes.byref(ace_pointer)):
raise _winerror("Read transaction DACL entry")
ace = ctypes.cast(ace_pointer, ctypes.POINTER(_AccessAllowedAce)).contents
expected_flags = (
_OBJECT_INHERIT_ACE | _CONTAINER_INHERIT_ACE if directory else 0
)
ace_address = ace_pointer.value
if ace_address is None:
raise OSError("Transaction DACL entry is unavailable")
sid_pointer = ctypes.c_void_p(
int(ace_address) + _AccessAllowedAce.SidStart.offset
)
if (
ace.Header.AceType != _ACCESS_ALLOWED_ACE_TYPE
or ace.Header.AceFlags != expected_flags
or ace.Mask != _FILE_ALL_ACCESS
or not _advapi32.EqualSid(sid_pointer, current_sid)
):
raise PermissionError("Transaction state DACL is not protected owner-only")
finally:
_kernel32.LocalFree(descriptor)
def _apply_private_dacl(handle: int, directory: bool) -> None:
current_sid, _sid_text = _current_user_sid()
owner = wintypes.LPVOID()
descriptor = wintypes.LPVOID()
result = _advapi32.GetSecurityInfo(
handle,
_SE_FILE_OBJECT,
_OWNER_SECURITY_INFORMATION,
ctypes.byref(owner),
None,
None,
None,
ctypes.byref(descriptor),
)
if result:
raise _winerror("Read transaction state owner", int(result))
try:
if not owner.value or not _advapi32.EqualSid(owner, current_sid):
raise PermissionError("Transaction state path is not owned by current user")
finally:
_kernel32.LocalFree(descriptor)
security_descriptor, dacl = _security_descriptor(directory)
try:
result = _advapi32.SetSecurityInfo(
handle,
_SE_FILE_OBJECT,
_DACL_SECURITY_INFORMATION | _PROTECTED_DACL_SECURITY_INFORMATION,
None,
None,
dacl,
None,
)
if result:
raise _winerror("Protect transaction state DACL", int(result))
finally:
_kernel32.LocalFree(security_descriptor)
_verify_owner_and_dacl(handle, directory)
def _verify_path_identity(path: str, handle: int, directory: bool) -> None:
expected = _identity(_file_information(handle, directory))
visible = _open_path(
path,
directory,
_READ_CONTROL | _FILE_READ_ATTRIBUTES,
_OPEN_EXISTING,
)
try:
if _identity(_file_information(visible, directory)) != expected:
raise OSError("Transaction state path changed while opening")
finally:
close_handle(visible)
def ensure_private_directory(path: str) -> None:
"""Create or harden one physical current-user transaction directory."""
descriptor, _dacl = _security_descriptor(True)
security = _SecurityAttributes(
ctypes.sizeof(_SecurityAttributes),
descriptor,
False,
)
try:
if not _kernel32.CreateDirectoryW(path, ctypes.byref(security)):
error = ctypes.get_last_error()
if error != _ERROR_ALREADY_EXISTS:
raise _winerror("Create transaction state directory", error)
finally:
_kernel32.LocalFree(descriptor)
handle = _open_path(
path,
True,
_READ_CONTROL | _WRITE_DAC | _FILE_READ_ATTRIBUTES,
_OPEN_EXISTING,
)
try:
_file_information(handle, True)
_apply_private_dacl(handle, True)
_verify_path_identity(path, handle, True)
finally:
close_handle(handle)
def open_private_file(path: str) -> int:
"""Open one physical current-user file with a protected owner-only DACL."""
descriptor, _dacl = _security_descriptor(False)
security = _SecurityAttributes(
ctypes.sizeof(_SecurityAttributes),
descriptor,
False,
)
try:
handle = _open_path(
path,
False,
_GENERIC_READ | _GENERIC_WRITE | _READ_CONTROL | _WRITE_DAC,
_OPEN_ALWAYS,
security,
)
finally:
_kernel32.LocalFree(descriptor)
try:
_file_information(handle, False)
_apply_private_dacl(handle, False)
_verify_path_identity(path, handle, False)
except Exception:
close_handle(handle)
raise
return handle
def verify_private_path(path: str, *, directory: bool) -> None:
"""Verify a physical current-user path has the exact private DACL contract."""
handle = _open_path(
path,
directory,
_READ_CONTROL | _FILE_READ_ATTRIBUTES,
_OPEN_EXISTING,
)
try:
_file_information(handle, directory)
_verify_owner_and_dacl(handle, directory)
_verify_path_identity(path, handle, directory)
finally:
close_handle(handle)
__all__ = [
"close_handle",
"ensure_private_directory",
"open_private_file",
"verify_private_path",
]