diff --git a/src/Files.App/Data/Contracts/IStorageSecurityService.cs b/src/Files.App/Data/Contracts/IStorageSecurityService.cs new file mode 100644 index 000000000000..a408dda75983 --- /dev/null +++ b/src/Files.App/Data/Contracts/IStorageSecurityService.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Windows.Win32.Foundation; + +namespace Files.App.Data.Contracts +{ + /// + /// Provides service to manage storage security objects on NTFS and ReFS. + /// + public interface IStorageSecurityService + { + /// + /// Get the owner of the object specified by the path. + /// + /// The file full path + /// The SID string of the owner + string GetOwner(string path); + + /// + /// Set the owner of the object specified by the path. + /// + /// The file full path + /// The owner security identifier (SID) + /// + bool SetOwner(string path, string sid); + + /// + /// Get information about an access control list (ACL). + /// + /// + /// + /// If the function succeeds, an instance of AccessControlList; otherwise, null. To get extended error information, call GetLastError. + WIN32_ERROR GetAcl(string path, bool isFolder, out AccessControlList acl); + + /// + /// Add an default Access Control Entry (ACE) to the specified object's DACL + /// + /// The object's path to add an new ACE to its DACL + /// Principal's SID + /// If the function succeeds, the return value is ERROR_SUCCESS. If the function fails, the return value is a nonzero error code defined in WinError.h. + WIN32_ERROR AddAce(string szPath, bool isFolder, string szSid); + + /// + /// Add an Access Control Entry (ACE) from the specified object's DACL + /// + /// The object's path to remove an ACE from its DACL + /// + /// + WIN32_ERROR DeleteAce(string szPath, uint dwAceIndex); + } +} diff --git a/src/Files.App/Utils/Storage/Security/AccessControlEntryFlags.cs b/src/Files.App/Data/Enums/AccessControlEntryFlags.cs similarity index 97% rename from src/Files.App/Utils/Storage/Security/AccessControlEntryFlags.cs rename to src/Files.App/Data/Enums/AccessControlEntryFlags.cs index 51fce3ab9560..1f11ab3ffd4d 100644 --- a/src/Files.App/Utils/Storage/Security/AccessControlEntryFlags.cs +++ b/src/Files.App/Data/Enums/AccessControlEntryFlags.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Enums { /// /// Represents inheritance flags of an ACE diff --git a/src/Files.App/Utils/Storage/Security/AccessControlEntryType.cs b/src/Files.App/Data/Enums/AccessControlEntryType.cs similarity index 90% rename from src/Files.App/Utils/Storage/Security/AccessControlEntryType.cs rename to src/Files.App/Data/Enums/AccessControlEntryType.cs index 3d1307a6423c..ee3ecc6076f2 100644 --- a/src/Files.App/Utils/Storage/Security/AccessControlEntryType.cs +++ b/src/Files.App/Data/Enums/AccessControlEntryType.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Enums { /// /// Represents ACE type. diff --git a/src/Files.App/Utils/Storage/Security/PrincipalType.cs b/src/Files.App/Data/Enums/AccessControlPrincipalType.cs similarity index 78% rename from src/Files.App/Utils/Storage/Security/PrincipalType.cs rename to src/Files.App/Data/Enums/AccessControlPrincipalType.cs index 85d926b0999f..1af4946348c4 100644 --- a/src/Files.App/Utils/Storage/Security/PrincipalType.cs +++ b/src/Files.App/Data/Enums/AccessControlPrincipalType.cs @@ -1,15 +1,15 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Enums { /// /// Represents an ACL owner or an ACE principal type /// - public enum PrincipalType + public enum AccessControlPrincipalType { /// - /// Unknwon principal type + /// Unknown principal type /// Unknown, diff --git a/src/Files.App/Utils/Storage/Security/AccessMaskFlags.cs b/src/Files.App/Data/Enums/AccessMaskFlags.cs similarity index 98% rename from src/Files.App/Utils/Storage/Security/AccessMaskFlags.cs rename to src/Files.App/Data/Enums/AccessMaskFlags.cs index d8059aec6b95..9b66811708b9 100644 --- a/src/Files.App/Utils/Storage/Security/AccessMaskFlags.cs +++ b/src/Files.App/Data/Enums/AccessMaskFlags.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Enums { /// /// Represents access mask flags of an ACE. diff --git a/src/Files.App/Utils/Storage/Security/AccessControlEntry.cs b/src/Files.App/Data/Items/AccessControlEntry.cs similarity index 92% rename from src/Files.App/Utils/Storage/Security/AccessControlEntry.cs rename to src/Files.App/Data/Items/AccessControlEntry.cs index d88d2b775353..7462fc29a312 100644 --- a/src/Files.App/Utils/Storage/Security/AccessControlEntry.cs +++ b/src/Files.App/Data/Items/AccessControlEntry.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Items { /// /// Represents an access control entry (ACE). @@ -17,7 +17,7 @@ public sealed class AccessControlEntry : ObservableObject /// The owner in the security descriptor (SD). /// NULL if the security descriptor has no owner SID. /// - public Principal Principal { get; set; } + public AccessControlPrincipal Principal { get; set; } /// /// Whether the ACE is inherited or not @@ -336,16 +336,16 @@ public AccessControlEntry(bool isFolder, string ownerSid, AccessControlEntryType { AccessMaskItems = SecurityAdvancedAccessControlItemFactory.Initialize(this, AreAdvancedPermissionsShown, IsInherited, IsFolder); - //ChangeAccessControlTypeCommand = new RelayCommand(x => - //{ - // AccessControlType = Enum.Parse(x); - //}); + ChangeAccessControlTypeCommand = new RelayCommand(x => + { + //AccessControlType = Enum.Parse(x); + }); - //ChangeInheritanceFlagsCommand = new RelayCommand(x => - //{ - // var parts = x.Split(','); - // InheritanceFlags = Enum.Parse(parts[0]); - //}); + ChangeInheritanceFlagsCommand = new RelayCommand(x => + { + //var parts = x.Split(','); + //InheritanceFlags = Enum.Parse(parts[0]); + }); IsFolder = isFolder; Principal = new(ownerSid); @@ -402,5 +402,24 @@ private void ToggleDenyAccess(AccessMaskFlags accessMask, bool value) DeniedAccessMaskFlags &= ~accessMask; } } + + /// + /// Gets access control list (ACL) initialized with default data. + /// + /// + /// + /// If the function succeeds, an instance of AccessControlList; otherwise, null. + public static AccessControlEntry GetDefault(bool isFolder, string ownerSid) + { + return new( + isFolder, + ownerSid, + AccessControlEntryType.Allow, + AccessMaskFlags.ReadAndExecute, + false, + isFolder + ? AccessControlEntryFlags.ContainerInherit | AccessControlEntryFlags.ObjectInherit + : AccessControlEntryFlags.None); + } } } diff --git a/src/Files.App/Utils/Storage/Security/AccessControlList.cs b/src/Files.App/Data/Items/AccessControlList.cs similarity index 85% rename from src/Files.App/Utils/Storage/Security/AccessControlList.cs rename to src/Files.App/Data/Items/AccessControlList.cs index 7506293da517..8e31c0476f52 100644 --- a/src/Files.App/Utils/Storage/Security/AccessControlList.cs +++ b/src/Files.App/Data/Items/AccessControlList.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Items { /// /// Represents an access control list (ACL). @@ -22,7 +22,7 @@ public sealed class AccessControlList : ObservableObject /// The owner in the security descriptor (SD). /// NULL if the security descriptor has no owner SID. /// - public Principal Owner { get; private set; } + public AccessControlPrincipal Owner { get; private set; } /// /// Validates an access control list (ACL). @@ -34,7 +34,7 @@ public sealed class AccessControlList : ObservableObject /// public ObservableCollection AccessControlEntries { get; private set; } - public AccessControlList(string path, bool isFolder, Principal owner, bool isValid) + public AccessControlList(string path, bool isFolder, AccessControlPrincipal owner, bool isValid) { Path = path; IsFolder = isFolder; diff --git a/src/Files.App/Utils/Storage/Security/Principal.cs b/src/Files.App/Data/Items/AccessControlPrincipal.cs similarity index 60% rename from src/Files.App/Utils/Storage/Security/Principal.cs rename to src/Files.App/Data/Items/AccessControlPrincipal.cs index be7367a2033d..9e99f94defb1 100644 --- a/src/Files.App/Utils/Storage/Security/Principal.cs +++ b/src/Files.App/Data/Items/AccessControlPrincipal.cs @@ -1,24 +1,24 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -using System.Text; -using Vanara.PInvoke; -using static Vanara.PInvoke.AdvApi32; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security; -namespace Files.App.Utils.Storage +namespace Files.App.Data.Items { /// /// Represents a principal of an ACE or an owner of an ACL. /// - public sealed class Principal : ObservableObject + public sealed class AccessControlPrincipal : ObservableObject { /// /// Account type. /// - public PrincipalType PrincipalType { get; private set; } + public AccessControlPrincipalType PrincipalType { get; private set; } /// - /// Acount security identifier (SID). + /// Account security identifier (SID). /// public string? Sid { get; private set; } @@ -43,8 +43,8 @@ public sealed class Principal : ObservableObject public string Glyph => PrincipalType switch { - PrincipalType.User => "\xE77B", - PrincipalType.Group => "\xE902", + AccessControlPrincipalType.User => "\xE77B", + AccessControlPrincipalType.Group => "\xE902", _ => "\xE716", }; @@ -66,26 +66,34 @@ public string? FullNameHumanized public string FullNameHumanizedWithBrackes => string.IsNullOrEmpty(Domain) ? string.Empty : $"({Domain}\\{Name})"; - public Principal(string sid) + public unsafe AccessControlPrincipal(string sid) { if (string.IsNullOrEmpty(sid)) return; Sid = sid; - var lpSid = ConvertStringSidToSid(sid); + PSID lpSid = default; + SID_NAME_USE snu = default; - StringBuilder lpName = new(), lpDomain = new(); - int cchName = 0, cchDomainName = 0; + fixed (char* cSid = sid) + PInvoke.ConvertStringSidToSid(new PCWSTR(cSid), &lpSid); + + PWSTR lpName = default; + PWSTR lpDomain = default; + uint cchName = 0, cchDomainName = 0; // Get size of account name and domain name - bool bResult = LookupAccountSid(null, lpSid, lpName, ref cchName, lpDomain, ref cchDomainName, out _); + bool bResult = PInvoke.LookupAccountSid(new PCWSTR(), lpSid, lpName, &cchName, lpDomain, &cchDomainName, null); // Ensure requested capacity - lpName.EnsureCapacity(cchName); - lpDomain.EnsureCapacity(cchDomainName); + fixed (char* cName = new char[cchName]) + lpName = new(cName); + + fixed (char* cDomain = new char[cchDomainName]) + lpDomain = new(cDomain); // Get account name and domain - bResult = LookupAccountSid(null, lpSid, lpName, ref cchName, lpDomain, ref cchDomainName, out var snu); + bResult = PInvoke.LookupAccountSid(new PCWSTR(), lpSid, lpName, &cchName, lpDomain, &cchDomainName, &snu); if(!bResult) return; @@ -96,24 +104,24 @@ var x when (x == SID_NAME_USE.SidTypeAlias || x == SID_NAME_USE.SidTypeGroup || x == SID_NAME_USE.SidTypeWellKnownGroup) - => PrincipalType.Group, + => AccessControlPrincipalType.Group, // User SID_NAME_USE.SidTypeUser - => PrincipalType.User, + => AccessControlPrincipalType.User, // Unknown - _ => PrincipalType.Unknown + _ => AccessControlPrincipalType.Unknown }; - lpDomain.Clear(); - // Replace domain name with computer name if the account type is user or alias type if (snu == SID_NAME_USE.SidTypeUser || snu == SID_NAME_USE.SidTypeAlias) { - lpDomain = new(256, 256); - uint size = (uint)lpDomain.Capacity; - bResult = Kernel32.GetComputerName(lpDomain, ref size); + uint size = 256; + fixed (char* cDomain = new char[size]) + lpDomain = new(cDomain); + + bResult = PInvoke.GetComputerName(lpDomain, ref size); if (!bResult) return; } diff --git a/src/Files.App/Utils/Storage/Security/AccessMaskItem.cs b/src/Files.App/Data/Items/AccessMaskItem.cs similarity index 97% rename from src/Files.App/Utils/Storage/Security/AccessMaskItem.cs rename to src/Files.App/Data/Items/AccessMaskItem.cs index 6d6e9a8c7584..79b15c771bcb 100644 --- a/src/Files.App/Utils/Storage/Security/AccessMaskItem.cs +++ b/src/Files.App/Data/Items/AccessMaskItem.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Storage +namespace Files.App.Data.Items { /// /// Represents an access mask details, such as its name and changeability. diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 530f3991ab4a..4cfced5b98df 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -197,6 +197,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() // ViewModels .AddSingleton() diff --git a/src/Files.App/NativeMethods.json b/src/Files.App/NativeMethods.json index 6355f2e8bbfb..f03b2ad6671d 100644 --- a/src/Files.App/NativeMethods.json +++ b/src/Files.App/NativeMethods.json @@ -1,28 +1,10 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. { "$schema": "https://aka.ms/CsWin32.schema.json", - - // Emit COM interfaces instead of structs, and allow generation of non-blittable structs for the sake of an easier to use API. "allowMarshaling": true, - - // A value indicating whether to generate APIs judged to be unnecessary or redundant given the target framework. - // This is useful for multi-targeting projects that need a consistent set of APIs across target frameworks - // to avoid too many conditional compilation regions. "multiTargetingFriendlyAPIs": false, - - // A value indicating whether friendly overloads should use safe handles. "useSafeHandles": true, - - // Omit ANSI functions and remove `W` suffix from UTF-16 functions. "wideCharOnly": true, - - // A value indicating whether to emit a single source file as opposed to types spread across many files. "emitSingleFile": false, - - // The name of a single class under which all p/invoke methods and constants are generated, regardless of imported module. "className": "PInvoke", - - // A value indicating whether to expose the generated APIs publicly (as opposed to internally). "public": true } diff --git a/src/Files.App/NativeMethods.txt b/src/Files.App/NativeMethods.txt index 07401d489bd7..92b8c114269d 100644 --- a/src/Files.App/NativeMethods.txt +++ b/src/Files.App/NativeMethods.txt @@ -70,3 +70,22 @@ D3D11CreateDevice IDXGIDevice DCompositionCreateDevice IDCompositionDevice +GetNamedSecurityInfo +ConvertSidToStringSid +ConvertStringSidToSid +SetNamedSecurityInfo +GetAclInformation +IsValidAcl +GetAce +SetEntriesInAcl +ACL_SIZE_INFORMATION +DeleteAce +EXPLICIT_ACCESS +ACCESS_ALLOWED_ACE +LookupAccountSid +GetComputerName +AddAccessAllowedAceEx +LocalAlloc +InitializeAcl +AddAce +LocalFree diff --git a/src/Files.App/Services/Storage/StorageSecurityService.cs b/src/Files.App/Services/Storage/StorageSecurityService.cs new file mode 100644 index 000000000000..c2ecf4386836 --- /dev/null +++ b/src/Files.App/Services/Storage/StorageSecurityService.cs @@ -0,0 +1,292 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security; +using Windows.Win32.Security.Authorization; +using Windows.Win32.System.Memory; +using SystemSecurity = System.Security.AccessControl; + +namespace Files.App.Services +{ + /// + public class StorageSecurityService : IStorageSecurityService + { + /// + public unsafe string GetOwner(string path) + { + PInvoke.GetNamedSecurityInfo( + path, + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION, + out var pSidOwner, + out _, + null, + null, + out _); + + PInvoke.ConvertSidToStringSid(pSidOwner, out var sid); + + return sid.ToString(); + } + + /// + public unsafe bool SetOwner(string path, string sid) + { + PSID pSid = default; + + // Get SID + fixed (char* cSid = sid) + PInvoke.ConvertStringSidToSid(new PCWSTR(cSid), &pSid); + + WIN32_ERROR result = default; + + fixed (char* cPath = path) + { + // Change owner + result = PInvoke.SetNamedSecurityInfo( + new PWSTR(cPath), + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION, + pSid, + new PSID((void*)0)); + } + + // Run PowerShell as Admin + if (result is not WIN32_ERROR.ERROR_SUCCESS) + { + return Win32Helper.RunPowershellCommand( + $"-command \"try {{ $path = '{path}'; $ID = new-object System.Security.Principal.SecurityIdentifier('{sid}'); $acl = get-acl $path; $acl.SetOwner($ID); set-acl -path $path -aclObject $acl }} catch {{ exit 1; }}\"", + true); + } + + return true; + } + + /// + public unsafe WIN32_ERROR GetAcl(string path, bool isFolder, out AccessControlList acl) + { + acl = new(); + ACL* pDACL = default; + + // Get DACL + var result = PInvoke.GetNamedSecurityInfo( + path, + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | OBJECT_SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, + out _, + out _, + &pDACL, + null, + out _); + + if (result is not WIN32_ERROR.ERROR_SUCCESS || pDACL == null) + return result; + + ACL_SIZE_INFORMATION aclSizeInfo = default; + + // Get ACL size info + bool bResult = PInvoke.GetAclInformation( + *pDACL, + &aclSizeInfo, + (uint)Marshal.SizeOf(), + ACL_INFORMATION_CLASS.AclSizeInformation); + + if (!bResult) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + // Get owner + var szOwnerSid = GetOwner(path); + var principal = new AccessControlPrincipal(szOwnerSid); + + var isValidAcl = PInvoke.IsValidAcl(pDACL); + + List aces = []; + + // Get ACEs + for (uint i = 0; i < aclSizeInfo.AceCount; i++) + { + bResult = PInvoke.GetAce(*pDACL, i, out var pAce); + if (!bResult) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + if (pAce is null) + continue; + + var ace = Marshal.PtrToStructure((nint)pAce); + + PWSTR pszSid = default; + + var offset = Marshal.SizeOf(typeof(ACE_HEADER)) + sizeof(uint); + + //if (pAce.IsObjectAce()) + // offset += sizeof(uint) + Marshal.SizeOf(typeof(Guid)) * 2; + + nint pAcePtr = new((long)pAce + offset); + PInvoke.ConvertSidToStringSid((PSID)pAcePtr, &pszSid); + + AccessControlEntryType type; + AccessControlEntryFlags inheritanceFlags = AccessControlEntryFlags.None; + AccessMaskFlags accessMaskFlags = (AccessMaskFlags)ace.Mask; + + var header = ace.Header; + type = (SystemSecurity.AceType)header.AceType switch + { + SystemSecurity.AceType.AccessAllowed => AccessControlEntryType.Allow, + _ => AccessControlEntryType.Deny + }; + + var flags = (SystemSecurity.AceFlags)header.AceFlags; + + bool isInherited = flags.HasFlag(SystemSecurity.AceFlags.InheritanceFlags); + + if (flags.HasFlag(SystemSecurity.AceFlags.ContainerInherit)) + inheritanceFlags |= AccessControlEntryFlags.ContainerInherit; + if (flags.HasFlag(SystemSecurity.AceFlags.ObjectInherit)) + inheritanceFlags |= AccessControlEntryFlags.ObjectInherit; + if (flags.HasFlag(SystemSecurity.AceFlags.NoPropagateInherit)) + inheritanceFlags |= AccessControlEntryFlags.NoPropagateInherit; + if (flags.HasFlag(SystemSecurity.AceFlags.InheritOnly)) + inheritanceFlags |= AccessControlEntryFlags.InheritOnly; + + // Initialize an ACE + aces.Add(new(isFolder, pszSid.ToString(), type, accessMaskFlags, isInherited, inheritanceFlags)); + } + + // Initialize with proper data + acl = new AccessControlList(path, isFolder, principal, isValidAcl); + + // Set access control entries + foreach (var ace in aces) + acl.AccessControlEntries.Add(ace); + + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + } + + /// + public unsafe WIN32_ERROR AddAce(string szPath, bool isFolder, string szSid) + { + ACL* pDACL = default; + ACL* pNewDACL = default; + PSID pSid = default; + ACL_SIZE_INFORMATION aclSizeInfo = default; + ACCESS_ALLOWED_ACE* pTempAce = default; + + // Get DACL for the specified object + var result = PInvoke.GetNamedSecurityInfo( + szPath, + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | OBJECT_SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, + out _, + out _, + &pDACL, + null, + out _); + + if (result is not WIN32_ERROR.ERROR_SUCCESS) + return result; + + // Get ACL size info + bool bResult = PInvoke.GetAclInformation( + pDACL, + &aclSizeInfo, + (uint)Marshal.SizeOf(), + ACL_INFORMATION_CLASS.AclSizeInformation); + + if (!bResult) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + var cbNewDACL = aclSizeInfo.AclBytesInUse + aclSizeInfo.AclBytesFree + Marshal.SizeOf() + 1024; + + pNewDACL = (ACL*)PInvoke.LocalAlloc(LOCAL_ALLOC_FLAGS.LPTR, (nuint)cbNewDACL); + if (pNewDACL == default) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + // Initialize the new DACL + PInvoke.InitializeAcl(pNewDACL, (uint)cbNewDACL, ACE_REVISION.ACL_REVISION); + + // Copy ACEs from the old DACL + for (uint dwAceIndex = 0u; dwAceIndex < aclSizeInfo.AceCount; dwAceIndex++) + { + bResult = PInvoke.GetAce(pDACL, dwAceIndex, (void**)&pTempAce); + PInvoke.AddAce(pNewDACL, ACE_REVISION.ACL_REVISION, uint.MaxValue, pTempAce, pTempAce->Header.AceSize); + } + + // Get the principal's SID of the new ACE + fixed (char* cSid = szSid) + PInvoke.ConvertStringSidToSid(new PCWSTR(cSid), &pSid); + + bResult = PInvoke.AddAccessAllowedAceEx( + pNewDACL, + ACE_REVISION.ACL_REVISION, + isFolder ? ACE_FLAGS.CONTAINER_INHERIT_ACE | ACE_FLAGS.OBJECT_INHERIT_ACE : ACE_FLAGS.NO_INHERITANCE, + 0x20000000 | 0x80000000 /* GENERIC_EXECUTE and GENERIC_READ */, + pSid); + + if (!bResult) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + fixed (char* cPath = szPath) + { + // Set the new ACL + result = PInvoke.SetNamedSecurityInfo( + new PWSTR(cPath), + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | OBJECT_SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, + new PSID((void*)0), + new PSID((void*)0), + pNewDACL); + } + + if (result is not WIN32_ERROR.ERROR_SUCCESS) + return result; + + return result; + } + + /// + public unsafe WIN32_ERROR DeleteAce(string szPath, uint dwAceIndex) + { + ACL* pDACL = default; + + // Get DACL for the specified object + var result = PInvoke.GetNamedSecurityInfo( + szPath, + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | OBJECT_SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, + out _, + out _, + &pDACL, + null, + out _); + + if (result is not WIN32_ERROR.ERROR_SUCCESS) + return result; + + // Remove an ACE + bool bResult = PInvoke.DeleteAce(pDACL, dwAceIndex); + + if (!bResult) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + fixed (char* cPath = szPath) + { + // Set the new ACL + result = PInvoke.SetNamedSecurityInfo( + new PWSTR(cPath), + SE_OBJECT_TYPE.SE_FILE_OBJECT, + OBJECT_SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | OBJECT_SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, + new PSID((void*)0), + new PSID((void*)0), + pDACL); + + if (result is not WIN32_ERROR.ERROR_SUCCESS) + return result; + + return result; + } + } + } +} diff --git a/src/Files.App/Utils/Storage/Helpers/FileSecurityHelpers.cs b/src/Files.App/Utils/Storage/Helpers/FileSecurityHelpers.cs deleted file mode 100644 index bbba2f4b4ce7..000000000000 --- a/src/Files.App/Utils/Storage/Helpers/FileSecurityHelpers.cs +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using Vanara.PInvoke; -using static Vanara.PInvoke.AdvApi32; -using SystemSecurity = System.Security.AccessControl; - -namespace Files.App.Utils.Storage -{ - /// - /// Represents a helper for file security information. - /// - public static class FileSecurityHelpers - { - /// - /// Get the owner of the object specified by the path. - /// - /// The file full path - /// - public static string GetOwner(string path) - { - GetNamedSecurityInfo( - path, - SE_OBJECT_TYPE.SE_FILE_OBJECT, - SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION, - out var pSidOwner, - out _, - out _, - out _, - out _); - - var szSid = ConvertSidToStringSid(pSidOwner); - - return szSid; - } - - /// - /// Set the owner of the object specified by the path. - /// - /// The file full path - /// The owner security identifier (SID) - /// - public static bool SetOwner(string path, string sid) - { - SECURITY_INFORMATION secInfo = SECURITY_INFORMATION.OWNER_SECURITY_INFORMATION; - - // Get PSID object from string sid - var pSid = ConvertStringSidToSid(sid); - - // Change owner - var result = SetNamedSecurityInfo(path, SE_OBJECT_TYPE.SE_FILE_OBJECT, secInfo, pSid); - - pSid.Dispose(); - - // Run PowerShell as Admin - if (result.Failed) - { - return Win32Helper.RunPowershellCommand( - $"-command \"try {{ $path = '{path}'; $ID = new-object System.Security.Principal.SecurityIdentifier('{sid}'); $acl = get-acl $path; $acl.SetOwner($ID); set-acl -path $path -aclObject $acl }} catch {{ exit 1; }}\"", - true); - } - - return true; - } - - /// - /// Get information about an access control list (ACL). - /// - /// - /// - /// If the function succeeds, an instance of AccessControlList; otherwise, null. To get extended error information, call GetLastError. - public static Win32Error GetAccessControlList(string path, bool isFolder, out AccessControlList acl) - { - acl = new(); - - // Get DACL - var win32Error = GetNamedSecurityInfo( - path, - SE_OBJECT_TYPE.SE_FILE_OBJECT, - SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, - out _, - out _, - out var pDacl, - out _, - out _); - - if (win32Error.Failed || pDacl == PACL.NULL) - return win32Error; - - // Get ACL size info - bool bResult = GetAclInformation(pDacl, out ACL_SIZE_INFORMATION aclSize); - if (!bResult) - return Kernel32.GetLastError(); - - // Get owner - var szOwnerSid = GetOwner(path); - var principal = new Principal(szOwnerSid); - - var isValidAcl = IsValidAcl(pDacl); - - List aces = []; - - // Get ACEs - for (uint i = 0; i < aclSize.AceCount; i++) - { - bResult = GetAce(pDacl, i, out var pAce); - if (!bResult) - return Kernel32.GetLastError(); - - var szSid = ConvertSidToStringSid(pAce.GetSid()); - - AccessControlEntryType type; - AccessControlEntryFlags inheritanceFlags = AccessControlEntryFlags.None; - AccessMaskFlags accessMaskFlags = (AccessMaskFlags)pAce.GetMask(); - - var header = pAce.GetHeader(); - type = header.AceType switch - { - SystemSecurity.AceType.AccessAllowed => AccessControlEntryType.Allow, - _ => AccessControlEntryType.Deny - }; - - bool isInherited = header.AceFlags.HasFlag(SystemSecurity.AceFlags.InheritanceFlags); - - if (header.AceFlags.HasFlag(SystemSecurity.AceFlags.ContainerInherit)) - inheritanceFlags |= AccessControlEntryFlags.ContainerInherit; - if (header.AceFlags.HasFlag(SystemSecurity.AceFlags.ObjectInherit)) - inheritanceFlags |= AccessControlEntryFlags.ObjectInherit; - if (header.AceFlags.HasFlag(SystemSecurity.AceFlags.NoPropagateInherit)) - inheritanceFlags |= AccessControlEntryFlags.NoPropagateInherit; - if (header.AceFlags.HasFlag(SystemSecurity.AceFlags.InheritOnly)) - inheritanceFlags |= AccessControlEntryFlags.InheritOnly; - - // Initialize an ACE - aces.Add(new(isFolder, szSid, type, accessMaskFlags, isInherited, inheritanceFlags)); - } - - // Initialize with proper data - acl = new AccessControlList(path, isFolder, principal, isValidAcl); - - // Set access control entries - foreach (var ace in aces) - acl.AccessControlEntries.Add(ace); - - return Kernel32.GetLastError(); - } - - /// - /// Get access control list (ACL) initialized with default data. - /// - /// - /// - /// If the function succeeds, an instance of AccessControlList; otherwise, null. - public static AccessControlEntry InitializeDefaultAccessControlEntry(bool isFolder, string ownerSid) - { - return new( - isFolder, - ownerSid, - AccessControlEntryType.Allow, - AccessMaskFlags.ReadAndExecute, - false, - isFolder - ? AccessControlEntryFlags.ContainerInherit | AccessControlEntryFlags.ObjectInherit - : AccessControlEntryFlags.None); - } - - /// - /// Add an default Access Control Entry (ACE) to the specified object's DACL - /// - /// The object's path to add an new ACE to its DACL - /// Principal's SID - /// If the function succeeds, the return value is ERROR_SUCCESS. If the function fails, the return value is a nonzero error code defined in WinError.h. - public static Win32Error AddAccessControlEntry(string szPath, string szSid) - { - // Get DACL for the specified object - var result = GetNamedSecurityInfo( - szPath, - SE_OBJECT_TYPE.SE_FILE_OBJECT, - SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, - out _, - out _, - out var pDACL, - out _, - out _); - - if (result.Failed) - return result; - - // Initialize default trustee - var explicitAccess = new EXPLICIT_ACCESS - { - grfAccessMode = ACCESS_MODE.GRANT_ACCESS, - grfAccessPermissions = ACCESS_MASK.GENERIC_READ | ACCESS_MASK.GENERIC_EXECUTE, - grfInheritance = INHERIT_FLAGS.NO_INHERITANCE, - Trustee = new TRUSTEE(new SafePSID(szSid)), - }; - - // Add an new ACE and get a new ACL - result = SetEntriesInAcl(1, [explicitAccess], pDACL, out var pNewDACL); - - if (result.Failed) - return result; - - // Set the new ACL - result = SetNamedSecurityInfo( - szPath, - SE_OBJECT_TYPE.SE_FILE_OBJECT, - SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, - ppDacl: pNewDACL); - - if (result.Failed) - return result; - - return result; - } - - /// - /// Add an Access Control Entry (ACE) from the specified object's DACL - /// - /// The object's path to remove an ACE from its DACL - /// - /// - public static Win32Error RemoveAccessControlEntry(string szPath, uint dwAceIndex) - { - // Get DACL for the specified object - var result = GetNamedSecurityInfo( - szPath, - SE_OBJECT_TYPE.SE_FILE_OBJECT, - SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, - out _, - out _, - out var pDACL, - out _, - out _); - - if (result.Failed) - return result; - - // Remove an ACE - bool bResult = DeleteAce(pDACL, dwAceIndex); - - if (!bResult) - return Kernel32.GetLastError(); - - // Set the new ACL - result = SetNamedSecurityInfo( - szPath, - SE_OBJECT_TYPE.SE_FILE_OBJECT, - SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | SECURITY_INFORMATION.PROTECTED_DACL_SECURITY_INFORMATION, - ppDacl: pDACL); - - if (result.Failed) - return result; - - return result; - } - } -} diff --git a/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs b/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs index 6233b591266d..82e85c16d1c6 100644 --- a/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Properties/SecurityAdvancedViewModel.cs @@ -2,13 +2,15 @@ // Licensed under the MIT License. See the LICENSE. using Microsoft.UI.Xaml; -using Vanara.PInvoke; using Windows.Storage; +using Windows.Win32.Foundation; namespace Files.App.ViewModels.Properties { public sealed class SecurityAdvancedViewModel : ObservableObject { + private readonly IStorageSecurityService StorageSecurityService = Ioc.Default.GetRequiredService(); + private readonly PropertiesPageNavigationParameter _navigationParameter; private readonly Window _window; @@ -166,14 +168,16 @@ private void LoadShieldIconResource() private void LoadAccessControlEntry() { - var error = FileSecurityHelpers.GetAccessControlList(_path, _isFolder, out _AccessControlList); + var error = StorageSecurityService.GetAcl(_path, _isFolder, out _AccessControlList); + OnPropertyChanged(nameof(AccessControlList)); + SelectedAccessControlEntry = AccessControlList.AccessControlEntries.FirstOrDefault(); if (!AccessControlList.IsValid) { DisplayElements = false; - if (error == Win32Error.ERROR_ACCESS_DENIED) + if (error is WIN32_ERROR.ERROR_ACCESS_DENIED) { ErrorMessage = "SecurityRequireReadPermissions".GetLocalizedResource() + @@ -185,7 +189,7 @@ private void LoadAccessControlEntry() ErrorMessage = "SecurityUnableToDisplayPermissions".GetLocalizedResource() + "\r\n\r\n" + - error.FormatMessage(); + error.ToString(); } } else @@ -204,7 +208,7 @@ private async Task ExecuteChangeOwnerCommandAsync() await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { // Set owner - FileSecurityHelpers.SetOwner(_path, sid); + StorageSecurityService.SetOwner(_path, sid); // Reload LoadAccessControlEntry(); @@ -221,10 +225,10 @@ private async Task ExecuteAddAccessControlEntryCommandAsync() await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => { // Run Win32API - var win32Result = FileSecurityHelpers.AddAccessControlEntry(_path, sid); + var win32Result = StorageSecurityService.AddAce(_path, _isFolder, sid); // Add a new ACE to the ACL - var ace = FileSecurityHelpers.InitializeDefaultAccessControlEntry(_isFolder, sid); + var ace = AccessControlEntry.GetDefault(_isFolder, sid); AccessControlList.AccessControlEntries.Insert(0, ace); }); } @@ -240,7 +244,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => var index = AccessControlList.AccessControlEntries.IndexOf(SelectedAccessControlEntry); // Run Win32API - var win32Result = FileSecurityHelpers.RemoveAccessControlEntry(_path, (uint)index); + var win32Result = StorageSecurityService.DeleteAce(_path, (uint)index); // Remove the ACE AccessControlList.AccessControlEntries.Remove(SelectedAccessControlEntry); diff --git a/src/Files.App/ViewModels/Properties/SecurityViewModel.cs b/src/Files.App/ViewModels/Properties/SecurityViewModel.cs index 4e162e96b6a7..9aa333129d12 100644 --- a/src/Files.App/ViewModels/Properties/SecurityViewModel.cs +++ b/src/Files.App/ViewModels/Properties/SecurityViewModel.cs @@ -3,13 +3,15 @@ using CommunityToolkit.WinUI; using Microsoft.UI.Xaml; -using Vanara.PInvoke; using Windows.Storage; +using Windows.Win32.Foundation; namespace Files.App.ViewModels.Properties { public sealed class SecurityViewModel : ObservableObject { + private readonly IStorageSecurityService StorageSecurityService = Ioc.Default.GetRequiredService(); + private readonly PropertiesPageNavigationParameter _navigationParameter; private readonly Window _window; @@ -89,13 +91,13 @@ public SecurityViewModel(PropertiesPageNavigationParameter parameter) break; }; - var error = FileSecurityHelpers.GetAccessControlList(_path, _isFolder, out _AccessControlList); + var error = StorageSecurityService.GetAcl(_path, _isFolder, out _AccessControlList); _SelectedAccessControlEntry = AccessControlList.AccessControlEntries.FirstOrDefault(); if (!AccessControlList.IsValid) { DisplayElements = false; - ErrorMessage = error == Win32Error.ERROR_ACCESS_DENIED + ErrorMessage = error is WIN32_ERROR.ERROR_ACCESS_DENIED ? "SecurityRequireReadPermissions".GetLocalizedResource() + "\r\n" + "SecurityClickAdvancedPermissions".GetLocalizedResource() : "SecurityUnableToDisplayPermissions".GetLocalizedResource(); } @@ -119,10 +121,10 @@ private async Task ExecuteAddAccessControlEntryCommandAsync() await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => { // Run Win32API - var win32Result = FileSecurityHelpers.AddAccessControlEntry(_path, sid); + var win32Result = StorageSecurityService.AddAce(_path, _isFolder, sid); // Add a new ACE to the ACL - var ace = FileSecurityHelpers.InitializeDefaultAccessControlEntry(_isFolder, sid); + var ace = AccessControlEntry.GetDefault(_isFolder, sid); AccessControlList.AccessControlEntries.Insert(0, ace); }); } @@ -135,7 +137,7 @@ await MainWindow.Instance.DispatcherQueue.EnqueueAsync(() => var index = AccessControlList.AccessControlEntries.IndexOf(SelectedAccessControlEntry); // Run Win32API - var win32Result = FileSecurityHelpers.RemoveAccessControlEntry(_path, (uint)index); + var win32Result = StorageSecurityService.DeleteAce(_path, (uint)index); // Remove the ACE AccessControlList.AccessControlEntries.Remove(SelectedAccessControlEntry); diff --git a/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml b/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml index 79cd8fb80148..acf82608686b 100644 --- a/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml +++ b/src/Files.App/Views/Properties/SecurityAdvancedPage.xaml @@ -5,11 +5,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:behaviors="using:Files.App.Data.Behaviors" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dataitems="using:Files.App.Data.Items" xmlns:extensions="using:Files.App.Extensions" xmlns:helpers="using:Files.App.Helpers" xmlns:i="using:Microsoft.Xaml.Interactivity" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:security="using:Files.App.Utils.Storage" xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Controls" xmlns:vm="using:Files.App.ViewModels.Properties" xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters" @@ -347,7 +347,7 @@ - + @@ -507,7 +507,7 @@ - + - +