PowerShell - Encrypting Passwords...Again

Written on March 3, 2019

I have covered working with passwords in PowerShell in a previous post, but wanted to go over some more advanced options.

The 2 methods I will cover are:

  • Secure String Key Encryption
  • Certificate Encryption

Secure String Key Encryption


As you probably know, there is a -Key parameter in both the ConvertTo-SecureString and ConvertFrom-SecureString cmdlets. If you have played around with this you have likely figured out how to use this parameter. There is however a more secure way of generating a random byte array for use as the key in these situations.

Here is an example:

$key=New-Object -TypeName Byte[] -ArgumentList 32
$rng=[Security.Cryptography.RNGCryptoServiceProvider]::Create()

$rng.GetBytes($key)
$secureString=Read-Host -AsSecureString -Prompt String

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

Write-Host "Encrypted String:"
Write-Output $encrypted

You could use the Get-Random cmdlet to generate the key, however it is more secure to use the RNGCryptoServiceProvider object as it will be more random in its number generation. Of course to be truly random, you would want to be using something less controlled than the CPU's clock, but that would potentially require special hardware and software.

Now to decrypt your string:

$secure=ConvertTo-SecureString -String $encrypted -Key $key
$tempCred=New-Object -TypeName PSCredential -ArgumentList 'temp',$secure

Write-Host "Decrypted String:"
$tempCred.GetNetworkCredential().Password
Remove-Variable tempCred

I should point out that the -Key parameter will only accept a ByteArray with a bit length of either 128, 192, or 256.

Armed with that simple example; we can make a general function for this:

Function New-RandomByteArray {
  [CmdletBinding()]
  Param(
    [ValidateRange(8,[int]::MaxValue)]
    [Parameter(Position=0)]
    [int]$BitLength=256
  )
  Try{
    if($BitLength%8 -ne 0){
      Write-Error -Message "BitLength must be divisible by 8" `
        -Category InvalidArgument `
        -ErrorAction Stop
    }
    $key=New-Object -TypeName Byte[] -ArgumentList ($BitLength/8)
    $rng=[Security.Cryptography.RNGCryptoServiceProvider]::Create()

    $rng.GetBytes($key)
    Write-Output $key
  }Catch{
    Write-Error -Category $_.CategoryInfo.Category `
      -Exception $_.Exception `
      -Message $_.Exception.Message
  }
}

