Revisited: PowerShell and Encryption

Back in November, I made a post about saving passwords for your PowerShell scripts. As I mentioned in that article, the ConvertFrom-SecureString cmdlet uses the Data Protection API to create an encrypted copy of the SecureString's contents. DPAPI uses master encryption keys that are saved in the user's profile; unless you enable either Roaming Profiles or Credential Roaming, you'll only be able to decrypt that value on the same computer where the encryption took place. Even if you do enable Credential Roaming, only the same user account who originally encrypted the data will be able to read it.

So, what do you do if you want to encrypt some data that can be decrypted by other user accounts?

The ConvertFrom-SecureString and ConvertTo-SecureString cmdlets have a pair of parameters (-Key and -SecureKey) that allow you to specify your own encryption key instead of using DPAPI. When you do this, the SecureString's contents are encrypted using AES. Anyone who knows the AES encryption key will be able to read the data.

That's the easy part. Encrypting data is simple; making sure your encryption keys don't get exposed is the trick. If you've hard-coded the keys in your script, you may as well have just stored the password in plain text, for all the good the encryption will do. There are several ways you can try to save and protect your AES key; you could place it in a file with strong NTFS permissions, or in a database with strict access control, for example. In this post, however, I'm going to focus on another technique: encrypting your AES key with RSA certificates.

If you have an RSA certificate (even a self-signed one), you can encrypt your AES key using the RSA public key. At that point, only someone who has the certificate's private key will be able to retrieve the AES key and read your data. Instead of trying to protect encryption keys yourself, we're back to letting the OS handle the heavy lifting; if it protects your RSA private keys well, then your AES key is also safe. Here's a brief example of creating a SecureString, saving it with a new random 32-byte AES key, and then using an RSA certificate to encrypt the key itself:

try
{
    $secureString = 'This is my password.  There are many like it, but this one is mine.' | 
                    ConvertTo-SecureString -AsPlainText -Force

    # Generate our new 32-byte AES key.  I don't recommend using Get-Random for this; the System.Security.Cryptography namespace
    # offers a much more secure random number generator.

    $key = New-Object byte[](32)
    $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create()

    $rng.GetBytes($key)

    $encryptedString = ConvertFrom-SecureString -SecureString $secureString -Key $key

    # This is the thumbprint of a certificate on my test system where I have the private key installed.

    $thumbprint = 'B210C54BF75E201BA77A55A0A023B3AE12CD26FA'
    $cert = Get-Item -Path Cert:\CurrentUser\My\$thumbprint -ErrorAction Stop

    $encryptedKey = $cert.PublicKey.Key.Encrypt($key, $true)

    $object = New-Object psobject -Property @{
        Key = $encryptedKey
        Payload = $encryptedString
    }

    $object | Export-Clixml .\encryptionTest.xml

}
finally
{
    if ($null -ne $key) { [array]::Clear($key, 0, $key.Length) }
}

Notice the use of try/finally and [array]::Clear() on the AES key's byte array. It's a good habit to make sure you're not leaving the sensitive data lying around in memory longer than absolutely necessary. (This is the same reason you get a warning if you use ConvertTo-SecureString -AsPlainText without the -Force switch; .NET doesn't allow you to zero out the memory occupied by a String.)

Any user who has the certificate installed, including its private key, will be able to load up the XML file and obtain the original SecureString as follows:

try
{
    $object = Import-Clixml -Path .\encryptionTest.xml

    $thumbprint = 'B210C54BF75E201BA77A55A0A023B3AE12CD26FA'
    $cert = Get-Item -Path Cert:\CurrentUser\My\$thumbprint -ErrorAction Stop

    $key = $cert.PrivateKey.Decrypt($object.Key, $true)

    $secureString = $object.Payload | ConvertTo-SecureString -Key $key
}
finally
{
    if ($null -ne $key) { [array]::Clear($key, 0, $key.Length) }
}

Using RSA certificates to protect your AES encryption keys is as simple as that: Get-Item, $cert.PublicKey.Key.Encrypt() , and $cert.PrivateKey.Decrypt() . You can even make multiple copies of the AES key with different RSA certificates, so that more than one person/certificate can decrypt the data.

I've posted several examples of data encryption techniques in PowerShell on the TechNet Gallery. Some are based on SecureStrings, as the code above, and others use .NET's CryptoStream class to encrypt basically anything (in this case, an entire file on disk.)

Posted in:
About the Author

Dave Wyatt

Dave Wyatt is a Microsoft MVP (PowerShell) and a member of PowerShell.org's Board of Directors.

