CommonUtils_endpoints.ps1

# This script contains common utility functions used in different functions

# CONSTANTS
$DPAPI_ENTROPY_CNG_KEY_PROPERTIES  = @(0x36,0x6A,0x6E,0x6B,0x64,0x35,0x4A,0x33,0x5A,0x64,0x51,0x44,0x74,0x72,0x73,0x75,0x00) # "6jnkd5J3ZdQDtrsu" + null terminator
$DPAPI_ENTROPY_CNG_KEY_BLOB           = @(0x78,0x54,0x35,0x72,0x5A,0x57,0x35,0x71,0x56,0x56,0x62,0x72,0x76,0x70,0x75,0x41,0x00) # "xT5rZW5qVVbrvpuA" + null terminator
$DPAPI_ENTROPY_CAPI_KEY_PROPERTIES = @(0x48,0x6a,0x31,0x64,0x69,0x51,0x36,0x6b,0x70,0x55,0x78,0x37,0x56,0x43,0x34,0x6d,0x00) # "Hj1diQ6kpUx7VC4m" + null terminator

# Gets property value using reflection
# Oct 14 2021
Function Get-ReflectionProperty
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject,
        [parameter(Mandatory=$false)]
        [psobject]$ValueObject,
        [parameter(Mandatory=$true)]
        [String]$PropertyName
    )
    Process
    {
        if(!$ValueObject)
        {
            $ValueObject = $TypeObject
        }

        $propertyInfo = $TypeObject.GetProperty($PropertyName,[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)
        return $propertyInfo.GetValue($ValueObject, $null)
    }
}

# Gets property value using reflection
# Oct 14 2021
Function Set-ReflectionProperty
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject,
        [parameter(Mandatory=$false)]
        [psobject]$ValueObject,
        [parameter(Mandatory=$true)]
        [String]$PropertyName,
        [parameter(Mandatory=$true)]
        [psobject]$Value
    )
    Process
    {
        if(!$ValueObject)
        {
            $ValueObject = $TypeObject
        }

        $propertyInfo = $TypeObject.GetProperty($PropertyName,[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)
        return $propertyInfo.SetValue($ValueObject, $Value,$null)
    }
}

# Gets object properties using reflection
# Oct 14 2021
Function Get-ReflectionProperties
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject
    )
    Process
    {
        $properties = $TypeObject.GetProperties([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)

        foreach($property in $properties)
        {
            New-Object psobject -Property ([ordered]@{
                    "Name"  = $property.Name
                    "Write" = $property.CanWrite
                    "Type"  = $property.PropertyType
                })
        }
    }
}

# Gets field value using reflection
# Feb 24 2022
Function Get-ReflectionField
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject,
        [parameter(Mandatory=$false)]
        [psobject]$ValueObject,
        [parameter(Mandatory=$true)]
        [String]$FieldName
    )
    Process
    {
        if(!$ValueObject)
        {
            $ValueObject = $TypeObject
        }
        $fieldInfo = $TypeObject.GetField($FieldName,[System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)
        return $fieldInfo.GetValue($ValueObject)
    }
}

# Gets object properties using reflection
# Feb 24 2022
Function Get-ReflectionFields
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject
    )
    Process
    {
        $fields = $TypeObject.GetFields([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)

        foreach($field in $fields)
        {
            New-Object psobject -Property ([ordered]@{
                    "Name"  = $field.Name
                    "Type"  = $field.FieldType
                    "Attributes" = $field.Attributes
                })
        }
    }
}

# Invokes the given method
# Feb 24 2022
Function Invoke-ReflectionMethod
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject,
        [parameter(Mandatory=$False)]
        [psobject]$GenericType,
        [parameter(Mandatory=$False)]
        [psobject]$ValueObject,
        [parameter(Mandatory=$true)]
        [String]$Method,
        [parameter(Mandatory=$False)]
        [Object[]]$Parameters = @()
    )
    Process
    {
        $methodInfo = $TypeObject.GetMethod($Method, [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)
        if($methodInfo.IsGenericMethodDefinition)
        {
            $genericMethod = $methodInfo.MakeGenericMethod($GenericType)
            return $genericMethod.Invoke($ValueObject,$Parameters)
        }
        else
        {
            return $methodInfo.Invoke($ValueObject,$Parameters)
        }
    }
}

# Gets object methods using reflection
# Feb 24 2022
Function Get-ReflectionMethods
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [psobject]$TypeObject
    )
    Process
    {
        $methods = $TypeObject.GetMethods([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Static)

        foreach($method in $methods)
        {
            New-Object psobject -Property ([ordered]@{
                    "Name"  = $method.Name
                    "Static" = $method.IsStatic
                    "Attributes" = $method.Attributes
                })
        }
    }
}


