Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions PowerShell Scripts/EncryptGatewayKeyPairCredentials.ps1
Original file line number Diff line number Diff line change
@@ -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 "<guid>" -username "myuser" `
-privatekey "<pem>" -passphrase "<secret>"

# Without passphrase
$result = Encrypt-Credentials -gateway_id "<guid>" -username "myuser" `
-privatekey "<pem>"
#>

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"
45 changes: 45 additions & 0 deletions Python/Encrypt KeyPair Credentials/README.md
Original file line number Diff line number Diff line change
@@ -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 = "<your-gateway-id>"
username = "<username>"
privatekey = "<private key>"
passphrase = "<passphrase>" # leave empty string if the key has no passphrase
output_path = "encrypted_payload.txt"
```

2. Execute:

```bash
python encrypt.py
```
142 changes: 142 additions & 0 deletions Python/Encrypt KeyPair Credentials/encrypt.py
Original file line number Diff line number Diff line change
@@ -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}")
2 changes: 2 additions & 0 deletions Python/Encrypt KeyPair Credentials/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pycryptodome>=3.20.0
requests>=2.31.0