Tome's Land of IT

IT Notes from the Powertoe – Tome Tanasovski

Storing Passwords to Disk in PowerShell with Machine-key Encryption

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.

  1. Get the password using the -AsSecureString parameter of Read-Host
  2. Convert the securestring to a string using ConvertFrom-SecureString using a 32 byte key
  3. Convert the string returned from step 2 into an array of bytes
  4. Create an RSA machine key in the cryptographic service provider (CSP)
  5. Encrypt the bytes in step 3 using RSA
  6. 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:
  1. Read in the decrypted bytes from the clixml file using Import-Clixml
  2. Initiate an instance of RSA that is using the machine key we created during the encryption process
  3. Decrypt the bytes using RSA
  4. Convert the decrypted bytes back into a string by first converting the bytes into chars and then joining them togethter
  5. Convert the string into a secure string using ConvertTo-SecureString along with the 32 byte key used by the encryption script
  6. Create the credential by using a constructor of the PsCredential class that accepts a username and a secure string as a password
  7. 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.

11 responses to “Storing Passwords to Disk in PowerShell with Machine-key Encryption

  1. Pingback: Storing and Using Password Credentials with key parameter

  2. Pingback: Powershell – Storing and Using Password Credentials with Key Parameter

  3. WarpSeeker May 9, 2013 at 10:33 am

    I created a script using this code, I ran it on my computer, to both perform the encryption, and the decryption, everything worked great. Then I went across the hall, had one of my other system admins try, using his account and his computer, and he received the following error, please help.

    Exception calling “Decrypt” with “2” argument(s): “Error occurred while decoding OAEP padding.”
    At \\coc\it\WSA\PowerShell\Misc\PwdCredsNew.ps1:41 char:33
    + $password = [char[]]$rsa.Decrypt <<<< ($encrypted, $true) -join "" |ConvertTo-SecureString -Key $key
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    New-Object : Exception calling ".ctor" with "2" argument(s): "Cannot process argument because the value of argument "password" is null. Change the value of argument "pass
    word" to a non-null value."
    At \\coc\it\WSA\PowerShell\Misc\PwdCredsNew.ps1:43 char:19
    + $cred = New-Object <<<< System.Management.Automation.PsCredential $tome,$password
    + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException
    + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

    • Dave September 30, 2014 at 1:15 pm

      I ran into this too, make sure that the following string on both ends of the script have the same value. That’s what ended up holding me up.

      $csp.KeyContainerName = “SuperSecretProcessOnMachine”

  4. Ivan August 28, 2013 at 9:51 am

    Yet again Tome, you have saved my bacon!! 😉 thanks mate, how all is well!!

  5. ‫שגיב אמרי‬‎ September 23, 2013 at 2:57 pm

    Very cool summery of an intresting idea. going to fiddle with this approach a bit more on my local wp testing enviroment. thanks for sharing mate.

  6. Mark October 24, 2013 at 12:10 pm

    Very well done. Did you ever update your scripts with the enhancements you mentioned like encrypting the $key with rsa?

  7. Pingback: Leveraging PowerShell to Help Minimize Risk While Performing Administrative Tasks - Hey, Scripting Guy! Blog - Site Home - TechNet Blogs

  8. Pingback: Exchange Online Migration Reporting - Cloud Trek 365 - Site Home - TechNet Blogs

  9. Cramp November 5, 2014 at 3:29 pm

    It looks to me that when you execute

    $csp.Flags = $csp.Flags -bor [System.Security.Cryptography.CspProviderFlags]::UseMachineKeyStore

    you are limiting the script to running on the machine the xml file was created on. Is there a way to export the key so it can be used elsewhere?

    I see there is a “UseArchivableKey” flag, however I can’t seem to figure out how to archive the key.

    • Tome January 17, 2015 at 10:49 am

      Unknown. Typically machine key encryption is intended to be tied to the machine it is running on. Backup/restore is not something I have done. The single script I provided to an operations team that leveraged this used a process to do the setup before the script would work. Someone always had to apply it manually.

Leave a comment