Powershell Password Encryption & Decryption

One of the common task in PowerShell script design and execution is credential encryption requirement. Some privileged account is used and its credential need to pass to the script in order to access resources. It becomes crucial especially when the execution tasks are being delegated to other users or being automated. As storing the password as clear text is huge security risk and the last thing desired, here in this blog post we discuss a few options on storing the credential securely.

If account credential could be avoided being stored in the script, attempt that first. If the predefined / privileged account used is a Windows account (eg. local or domain account), and its process is executed through some task like Windows task scheduler (or PowerShell scheduled job), the privileged account could be setup as the running account of the task and let Windows handles the credential encryption securely during the setup. When the delegated user initiate the task on demand or the task is executed in a schedule automatically, the predefined account as the running account is used to access all the required resources with Windows integrated authentication.
Or assigning with PowerShell
$principal = New-ScheduledTaskPrincipal -UserId DomainA\TestUser
Set-ScheduledTask -TaskName TestingTask -Principal $principal
However often cases different Windows account(s) or third party software account(s) are used to access different type of resource within the execution. In these cases, it is likely that there will be a need to manage the account credential encryption ourselves.

PowerShell provides some native command for encryption. The commonly use command is the ConvertTo-SecureString and ConvertFrom-SecureString.

Take an example, this script is to obtain some server information of a remote server. However, the user who run the PowerShell script does not have the access to a remote server.
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA
As the executing account (user Windows account) doesn't have enough privilege, the access denied error is encountered

Get-WmiObject : Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
At line:1 char:2
+  Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WmiObject], UnauthorizedAccessException
    + FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

To incorporate a predefined Windows account which has been granted permission to the server to obtain this information,
$User = 'TestUser'
$Password = 'B@dPassw0rd!'
$SecurePassword = $Password | ConvertTo-SecureString -AsPlainText -Force
$UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword)
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
With the password in plain text, we would need to encrypt the password here.

The ConvertTo-SecureString cmdlet converts the password into a System.Security.SecureString object. This object represents text that should be kept confidential, such as removing it from the computer memory when no longer needed.

To pre-encrypt the password, we could use the same command and hard code the encrypted password in the script, save it in another file, in the registry or some other places.

Store encrypted password in a file (txt file)
To save the encrypted password text into a file,
$User = 'TestUser'
$Password = 'B@dPassw0rd!'
$Password | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File C:\encrypted.txt
Note: The Force flag is optional starting in PowerShell 7.

ConvertFrom-SecureString cmdlet convert a secure string (System.Security.SecureString) to an encrypted standard string using the Windows Data Protection API (DPAPI) using user account as default (we will discuss some issues with this default setting later in this post) OR with an encryption key if provided. The encrypted password text (eg.  '01000000d08c9ddf0115d1118c7a00c04fc29...') is then save into a file.

A better approach is not displaying the password in clear text at all,
$UserCred = Get-Credential
$UserCred.Password | ConvertFrom-SecureString | Out-File C:\encrypted.txt
The Get-Credential prompt for user name and password as directly save into a secure PSCredential object.

Decrypt encrypted password in a file (txt file)
With the password encrypted as stored in the file, now the script simply have to extract the encrypted password and pass it to the PSCredential object,
$User = 'TestUser'
$SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString
$UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword)
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Store encrypted password in a file using Export-Clixml (xml file)
Another option is to save it as xml file. We could save the PSCredential object with both user and encrypted password to a xml file.
Get-Credential | Export-Clixml -Path C:\encrypted.xml
Decrypt encrypted password in a file using Import-Clixml (xml file)
To load the xml directly back into a PSCredential object
$UserCred = Import-Clixml -Path C:\encrypted.xml
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Note that the ConvertTo-SecureString converts the encrypted password text into a SecureString object. A lot of (but not all) Windows and 3rd party PowerShell cmdlet utilize the SecureString object for authentication purpose like PSCredential object or Compellent Get-SCConnection. In general, the password does not need to be decrypted back to plain text as string object as it is exposed in the memory until it is removed by the garbage collector.

