I recently was handed these requirements for a task at work:
- Store a password encrypted to disk so that it can be used to launch another powershell script.
- The script may need to be launched by any user who belong to the Administrator group on the server.
I decided to add an additional requirement:
- Try to also keep the password encrypted in memory using a SecureString.
Before I show you how I tackled this problem I’d like to be clear that I realize that there is no such thing as perfect security and encryption of a password in a script. It’s usually better to use something like schtasks to store the credentials and provide a user permissions to launch the script via the task. In my case I wanted the user to easily be able to modify the arguments passed to the script. So really my intention is not to secure the password from the people using it, but to make it very difficult for someone outside of the group who will use the script to read the text in the script and be able to find the password.
If you are not aware, it is very easy to do this per user using a SecureString. There are two Convert cmdlets that allow us to convert to and from a secure string: ConvertTo-SecureString and ConvertFrom-SecureString. For example, this is an extremely common way that people store a password to disk:
(get-credential).password|convertFrom-SecureString|set-content c:\password.txt
The way to load this password from disk is as follows:
$user = 'domainA\Tome'
$cred = New-Object System.Management.Automation.PsCredential $user,(Get-Content c:\password.txt| ConvertTo-SecureString)
The above two examples require the commands to be run using the same credentials in order for it to work properly. However, ConvertTo-SecureString and ConvertFrom-SecureString also have a -key parameter so that you can perform these conversions by different users on the same computer or on a different computer altogether. The problem with using the -Key parameter is that you need to store the key within your script. Hardly very secure when the entire point of this exercise is to keep password out of the hands of someone who is able to read the text in the script.
In the end my solution used the Key parameter of the SecureString cmdlets, but only to ensure that the password is never in plain text in memory. The Key is stored in the script, but it is the converted SecureString that I encrypt using RSA encryption with a machine key. Here are the high-level steps that writes the password encrypted to disk.
- Get the password using the -AsSecureString parameter of Read-Host
- Convert the securestring to a string using ConvertFrom-SecureString using a 32 byte key
- Convert the string returned from step 2 into an array of bytes
- Create an RSA machine key in the cryptographic service provider (CSP)
- Encrypt the bytes in step 3 using RSA
- Serialize the encrypted bytes to disk as clixml using Export-Clixml
Here is the script used to perform the above logic:
$key = (2,3,56,34,254,222,1,1,2,23,42,54,33,233,1,34,2,7,6,5,35,43,6,6,6,6,6,6,31,33,60,23)
$pass = Read-Host -AsSecureString
$securepass = $pass |ConvertFrom-SecureString -Key $key
$bytes = [byte[]][char[]]$securepass
$csp = New-Object System.Security.Cryptography.CspParameters
$csp.KeyContainerName = "SuperSecretProcessOnMachine"
$csp.Flags = $csp.Flags -bor [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore
$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider -ArgumentList 5120,$csp
$rsa.PersistKeyInCsp = $true
$encrypted = $rsa.Encrypt($bytes,$true)
$encrypted |Export-Clixml 'C:\Dropbox\My Dropbox\scripts\word.xml'
The high level workflow to decrypt the password directly into a secure string is as follows:
- Read in the decrypted bytes from the clixml file using Import-Clixml
- Initiate an instance of RSA that is using the machine key we created during the encryption process
- Decrypt the bytes using RSA
- Convert the decrypted bytes back into a string by first converting the bytes into chars and then joining them togethter
- Convert the string into a secure string using ConvertTo-SecureString along with the 32 byte key used by the encryption script
- Create the credential by using a constructor of the PsCredential class that accepts a username and a secure string as a password
- Launch the other script using start-process
Here is the decryption script
$encrypted = Import-Clixml 'C:\Dropbox\My Dropbox\scripts\word.xml'
$key = (2,3,56,34,254,222,1,1,2,23,42,54,33,233,1,34,2,7,6,5,35,43,6,6,6,6,6,6,31,33,60,23)
$csp = New-Object System.Security.Cryptography.CspParameters
$csp.KeyContainerName = "SuperSecretProcessOnMachine"
$csp.Flags = $csp.Flags -bor [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore
$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider -ArgumentList 5120,$csp
$rsa.PersistKeyInCsp = $true
$password = [char[]]$rsa.Decrypt($encrypted, $true) -join "" |ConvertTo-SecureString -Key $key
$cred = New-Object System.Management.Automation.PsCredential 'tome',$password
Start-Process -Credential $cred -FilePath 'powershell.exe' -ArgumentList '-noprofile','-file','"c:\dropbox\my dropbox\scripts\t.ps1"', "blah"
The full scripts are not actually the complete scripts I used. I wound up creating wrapper scripts for each script I needed the users to run with parameters. The wrapper scripts had the same parameters and some logic within them.
I know there are some shortcuts you can take, e.g., you can convert strings to and from bytes using the System.Text functions. I also realize that it makes more sense to encrypt the 32 byte key using RSA rather than encrypting the SecureString, but to be honest, I realized this a bit late. I left it as is figuring that an encrypted 192 byte string using a larger RSA key is probably better than 32 bytes encrypted with a smaller key, but it probably doesn’t matter much either way. You take a bit of a hit when you generate the larger RSA key, but it’s really negligible. In the end, I created something that is working fine for me using the techniques that I was quickest with.
It may not be perfect in a security conscious world, but it met my requirements and is working nicely for me.
Oh, one final note. If you wanted to use this technique for users who are not administrators on the server you only need to grant them access to the Machine Key file created in c:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys.
Like this:
Like Loading...