Files
Nestori ff0457aa95 v0.9.8
2025-04-18 14:41:13 +03:00

1110 lines
37 KiB
PowerShell

# 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 PRTToken
# Aug 20th 2020
function Get-AccessTokenWithPRTToken
{
[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,[Parameter(Mandatory=$False)]
[String]$SubScope
)
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 = "$(Get-TenantLoginUrl -SubScope $SubScope)/$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" = Get-UserAgent
"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
$config = Parse-LoginMicrosoftOnlineComConfig -Body $response
if($config.sErrorCode)
{
Throw (Get-Error -ErrorCode $config.sErrorCode)
}
$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
}
if($CAE)
{
$body["claims"] = Get-CAEClaims
}
# Make the second request to get the access token
$response = Invoke-RestMethod -UseBasicParsing -Uri "$(Get-TenantLoginUrl -SubScope $SubScope)/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,
[Parameter(Mandatory=$False)]
[String]$SubScope
)
Process
{
Get-AccessTokenWithRefreshToken -Resource "urn:ms-drs:enterpriseregistration.windows.net" -ClientId "b90d5b8f-5503-4153-b545-b31cecfaece2" -TenantId "Common" -RefreshToken $BPRT -SubScope $SubScope
}
}
# Gets the access token using a signed PRT request
# Feb 4th 2025
function Get-AccessTokenWithSignedPRTRequest
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$RefreshToken,
[Parameter(Mandatory=$True)]
[String]$SessionKey,
[Parameter(Mandatory=$False)]
[String]$Context="8J+YiEFBREludGVybmFscyBGVFfwn5Kp",
[Parameter(Mandatory=$True)]
$ClientId,
[Parameter(Mandatory=$True)]
$Resource,
[Parameter(Mandatory=$False)]
[String]$SubScope,
[Parameter(Mandatory=$False)]
[bool]$CAE
)
Process
{
# Create a signed PRT request
$jwt = New-PRTSignedJWE -RefreshToken $RefreshToken -SessionKey $SessionKey -Context $Context -KdfV2 $true -ClientId $ClientId -Resource $Resource -Request
# Set the body for API call
$body = @{
"windows_api_version" = "2.0"
"grant_type" = "urn:ietf:params:oauth:grant-type:jwt-bearer"
"request" = $jwt
"client_info" = "1"
"tgt" = "true"
}
# Set Continuous Access Evaluation (CAE) token claims
if($CAE)
{
$body["claims"] = Get-CAEClaims
}
# Get the encrypted response
$jwe = Invoke-RestMethod -UseBasicParsing -Method Post -Uri "$(Get-TenantLoginUrl -SubScope $SubScope)/Common/oauth2/token" -Body $body #-Headers $headers -WebSession $session
# Convert session key to byte array and decrypt JWE
$sKey = Convert-B64ToByteArray -B64 $SessionKey
$response = [text.encoding]::UTF8.GetString((Decrypt-JWE -JWE $jwe -SessionKey $sKey)) | ConvertFrom-Json
# Debug
Write-Debug "ACCESS TOKEN RESPONSE: $response"
return $response
}
}
# 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 -notin "A256GCM","A128CBC-HS256")
{
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 -in "Deflate","DEF")
{
$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()
}
}
# 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"
}
}
}
# Creates a PRT signed JWE
# Feb 4th 2025
function New-PRTSignedJWE
{
[cmdletbinding()]
Param(
[Parameter(Mandatory=$True)]
[String]$RefreshToken,
[Parameter(Mandatory=$True)]
[String]$SessionKey,
[Parameter(Mandatory=$False)]
[String]$Context="8J+YiEFBREludGVybmFscyBGVFfwn5Kp",
[Parameter(Mandatory=$False)]
[bool]$KdfV2 = $true,
[Parameter(Mandatory=$False)]
[String]$SubScope,
[Parameter(ParameterSetName='PRTToken',Mandatory=$True)]
[switch]$PRTToken,
[Parameter(ParameterSetName='Request',Mandatory=$True)]
[switch]$Request,
[Parameter(ParameterSetName='PRTToken',Mandatory=$False)]
[Parameter(ParameterSetName='Request',Mandatory=$True)]
[string]$ClientId,
[Parameter(ParameterSetName='PRTToken',Mandatory=$False)]
[Parameter(ParameterSetName='Request',Mandatory=$True)]
[string]$Resource
)
Process
{
# Define variables
$ctx = Convert-B64ToByteArray -B64 $Context
$sKey = Convert-B64ToByteArray -B64 $SessionKey
$iat = [int]((Get-Date).ToUniversalTime() - $epoch).TotalSeconds
$nonce = (Invoke-RestMethod -UseBasicParsing -Method Post -Uri "$(Get-TenantLoginUrl -SubScope $SubScope)/Common/oauth2/token" -Body "grant_type=srv_challenge").Nonce
# Create the header and body
$hdr = [ordered]@{
"alg" = "HS256"
"typ" = "JWT"
"ctx" = $Context
}
if($PRTToken)
{
$pld = [ordered]@{
"refresh_token" = $RefreshToken
"is_primary" = "true"
"iat" = $iat
"request_nonce" = $nonce
}
}
else
{
$pld=[ordered]@{
#"win_ver"= "10.0.19041.1620"
"scope"= "openid aza"
"resource" = $resource
"request_nonce" = $nonce
"refresh_token" = $RefreshToken
#"redirect_uri" = "ms-appx-web://Microsoft.AAD.BrokerPlugin/$clientId"
"redirect_uri" = Get-AuthRedirectUrl -ClientId $ClientId -Resource $Resource
"iss" = "aad:brokerplugin"
"grant_type" = "refresh_token"
"client_id" = $clientId
"aud" = "login.microsoftonline.com"
}
}
# Derive the key from session key and context
if($KdfV2)
{
$hdr["kdf_ver"] = 2
$derivedContext = Get-KDFv2Context -Context $ctx -Payload $pld
}
else
{
$derivedContext = $ctx
}
$key = Get-PRTDerivedKey -Context $derivedContext -SessionKey $sKey
# Create the JWT
$jwt = New-JWT -Key $key -Header $hdr -Payload $pld
# Return
return $jwt
}
}