However, some cmdlet does not utilize the SecureString and require password in plain text. The SecureString object in this case will need to be decrypted into to plain text. If the account information has been constructed into a PSCredential object, the password could be extracted in plain text,
$Password = $UserCred.GetNetworkCredential().Password
OR starting in PowerShell 7
$Password = $UserCred.Password | ConvertFrom-SecureString -AsPlainText
Store encrypted password in a file for 3rd party password (txt file)
In the case of encrypting a third party account password,
$SecurePassword = Read-Host -AsSecureString
$SecurePassword | ConvertFrom-SecureString | Out-File C:\encrypted.txt
Decrypt encrypted password in a file for 3rd party password (txt file)
Decrypt 3rd party password into plain text by converting the SecureString into Binary String object,
$User = 'TestUser'
$SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString
$Marshal = [System.Runtime.InteropServices.Marshal]
$Bstr = $Marshal::SecureStringToBSTR($SecurePassword)
$Password = $Marshal::PtrToStringAuto($Bstr)
The last line ZeroFreeBSTR is to clear the unmanaged memory. OR starting in PowerShell 7
$User = 'TestUser'
$SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString
$Password = $SecurePassword | ConvertFrom-SecureString -AsPlainText
Store encrypted password in registry
Another method of saving encrypted password is to save it in registry. The example below uses HKCU (HKEY_CURRENT_USER) hive which the registry only applicable to the current user. You may want to use other more appropriate hives (eg. HKLM) for all other users access.
$SecurePasswordText = 'B@dPassw0rd!' | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
New-Item -Path HKCU:\Software\Test -Value $SecurePasswordText
$UserCred = Get-Credential
New-Item -Path HKCU:\Software\Test -Value ($UserCred.Password | ConvertFrom-SecureString)
OR storing both user name and password in the registry,
New-Item -Path HKCU:\Software\Test
New-ItemProperty -Path HKCU:\Software\Test -Name User -Value ($UserCred.UserName)
New-ItemProperty -Path HKCU:\Software\Test -Name Password -Value ($UserCred.Password | ConvertFrom-SecureString)
To decrypt encrypted password stored in registry
With the password encrypted as stored in the registry, here is how to extract it for PSCredential in the script,
$User = 'TestUser'
$SecurePassword = (Get-ItemProperty -Path HKCU:\Software\Test).'(Default)' | ConvertTo-SecureString
$UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword)
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
$User = (Get-ItemProperty -Path HKCU:\Software\Test).User
$SecurePassword = (Get-ItemProperty -Path HKCU:\Software\Test).Password | ConvertTo-SecureString
$UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword)
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Issue with default encryption using user account
By default, ConvertTo-SecureString cmdlet uses current user's password to generate an encryption key, and it is stored within the user profile (eg. %Userprofile%\Application Data\Microsoft\Crypto\RSA\User SID for RSA key). The encryption key is then used to encrypt the intended string. The same user's user profile is created independently on different computer. Unless the particular person's user account has been set as roaming profile, the encryption key on his user profile on one computer does not synchronize with his user profile on another computer. This creates some issues. If a person pre-encrypt the password on his computer, then deploy that to another server. First, the encrypted password text can't be decrypted because the encryption key is not present on the person' user profile of the server. Needless to say, other users won't be able to decrypt the password as well because they don't have the encryption key. This is the error received.

ConvertTo-SecureString : Key not valid for use in specified state.
At line:1 char:336
+ ... bf6ff4d7ae3" | ConvertTo-SecureString
+                    ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [ConvertTo-SecureString], CryptographicException
    + FullyQualifiedErrorId : ImportSecureString_InvalidArgument_CryptographicError,Microsoft.PowerShell.Commands.Conv

If it is only to resolve the issue locally on one server, a task could be setup in a way that always use the same running account which perform execute the script. This way, we could encrypt the predefined account password using the running account. As the running account has the encryption key stored in its user profile, it is able to decrypt the password during task execution regardless who initiated the task. However, that also means a person will be the developer of the script, the admin of the server, and have access to the running account credential.

To perform encryption with a running account, we could login to the server as the running account, or use the RunAs command in our own remote session to open a PowerShell session as the running account to perform the encryption,
runas /profile /user:RunningAcctA powershell.exe
Note that running account profile needs to be loaded in order to store the encryption key as discussed earlier. /Profile is a default parameter. The /profile parameter is listed as an explicit example.

In the PowerShell session. use whoami to identify the account of that session. Once verified, generate the encrypted password file from that session.

This trick applies to using LocalSystem account as well. The easiest way to run a PowerShell session as LocalSystem is using PsExec by Mark Russinovich from Microsoft. You can find this download from this Windows internal link or I have some PsExec example here in this post as well toward the middle section.
psexec -s -i powershell.exe
Keep in mind that if the script is migrated to other server, the encryption step need to be re-performed again as the previous encryption key generated only store in the running account profile of that particular server.