# Gets Azure and Azure Stack WireServer ip address using DHCP
# Nov 18 2021
Function Get-AzureWireServerAddress
{
<#
    .SYNOPSIS
    Gets Azure and Azure Stack WireServer ip address using DHCP
 
    .DESCRIPTION
    Gets Azure and Azure Stack WireServer ip address using DHCP. If DHCP query fails, returns the default address (168.63.129.16)
 
    .Example
    Get-AADIntAzureWireServerAddress
 
    168.63.129.16
 
 
     
     
#>

    [cmdletbinding()]

    param()
    Begin
    {
    }
    Process
    {
        # Get adapter that are up
        $adapters = Get-NetAdapter | Where AdminStatus -eq "Up" 

        # Loop through the adapters
        foreach($adapter in $adapters)
        {
            # Get IPv4 interfaces that have DHCP enabled
            if((Get-NetIPInterface -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4).Dhcp -eq "Enabled")
            {
                # Try to query for the address (uses DHCP option 245 and "WindowsAzureGuestAgent" as RequestIdString)
                $ipAddress = [AADInternals.Native]::getWireServerIpAddress($adapter.InterfaceGuid)
            }

            # Return if we found the address
            if($ipAddress)
            {
                return $ipAddress.ToString()
            }
        }
        Write-Warning "WireServer address not found with DHCP, returning default address 168.63.129.16"
        return "168.63.129.16"
    }
}

# Create a new self-signed certificate
# Jan 31st 2021
function New-Certificate
{
<#
    .SYNOPSIS
    Creates a new self signed certificate.
 
    .DESCRIPTION
    Creates a new self signed certificate for the given subject name and returns it as System.Security.Cryptography.X509Certificates.X509Certificate2 or exports directly to .pfx and .cer files.
    The certificate is valid for 100 years.
 
    .Parameter SubjectName
    The subject name of the certificate, MUST start with CN=
 
    .Parameter Export
    Export the certificate (PFX and CER) instead of returning the certificate object. The .pfx file does not have a password.
   
    .Example
    PS C:\>$certificate = New-AADIntCertificate -SubjectName "CN=MyCert"
 
    .Example
    PS C:\>$certificate = New-AADIntCertificate -SubjectName "CN=MyCert"
 
    PS C:\>$certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx) | Set-Content MyCert.pfx -Encoding Byte
 
    .Example
    PS C:\>$certificate = New-AADIntCertificate -SubjectName "CN=MyCert"
 
    PS C:\>$certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) | Set-Content MyCert.cer -Encoding Byte
 
    .Example
    PS C:\>New-AADIntCertificate -SubjectName "CN=MyCert" -Export
 
    Certificate successfully exported:
      CN=MyCert.pfx
      CN=MyCert.cer
#>

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$true,ValueFromPipeline)]
        [ValidatePattern("[c|C][n|N]=.+")] # Must start with CN=
        [String]$SubjectName,
        [Switch]$Export
    )
    Process
    {
        # Create a private key
        $rsa = [System.Security.Cryptography.RSA]::Create(2048)

        # Initialize the Certificate Signing Request object
        $req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($SubjectName, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
        $req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true,$false,0,$true))
        $req.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($req.PublicKey,$false))

        # Create a self-signed certificate
        $selfSigned = $req.CreateSelfSigned((Get-Date).ToUniversalTime().AddMinutes(-5),(Get-Date).ToUniversalTime().AddYears(100))
        

        # Store the private key to so that it can be exported
        $cspParameters = [System.Security.Cryptography.CspParameters]::new()
        $cspParameters.ProviderName =    "Microsoft Enhanced RSA and AES Cryptographic Provider"
        $cspParameters.ProviderType =    24
        $cspParameters.KeyContainerName ="AADInternals"
            
        # Set the private key
        $privateKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters)
        $privateKey.ImportParameters($rsa.ExportParameters($true))
        $selfSigned.PrivateKey = $privateKey

        if($Export)
        {
            Set-BinaryContent -Path "$SubjectName.pfx" -Value $selfSigned.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx)
            Set-BinaryContent -Path "$SubjectName.cer" -Value $selfSigned.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)

            # Print out information
            Write-Host "Certificate successfully exported:"
            Write-Host " $SubjectName.pfx"
            Write-Host " $SubjectName.cer"
        }
        else
        {
            return $selfSigned
        }
    }
}

