Storing SecureStrings Machine-Independently
As part of a brown bag, I extracted out the logic CredLocker uses to store credentials. Here’s the guts of the code.
The short form is unchanged from the Credlocker post, but I’ve cleaned it up.
- It prompts the user for a password if $Host.CredentialStoreCredential doesn’t exist, or if Get-CredentialStoreCredential –Force is called. This password is stored as a PSCredential NoteProperty to $Host.
- The credential store key is never stored in memory. It’s still an SHA256 hash, and is hashed each time it’s used from the password portion of $Host.CredentialStoreCredential, which is dynamically decrypted at time of use.
- The functions below don’t bother with how the names are stored, how password history is tracked, etc. They only deal with how text is encrypted and decrypted in such a way that it can be read across machines, but can only be read within the specific PowerShell window in which the user has already entered the credential store password.
####################
function Get-CredentialStoreCredential
####################
{
<#
.synopsis
Prompts user for credential store password if not already set'
.description
Prompts user for credential store password if not already set, or if -Force is specified. Stores the password in PSCredential form as $Host.CredentialStoreCredential. If $Host.CredentialStoreCredential is already set and -Force is not specified, does not prompt user.
.parameter Force
Prompts user for credential store password if already set'
.inputs
None.
.outputs
None.
#>
param ([switch]$Force);
$Local:ErrorActionPreference = 'SilentlyContinue';
if (!(Get-Member -InputObject $Host -Name CredentialStoreCredential))
{
Add-Member -InputObject $Host -Name CredentialStoreCredential -MemberType NoteProperty -Value $null -ErrorVariable err1;
if ($err1)
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $($err1.Exception.Message -replace '`r`n', ' ')";
return;
} # if ($err1)
} # if (!(Get-Member -InputObject ...
if ($Force -or !$Host.CredentialStoreCredential)
{
$host.CredentialStoreCredential = Get-Credential -Message 'Enter Credential Store Password' -UserName 'NotApplicable' -ErrorVariable err1;
if ($err1)
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $($err1.Exception.Message -replace '`r`n', ' ')";
return;
} # if ($err1)
} # if ($Force ...
if (!$Host.CredentialStoreCredential)
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: Can't get Credential Store credential.";
} # if (!$Host.CredentialStoreCredential)
} # function Get-CredentialStoreCredential
####################
function Get-CredentialStoreKey
####################
{
<#
.synopsis
Returns the 256-byte array credential store key.
.description
The credential system relies on a 256-byte array key that is derived from the credential store password (stored as $Host.CredentialStoreCredential) as a SHA256 hash.
.inputs
None.
.outputs
[byte[]]
#>
if (Test-Path "Function:Get-CredentialStoreCredential")
{
Get-CredentialStoreCredential;
try
{
[System.Security.Cryptography.SHA256]::Create().ComputeHash(
[byte[]](
[char[]](
[System.Runtime.InteropServices.marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.marshal]::SecureStringToBSTR(
$Host.CredentialStoreCredential.Password
)
)
)
)
);
} # try
catch
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $(_.Exception.Message -replace '`r`n', ' ')";
} # catch
} # if (Test-Path ...
else
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: Can't get Credential Store key.";
} # if (Test-Path ... else
} # function Get-CredentialStoreKey
####################
function ConvertTo-EncryptedString
####################
{
<#
.synopsis
Encrypts specified string of text.
.description
Encrypts specified string of text with 256-byte array credential store key.
This key is the the same for the same credential store password, so this encrypted text can be decrypted by a different user, on a different machine.
The resulting encrypted string is of type [String].
.parameter PlainText
Text to encrypt with credential store key.
.inputs
None.
.outputs
[String]
.notes
While ConvertTo-SecureString | ConvertFrom-SecureString works for long strings of text, it is not performant.
Please remember the output is of type [String], not [SecureString].
#>
param (
[String]$PlainText = $null
);
if ($PlainText)
{
if ($PlainText.GetType().Name -eq 'String')
{
$Local:ErrorActionPreference = 'SilentlyContinue';
ConvertFrom-SecureString -Key (Get-CredentialStoreKey) -SecureString (
ConvertTo-SecureString -AsPlainText $PlainText -Force -ErrorVariable err1
) -ErrorVariable err2;
if ($err1)
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $($err1.Exception.Message -replace '`r`n', ' ')";
} # if ($err1)
if ($err2)
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $($err2.Exception.Message -replace '`r`n', ' ')";
} # if ($err2)
} # if ($PlainText.GetType().Name ...
else
{
Write-Warning "$($MyInvocation.MyCommand.Name) Value '$PlainText' is not a string."
} # if ($PlainText.GetType().Name ... else
} # if ($PlainText)
else
{
Write-Warning "$($MyInvocation.MyCommand.Name) -Plaintext not specified."
} # if ($PlainText)
} # function ConvertTo-EncryptedString
####################
function ConvertFrom-EncryptedString
####################
{
<#
.synopsis
Decrypts specified string of text.
.description
Decrypts specified string of text with 256-byte array credential store key.
This key is the the same for the same credential store password, so text encrypted by a different user, on a different machine c can be decrypted with the same credential store password.
The resulting decrypte string is of type [SecureString] unless -AsPlainText is specified, at which point it is of type [String]
.parameter EncryptedText
Text to decrypt with credential store key.
.parameter AsPlainText
Output decrypted text as [string] (i.e. human readable plain text), not [SecureString]
.inputs
None.
.outpus
[SecureString] by default. [String] if -AsPlainText is specified.
.notes
While ConvertTo-SecureString | ConvertFrom-SecureString works for long strings of text, it is not performant.
#>
param (
[String]$EncryptedText = $null,
[switch]$AsPlainText
)
if ($EncryptedText)
{
$Local:ErrorActionPreference = 'SilentlyContinue';
$secureString = ConvertTo-SecureString -Key (Get-CredentialStoreKey) -String $EncryptedText -ErrorVariable err1;
if ($err1)
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $($err1.Exception.Message -replace '`r`n', ' ')";
} # if ($err1)
else
{
if ($AsPlainText)
{
try
{
[System.Runtime.InteropServices.marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.marshal]::SecureStringToBSTR($secureString)
);
} # try
catch
{
Write-Warning "$($MyInvocation.MyCommand.Name) failed: $($_.Exception.Message -replace '`r`n', ' ')";
} # catch
} # if ($AsPlainText)
else
{
$secureString;
} # if ($AsPlainText)...
} # if ($err1) ... else
} # if ($EncryptedText)
} # function ConvertFrom-EncryptedString