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