# Parses the given Cng blob
# Dec 17th 2021
function Parse-CngBlob
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [byte[]]$Data,
        [Parameter(Mandatory=$false)]
        [switch]$Decrypt,
        [Parameter(Mandatory=$false)]
        [switch]$LocalMachine
    )
    Begin
    {
        Add-Type -AssemblyName System.Security
    }
    Process
    {
        # Parse the header
        $version =  [System.BitConverter]::ToInt32($Data,0)
        if($version -ne 1)
        {
            Throw "Unsupported version ($Version), expected 1"
        }
        $unknown =  [System.BitConverter]::ToInt32($Data,4)
        $nameLen =  [System.BitConverter]::ToInt32($Data,8)
        $type    =  [System.BitConverter]::ToInt32($Data,12)

        $publicPropertiesLen  = [System.BitConverter]::ToInt32($Data,16)
        $privatePropertiesLen = [System.BitConverter]::ToInt32($Data,20)
        $privateKeyLen        = [System.BitConverter]::ToInt32($Data,24)
        
        $unknownArray = $Data[28..43]
        
        $name = [text.encoding]::Unicode.GetString($Data, 44, $nameLen)

        Write-Debug "Version: $version"
        Write-Debug "Unknown: $unknown"
        Write-Debug "Name length: $nameLen"
        Write-Debug "Type: $type"
        Write-Debug "Public properties length: $publicPropertiesLen"
        Write-Debug "Private properties length: $privatePropertiesLen"
        Write-Debug "Private key length: $privateKeyLen"
        Write-Debug "Unknown array: $(Convert-ByteArrayToHex -Bytes $unknownArray)"
        Write-Debug "Name: $name`n`n"

        Write-Verbose "Parsing Cng key: $name"

        # Set the position
        $p = 44+$nameLen

        # Parse public properties
        $publicProperties = @{}
        $publicPropertiesTotal = 0
        while($publicPropertiesTotal -lt $publicPropertiesLen)
        {
            $pubStructLen         = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $pubStructType        = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $pubStructUnk         = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $pubStructNameLen     = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $pubStructPropertyLen = [System.BitConverter]::ToInt32($Data,$p); $p += 4

            $pubStructName        = [text.encoding]::Unicode.GetString($Data, $p, $pubStructNameLen); $p += $pubStructNameLen
            $pubStructProperty    = $Data[$p..$($p + $pubStructPropertyLen - 1)]; $p += $pubStructPropertyLen

            $publicPropertiesTotal += $pubStructLen

            if([string]::IsNullOrEmpty($pubStructName))
            {
                $pubStructName = "Public Key"
            }
            elseif($pubStructName -eq "Modified")
            {
               $fileTimeUtc =  [System.BitConverter]::ToInt64($pubStructProperty,0)
               Remove-Variable pubStructProperty
               $pubStructProperty = [datetime]::FromFileTimeUtc($fileTimeUtc)
            }

            Write-Debug "Public property struct length: $pubStructLen"
            Write-Debug "Public property struct type: $pubStructType"
            Write-Debug "Public property unknown: $pubStructUnk"
            Write-Debug "Public property name length: $pubStructNameLen"
            Write-Debug "Public property length: $pubStructPropertyLen"
            Write-Debug "Public property name: $pubStructName"

            if($pubStructName -eq "Modified")
            {
                Write-Verbose "Modified: $($pubStructProperty.ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture))z`n`n"
            }
            else
            {
                Write-Debug "Public property: $(Convert-ByteArrayToHex -Bytes $pubStructProperty)`n`n"
            }

            $publicProperties[$pubStructName] = $pubStructProperty
        }
        
        # Parse private properties
        $privateProperties = @{}
        $privatePropertiesTotal = 0

        $privatePropertiesBlob = $Data[$p..$($p + $privatePropertiesLen -1)]
        $privateKeyBlob        = $Data[$($p + $privatePropertiesLen)..$($p + $privatePropertiesLen + $privateKeyLen -1)]
        
        $attributes = [ordered]@{
            "Name"          = $name
            "PublicKeyBlob" = $publicProperties["Public Key"]
            "PrivateKeyBlob" = @()
            "RSAParameters" = Parse-KeyBLOB -Key $publicProperties["Public Key"]
        }
        if($Decrypt)
        {
            $dpapiScope = "CurrentUser"
            
            if($LocalMachine)
            {
                if(!(Is-System))
                {
                    Write-Warning "Trying to decrypt LocalMachine DPAPI while not running as SYSTEM!"
                }
                $dpapiScope = "LocalMachine"
            }
            
            # Decrypt the private key properties using DPAPI
            $decPrivateProperties = [Security.Cryptography.ProtectedData]::Unprotect($privatePropertiesBlob, $DPAPI_ENTROPY_CNG_KEY_PROPERTIES, $dpapiScope)
            $attributes["PrivateKeyProperties"] = $decPrivateProperties

            # Decrypt the private key blob using DPAPI
            $decPrivateBlob = [Security.Cryptography.ProtectedData]::Unprotect($privateKeyBlob, $DPAPI_ENTROPY_CNG_KEY_BLOB, $dpapiScope)
            $attributes["PrivateKeyBlob"] = $decPrivateBlob

            # Convert to RSAFULLPRIVATEBLOB to get all parameters
            $fullPrivateBlob = [AADInternals.Native]::convertKey($decPrivateBlob,"RSAPRIVATEBLOB", "RSAFULLPRIVATEBLOB")
            $attributes["FullPrivateKeyBlob"] = $fullPrivateBlob
            $attributes["RSAParameters"] = Parse-KeyBLOB -Key $fullPrivateBlob
            
        }

        return New-Object psobject -Property $attributes
        
    }
}

