Public/Protect-ConfigValue.ps1

<#
.SYNOPSIS
    Encrypts a string value for secure storage in configuration files.

.DESCRIPTION
    The Protect-ConfigValue function encrypts a plaintext string and returns a self-describing
    protected string ('tcs:v1:<method>:<data>') that Unprotect-ConfigValue can decrypt.

    Windows:
      Uses the Windows Data Protection API (DPAPI).
      - CurrentUser: only the same user on the same machine can decrypt.
      - LocalMachine: any user on the same machine can decrypt.

    Linux and macOS (DPAPI is not available):
      Uses AES-256-CBC with HMAC-SHA256 authentication and a random 32-byte key file.
      - CurrentUser: key stored in the user's config folder
        (~/.config/PowerShell/Config/tcs.core/protection.key, mode 600). Created on first use.
      - LocalMachine: key stored at /etc/tcs.core/protection.key (mode 644, override with
        the TCS_MACHINE_KEY_PATH environment variable). Must be created once by root.

    Any platform:
      Supply -Key to encrypt with your own 32-byte key, for example to share a protected
      value between machines or with a CI pipeline. The same key is required to decrypt.

.PARAMETER Value
    The plaintext string value to protect.

.PARAMETER Scope
    Who can decrypt the value: 'CurrentUser' (default) or 'LocalMachine'.

.PARAMETER Key
    A 32-byte key to encrypt with instead of the platform key store.

.INPUTS
    System.String
    You can pipe one or more plaintext strings to Protect-ConfigValue.

.OUTPUTS
    System.String
    Returns the protected string.

.EXAMPLE
    Protect-ConfigValue -Value "MySecretPassword"

    Encrypts the string so that only the current user on this machine can decrypt it.

.EXAMPLE
    "api-key-12345" | Protect-ConfigValue -Scope 'LocalMachine'

    Encrypts the string so that any user on this machine can decrypt it.

.EXAMPLE
    $key = [byte[]]::new(32); [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($key)
    $protected = Protect-ConfigValue -Value "ConnectionString" -Key $key

    Encrypts the string with a caller-managed key that can be used on any machine.

.NOTES
    Author: Nigel Tatschner
    Company: TheCodeSaiyan

    Values produced by tcs.core 0.2.x (no 'tcs:v1' prefix) can still be read by
    Unprotect-ConfigValue; protect them again to move them to the new format.

.LINK
    Unprotect-ConfigValue
#>

function Protect-ConfigValue {
    [CmdletBinding(DefaultParameterSetName = 'Scope')]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [string]$Value,

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

        [Parameter(Mandatory = $true, ParameterSetName = 'Key')]
        [ValidateCount(32, 32)]
        [byte[]]$Key
    )

    process {
        $plainBytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
        try {
            if ($PSCmdlet.ParameterSetName -eq 'Key') {
                $method = 'aes-key'
                $protectedBytes = Protect-BytesWithKey -Data $plainBytes -MasterKey $Key
            }
            elseif (Test-IsWindowsPlatform) {
                Initialize-DataProtection
                if ($Scope -eq 'LocalMachine') {
                    $method = 'dpapi-lm'
                    $dpapiScope = [System.Security.Cryptography.DataProtectionScope]::LocalMachine
                }
                else {
                    $method = 'dpapi-cu'
                    $dpapiScope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser
                }
                $protectedBytes = [System.Security.Cryptography.ProtectedData]::Protect($plainBytes, $script:DpapiEntropy, $dpapiScope)
            }
            else {
                $method = if ($Scope -eq 'LocalMachine') { 'aes-lm' } else { 'aes-cu' }
                $masterKey = Get-ProtectionKey -Scope $Scope -Create
                $protectedBytes = Protect-BytesWithKey -Data $plainBytes -MasterKey $masterKey
            }
        }
        finally {
            [Array]::Clear($plainBytes, 0, $plainBytes.Length)
        }

        return '{0}:{1}:{2}' -f $script:ProtectedValuePrefix, $method, [Convert]::ToBase64String($protectedBytes)
    }
}