diff --git a/PowerShell Scripts/EncryptGatewayKeyPairCredentials.ps1 b/PowerShell Scripts/EncryptGatewayKeyPairCredentials.ps1 new file mode 100644 index 00000000..e41cb002 --- /dev/null +++ b/PowerShell Scripts/EncryptGatewayKeyPairCredentials.ps1 @@ -0,0 +1,186 @@ +<# +.SYNOPSIS + Encrypts KeyPair credentials for a Fabric / Power BI on-premises gateway. + +.DESCRIPTION + Implements the full AES-256-CBC + RSA-OAEP-SHA256 + HMAC-SHA256 encryption + pipeline required by the Power BI REST API when updating gateway data-source + credentials of type KeyPair (username + private key, optional passphrase). + + Credential type : KeyPair + Required fields : username, privatekey + Optional fields : passphrase (omit or leave empty if the key is unprotected) + + Authentication : Azure token via Az PowerShell module. + Run `Connect-AzAccount` before executing this script. + +.PREREQUISITE + Install-Module -Name Az -Scope CurrentUser + +.EXAMPLE + # With passphrase + $result = Encrypt-Credentials -gateway_id "" -username "myuser" ` + -privatekey "" -passphrase "" + + # Without passphrase + $result = Encrypt-Credentials -gateway_id "" -username "myuser" ` + -privatekey "" +#> + +function Get-PublicKeyFromGateway { + param ( + [string]$gateway_id + ) + $secureToken = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token + $fabricToken = [System.Net.NetworkCredential]::new('', $secureToken).Password + $url = "https://api.fabric.microsoft.com/v1/gateways/$gateway_id" + $headers = @{ + 'Authorization' = "Bearer $fabricToken" + 'Accept' = 'application/json' + } + $response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get + return @{ + Exponent = $response.publicKey.exponent + Modulus = $response.publicKey.modulus + } +} + +function Add-PKCS7Padding { + param ([byte[]]$data, [int]$blockSize = 16) + $padLen = $blockSize - ($data.Length % $blockSize) + return $data + ([byte[]]@($padLen) * $padLen) +} + +function Concat-Bytes { + param ([byte[][]]$arrays) + $totalLength = ($arrays | Measure-Object -Property Length -Sum).Sum + $result = New-Object byte[] $totalLength + $offset = 0 + foreach ($arr in $arrays) { + [Array]::Copy($arr, 0, $result, $offset, $arr.Length) + $offset += $arr.Length + } + return $result +} + +function Get-SignedPayload { + param ( + [byte[]]$ciphertext, + [byte[]]$iv, + [byte[]]$signKey + ) + + $algorithms = [byte[]](0, 0) + $toSign = Concat-Bytes @($algorithms, $iv, $ciphertext) + + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + $hmac.Key = $signKey + $signature = $hmac.ComputeHash($toSign) + + $fullPayload = Concat-Bytes @($algorithms, $signature, $iv, $ciphertext) + return [Convert]::ToBase64String($fullPayload) +} + +function Encrypt-Keys { + param ( + [string]$modulus_b64, + [string]$exponent_b64, + [byte[]]$symmetricKey, + [byte[]]$signKey + ) + + $modulus = [Convert]::FromBase64String($modulus_b64) + $exponent = [Convert]::FromBase64String($exponent_b64) + + $rsa = New-Object System.Security.Cryptography.RSACng + $rsa.ImportParameters([System.Security.Cryptography.RSAParameters]@{ + Modulus = $modulus + Exponent = $exponent + }) + + if ($symmetricKey.Length -eq 32) { $symLength = 0 } + elseif ($symmetricKey.Length -eq 64) { $symLength = 1 } + else { throw "Unsupported key length: $($symmetricKey.Length)" } + + if ($signKey.Length -eq 32) { $signLength = 0 } + elseif ($signKey.Length -eq 64) { $signLength = 1 } + else { throw "Unsupported key length: $($signKey.Length)" } + + $lengths = [byte[]]@($symLength, $signLength) + $combined = Concat-Bytes @($lengths, $symmetricKey, $signKey) + $encrypted = $rsa.Encrypt($combined, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256) + return [Convert]::ToBase64String($encrypted) +} + + +function Encrypt-Credentials { + param ( + [Parameter(Mandatory = $true)] + [string]$gateway_id, + + [Parameter(Mandatory = $true)] + [string]$username, + + [Parameter(Mandatory = $true)] + [string]$privatekey, + + # Leave empty string if the private key has no passphrase + [Parameter(Mandatory = $false)] + [string]$passphrase = "" + ) + + $publicKey = Get-PublicKeyFromGateway -gateway_id $gateway_id + $modulus_b64 = $publicKey.Modulus + $exponent_b64 = $publicKey.Exponent + + $credentials = @{ + credentialData = @( + @{ name = "username"; value = $username }, + @{ name = "privatekey"; value = $privatekey }, + @{ name = "passphrase"; value = $passphrase } + ) + } | ConvertTo-Json -Depth 3 -Compress + + $aesKey = New-Object byte[] 32 + $iv = New-Object byte[] 16 + $signKey = New-Object byte[] 64 + $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() + $rng.GetBytes($aesKey) + $rng.GetBytes($iv) + $rng.GetBytes($signKey) + + $aes = [System.Security.Cryptography.Aes]::Create() + $aes.Mode = 'CBC' + $aes.Key = $aesKey + $aes.IV = $iv + $aes.Padding = 'None' + + $plainBytes = [System.Text.Encoding]::UTF8.GetBytes($credentials) + $padded = Add-PKCS7Padding -data $plainBytes + + $encryptor = $aes.CreateEncryptor() + $ciphertext = $encryptor.TransformFinalBlock($padded, 0, $padded.Length) + + $signed = Get-SignedPayload -ciphertext $ciphertext -iv $iv -signKey $signKey + $encryptedKeys = Encrypt-Keys -modulus_b64 $modulus_b64 -exponent_b64 $exponent_b64 -symmetricKey $aesKey -signKey $signKey + + [Array]::Clear($signKey, 0, $signKey.Length) + return $encryptedKeys + $signed +} + +# --------------------------------------------------------------------------- +# Example execution – fill in values before running +# --------------------------------------------------------------------------- +# Required +$gateway_id = "" # Fabric / Power BI gateway ID (GUID) +$username = "" # Username associated with the private key +$privatekey = "" # Private key content (PEM string) +$outputFilePath = "" # File path to write the encrypted payload + +# Optional – leave empty string if the private key has no passphrase +$passphrase = "" + +$result = Encrypt-Credentials -gateway_id $gateway_id -username $username ` + -privatekey $privatekey -passphrase $passphrase +$result | Set-Content -Path $outputFilePath -Encoding UTF8 +Write-Host "Encrypted payload written to: $outputFilePath" diff --git a/Python/Encrypt KeyPair Credentials/README.md b/Python/Encrypt KeyPair Credentials/README.md new file mode 100644 index 00000000..a703dd58 --- /dev/null +++ b/Python/Encrypt KeyPair Credentials/README.md @@ -0,0 +1,45 @@ +# Encrypt KeyPair Credentials + +Sample script for programmatically encrypting **KeyPair** credentials against a +Fabric / Power BI on-premises gateway. + +--- + +## Credential fields + +| Field | Required | Description | +|--------------|----------|----------------------------------------------------------| +| `username` | Yes | Username associated with the private key | +| `privatekey` | Yes | Private key in PKCS #8 PEM format | +| `passphrase` | No | Passphrase protecting the private key. Omit if not set. | + +--- + +## Prerequisites + +- Python 3.8 or later +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) — run `az login` once before executing the script + +Install Python dependencies: + +```bash +pip install -r requirements.txt +``` + +## Run + +1. Open `encrypt.py` and fill in the placeholder values in the `if __name__ == "__main__":` block: + + ```python + gateway_id = "" + username = "" + privatekey = "" + passphrase = "" # leave empty string if the key has no passphrase + output_path = "encrypted_payload.txt" + ``` + +2. Execute: + + ```bash + python encrypt.py + ``` diff --git a/Python/Encrypt KeyPair Credentials/encrypt.py b/Python/Encrypt KeyPair Credentials/encrypt.py new file mode 100644 index 00000000..adcb9ca5 --- /dev/null +++ b/Python/Encrypt KeyPair Credentials/encrypt.py @@ -0,0 +1,142 @@ +# --------------------------------------------------------------------------- +# encrypt_keypair_credentials.py +# +# Sample script for encrypting KeyPair credentials (username, private key, +# and optional passphrase) against a Fabric/Power BI on-premises gateway. +# +# Credential type: KeyPair +# Required fields : username, privatekey +# Optional fields : passphrase +# +# Authentication : Azure CLI (run `az login` before executing) +# Dependencies : see requirements.txt +# --------------------------------------------------------------------------- + +import subprocess +import json +import requests +from base64 import b64decode, b64encode +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_OAEP, AES +from Crypto.Random import get_random_bytes +from Crypto.Hash import HMAC, SHA256 + + +def get_azure_cli_access_token(resource: str = 'https://api.fabric.microsoft.com') -> str: + cmd = ['az', 'account', 'get-access-token', '--resource', resource, '--output', 'json'] + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, # <-- This enables cmd.exe resolution + text=True + ) + if result.returncode != 0: + raise RuntimeError(f"Azure CLI error: {result.stderr}") + token_data = json.loads(result.stdout) + return token_data['accessToken'] + + +def get_public_key_from_gateway(gateway_id: str, access_token: str) -> (str, str): + url = f"https://api.fabric.microsoft.com/v1/gateways/{gateway_id}" + headers = { + 'Authorization': f'Bearer {access_token}', + 'Accept': 'application/json' + } + response = requests.get(url, headers=headers) + response.raise_for_status() + data = response.json() + return data['publicKey']['exponent'], data['publicKey']['modulus'] + + +# ==== Existing encryption logic unchanged ==== + +def map_generated_key_length(key: bytes) -> int: + if len(key) == 32: + return 0 + elif len(key) == 64: + return 1 + else: + raise ValueError(f"Unsupported key length: {len(key)}") + + +def concat_arrays(*arrays: bytes) -> bytes: + return b''.join(arrays) + + +def get_signed_payload(ciphertext: bytes, iv: bytes, sign_key: bytes) -> str: + algorithms = bytes([0, 0]) + to_sign = concat_arrays(algorithms, iv, ciphertext) + hmac = HMAC.new(sign_key, digestmod=SHA256) + hmac.update(to_sign) + signature = hmac.digest() + full_payload = concat_arrays(algorithms, signature, iv, ciphertext) + return b64encode(full_payload).decode('utf-8') + + +def encrypt_keys(modulus_b64: str, exponent_b64: str, symmetric_key: bytes, sign_key: bytes) -> str: + modulus_bytes = b64decode(modulus_b64 + '==') + exponent_bytes = b64decode(exponent_b64 + '==') + modulus_int = int.from_bytes(modulus_bytes, byteorder='big') + exponent_int = int.from_bytes(exponent_bytes, byteorder='big') + rsa_key = RSA.construct((modulus_int, exponent_int)) + cipher_rsa = PKCS1_OAEP.new(rsa_key, hashAlgo=SHA256) + + lengths = bytes([map_generated_key_length(symmetric_key), map_generated_key_length(sign_key)]) + combined_keys = concat_arrays(lengths, symmetric_key, sign_key) + encrypted_keys = cipher_rsa.encrypt(combined_keys) + return b64encode(encrypted_keys).decode('utf-8') + + +def encrypt_credentials_full(credentials: str, gateway_id: str) -> str: + access_token = get_azure_cli_access_token() + exponent_b64, modulus_b64 = get_public_key_from_gateway(gateway_id, access_token) + + aes_key = get_random_bytes(32) + iv = get_random_bytes(16) + sign_key = bytearray(get_random_bytes(64)) + + cipher_aes = AES.new(aes_key, AES.MODE_CBC, iv) + padding_len = 16 - (len(credentials.encode()) % 16) + padded_credentials = credentials.encode() + bytes([padding_len] * padding_len) + ciphertext = cipher_aes.encrypt(padded_credentials) + + signed_payload = get_signed_payload(ciphertext, iv, sign_key) + encrypted_keys = encrypt_keys(modulus_b64, exponent_b64, aes_key, sign_key) + + for i in range(len(sign_key)): + sign_key[i] = 0 + + return encrypted_keys + signed_payload + + +def build_key_pair_credentials(username: str, privatekey: str, passphrase: str) -> str: + payload = { + "credentialData": [ + {"name": "username", "value": username}, + {"name": "privatekey", "value": privatekey}, + {"name": "passphrase", "value": passphrase}, + ] + } + return json.dumps(payload, separators=(',', ':')) + + +# === Sample Run === +if __name__ == "__main__": + # ---------------------------------------------------------------- + # Fill in the values below before running. + # Leave passphrase as empty string if not required. + # ---------------------------------------------------------------- + gateway_id = "" + username = "" + privatekey = "" + passphrase = "" + output_path = "" + + credentials = build_key_pair_credentials(username, privatekey, passphrase) + print("Starting encryption using Azure CLI authentication...") + encrypted = encrypt_credentials_full(credentials, gateway_id) + + with open(output_path, "w") as f: + f.write(encrypted) + print(f"Encrypted payload written to {output_path}") diff --git a/Python/Encrypt KeyPair Credentials/requirements.txt b/Python/Encrypt KeyPair Credentials/requirements.txt new file mode 100644 index 00000000..6051451c --- /dev/null +++ b/Python/Encrypt KeyPair Credentials/requirements.txt @@ -0,0 +1,2 @@ +pycryptodome>=3.20.0 +requests>=2.31.0