# Parses the given CAPI blob
# Mar 3th 2022
function Parse-CapiBlob
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [byte[]]$Data,
        [Parameter(Mandatory=$false)]
        [switch]$Decrypt,
        [Parameter(Mandatory=$false)]
        [switch]$LocalMachine
    )
    Begin
    {
        Add-Type -AssemblyName System.Security
    }
    Process
    {
        # Parse the header
        $version =  [System.BitConverter]::ToInt32($Data,0)
        if($version -ne 2)
        {
            Throw "Unsupported version ($Version), expected 2"
        }
        $unk1          = [System.BitConverter]::ToInt32($Data,4)
        $nameLen       = [System.BitConverter]::ToInt32($Data,8)
        $unk2          = [System.BitConverter]::ToInt32($Data,12)
        $unk3          = [System.BitConverter]::ToInt32($Data,16)
        $publicKeyLen  = [System.BitConverter]::ToInt32($Data,20)
        $privateKeyLen = [System.BitConverter]::ToInt32($Data,24)
        $unk4          = [System.BitConverter]::ToInt32($Data,28)
        $unk5          = [System.BitConverter]::ToInt32($Data,32)
        $privatePropertiesLen = [System.BitConverter]::ToInt32($Data,36)

        $name = [text.encoding]::Ascii.GetString($Data, 40, $nameLen-1)

        Write-Verbose "Parsing CAPI key: $name"

        # Set the position
        $p = 40+$nameLen

        $unkArray = $Data[$p..($p + 20 -1)]; $p += 20

        # Public key CAPI blob
        $publicKeyBlob = $Data[$p..$($p + $publicKeyLen - 1)]; $p += $publicKeyLen
        
        # Get the private key and private properties blobs
        $privateKeyBlob        = $Data[$p..$($p + $privateKeyLen -1)] ; $p += $privateKeyLen
        $privatePropertiesBlob = $Data[$p..$($p + $privatePropertiesLen -1)] 

        $attributes = [ordered]@{
            "Name"           = $name
            "PrivateKeyBlob" = @()
            "RSAParameters"  = Parse-CAPIKeyBLOB -Key $publicKeyBlob
        }
        if($Decrypt)
        {
            $dpapiScope = "CurrentUser"
            
            if($LocalMachine)
            {
                $CurrentUser = "{0}\{1}" -f $env:USERDOMAIN,$env:USERNAME
        
                $dpapiScope = "LocalMachine"
                # Elevate to get access to the DPAPI keys
                if([AADInternals.Native]::copyLsassToken())
                {
                    Write-Warning "Running as LOCAL SYSTEM. You MUST restart PowerShell to restore $CurrentUser rights."
                }
                else
                {
                    Write-Error "Could not elevate, unable to decrypt. MUST be run as administrator!"
                    return
                }
            }
            
            # Decrypt the private key properties using DPAPI
            $decPrivateProperties = [Security.Cryptography.ProtectedData]::Unprotect($privatePropertiesBlob, $DPAPI_ENTROPY_CAPI_KEY_PROPERTIES, $dpapiScope)
            $attributes["PrivateKeyProperties"] = $decPrivateProperties

            # Decrypt the private key blob using DPAPI
            $decPrivateBlob = [Security.Cryptography.ProtectedData]::Unprotect($privateKeyBlob, $null, $dpapiScope)
            
            # Parse the CAPI blob
            $attributes["RSAParameters"] = Parse-CAPIKeyBLOB -Key $decPrivateBlob
        }

        return New-Object psobject -Property $attributes
        
    }
}

# Parses the given CAPI Key BLOB and returns RSAParameters
# Mar 8th 2022
Function Parse-CAPIKeyBLOB
{
    [cmdletbinding()]
    param(
        [parameter(Mandatory=$false,ValueFromPipeline)]
        [Byte[]]$Key
    )
    process
    {
        if($Key -eq $null)
        {
            return $null
        }

        $magic    = [text.encoding]::ASCII.GetString($Key[0..3])
        $modlen   = [bitconverter]::ToUInt32($Key,4)
        $bitlen   = [bitconverter]::ToUInt32($Key,8)
        $unknown  = [bitconverter]::ToUInt32($Key,12)
        $publen   = 4

        $headerLen = 4 * [System.Runtime.InteropServices.Marshal]::SizeOf([uint32]::new())

        # Parse RSA1
        $p = $headerLen
        $pubexp  = $Key[($p)..($p + $publen -1)]; $p += $publen
        $modulus = $key[($p)..($p + $modlen -9)]; $p += $modlen
        
        # Parse RSA2 (RSAPRIVATEBLOB)
        if($magic -eq "RSA2") 
        {
            $prime1 =           $key[($p)..($p-1 + $bitlen/16)] ; $p += $bitlen/16
            $p += 4
            $prime2 =           $key[($p)..($p-1 + $bitlen/16)] ; $p += $bitlen/16
            $p += 4
            $exponent1 =        $key[($p)..($p-1 + $bitlen/16)] ; $p += $bitlen/16
            $p += 4
            $exponent2 =        $key[($p)..($p-1 + $bitlen/16)] ; $p += $bitlen/16
            $p += 4
            $coefficient =      $key[($p)..($p-1 + $bitlen/16)] ; $p += $bitlen/16
            $p += 4
            $privateExponent =  $key[($p)..($p-1 + $bitlen/8)] 
        }
        
        $attributes=@{
            "D" =        $privateExponent
            "DP" =       $exponent1
            "DQ" =       $exponent2
            "Exponent" = $pubexp
            "InverseQ" = $coefficient
            "Modulus" =  $modulus
            "P" =        $prime1
            "Q"=         $prime2
        }

        # Reverse
        foreach($name in $attributes.Keys)
        {
            if($attributes[$name])
            {
                [Array]::Reverse($attributes[$name])
            }
        }

        [System.Security.Cryptography.RSAParameters]$RSAParameters = New-Object psobject -Property $attributes

        return $RSAParameters
    }
}

# Checks is the current user running as Administrator
# Feb 6th 2022
function Test-LocalAdministrator  
{
    [cmdletbinding()]

    param(
        [parameter(Mandatory=$False)]
        [switch]$Throw,
        [parameter(Mandatory=$False)]
        [switch]$Warn
    )
    Process
    {  
        $isAdmin = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)

        if(!$isAdmin -and $Warn)
        {
            Write-Warning "The PowerShell session is not elevated, please run as Administrator."
        }
        elseif(!$isAdmin -and $Throw)
        {
            Throw "The PowerShell session is not elevated, please run as Administrator."
        }
        return $isAdmin
    }
}

