diff --git a/src/utils.cxx b/src/utils.cxx index 76f9783..25809b4 100644 --- a/src/utils.cxx +++ b/src/utils.cxx @@ -4,6 +4,8 @@ * SPDX-License-Identifier: (Apache-2.0 OR MIT) */ #include "utils.h" +#include +#include #include #include #include @@ -12,6 +14,8 @@ #include #include #include +#include +#include #include #include #include @@ -926,6 +930,253 @@ char* findstr(char* search_str, const char* substr, size_t size) { return nullptr; } +ScopedSid FileSecurity::GetCurrentUserSid() { + HANDLE token_handle = nullptr; + if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, + &token_handle)) { + return nullptr; + } + std::unique_ptr const scoped_token( + token_handle, &::CloseHandle); + + DWORD buffer_size = 0; + ::GetTokenInformation(token_handle, TokenUser, nullptr, 0, &buffer_size); + + std::vector buffer(buffer_size); + auto* token_user = reinterpret_cast(buffer.data()); + + if (!::GetTokenInformation(token_handle, TokenUser, token_user, buffer_size, + &buffer_size)) { + return nullptr; + } + + DWORD const sid_len = ::GetLengthSid(token_user->User.Sid); + void* sid_copy = std::malloc(sid_len); + if (sid_copy) { + ::CopySid(sid_len, sid_copy, token_user->User.Sid); + return ScopedSid(sid_copy); + } + return nullptr; +} + +/** + * Determines whether a given file has a given set of file permissions + * \param file to check permissions for + * \param access_mask type of permissions to check for + * \param sid user identifier to check for permissions in the context of + * + */ +bool FileSecurity::AclHasAccess(const std::wstring& file_path, + DWORD access_mask, PSID sid) { + PACL dacl = nullptr; + PSECURITY_DESCRIPTOR sd_raw = nullptr; + DWORD result = ::GetNamedSecurityInfoW(file_path.c_str(), SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, nullptr, + nullptr, &dacl, nullptr, &sd_raw); + + if (result != ERROR_SUCCESS) + return false; + ScopedLocalInfo const scoped_sd(sd_raw); + + TRUSTEE_W trustee = {nullptr}; + trustee.TrusteeForm = TRUSTEE_IS_SID; + trustee.TrusteeType = TRUSTEE_IS_USER; + trustee.ptstrName = static_cast(sid); + + ACCESS_MASK effective_rights = 0; + result = ::GetEffectiveRightsFromAclW(dacl, &trustee, &effective_rights); + + if (result != ERROR_SUCCESS) + return false; + + if ((access_mask & GENERIC_WRITE) && (effective_rights & FILE_WRITE_DATA)) + return true; + if ((access_mask & GENERIC_READ) && (effective_rights & FILE_READ_DATA)) + return true; + if ((access_mask & GENERIC_ALL) && (effective_rights & FILE_ALL_ACCESS)) + return true; + + return (effective_rights & access_mask) == access_mask; +} + +/** + * \param file_path path to the file for which we are granting a permission + * \param access_mask the bitmask for the ACE permissions being requested + * \param sid pointer to the security identifier for a given trustee (typically the current user) + * \param out_old_sid output param, pointer to variable containing pointer to the pre-modified DACL + * useful to determine whether the security descriptor has been modified and + * provide a baseline sid + */ +bool FileSecurity::AddAccessControlEntry(const std::wstring& file_path, + DWORD access_mask, PSID sid, + PSECURITY_DESCRIPTOR* out_old_sd) { + PACL old_dacl = nullptr; + PSECURITY_DESCRIPTOR sd_raw = nullptr; + + DWORD result = ::GetNamedSecurityInfoW( + file_path.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, + nullptr, &old_dacl, nullptr, &sd_raw); + + if (result != ERROR_SUCCESS) + return false; + + if (out_old_sd) + *out_old_sd = sd_raw; + ScopedLocalInfo const temp_sd_wrapper(out_old_sd ? nullptr : sd_raw); + + EXPLICIT_ACCESS_W ea = {0}; + ea.grfAccessPermissions = access_mask; + ea.grfAccessMode = GRANT_ACCESS; + ea.grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.TrusteeType = TRUSTEE_IS_USER; + ea.Trustee.ptstrName = static_cast(sid); + + PACL new_dacl = nullptr; + result = ::SetEntriesInAclW(1, &ea, old_dacl, &new_dacl); + if (result != ERROR_SUCCESS) + return false; + + ScopedLocalInfo const scoped_new_dacl(new_dacl); + result = ::SetNamedSecurityInfoW(const_cast(file_path.c_str()), + SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, + nullptr, nullptr, new_dacl, nullptr); + return (result == ERROR_SUCCESS); +} + +/** + * Applies security descriptor to file + * \param file_path file to apply SD to + * \param sd security descriptor to apply to file + */ +bool FileSecurity::SetAclFromDescriptor(const std::wstring& file_path, + PSECURITY_DESCRIPTOR sd) { + if (!sd) + return false; + BOOL present = FALSE; + BOOL defaulted = FALSE; + PACL dacl = nullptr; + if (!::GetSecurityDescriptorDacl(sd, &present, &dacl, &defaulted) || + !present || !dacl) + return false; + + return ::SetNamedSecurityInfoW(const_cast(file_path.c_str()), + SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, + nullptr, nullptr, dacl, + nullptr) == ERROR_SUCCESS; +} + +/** + * Obtain current file attributes for file indicated by file path + * \param file_path file to obtain permissions for + * \param out_attr output parameter to hold permissions + */ +bool FileSecurity::GetAttributes(const std::wstring& file_path, + DWORD* out_attr) { + DWORD const attr = ::GetFileAttributesW(file_path.c_str()); + if (attr == INVALID_FILE_ATTRIBUTES) + return false; + if (out_attr) + *out_attr = attr; + return true; +} + +/** + * Set file attribute attr on file indicated by file_path + * \param file_path file to set attributes for + * \param attr attributes to obtain + */ +bool FileSecurity::SetAttributes(const std::wstring& file_path, DWORD attr) { + return ::SetFileAttributesW(file_path.c_str(), attr) != 0; +} + +/** + * Construct FileAccess object + * \param file_path file on which we want to obtain permissions + * \param desired_acess access control permissions we want to obtain for the file + */ +ScopedFileAccess::ScopedFileAccess(std::wstring file_path, DWORD desired_access) + : file_path_(std::move(file_path)), + desired_access_(desired_access), + original_sd_(nullptr), + current_user_sid_(nullptr), + acl_needs_revert_(false), + original_attributes_(0), + attributes_changed_(false) { +} + +/** + * Obtain permissions established by constructor + * + */ +void ScopedFileAccess::Access() { + + + // We must ensure we have permissions *first* before we try to + // change the file attributes in Phase 2. + + current_user_sid_ = FileSecurity::GetCurrentUserSid(); + if (!current_user_sid_) { + throw std::system_error(static_cast(::GetLastError()), + std::system_category(), "Failed to get SID"); + } + + // Check if we need to modify ACLs + if (!FileSecurity::AclHasAccess(file_path_, desired_access_, + current_user_sid_.get())) { + if (!FileSecurity::AddAccessControlEntry(file_path_, desired_access_, + current_user_sid_.get(), + &original_sd_)) { + throw std::system_error(static_cast(::GetLastError()), + std::system_category(), + "Failed to grant ACL"); + } + acl_needs_revert_ = true; + } + + if (FileSecurity::GetAttributes(file_path_, &original_attributes_)) { + if (original_attributes_ & FILE_ATTRIBUTE_READONLY) { + // Remove the Read-Only bit + DWORD const new_attributes = + original_attributes_ & ~FILE_ATTRIBUTE_READONLY; + + if (FileSecurity::SetAttributes(file_path_, new_attributes)) { + attributes_changed_ = true; + } else { + // If we fail to remove Read-Only, we might still fail to write later. + // We throw here to be safe and consistent. + throw std::system_error(static_cast(::GetLastError()), + std::system_category(), + "Failed to remove Read-Only attribute"); + } + } + } else { + throw std::system_error(static_cast(::GetLastError()), + std::system_category(), + "Failed to get file attributes"); + } +} + +ScopedFileAccess::~ScopedFileAccess() { + // We must restore attributes *before* we revert ACLs, because reverting ACLs + // might remove our permission to write attributes. + if (attributes_changed_) { + // We ignore errors in destructors to prevent termination + FileSecurity::SetAttributes(file_path_, original_attributes_); + } + + if (acl_needs_revert_ && original_sd_) { + FileSecurity::SetAclFromDescriptor(file_path_, original_sd_); + ::LocalFree(original_sd_); + } +} + +bool ScopedFileAccess::IsAccessGranted() const { + + return FileSecurity::AclHasAccess(file_path_, desired_access_, + current_user_sid_.get()); +} + NameTooLongError::NameTooLongError(char const* const message) : std::runtime_error(message) {} diff --git a/src/utils.h b/src/utils.h index 5dda860..f0eb71b 100644 --- a/src/utils.h +++ b/src/utils.h @@ -234,6 +234,21 @@ int SafeHandleCleanup(HANDLE& handle); // System Helpers // std::string reportLastError(); +struct LocalFreeDeleter { + void operator()(void* p) const { + if (p) + ::LocalFree(p); + } +}; + +// Custom deleter for Standard C pointers. +struct FreeDeleter { + void operator()(void* p) const { + if (p) + std::free(p); + } +}; + // Data helpers // // Converts big endian data to little endian form @@ -278,6 +293,58 @@ class LibraryFinder { void EvalSearchPaths(); }; +using ScopedLocalInfo = std::unique_ptr; + +using ScopedSid = std::unique_ptr; + +class FileSecurity { + public: + FileSecurity() = delete; + + static ScopedSid GetCurrentUserSid(); + + static bool AclHasAccess(const std::wstring& file_path, DWORD access_mask, + PSID sid); + + static bool AddAccessControlEntry(const std::wstring& file_path, + DWORD access_mask, PSID sid, + PSECURITY_DESCRIPTOR* out_old_sd); + + static bool SetAclFromDescriptor(const std::wstring& file_path, + PSECURITY_DESCRIPTOR sd); + + // Retrieves file attributes (e.g., ReadOnly, Hidden). + // Returns false if the file cannot be accessed. + static bool GetAttributes(const std::wstring& file_path, DWORD* out_attr); + + // Sets file attributes. + // Returns false if the operation fails. + static bool SetAttributes(const std::wstring& file_path, DWORD attr); +}; + +class ScopedFileAccess { + public: + explicit ScopedFileAccess(std::wstring file_path, + DWORD desired_access = GENERIC_WRITE); + ~ScopedFileAccess(); + void Access(); + + bool IsAccessGranted() const; + + private: + std::wstring file_path_; + DWORD desired_access_; + + // ACL State + PSECURITY_DESCRIPTOR original_sd_; + ScopedSid current_user_sid_; + bool acl_needs_revert_; + + // Attribute State + DWORD original_attributes_; + bool attributes_changed_; +}; + const std::map special_character_to_path{{'|', '\\'}, {';', ':'}}; const std::map path_to_special_characters{{'\\', '|'},