Public/Unprotect-ConfigValue.ps1

<#
.SYNOPSIS
    Decrypts a value previously protected by Protect-ConfigValue.

.DESCRIPTION
    The Unprotect-ConfigValue function decrypts a protected string produced by
    Protect-ConfigValue. The protection method is read from the value itself, so Scope does
    not need to be given for values created by tcs.core 0.3.0 or later.

    Values created by tcs.core 0.2.x (without the 'tcs:v1' prefix) are still supported, with
    a warning that recommends protecting the value again. Their format shows which scope was
    used, so Scope does not need to match. LocalMachine values are tried with the computer
    name from COMPUTERNAME and [Environment]::MachineName, and with an empty name (what
    0.2.x used on Linux and macOS, where COMPUTERNAME is not set).

    A value that is neither format (for example plain text that was never protected) gives
    a clear error and no warning.

.PARAMETER EncryptedValue
    The protected string to decrypt.

.PARAMETER Scope
    Kept for compatibility. The scope of legacy (0.2.x) values is now detected from the
    value, so this parameter is ignored.

.PARAMETER Key
    The 32-byte key used with Protect-ConfigValue -Key.

.PARAMETER AsSecureString
    Returns the decrypted value as a SecureString instead of plain text.

.INPUTS
    System.String
    You can pipe one or more protected strings to Unprotect-ConfigValue.

.OUTPUTS
    System.String
    System.Security.SecureString (with -AsSecureString)

.EXAMPLE
    Unprotect-ConfigValue -EncryptedValue $protected

    Decrypts a value protected with Protect-ConfigValue.

.EXAMPLE
    $protected | Unprotect-ConfigValue -AsSecureString

    Decrypts the value and returns it as a SecureString.

.EXAMPLE
    Unprotect-ConfigValue -EncryptedValue $protected -Key $key

    Decrypts a value that was protected with a caller-managed key.

.NOTES
    Author: Nigel Tatschner
    Company: TheCodeSaiyan

    Decryption fails with an error if the value was protected by another user, on another
    machine, with a different key, or if it has been modified.

.LINK
    Protect-ConfigValue
#>

function Unprotect-ConfigValue {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '',
        Justification = 'Only used for -AsSecureString, to return the decrypted value as a SecureString.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Scope',
        Justification = 'Kept for compatibility; the scope of legacy values is detected from the value.')]
    [CmdletBinding()]
    [OutputType([System.String], [System.Security.SecureString])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [string]$EncryptedValue,

        [Parameter()]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope = 'CurrentUser',

        [Parameter()]
        [ValidateCount(32, 32)]
        [byte[]]$Key,

        [Parameter()]
        [switch]$AsSecureString
    )

    process {
        try {
            $parts = $EncryptedValue.Split(':')
            if ($parts.Count -eq 4 -and ('{0}:{1}' -f $parts[0], $parts[1]) -eq $script:ProtectedValuePrefix) {
                $method = $parts[2]
                $protectedBytes = [Convert]::FromBase64String($parts[3])

                switch ($method) {
                    'aes-key' {
                        if (-not $Key) {
                            throw 'This value was protected with a caller-supplied key. Pass the same key with -Key.'
                        }
                        $plainBytes = Unprotect-BytesWithKey -Data $protectedBytes -MasterKey $Key
                    }
                    { $_ -in 'dpapi-cu', 'dpapi-lm' } {
                        if (-not (Test-IsWindowsPlatform)) {
                            throw 'This value was protected with Windows DPAPI and can only be decrypted on Windows.'
                        }
                        Initialize-DataProtection
                        $dpapiScope = if ($method -eq 'dpapi-lm') {
                            [System.Security.Cryptography.DataProtectionScope]::LocalMachine
                        }
                        else {
                            [System.Security.Cryptography.DataProtectionScope]::CurrentUser
                        }
                        $plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect($protectedBytes, $script:DpapiEntropy, $dpapiScope)
                    }
                    { $_ -in 'aes-cu', 'aes-lm' } {
                        $keyScope = if ($method -eq 'aes-lm') { 'LocalMachine' } else { 'CurrentUser' }
                        $masterKey = Get-ProtectionKey -Scope $keyScope
                        $plainBytes = Unprotect-BytesWithKey -Data $protectedBytes -MasterKey $masterKey
                    }
                    default {
                        throw "Unknown protection method '$method'."
                    }
                }

                $plaintext = [System.Text.Encoding]::UTF8.GetString($plainBytes)
                [Array]::Clear($plainBytes, 0, $plainBytes.Length)
            }
            else {
                $secureString = ConvertFrom-LegacyProtectedValue -Value $EncryptedValue
                if ($AsSecureString) {
                    return $secureString
                }
                return [System.Net.NetworkCredential]::new('', $secureString).Password
            }

            if ($AsSecureString) {
                return (ConvertTo-SecureString -String $plaintext -AsPlainText -Force)
            }
            return $plaintext
        }
        catch {
            Write-Error -Message "Failed to decrypt the provided value. $($_.Exception.Message)" -Exception $_.Exception
        }
    }
}