# Parses the given Cert BLOB and returns the parsed attributes
# Aug 17th 2022
function Parse-CertBlob
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [byte[]]$Data
    )
    
    Process
    {
        function Get-UnicodeString
        {
            [CmdletBinding()]
            param(
                [Parameter(Mandatory=$true)]
                [byte[]]$Data,
                [Parameter(Mandatory=$true)]
                [int]$p
            )
    
            Process
            {
                $s = $p
                while($Data[$p] -ne 0 -and $Data[$p+1] -eq 0)
                {
                    $p+=2
                }
                $p+=2
                return [System.Text.Encoding]::Unicode.GetString($Data,$s,$p-$s)
            }
        }

        $p = 0;

        $attributes = [psobject]::new()

        while($p -lt $Data.Length)
        {
            $propId   = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $flags    = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $dataLen  = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            $propData = $Data[$p..($p+$dataLen-1)]; $p += $dataLen

            switch($propId)
            {
                # Provider info
                2 {
                    $pp = 0
                    $containerNameOffset = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4
                    $providerNameOffset  = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4
                    $providerType        = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4
                    $providerFlags       = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4
                    $providerParam       = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4
                    $providerParamOffset = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4
                    $keySpec             = [System.BitConverter]::ToInt32($propData,$pp); $pp += 4

                    $attributes | Add-Member -NotePropertyName "Container" -NotePropertyValue (Get-UnicodeString -Data $propData -p $containerNameOffset)
                    $attributes | Add-Member -NotePropertyName "Provider"  -NotePropertyValue (Get-UnicodeString -Data $propData -p $providerNameOffset)

                    break
                }
                # SHA1
                3 {
                    $attributes | Add-Member -NotePropertyName "SHA1" -NotePropertyValue ((Convert-ByteArrayToHex -Bytes $propData).ToUpper())
                    break
                }
                # MD5
                4 {
                    $attributes | Add-Member -NotePropertyName "MD5" -NotePropertyValue ((Convert-ByteArrayToHex -Bytes $propData).ToUpper())
                    break
                }
                # Friendly Name
                10 {
                    $attributes | Add-Member -NotePropertyName "FriendlyName" -NotePropertyValue (Get-UnicodeString -Data $propData -p 0)
                    break
                }
                # Signature hash
                15 {
                    $attributes | Add-Member -NotePropertyName "SignatureHash" -NotePropertyValue ((Convert-ByteArrayToHex -Bytes $propData).ToUpper())
                    break
                }
                # Key Identifier
                20 {
                    $attributes | Add-Member -NotePropertyName "KeyIdentifier" -NotePropertyValue ((Convert-ByteArrayToHex -Bytes $propData).ToUpper())
                    break
                }
                # Issuer Public Key MD5
                24 {
                    $attributes | Add-Member -NotePropertyName "IssuerPublicKeyMD5" -NotePropertyValue ((Convert-ByteArrayToHex -Bytes $propData).ToUpper())
                    break
                }
                # Subject Public Key MD5
                25 {
                    $attributes | Add-Member -NotePropertyName "SubjectPublicKeyMD5" -NotePropertyValue ((Convert-ByteArrayToHex -Bytes $propData).ToUpper())
                    break
                }
                # DER
                32 {
                    $attributes | Add-Member -NotePropertyName "DER" -NotePropertyValue $propData
                    break
                }
                # SmartCardReader
                101 {
                    $attributes | Add-Member -NotePropertyName "SmartCardReader" -NotePropertyValue (Get-UnicodeString -Data $propData -p 0)
                    break
                }
                Default {
                    Write-Verbose "Unknown certificate property ($propId), size ($dataLen)"
                    break
                }
            }

        }

        return $attributes
    }
}