Custom Encryption Key
One other way to address the multiple server and different user issue is to use a specific encryption key. ConvertTo-SecureString cmdlet allows a key to be provided for the encryption. The valid encryption key lengths are 16, 24, and 32 bytes. With the use of encryption key, it allows the encrypted password to be decrypted on different server with different account.

To generate a valid key, we could use RNGCryptoServiceProvider class to generate random number for the key.
$EncryptKey = New-Object Byte[] 16  #An example of 16 bytes key
In this example, a simple 1 to 16 array is used as the key.
[byte[]] $EncryptKey = (1..16)   #An example of a simple key (1. 2, 3,...,14, 15, 16)
$UserCred = Get-Credential
$UserCred.Password | ConvertFrom-SecureString -Key $EncryptedKey | Out-File C:\encrypted.txt
In the script,
[byte[]] $EncryptedKey = (1..16)
$User = 'TestUser'
$SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString -Key $EncryptedKey
$UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword)
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
However, by hard cording the encryption key in the script, it exposes the key and allows unintended person to potentially decrypt the encrypted password. There are a few options to manage the key.

Store the key in a file with access privilege granted only to intended user or executing/service account, this should be done with the encrypted password file as well. For example,
$User = 'TestUser'
$SecureKey = Get-Content C:\Key.txt | ConvertTo-SecureString
$SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString -SecureKey $SecureKey
$UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword)
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Just a reminder that if the process is going to be executed by a delegated user, the user account will need the read access to the encrypted password and key file. This method prevent unintended person from obtaining the key. However, if the intent is to prevent the delegated user to obtain the password, this method would not be sufficient. One way to address this is setup a task / service to execute the script with a service account (executing account) and only that account has the read access (through Access Control List, ACL) to the encrypted password and key file. When the delegated user initiates the task / service, the process utilizes the service (executing account) to access the files. This limits the exposure of the encrypted password and key file only to the service account.

There are other option like using a certificate to encrypt the key file. Dave Wyatt has a good post of this here. The way it works is the user needs to have a private key of the certificate in order to decrypt the encryption key. This option faces the similar concern like the key file.

Using DPAPI ProtectedData Class for encryption
DPAPI ProtectedData class provides another method to encrypt and decrypt data. As we discussed earlier that ConvertTo-SecureString uses user account by default, this Protected class provide encryption options as CurrentUser or LocalMachine (LocalSystem profile). For scenario like having delegated users to run the script on the server, the predefined account password could be encrypted with LocalMachine option (scope) and any user could decrypt the password on that machine.
$Password = "B@dPassw0rd!"
$PasswordBytes = [System.Text.Encoding]::Unicode.GetBytes($Password) $SecurePassword = [Security.Cryptography.ProtectedData]::Protect($PasswordBytes, $null, [Security.Cryptography.DataProtectionScope]::LocalMachine) $SecurePasswordStr = [System.Convert]::ToBase64String($SecurePassword)
To decrypt the encrypted password,
$SecureStr = [System.Convert]::FromBase64String($SecurePasswordStr)
$StringBytes = [Security.Cryptography.ProtectedData]::Unprotect($SecureStr, $null, [Security.Cryptography.DataProtectionScope]::LocalMachine)
$PasswordStr = [System.Text.Encoding]::Unicode.GetString($StringBytes)
Note that ProtectedData class is returning byte array object (and later convert to string) as opposed to SecureString object. If other cmdlet need the SecureString as its parameter, the password will need to be converted to the SecureString object.

Use SecretManagement and SecretStore (or other 3rd party extension vault) PowerShell module
Microsoft released these two new modules in 2021. SecretManagement module help user to manage secrets that are stored across vaults (local or remote). SecretStore module is a cross-platform local extension vault supported in all environment as PowerShell 7. The SecretStore stores the secrets locally for the current user and uses .NET Core cryptographic APIs to encrypt file content. Even without vault password option, it still encrypt the secrets but the key is stored in current user location.

The modules required to be installed first and some configuration.
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore -Scope CurrentUser
Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault

# Setting Secret Store Password. It prompts to enter the vault password since not provided
Setting up a secret
# Setting a secret. If not provided, it will promot to enter in secure format
Set-Secret -Name TestUserPassword -Secret 'B@dPassw0rd!'
To retrieve the password, first to unlock the vault. The vault stay unlocked within the PasswordTimeOut setting (default 15 minutes).
# Unlock the vault with the vault password. Provide valut password as SecureString. 
# If not provided, it prompts to enter vault password

Get-Secret -Name TestUserPassword -AsPlainText
There are so much more to these SecretManagement and SecretStore (and also 3rd party vault) modules. More details in another post.

No comments:

Post a Comment