Interop.psm1

. $PSScriptRoot\SddlUtils.ps1 -Force
. $PSScriptRoot\Convert.ps1 -Force

$signature = @'
using System;
using System.Runtime.InteropServices;
 
[StructLayout(LayoutKind.Sequential)]
public struct GENERIC_MAPPING
{
    public uint GenericRead;
    public uint GenericWrite;
    public uint GenericExecute;
    public uint GenericAll;
}
 
public static class NativeMethods {
    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool CreatePrivateObjectSecurityEx(
        System.IntPtr ParentDescriptor,
        System.IntPtr CreatorDescriptor,
        out System.IntPtr NewDescriptor,
        System.IntPtr ObjectType,
        [MarshalAs(UnmanagedType.Bool)] bool IsContainerObject,
        uint AutoInheritFlags,
        System.IntPtr Token,
        ref GENERIC_MAPPING GenericMapping);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DestroyPrivateObjectSecurity(ref IntPtr ObjectDescriptor);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern int GetSecurityDescriptorLength(System.IntPtr pSecurityDescriptor);
}
'@


if (-not ([System.Management.Automation.PSTypeName]'NativeMethods').Type) {
    Add-Type -TypeDefinition $signature -Language CSharp
}

function MarshalSecurityDescriptor {
    [CmdletBinding()]
    [OutputType([System.IntPtr])]
    param (
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.GenericSecurityDescriptor]$SecurityDescriptor
    )

    $length = $SecurityDescriptor.BinaryLength
    $bytes = New-Object byte[] $length
    $SecurityDescriptor.GetBinaryForm($bytes, 0)
    $intPtr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($length)
    [System.Runtime.InteropServices.Marshal]::Copy($bytes, 0, $intPtr, $length)
    return $intPtr
}

function UnmarshalSecurityDescriptor {
    [CmdletBinding()]
    [OutputType([System.Security.AccessControl.CommonSecurityDescriptor])]
    param (
        [Parameter(Mandatory = $true)]
        [IntPtr]$IntPtr,

        [Parameter(Mandatory = $true)]
        [bool]$IsDirectory
    )

    if ($IntPtr -eq [System.IntPtr]::Zero) {
        return $null
    }

    $length = [NativeMethods]::GetSecurityDescriptorLength($IntPtr)
    $bytes = New-Object byte[] $length
    [System.Runtime.InteropServices.Marshal]::Copy($IntPtr, $bytes, 0, $length)

    return [System.Security.AccessControl.CommonSecurityDescriptor]::new($IsDirectory, $false, $bytes, 0)
}

[Flags()]
enum AutoInheritFlags {
    # The new discretionary access control list (DACL) contains ACEs inherited from the DACL of ParentDescriptor, as
    # well as any explicit ACEs specified in the DACL of CreatorDescriptor. If this flag is not set, the new DACL does
    # not inherit ACEs.
    SEF_DACL_AUTO_INHERIT = 0x01

    # The new system access control list (SACL) contains ACEs inherited from the SACL of ParentDescriptor, as well as
    # any explicit ACEs specified in the SACL of CreatorDescriptor. If this flag is not set, the new SACL does not
    # inherit ACEs.
    SEF_SACL_AUTO_INHERIT = 0x02

    # CreatorDescriptor is the default descriptor for the type of object specified by ObjectType. As such,
    # CreatorDescriptor is ignored if ParentDescriptor has any object-specific ACEs for the type of object specified by
    # the ObjectType parameter. If no such ACEs are inherited, CreatorDescriptor is handled as though this flag were not
    # specified.
    SEF_DEFAULT_DESCRIPTOR_FOR_OBJECT = 0x04

    # The function does not perform privilege checking. If the SEF_AVOID_OWNER_CHECK flag is also set, the Token
    # parameter can be NULL. This flag is useful while implementing automatic inheritance to avoid checking privileges
    # on each child updated.
    SEF_AVOID_PRIVILEGE_CHECK = 0x08

    # The function does not check the validity of the owner in the resultant NewDescriptor as described in Remarks
    # below. If the SEF_AVOID_PRIVILEGE_CHECK flag is also set, the Token parameter can be NULL.
    SEF_AVOID_OWNER_CHECK = 0x10

    # The owner of NewDescriptor defaults to the owner from ParentDescriptor. If not set, the owner of NewDescriptor
    # defaults to the owner of the token specified by the Token parameter. The owner of the token is specified in the
    # token itself. In either case, if the CreatorDescriptor parameter is not NULL, the NewDescriptor owner is set to
    # the owner from CreatorDescriptor.
    SEF_DEFAULT_OWNER_FROM_PARENT = 0x20 

    # The group of NewDescriptor defaults to the group from ParentDescriptor. If not set, the group of NewDescriptor
    # defaults to the group of the token specified by the Token parameter. The group of the token is specified in the
    # token itself. In either case, if the CreatorDescriptor parameter is not NULL, the NewDescriptor group is set to
    # the group from CreatorDescriptor.
    SEF_DEFAULT_GROUP_FROM_PARENT = 0x40

    # When this flag is set, the mandatory label ACE in CreatorDescriptor is not used to create a mandatory label ACE
    # in NewDescriptor. Instead, a new SYSTEM_MANDATORY_LABEL_ACE with an access mask of
    # SYSTEM_MANDATORY_LABEL_NO_WRITE_UP and the SID from the token's integrity SID is added to NewDescriptor.
    SEF_MACL_NO_WRITE_UP = 0x100

    # When this flag is set, the mandatory label ACE in CreatorDescriptor is not used to create a mandatory label ACE
    # in NewDescriptor. Instead, a new SYSTEM_MANDATORY_LABEL_ACE with an access mask of
    # SYSTEM_MANDATORY_LABEL_NO_READ_UP and the SID from the token's integrity SID is added to NewDescriptor.
    SEF_MACL_NO_READ_UP = 0x200