# Jul 24th 2024
# Returns the name of the current user
function Get-CurrentUser
{
    return [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
}

# Jul 24th 2024
# Return true if running as system
function Is-System
{
    (Get-CurrentUser).equals("NT AUTHORITY\SYSTEM")
}

# Jul 24th 2024
# Run the given command as a service as the given user
function Invoke-ScriptAs
{
<#
    .SYNOPSIS
    Invokes the given PS command as the given user.
 
    .DESCRIPTION
    Invokes the given PS command as the given user by creating and starting a service.
 
    .PARAMETER Command
    Command to be executed. Must be shorter than 8191 characters!
 
    .PARAMETER Credentials
    Credentials of the user or service account
 
    .PARAMETER GMSA
    Name of the MSA or GMSA service account. Must be available (installed) on the local computer.
 
    .PARAMETER ServiceName
    Name of the service to be created. Defaults to "AADInternals????" where ???? is a four digit random number.
 
    .Example
    Invoke-AADIntScriptAs -Command "whoami" -GMSA 'CONTOSO\ADSyncMSA55a35$' -Verbose
 
    VERBOSE: Creating service AADInternals3486
    VERBOSE: Creating service to be run as Local System
    VERBOSE: Changing user to CONTOSO\ADSyncMSA55a35$
    VERBOSE: Setting ServiceAccountManaged property
    VERBOSE: Starting service AADInternals3486
    VERBOSE: Creating outbound named pipe AADInternals3486-out
    VERBOSE: Sending command AADInternals3486-out
    VERBOSE: Creating inbound named pipe AADInternals3486-in
    VERBOSE: Waiting for connection
    VERBOSE: Reading response from AADInternals3486-in
    contoso\adsyncmsa55a35$
    VERBOSE: Stopping service AADInternals3486
    VERBOSE: Deleting service AADInternals3486
    VERBOSE: Deleting service executable C:\Program Files\WindowsPowerShell\Modules\AADinternals-endpoints\0.9.5\AADInternals3486.exe
 
    .Example
 
    Invoke-AADIntScriptAs -Command "whoami" -Verbose
    VERBOSE: Creating service AADInternals5749
    VERBOSE: Creating service to be run as Local System
    VERBOSE: Starting service AADInternals5749
    VERBOSE: Creating outbound named pipe AADInternals5749-out
    VERBOSE: Sending command AADInternals5749-out
    VERBOSE: Creating inbound named pipe AADInternals5749-in
    VERBOSE: Waiting for connection
    VERBOSE: Reading response from AADInternals5749-in
    nt authority\system
    VERBOSE: Stopping service AADInternals5749
    VERBOSE: Deleting service AADInternals5749
    VERBOSE: Deleting service executable C:\Program Files\WindowsPowerShell\Modules\AADinternals-endpoints\0.9.5\AADInternals5749.exe
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [String]$Command,           
        [Parameter(Mandatory=$false)]
        [pscredential]$Credentials,
        [Parameter(Mandatory=$false)]
        [String]$GMSA,
        [Parameter(Mandatory=$false)]
        [String]$ServiceName="AADInternals$(Get-Random -Minimum 1000 -Maximum 9999)"
      )
    Begin
    {

    }
    Process
    {
        if($command.Length -gt 8191)
        {
            Write-Warning "Command length $($command.Length) greater than 8191, execution probably fails!"
        }
        $description = "Service to run PowerShell commands as System or other users"
        
        # Path to service executable.
        $folder = $PSScriptRoot
        if([string]::IsNullOrEmpty($folder))
        {
            $folder = (Get-Location).Path
        }
        $servicePath="$folder\$ServiceName.exe"

        # The service source code
        $serviceSource=@"
using System;
using System.IO.Pipes;
using System.IO;
using System.Reflection;
using System.ServiceProcess;
using System.Threading;
using System.Management;
using System.Diagnostics;
using System.Text;
 
namespace AADInternals
{
    public class $ServiceName : ServiceBase
    {
        public static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[]
            {
                new $ServiceName()
            };
            ServiceBase.Run(ServicesToRun);
        }
 
 
        protected override void OnStart(string[] args)
        {
            new Thread(Service).Start();
        }
 
        private static void Service()
        {
            string command = "";
 
            //
            // Wait for the command
            //
            using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("$ServiceName-out", PipeDirection.InOut))
            {
                // Wait for a client to connect
                pipeServer.WaitForConnection();
 
                try
                {
                    // Read the command
                    using (StreamReader sr = new StreamReader(pipeServer))
                    {
                        while (!sr.EndOfStream)
                            command += sr.ReadLine();
                    }
 
                }
                catch (IOException e){}
            }
 
            //
            // Run the command
            //
            string returnValue;
            try
            {
                Process p = new Process();
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.RedirectStandardOutput = true;
                p.StartInfo.FileName = "PowerShell.exe";
                p.StartInfo.Arguments = String.Format("-ExecutionPolicy Bypass -Command \"& {{{0}}}\"", command);
                p.Start();
 
                // Read the output
                returnValue = p.StandardOutput.ReadToEnd();
                p.WaitForExit();
            }
            catch (Exception e)
            {
                returnValue = e.InnerException.Message.Replace(System.Environment.NewLine, "");
            }
 
            //
            // Send the response back to client
            //
 
            using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", "$ServiceName-in", PipeDirection.InOut))
            {
                // Connect
                pipeClient.Connect();
 
                try
                {
                    using (StreamWriter sw = new StreamWriter(pipeClient,Encoding.UTF8,UInt16.MaxValue))
                    {
                         
                        sw.AutoFlush = true;
                        sw.WriteLine(returnValue);
                    }
                }
                catch (IOException e){};
            }
        }
    }
}
"@

        try
        {

            # Create the service executable
            try
            {
                Add-Type -TypeDefinition $serviceSource -Language CSharp -OutputAssembly $servicePath -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false -IgnoreWarnings
            }
            catch
            {
                throw "Unable to create service executable ($servicePath): $($_.Exception.Message)"
            }

            # Create the service
            Write-Verbose "Creating service $ServiceName"

            
            # Group Managed Service Account
            if($GMSA)
            {
                Write-Verbose " Creating service to be run as Local System"
                $service = New-Service -Name $ServiceName -BinaryPathName $servicePath -Description $Description -ErrorAction SilentlyContinue

                # Change the user to provided service account
                Write-Verbose " Changing user to $GMSA"
                Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName" -Name "ObjectName"            -Value $GMSA

                # Set the account to service account managed - (not required)
                Write-Verbose " Setting ServiceAccountManaged property"
                Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName" -Name "ServiceAccountManaged" -Value ([System.BitConverter]::GetBytes([int32]1)) 
            }
            elseif($Credentials)
            {
                # First, give permissions to service executable
                Write-Verbose " Adding ReadAndExecute permissions to $servicePath for Everyone"
                $permissions = Get-Acl -Path $servicePath -ErrorAction SilentlyContinue

                $accessRule = [Security.AccessControl.FileSystemAccessrule]::new("Everyone", [System.Security.AccessControl.FileSystemRights]::ReadAndExecute, [System.Security.AccessControl.AccessControlType]::Allow)
                $permissions.AddAccessRule($accessRule)
                
                Set-Acl -Path $servicePath -AclObject $permissions -ErrorAction SilentlyContinue

                # Start with provided credentials
                Write-Verbose " Creating service to be run as $($Credentials.UserName) with password $($Credentials.GetNetworkCredential().Password)"
                $service = New-Service -Name $ServiceName -BinaryPathName $servicePath -Description $Description -Credential $credentials -ErrorAction SilentlyContinue
            }
            else
            {
                # Start as Local System
                Write-Verbose " Creating service to be run as Local System"
                $service = New-Service -Name $ServiceName -BinaryPathName $servicePath -Description $Description -ErrorAction SilentlyContinue
            }

            # Start the service
            if($service)
            {
                Write-Verbose " Starting service $ServiceName"
                
                Start-Service -Name $ServiceName -ErrorAction Stop
            }
            else
            {
                Throw "Could not create service $ServiceName"
            }

            # Create an output named piped client to connect to the service
            try 
            {
                Write-Verbose " Creating outbound named pipe $ServiceName-out"
                $pipeOut = [System.IO.Pipes.NamedPipeClientStream]::new(".","$ServiceName-out")
                $pipeOut.Connect(5000) # Timeout 5 seconds

                $sw = [System.IO.StreamWriter]::new($pipeOut)
                $sw.AutoFlush = $true
    
                # Send the configuration to the service
                Write-Verbose " Sending command $ServiceName-out"
                $sw.WriteLine($Command)
            } 
            catch
            {
                Throw "Error send message to service: $_"
            } 
            finally 
            {
                if ($sw) 
                {
                    $sw.Dispose() 
                }
            }
            if ($pipeOut) 
            {
                $pipeOut.Dispose()
            }
        
            # Create an input named piped client to receive output from the service
            try 
            {
                Write-Verbose " Creating inbound named pipe $ServiceName-in"
                # Allow everyone to access the pipe
                $pse = [System.IO.Pipes.PipeSecurity]::new()
                $sid = [System.Security.Principal.SecurityIdentifier]::new([System.Security.Principal.WellKnownSidType]::WorldSid, $null)
                $par = [System.IO.Pipes.PipeAccessRule]::new($sid, [System.IO.Pipes.PipeAccessRights]::ReadWrite, [System.Security.AccessControl.AccessControlType]::Allow)
                $pse.AddAccessRule($par)
                $pipeIn = [System.IO.Pipes.NamedPipeServerStream]::new("$ServiceName-in",[System.IO.Pipes.PipeDirection]::InOut,1,[System.IO.Pipes.PipeTransmissionMode]::Message, [System.IO.Pipes.PipeOptions]::None,4096,4096,$pse)
            
                Write-Verbose " Waiting for connection"
                $pipeIn.WaitForConnection()

                Write-Verbose " Reading response from $ServiceName-in"
                $sr = [System.IO.StreamReader]::new($pipeIn)
            
                while(!$sr.EndOfStream)
                {
                    $message += $sr.Readline()
                }
            } 
            catch 
            {
                Throw "Error receiving message from service: $_"
            } 
            finally 
            {
                if ($sr) 
                {
                    $sr.Dispose() 
                }
                if ($pipeIn) 
                {
                    $pipeIn.Dispose()
                }
            }

            Write-Debug " Message: $message"
            return $message
        }
        catch
        {
            throw $_
        }
        Finally
        {
            # Clean up
            Remove-Services -ServiceName $ServiceName
        }
    }
}

