mirror of
https://github.com/Gerenios/AADInternals
synced 2026-06-08 11:09:38 +00:00
1110 lines
37 KiB
PowerShell
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
|
|
}
|
|
}
|