7 Comments

  1. Hello Dave !

    Nice post ! Is there a way we can encrypt the whole file without the need of passing a string ? I was looking for powershell encrypting the whole file, not just a string..

    Thanks !

  2. Hi, thanks to your tutorial, but i can't do it.
    I can encrypt but not decrypt.

    Incorrect Key..

    + $Key = $Cert.PrivateKey.Decrypt($Object.Key, $true)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : CryptographicException

    I try many things, i read classes documentation.. Here are your scripts :

    Encrypt.ps1
    Import-Module Microsoft.PowerShell.Security

    Clear-Host

    Try {
    # Get imported certificate by using Thumbprint
    $Thumbprint = ('‎20 84 2d 2a c8 3e 5f ef 7a 87 20 a4 5b 2a da 1e 4a f8 00 02').ToUpper() -replace '\s',''
    $Cert = Get-ChildItem Cert:\LocalMachine\My\ | Where-Object { $_.Thumbprint -eq $Thumbprint } -ErrorAction Stop
    If (!($Cert.PrivateKey -is [System.Security.Cryptography.RSACryptoServiceProvider])){
    Write-Error 'The certificate is not valable'
    Break
    }

    # Convert password to SecureString
    $SecureString = Read-Host -Prompt 'Please Input your password' -AsSecureString

    # Create AES encryption key
    $Key = New-Object Byte[](32)
    # Create cryptographic Random Number Generator (RNG) using the implementation provided by the cryptographic service provider (CSP).
    $Rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create()
    # Fills an array of bytes with a cryptographically strong sequence of random values
    $Rng.GetBytes($Key)

    # Encrypt SecureString password with the AES encryption key
    $EncryptedSecureString = ConvertFrom-SecureString -SecureString $SecureString -Key $Key

    # To encrypt the key using RSA, we use the PublicKey.Key.Encrypt() method on the certificate.
    $EncryptedKey = $Cert.PublicKey.Key.Encrypt($Key, $true)

    # Save the encrypted data and our protected copie of the AES key to a file. In this case, Export-CliXml is a handy way to do this,
    # though the text representation does take up a fair amount of space.
    $Object = New-Object PSObject -Property @{
    Key = $EncryptedKey
    Payload = $EncryptedSecureString
    }
    $Object | Export-Clixml "C:\Temp\password.xml" -Force
    }
    Finally {
    # Clear key from memory
    If ($null -ne $Key) {
    [Array]::Clear($Key, 0, $Key.Length)
    }
    }

    Decrypt.ps1

    Import-Module Microsoft.PowerShell.Security

    Clear-Host

    Try {
    # Get imported certificate by using Thumbprint
    $Thumbprint = ('‎20 84 2d 2a c8 3e 5f ef 7a 87 20 a4 5b 2a da 1e 4a f8 00 02').ToUpper() -replace '\s',''
    $Cert = Get-ChildItem Cert:\LocalMachine\My\ | Where-Object { $_.Thumbprint -eq $Thumbprint } -ErrorAction Stop
    If (!($Cert.PrivateKey -is [System.Security.Cryptography.RSACryptoServiceProvider])){
    Write-Error 'The certificate is not valable'
    Break
    }

    # Import file with encrypted password
    $Object = Import-Clixml -Path "C:\Temp\password.xml"

    # Decrypt Key with the certificat
    $Key = $Cert.PrivateKey.Decrypt($Object.Key, $true)

    # Now we have our original AES key, and can use it to decrypt the data back into SecureString form.
    $SecureString = $Object.Payload | ConvertTo-SecureString -Key $Key

    # As before, we'll display the decrypted version of the string, to make sure it's correct (though this does violate our "don't leave sensitive data lying around in memory" practice, for demonstration purposes only.)
    $cred = New-Object System.Management.Automation.PSCredential('UserName', $secureString)
    $plainText = $cred.GetNetworkCredential().Password
    Write-Host "Plain text: $plainText"
    }
    Finally {
    # Clear key from memory
    If ($null -ne $Key) {
    [Array]::Clear($Key, 0, $Key.Length)
    }
    }

    To generate my test certificate i used "Makecert" with the following arguments :
    makecert.exe -sk C:\temp\Cert\cert.crt -n "CN=Powershell" -pe -sr localmachine -ss My -# 1 -$ individual -r

    Thanks 🙂

  3. Can we get New-SelfSignedCertificate syntax which will allow you to create proper RSA certificate to enable this functionality. Default invocation seems to be missing some properties needed for encryption.

    • Found it if anybody in the same boat. Correct way to generate self signed certificate with built-in New-SelfSingedCertificate in powershell is below (this works only on Windows 2016 and Windows 10 though)

      New-SelfSignedCertificate -Subject "Mycert" -KeyUsage KeyEncipherment, DataEncipherment -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider"