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 +{ + /// <summary> + /// Provides service to manage storage security objects on NTFS and ReFS. + /// </summary> + public interface IStorageSecurityService + { + /// <summary> + /// Get the owner of the object specified by the path. + /// </summary> + /// <param name="path">The file full path</param> + /// <returns>The SID string of the owner</returns> + string GetOwner(string path); + + /// <summary> + /// Set the owner of the object specified by the path. + /// </summary> + /// <param name="path">The file full path</param> + /// <param name="sid">The owner security identifier (SID)</param> + /// <returns></returns> + bool SetOwner(string path, string sid); + + /// <summary> + /// Get information about an access control list (ACL). + /// </summary> + /// <param name="path"></param> + /// <param name="isFolder"></param> + /// <returns>If the function succeeds, an instance of AccessControlList; otherwise, null. To get extended error information, call GetLastError.</returns> + WIN32_ERROR GetAcl(string path, bool isFolder, out AccessControlList acl); + + /// <summary> + /// Add an default Access Control Entry (ACE) to the specified object's DACL + /// </summary> + /// <param name="path">The object's path to add an new ACE to its DACL</param> + /// <param name="sid">Principal's SID</param> + /// <returns> 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.</returns> + WIN32_ERROR AddAce(string szPath, bool isFolder, string szSid); + + /// <summary> + /// Add an Access Control Entry (ACE) from the specified object's DACL + /// </summary> + /// <param name="szPath">The object's path to remove an ACE from its DACL</param> + /// <param name="dwAceIndex"></param> + /// <returns></returns> + 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 { /// <summary> /// 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 { /// <summary> /// 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 { /// <summary> /// Represents an ACL owner or an ACE principal type /// </summary> - public enum PrincipalType + public enum AccessControlPrincipalType { /// <summary> - /// Unknwon principal type + /// Unknown principal type /// </summary> 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 { /// <summary> /// 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 { /// <summary> /// 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. /// </summary> - public Principal Principal { get; set; } + public AccessControlPrincipal Principal { get; set; } /// <summary> /// 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<string>(x => - //{ - // AccessControlType = Enum.Parse<AccessControlType>(x); - //}); + ChangeAccessControlTypeCommand = new RelayCommand<string>(x => + { + //AccessControlType = Enum.Parse<AccessControlType>(x); + }); - //ChangeInheritanceFlagsCommand = new RelayCommand<string>(x => - //{ - // var parts = x.Split(','); - // InheritanceFlags = Enum.Parse<AccessControlEntryFlags>(parts[0]); - //}); + ChangeInheritanceFlagsCommand = new RelayCommand<string>(x => + { + //var parts = x.Split(','); + //InheritanceFlags = Enum.Parse<AccessControlEntryFlags>(parts[0]); + }); IsFolder = isFolder; Principal = new(ownerSid); @@ -402,5 +402,24 @@ private void ToggleDenyAccess(AccessMaskFlags accessMask, bool value) DeniedAccessMaskFlags &= ~accessMask; } } + + /// <summary> + /// Gets access control list (ACL) initialized with default data. + /// </summary> + /// <param name="isFolder"></param> + /// <param name="ownerSid"></param> + /// <returns>If the function succeeds, an instance of AccessControlList; otherwise, null.</returns> + 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 { /// <summary> /// 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. /// </summary> - public Principal Owner { get; private set; } + public AccessControlPrincipal Owner { get; private set; } /// <summary> /// Validates an access control list (ACL). @@ -34,7 +34,7 @@ public sealed class AccessControlList : ObservableObject /// </summary> public ObservableCollection<AccessControlEntry> 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 { /// <summary> /// Represents a principal of an ACE or an owner of an ACL. /// </summary> - public sealed class Principal : ObservableObject + public sealed class AccessControlPrincipal : ObservableObject { /// <summary> /// Account type. /// </summary> - public PrincipalType PrincipalType { get; private set; } + public AccessControlPrincipalType PrincipalType { get; private set; } /// <summary> - /// Acount security identifier (SID). + /// Account security identifier (SID). /// </summary> 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 { /// <summary> /// 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<IStartMenuService, StartMenuService>() .AddSingleton<IStorageCacheService, StorageCacheService>() .AddSingleton<IStorageArchiveService, StorageArchiveService>() + .AddSingleton<IStorageSecurityService, StorageSecurityService>() .AddSingleton<IWindowsCompatibilityService, WindowsCompatibilityService>() // ViewModels .AddSingleton<MainPageViewModel>() 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 +{ + /// <inheritdoc cref="IStorageSecurityService"/> + public class StorageSecurityService : IStorageSecurityService + { + /// <inheritdoc/> + 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(); + } + + /// <inheritdoc/> + 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; + } + + /// <inheritdoc/> + 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_SIZE_INFORMATION>(), + 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<AccessControlEntry> 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<ACCESS_ALLOWED_ACE>((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(); + } + + /// <inheritdoc/> + 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_SIZE_INFORMATION>(), + ACL_INFORMATION_CLASS.AclSizeInformation); + + if (!bResult) + return (WIN32_ERROR)Marshal.GetLastPInvokeError(); + + var cbNewDACL = aclSizeInfo.AclBytesInUse + aclSizeInfo.AclBytesFree + Marshal.SizeOf<ACCESS_ALLOWED_ACE>() + 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; + } + + /// <inheritdoc/> + 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 -{ - /// <summary> - /// Represents a helper for file security information. - /// </summary> - public static class FileSecurityHelpers - { - /// <summary> - /// Get the owner of the object specified by the path. - /// </summary> - /// <param name="path">The file full path</param> - /// <returns></returns> - 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; - } - - /// <summary> - /// Set the owner of the object specified by the path. - /// </summary> - /// <param name="path">The file full path</param> - /// <param name="sid">The owner security identifier (SID)</param> - /// <returns></returns> - 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; - } - - /// <summary> - /// Get information about an access control list (ACL). - /// </summary> - /// <param name="path"></param> - /// <param name="isFolder"></param> - /// <returns>If the function succeeds, an instance of AccessControlList; otherwise, null. To get extended error information, call GetLastError.</returns> - 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<AccessControlEntry> 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(); - } - - /// <summary> - /// Get access control list (ACL) initialized with default data. - /// </summary> - /// <param name="isFolder"></param> - /// <param name="ownerSid"></param> - /// <returns>If the function succeeds, an instance of AccessControlList; otherwise, null.</returns> - public static AccessControlEntry InitializeDefaultAccessControlEntry(bool isFolder, string ownerSid) - { - return new( - isFolder, - ownerSid, - AccessControlEntryType.Allow, - AccessMaskFlags.ReadAndExecute, - false, - isFolder - ? AccessControlEntryFlags.ContainerInherit | AccessControlEntryFlags.ObjectInherit - : AccessControlEntryFlags.None); - } - - /// <summary> - /// Add an default Access Control Entry (ACE) to the specified object's DACL - /// </summary> - /// <param name="path">The object's path to add an new ACE to its DACL</param> - /// <param name="sid">Principal's SID</param> - /// <returns> 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.</returns> - 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; - } - - /// <summary> - /// Add an Access Control Entry (ACE) from the specified object's DACL - /// </summary> - /// <param name="szPath">The object's path to remove an ACE from its DACL</param> - /// <param name="dwAceIndex"></param> - /// <returns></returns> - 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<IStorageSecurityService>(); + 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<IStorageSecurityService>(); + 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 @@ </ListView.Header> <ListView.ItemTemplate> - <DataTemplate x:DataType="security:AccessControlEntry"> + <DataTemplate x:DataType="dataitems:AccessControlEntry"> <Grid ColumnSpacing="13"> <Grid.ColumnDefinitions> <ColumnDefinition Width="20" /> @@ -507,7 +507,7 @@ </GridView.ItemsPanel> <GridView.ItemTemplate> - <DataTemplate x:DataType="security:AccessMaskItem"> + <DataTemplate x:DataType="dataitems:AccessMaskItem"> <CheckBox IsChecked="{x:Bind IsEnabled, Mode=TwoWay}" IsEnabled="{x:Bind IsEditable}"> <TextBlock Text="{x:Bind AccessMaskName}" diff --git a/src/Files.App/Views/Properties/SecurityPage.xaml b/src/Files.App/Views/Properties/SecurityPage.xaml index a91ed91914cb..0ef10f6d6691 100644 --- a/src/Files.App/Views/Properties/SecurityPage.xaml +++ b/src/Files.App/Views/Properties/SecurityPage.xaml @@ -4,9 +4,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dataitems="using:Files.App.Data.Items" xmlns:helpers="using:Files.App.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:security="using:Files.App.Utils.Storage" xmlns:vm="using:Files.App.ViewModels.Properties" xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters" DataContext="{x:Bind SecurityViewModel, Mode=OneWay}" @@ -135,7 +135,7 @@ SelectionMode="Single"> <ListView.ItemTemplate> - <DataTemplate x:DataType="security:AccessControlEntry"> + <DataTemplate x:DataType="dataitems:AccessControlEntry"> <Grid ColumnSpacing="4"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" />