    # When this flag is set, the mandatory label ACE in CreatorDescriptor is not used to create a mandatory label ACE
    # in NewDescriptor. Instead, a new SYSTEM_MANDATORY_LABEL_ACE with an access mask of
    # SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP and the SID from the token's integrity SID is added to NewDescriptor.
    SEF_MACL_NO_EXECUTE_UP = 0x400

    # Any restrictions specified by the ParentDescriptor that would limit the caller's ability to specify a DACL in the
    # CreatorDescriptor are ignored.
    SEF_AVOID_OWNER_RESTRICTION = 0x1000
}

function CreatePrivateObjectSecurityEx {
    [CmdletBinding()]
    [OutputType([System.Security.AccessControl.GenericSecurityDescriptor])]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [System.Security.AccessControl.GenericSecurityDescriptor]$ParentDescriptor,
        
        [Parameter(Mandatory = $true)]
        [System.Security.AccessControl.GenericSecurityDescriptor]$CreatorDescriptor,
        
        [Parameter(Mandatory = $true)]
        [bool]$IsDirectory
    )

    $parentSdIntPtr = [System.IntPtr]::Zero
    $creatorSdIntPtr = [System.IntPtr]::Zero
    $newDescriptorIntPtr = [System.IntPtr]::Zero

    try {
        # Parent is allowed to be null by this implementation.
        if ($null -ne $ParentDescriptor) {
            $parentSdIntPtr = MarshalSecurityDescriptor $ParentDescriptor
        }

        # Creator is not allowed to be null by this implementation.
        # This simplifying assumption is made to avoid having to deal with the token,
        # and is good enough for our use-case of computing inheritance on existing ACLs.
        $creatorSdIntPtr = MarshalSecurityDescriptor $CreatorDescriptor

        # Since we only deal with file system objects, we will never have object GUIDs.
        # We can safely set the object type to null.
        $objectTypeIntPtr = [System.IntPtr]::Zero

        [bool]$isContainerObject = $IsDirectory

        # We don't want to run this with a token, so set AVOID_PRIVILEGE_CHECK and AVOID_OWNER_CHECK.
        $autoInheritFlags = (
            [AutoInheritFlags]::SEF_AVOID_PRIVILEGE_CHECK -bor
            [AutoInheritFlags]::SEF_AVOID_OWNER_CHECK
        )     

        # TODO: this is a hack. This logic should be in the caller...
        # We want to opt in to ACL inheritance, so set the SEF_DACL_AUTO_INHERIT and SEF_SACL_AUTO_INHERIT flags if the DACL/SACL are not protected.
        if (-not ($CreatorDescriptor.ControlFlags -band [System.Security.AccessControl.ControlFlags]::DiscretionaryAclProtected)) {
            $autoInheritFlags = $autoInheritFlags -bor [AutoInheritFlags]::SEF_DACL_AUTO_INHERIT
        }

        if (-not ($CreatorDescriptor.ControlFlags -band [System.Security.AccessControl.ControlFlags]::SystemAclProtected)) {
            $autoInheritFlags = $autoInheritFlags -bor [AutoInheritFlags]::SEF_SACL_AUTO_INHERIT
        }

        # We do not allow CreatorDescriptor to be null in this implementation, so according to
        # MS-DTYP 2.5.3.4, the token will never be used. Hence we can safely set it to null.
        # This also makes things easier for us -- one less thing to marshal.
        # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/0f0c6ffc-f57d-47f8-a6c8-63889e874e24
        $token = [System.IntPtr]::Zero

        # Build generic mapping object. Since this module only deals with file system objects,
        # we can hard-code the file generic rights mapping.
        # https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-generic_mapping
        $genericMapping = New-Object GENERIC_MAPPING
        $genericMapping.GenericRead = [FileGenericRightsMapping]::FILE_GENERIC_READ
        $genericMapping.GenericWrite = [FileGenericRightsMapping]::FILE_GENERIC_WRITE
        $genericMapping.GenericExecute = [FileGenericRightsMapping]::FILE_GENERIC_EXECUTE
        $genericMapping.GenericAll = [FileGenericRightsMapping]::FILE_ALL_ACCESS

        $success = [NativeMethods]::CreatePrivateObjectSecurityEx(
            $parentSdIntPtr, 
            $creatorSdIntPtr, 
            [ref] $newDescriptorIntPtr, 
            $objectTypeIntPtr,
            $isContainerObject,
            [uint32]$autoInheritFlags,
            $token, 
            [ref] $genericMapping
        )

        if (-not $success) {
            $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
            Write-Error "CreatePrivateObjectSecurityEx failed with result: $success, error code: $errorCode"
            return $null
        }

        return UnmarshalSecurityDescriptor $newDescriptorIntPtr -IsDirectory $IsDirectory
    } catch {
        Write-Error "Failure: $_"
        throw $_
    } finally {
        # Free all the allocated memory
        if ($parentSdIntPtr -ne [System.IntPtr]::Zero) {
            [System.Runtime.InteropServices.Marshal]::FreeHGlobal($parentSdIntPtr)
        }
        if ($creatorSdIntPtr -ne [System.IntPtr]::Zero) {
            [System.Runtime.InteropServices.Marshal]::FreeHGlobal($creatorSdIntPtr)
        }
        if ($newDescriptorIntPtr -ne [System.IntPtr]::Zero) {
            $success = [NativeMethods]::DestroyPrivateObjectSecurity([ref] $newDescriptorIntPtr)
            if (-not $success) {
                Write-Error "DestroyPrivateObjectSecurity failed with result: $success, error code: $errorCode" -ErrorAction Stop
            }
        }
    }
}