PowerShell Encrypt and Decrypt Strings with AES in Scripts
─□✕

PowerShell Encrypt and Decrypt Strings with AES in Scripts

PowerShell Tips Editor 3 min read
PowerShell Encrypt and Decrypt Strings with AES in Scripts

Storing passwords and API keys as plaintext in script files or config files is the single most common credential hygiene failure in Windows automation. PowerShell’s built-in SecureString serialization is machine- and user-bound — useful for interactive scripts but not for service accounts or cross-machine automation where the script and the key store must be separated. Symmetric AES encryption using .NET’s cryptography classes gives you a pragmatic middle ground: strong 256-bit encryption with explicit control over key and IV management.

Quick Answer

Use [System.Security.Cryptography.Aes]::Create() to get an AES provider, generate a random key and IV with RandomNumberGenerator, encrypt to Base64 for safe storage, and decrypt with the same key and IV. Store the key in the Windows Certificate Store or DPAPI — never alongside the ciphertext.

Understanding AES: Key, IV, and Block Mode Choices

AES is a symmetric block cipher. Three parameters define its security profile in your script:

  • Key size: 128, 192, or 256 bits. Use 256 bits — the performance difference is negligible in scripting contexts.
  • IV (Initialization Vector): A random 16-byte value unique to each encryption operation. Never reuse an IV with the same key — doing so breaks semantic security, allowing pattern analysis across ciphertexts.
  • Block cipher mode: Use CBC (Cipher Block Chaining) with PKCS7 padding for general-purpose string encryption. Avoid ECB mode — it produces identical ciphertext blocks for identical plaintext blocks.
$aes           = [System.Security.Cryptography.Aes]::Create()
$aes.KeySize   = 256
$aes.BlockSize = 128
$aes.Mode      = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding   = [System.Security.Cryptography.PaddingMode]::PKCS7

Write-Host "Key size:   $($aes.KeySize) bits"
Write-Host "Block size: $($aes.BlockSize) bits"
Write-Host "Mode:       $($aes.Mode)"

Generating a Secure Key and IV with RNGCryptoServiceProvider

Never generate a key from a password string without a proper KDF. For script-to-script automation, generate a cryptographically random key once and store it securely. Aes.GenerateKey() and Aes.GenerateIV() use the OS CSPRNG internally.

# Generate fresh random key and IV
$aes = [System.Security.Cryptography.Aes]::Create()
$aes.KeySize = 256
$aes.GenerateKey()
$aes.GenerateIV()

# Convert to Base64 for storage (key must be stored separately from ciphertext)
$keyB64 = [Convert]::ToBase64String($aes.Key)
$ivB64  = [Convert]::ToBase64String($aes.IV)

Write-Host "Key (store securely, NOT with ciphertext):"
Write-Host $keyB64

Write-Host "`nIV (safe to store with ciphertext — new IV per message):"
Write-Host $ivB64

The IV is not secret — it can be stored alongside the ciphertext. The key is secret and must never appear in the same file or location as the data it encrypts.

Encrypting a String to Base64 with System.Security.Cryptography.Aes

Convert the plaintext to bytes, create an encryptor transform, and pipe through a CryptoStream into a MemoryStream.

function Protect-String {
    param (
        [string] $PlainText,
        [byte[]] $Key
    )

    $aes         = [System.Security.Cryptography.Aes]::Create()
    $aes.KeySize = 256
    $aes.Key     = $Key
    $aes.GenerateIV()   # fresh IV for every encryption

    $encryptor   = $aes.CreateEncryptor($aes.Key, $aes.IV)
    $plainBytes  = [System.Text.Encoding]::UTF8.GetBytes($PlainText)

    $ms = [System.IO.MemoryStream]::new()
    $cs = [System.Security.Cryptography.CryptoStream]::new(
              $ms, $encryptor,
              [System.Security.Cryptography.CryptoStreamMode]::Write)

    $cs.Write($plainBytes, 0, $plainBytes.Length)
    $cs.FlushFinalBlock()
    $cs.Dispose()

    # Return IV prepended to ciphertext, all Base64-encoded
    $combined = $aes.IV + $ms.ToArray()
    return [Convert]::ToBase64String($combined)
}

Prepending the IV to the ciphertext is the standard convention — the IV is always 16 bytes for AES, so the decryptor knows exactly where to split. This makes the output a single self-contained Base64 string.

Decrypting Back to Plaintext