# Jul 25th 2024
# Finds the private key using a key or file name.
function Find-PrivateKey
{
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName = "KeyName",Mandatory=$True)]
        [String]$KeyName,
        [Parameter(ParameterSetName = "FileName",Mandatory=$True)]
        [String]$FileName,
        [Parameter(Mandatory=$False)]
        [switch]$Elevate,
        [Parameter(Mandatory=$False)]
        [switch]$AsJson,
        [Parameter(Mandatory=$False)]
        [string[]]$Paths
    )
    Begin
    {
        $sha256 = [System.Security.Cryptography.SHA256]::Create()
    }
    Process
    {
        # Required to run as system?
        if($Elevate -and !(Is-System))
        {
            Write-Verbose "Elevating to LOCAL SYSTEM."
            $cmdToRun = "Set-Location '$PSScriptRoot';. '.\Win32Ntv.ps1';. '.\CommonUtils.ps1';. '.\CommonUtils_endpoints.ps1'; Find-PrivateKey -Elevate -AsJson"
            if($KeyName)
            {
                $cmdToRun += " -KeyName '$KeyName'"
            }
            else
            {
                $cmdToRun += " -FileName '$FileName'"
            }

            if($Paths)
            {
                $cmdToRun += " -Paths '$($Paths -join "','")'"
            }

            Write-Verbose "Command = $cmdTorun"
                
            try
            {
                $keyJson = Invoke-ScriptAs -Command $cmdToRun
                $key = ConvertFrom-Json -InputObject $keyJson

                # Re-create RSAParameters
                $rsaParameters = [System.Security.Cryptography.RSAParameters]$key.rsaparameters
                $key.PSObject.Properties.Remove("RSAParameters")
                $key | Add-Member -NotePropertyName "RSAParameters" -NotePropertyValue $rsaParameters

                return $key
            }
            catch
            {
                throw "Unable to get private key as LOCAL SYSTEM"
            }
        }

        # Get the key blob
        if($Elevate)
        {
            $keyPath = "$env:ALLUSERSPROFILE"
        }
        else
        {
            $keyPath = "$env:APPDATA"
        }

        # CryptoAPI and CNG stores keys in different directories
        # https://docs.microsoft.com/en-us/windows/win32/seccng/key-storage-and-retrieval
        if(!$Paths)
        {
            $paths = @(
                "$keyPath\Microsoft\Crypto\RSA\MachineKeys"
                "$keyPath\Microsoft\Crypto\Keys"
                "$keyPath\Microsoft\Crypto\SystemKeys"
                "$keyPath\Application Data\Microsoft\Crypto\Keys"
                "$env:windir\ServiceProfiles\NetworkService\AppData\Roaming\Microsoft\Crypto\RSA\S-1-5-20\"
                )
        }    
        # Loop through the paths
        foreach($path in $paths)
        {
            # If path exists..
            if(Test-Path -Path $path)
            {
                Write-Verbose "Processing $path"
                # If filename provided, try to open the file
                if($FileName)
                {
                    if(Test-Path -Path "$path\$FileName")
                    {
                        Write-Verbose "Key for file name $FileName found!"
                        $keyBlob = Get-BinaryContent -Path "$path\$FileName"
                        break
                    }
                }
                else 
                {
                    # Open each file until matching key is found
                    $keyFiles = Get-ChildItem -Path $path
                    foreach($keyFile in $keyFiles)
                    {
                        $keyBlob = Get-BinaryContent -Path $keyFile.FullName

                        # Parse the blob to get the name
                        $blobType = [System.BitConverter]::ToInt32($keyBlob,0)
                        switch($blobType)
                        {
                            1 { $key = Parse-CngBlob  -Data $keyBlob }
                            2 { $key = Parse-CapiBlob -Data $keyBlob }
                            default { throw "Unsupported key blob type" }
                        }
                                                        
                        if($key.name -eq $transPortKeyName)
                        {
                            Write-Verbose "Key for name $KeyName found!"
                            break
                        }
                    }
                }
            }
        }

        # Decrypt the blob
        if($keyBlob)
        {
            # Parse the key blob
            $blobType = [System.BitConverter]::ToInt32($keyBlob,0)
            switch($blobType)
            {
                1 { $key = Parse-CngBlob  -Data $keyBlob -Decrypt }
                2 { $key = Parse-CapiBlob -Data $keyBlob -Decrypt }
                default { throw "Unsupported key blob type" }
            }

            if($AsJson)
            {
                return (ConvertTo-Json -InputObject $key -Compress)
            }
            else
            {
                return $key
            }
        }
        else
        {
            if($AsJson)
            {
                return $null
            }
            else
            {
                throw "Key not found!"
            }
        }
            
    }

}