Function ConvertTo-EncryptedString {
  [CmdletBinding(DefaultParameterSetName='String')]
  Param(
    [Parameter(
      Position=0,
      Mandatory=$true,
      ValueFromPipeline=$true,
      ParameterSetName='String'
    )]
    [String]$String,

    [Parameter(
      Position=0,
      Mandatory=$true,
      ValueFromPipeline=$true,
      ParameterSetName='SecureString'
    )]
    [Security.SecureString]$SecureString,

    [ValidateCount(16,32)]
    [Parameter(Position=1,Mandatory=$true)]
    [Byte[]]$Key
  )
  Begin{}
  Process{
    Try{
      if($PSCmdlet.ParameterSetName -eq 'String'){
        $SecureString=ConvertTo-SecureString -String $String `
          -AsPlainText `
          -Force `
          -ErrorAction Stop
      }
      ConvertFrom-SecureString -SecureString $SecureString `
        -Key $Key
    }Catch{
      Write-Error -Category $_.CategoryInfo.Category `
        -Exception $_.Exception `
        -Message $_.Exception.Message
    }
  }
  End{}
}

Function ConvertFrom-EncryptedString {
  [CmdletBinding()]
  Param(
    [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
    [String]$EncryptedString,

    [ValidateCount(16,32)]
    [Parameter(Position=1)]
    [Byte[]]$Key,

    [Parameter(Position=2)]
    [Switch]$Secure
  )
  Begin{
    if(!($PSBoundParameters.ContainsKey('Key'))){
      Write-Error "No key specified. Unable to decrypt string" `
        -Category SecurityError
      break
    }
  }
  Process{
    Try{
      $secureStr=ConvertTo-SecureString -String $EncryptedString `
        -Key $Key `
        -ErrorAction Stop

      if($Secure){
        return $secureStr
      }

      $bstr=[Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureStr)
      [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)

    }Catch{
      Write-Error -Category $_.CategoryInfo.Category `
        -Exception $_.Exception `
        -Message $_.Exception.Message
    }
  }
  End{}
}

Example 1


Here is a basic example of how to encrypt/decrypt a string with this method:

$key=New-RandomByteArray -BitLength 256
$encrypt=ConvertTo-EncryptedString -String 'SecureMePlz!' -Key $key
Write-Output $encrypt

# Decrypt
ConvertFrom-EncryptedString -EncryptedString $encrypt -Key $key

You will only be able to decrypt this string if you have the key that was used to encrypt it. You can try to generate a new random key and test the results.

Example 2


For additional security, you can opt to encrypt/decrypt the string and using SecureString objects.

$key=New-RandomByteArray
$secure=Read-Host -AsSecureString -Prompt Password

$encryptedSecure=ConvertTo-EncryptedString -SecureString $secure -Key $key
Write-Output $encryptedSecure

# Decrypt to securestring using -Secure
$decrypted=ConvertFrom-EncryptedString -EncryptedString $encryptedSecure `
  -Secure `
  -Key $key

Write-Output $decrypted

# Now to prove it is decrypted correctly
$tempCred=New-Object -TypeName PSCredential `
  -ArgumentList 'temp',$decrypted
$tempCred.GetNetworkCredential().Password
Remove-Variable tempCred

Encrypt Passwords with Certificates


I made a post recently that discussed how to encrypt credentials with Desired State Configuration (DSC).
It just so happens that if you create a certificate using either of the methods used in that post, you can encrypt your own passwords using that certificate.

To complete the following example, you will need to be running PowerShell as an Administrator. Here's an example using the self-signed certificate example from that post:

Create the certificate

# Create and import certificate to the LocalComputer\My certificate store
$certFolder='C:\cert'
$certStore='Cert:\LocalMachine\My'
$pubCertPath=Join-Path -Path $certFolder -ChildPath SelfSigned.cer
$expiryDate=(Get-Date).AddYears(2)

# You may want to delete this file after completing
$privateKeyPath=Join-Path -Path $ENV:TEMP -ChildPath SelfPrivKey.pfx

$privateKeyPass=Read-Host -AsSecureString -Prompt "Private Key Password"

if(!(Test-Path -Path $certFolder)){
    New-Item -Path $certFolder -Type Directory | Out-Null
}

$cert=New-SelfSignedCertificate -Type DocumentEncryptionCertLegacyCsp `
  -DnsName 'TestSelfSigned' `
  -HashAlgorithm SHA512 `
  -NotAfter $expiryDate `
  -KeyLength 4096 `
  -CertStoreLocation $certStore

$cert | Export-PfxCertificate -FilePath $privateKeyPath `
  -Password $privateKeyPass `
  -Force

$cert | Export-Certificate -FilePath $pubCertPath 

Import-Certificate -FilePath $pubCertPath `
  -CertStoreLocation $certStore

Import-PfxCertificate -FilePath $privateKeyPath `
  -CertStoreLocation $certStore `
  -Password $privateKeyPass | Out-Null

Encrypt using the certificate public key

$certificate=Get-Item -Path "Cert:\LocalMachine\My\$($cert.Thumbprint)"
$encryptMe='Encrypt all the things!'
$encodeBytes=[Text.Encoding]::UTF8.GetBytes($encryptMe)

# Encrypt
[byte[]]$encryptBytes=$certificate.PublicKey.Key.Encrypt($encodeBytes, $true)
$encrypted=[Convert]::ToBase64String($encryptBytes)
Write-Output $encrypted

Decrypt using certificate private key

$encryptedBytes=[Convert]::FromBase64String($encrypted)
if($certificate.PrivateKey){
  $bytes=$certificate.PrivateKey.Decrypt($encryptedBytes, $true)
  $decrypted=[Text.Encoding]::UTF8.GetString($bytes)
  Write-Output $decrypted
}else{
  Write-Error "The Certificate does not have a private key accessible!" `
    -ErrorAction Stop
}

Functions for Certificate Encryption


Function CreateX509CertFromFile {
  [CmdletBinding()]
  Param(
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    [String]$filePath
  )
  Try{
    $pathResolve=Resolve-Path -Path $filePath -ErrorAction Stop
    if($pathResolve.Provider.Name -eq 'Certificate'){
      Get-Item -Path $filePath
    }else{
      $cert=[Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile(
        $filePath
      )
      [Security.Cryptography.X509Certificates.X509Certificate2]$cert
    }
  }Catch{
    Write-Error -Category $_.CategoryInfo.Category `
      -Exception $_.Exception `
      -Message $_.Exception.Message
  }
}

Function ConvertTo-CertificateEncryptedString {
  [CmdletBinding(DefaultParameterSetName='CertFile')]
  Param(
    [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
    [String]$String,

    [Parameter(
      Position=1,
      Mandatory=$true,
      ValueFromPipeline=$true,
      ParameterSetName='Cert'
    )]
    [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

    [Parameter(
      Position=1,
      Mandatory=$true,
      ParameterSetName='CertFile'
    )]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    [String]$CertFilePath
  )
  Begin{}
  Process{
    Try{
      if($PSCmdlet.ParameterSetName -eq 'CertFile'){
        $Certificate=CreateX509CertFromFile -filePath $CertFilePath `
          -ErrorAction Stop
      }
      $encodeBytes=[Text.Encoding]::UTF8.GetBytes($String)
      
      [byte[]]$encryptBytes=$Certificate.PublicKey.Key.Encrypt($encodeBytes, $true)
      [Convert]::ToBase64String($encryptBytes)
    }Catch {
      Write-Error -Message $_.Exception.Message `
        -Exception $_.Exception `
        -Category $_.CategoryInfo.Category
    }
  }
  End{}
}

Function ConvertFrom-CertificateEncryptedString {
  [CmdletBinding(DefaultParameterSetName='CertFile')]
  Param(
    [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)]
    [String]$EncryptedString,

    [Parameter(
      Position=1,
      Mandatory=$true,
      ValueFromPipeline=$true,
      ParameterSetName='Cert'
    )]
    [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

    [Parameter(
      Position=1,
      Mandatory=$true,
      ParameterSetName='CertFile'
    )]
    [ValidateScript({Test-Path -Path $_})]
    [String]$CertFilePath
  )
  Begin{}
  Process{
    Try{
      if($PSCmdlet.ParameterSetName -eq 'CertFile'){
        $Certificate=CreateX509CertFromFile -filePath $CertFilePath `
          -ErrorAction Stop
      }
      if(!$Certificate.PrivateKey){
        $privKeyException=[Management.Automation.PropertyNotFoundException](
          "Certificate [$($Certificate.Thumbprint)] Private Key not found."
        )
        Write-Error -Category ResourceUnavailable `
          -Exception $privKeyException `
          -Message $privKeyException.Message `
          -ErrorAction Stop
      }
      $encryptBytes=[Convert]::FromBase64String($EncryptedString)
      $bytes=$Certificate.PrivateKey.Decrypt($encryptBytes, $true)
      [Text.Encoding]::UTF8.GetString($bytes)
    }Catch{
      Write-Error -Category $_.CategoryInfo.Category `
        -Exception $_.Exception `
        -Message $_.Exception.Message
    }
  }
  End{}
}

Example 1


Use a certificate from the certificate store

# Encrypt
$certPath='Cert:\LocalMachine\My\3B99C626C216A9626193D85C61587132C42FBCC2'
$secret=ConvertTo-CertificateEncryptedString -String 'Testing Encryption' `
  -CertFilePath $certPath
Write-Output $secret

# Decrypt
$secret | ConvertFrom-CertificateEncryptedString -CertFilePath $certPath

Example 2


Use a certificate object

# Encrypt 
$cert=ls Cert:\LocalMachine\My\ | Where {$_.Subject -eq 'CN=TestSelfSigned'}
$encrypted=$cert | ConvertTo-CertificateEncryptedString -String 'Hello World!'
Write-Output $encrypted

# Decrypt
$cert | ConvertFrom-CertificateEncryptedString -EncryptedString $encrypted

Thanks for reading

PS> exit