Split the combined Base64 blob back into IV and ciphertext, then reverse the operation with a decryptor transform.

function Unprotect-String {
    param (
        [string] $CipherBase64,
        [byte[]] $Key
    )

    $combined    = [Convert]::FromBase64String($CipherBase64)
    $iv          = $combined[0..15]           # first 16 bytes are the IV
    $cipherBytes = $combined[16..($combined.Length - 1)]

    $aes         = [System.Security.Cryptography.Aes]::Create()
    $aes.KeySize = 256
    $aes.Key     = $Key
    $aes.IV      = $iv

    $decryptor = $aes.CreateDecryptor($aes.Key, $aes.IV)
    $ms        = [System.IO.MemoryStream]::new($cipherBytes)
    $cs        = [System.Security.Cryptography.CryptoStream]::new(
                     $ms, $decryptor,
                     [System.Security.Cryptography.CryptoStreamMode]::Read)

    $reader    = [System.IO.StreamReader]::new($cs)
    $plainText = $reader.ReadToEnd()
    $reader.Dispose()

    return $plainText
}

# Round-trip test
$key     = [Convert]::FromBase64String($keyB64)
$cipher  = Protect-String -PlainText "MySecretApiKey123!" -Key $key
$plain   = Unprotect-String -CipherBase64 $cipher -Key $key
Write-Host "Decrypted: $plain"

Storing the Key Securely in the Windows Certificate Store

Embed the AES key bytes in a self-signed certificate’s extended attribute, stored in the user or machine certificate store. The private key protects access; the certificate provides a searchable, manageable container.

# Create a self-signed cert to act as a key container
$cert = New-SelfSignedCertificate `
            -Subject "CN=DSCScriptKey,O=Automation" `
            -CertStoreLocation "Cert:\LocalMachine\My" `
            -KeyUsage KeyEncipherment, DataEncipherment `
            -NotAfter (Get-Date).AddYears(2)

# Encrypt the AES key using the cert's public key
$rsaPublic  = $cert.PublicKey.GetRSAPublicKey()
$encKeyBytes = $rsaPublic.Encrypt($key, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256)
$encKeyB64   = [Convert]::ToBase64String($encKeyBytes)

# Store encrypted key in config (alongside ciphertext is safe — RSA-protected)
Write-Host "RSA-encrypted AES key (store in config): $encKeyB64"
Write-Host "Cert thumbprint (reference in config):   $($cert.Thumbprint)"

Building Protect-String and Unprotect-String Helper Functions

Wrap the full encrypt/decrypt cycle — including key retrieval from the cert store — into reusable module-level functions.

function Get-AesKeyFromCert {
    param ([string] $Thumbprint, [string] $EncryptedKeyB64)

    $cert       = Get-Item "Cert:\LocalMachine\My\$Thumbprint" -ErrorAction Stop
    $rsaPrivate = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
    $encBytes   = [Convert]::FromBase64String($EncryptedKeyB64)
    return $rsaPrivate.Decrypt($encBytes,
           [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256)
}

# Usage in a script:
$aesKey  = Get-AesKeyFromCert -Thumbprint $config.CertThumbprint `
                               -EncryptedKeyB64 $config.EncryptedAesKey
$apiKey  = Unprotect-String -CipherBase64 $config.ApiKeyCipher -Key $aesKey
Write-Host "Retrieved API key: $($apiKey.Substring(0,4))****"

Common Errors

  • Reusing the same IV for multiple encryptions: If you encrypt two different strings with the same key and the same IV, an attacker who intercepts both ciphertexts can XOR them together to cancel out the keystream and analyze the plaintext relationship. Always call $aes.GenerateIV() immediately before each CreateEncryptor call.
  • Key stored in script file negates encryption: An AES key hardcoded in the script that uses it provides no security — anyone who can read the script can read the key and decrypt the ciphertext. The key must live in a separate, access-controlled location: the Windows Certificate Store, DPAPI-encrypted, or a secrets manager like Azure Key Vault or HashiCorp Vault.

Related Cmdlets / See Also

Wrapping Up

AES encryption in PowerShell requires three discipline points: always generate a fresh IV per message, never co-locate the key with the ciphertext, and use the Windows Certificate Store or DPAPI to protect the key itself. The Protect-String and Unprotect-String functions above give you a reusable, auditable pattern that you can drop into any automation script that needs to handle sensitive configuration values.

Send-Item -To