# Nov 5th 2024
# Remove the given or all AADInternals services used by Invoke-ScriptAs
function Remove-Services
{
<#
    .SYNOPSIS
    Removes the given or all AADInternalsXXXX services created by Invoke-AADIntScriptAs
 
    .DESCRIPTION
    Removes the given or all AADInternalsXXXX services created by Invoke-AADIntScriptAs.
    If the invoke fails, service and service executable may remain on the disk.
 
    .PARAMETER ServiceName
    Name of the service to be removed
 
    .Example
    Remove-AADIntServices -Verbose
 
    VERBOSE: Removing all AADInternals services
    VERBOSE: Stopping service AADInternals1522
    VERBOSE: Deleting service AADInternals1522
    VERBOSE: Deleting service executable C:\Program Files\WindowsPowerShell\Modules\AADinternals-endpoints\0.9.5\AADInternals1522.exe
    VERBOSE: Deleting service executable C:\Program Files\WindowsPowerShell\Modules\AADinternals-endpoints\0.9.5\AADInternals3279.exe
    VERBOSE: Deleting service executable C:\Program Files\WindowsPowerShell\Modules\AADinternals-endpoints\0.9.5\AADInternals4934.exe
#>

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$ServiceName
      )
    
    Process
    {
        # Path to service executable.
        $folder = $PSScriptRoot
        if([string]::IsNullOrEmpty($folder))
        {
            $folder = (Get-Location).Path
        }

        $servicePaths=@()

        if([string]::IsNullOrEmpty($ServiceName))
        {
            Write-Verbose " Removing all AADInternals services"
            foreach($servicePath in Get-ChildItem -Path $folder -Filter "AADInternals????.exe")
            {
                $servicePaths += $servicePath.FullName
            }
        }
        else
        {
            $servicePaths += "$folder\$ServiceName.exe"
        }
        
        foreach($servicePath in $servicePaths)
        {
            $ServiceName = (Get-ChildItem -Path $servicePath).PSChildName.Split(".")[0]

            if(Get-Service -Name $ServiceName -ErrorAction SilentlyContinue)
            {
                Write-Verbose " Stopping service $ServiceName"
                Stop-Service $ServiceName -ErrorAction SilentlyContinue | Out-Null
                Write-Verbose " Deleting service $ServiceName"
                SC.exe DELETE $ServiceName | Out-Null
            }

            Write-Verbose " Deleting service executable $servicePath"
            Remove-Item -Path $servicePath -Force -ErrorAction SilentlyContinue

        }
    }
}