Public/Get-PSUSecret.ps1
|
function Get-PSUSecret { <# .SYNOPSIS Retrieves a secret as a SecureString from the first available secure source. .DESCRIPTION Resolves a named secret from three sources, in this order, and stops at the first hit: 1. Windows Credential Manager, via the credential store this module already uses. 2. Microsoft.PowerShell.SecretManagement, when that module is installed. 3. An environment variable of the same name. The environment-variable tier exists only for backward compatibility with the way these commands were configured before secure storage was available. It is the least secure option, because environment variables are readable by any process in the session and are commonly captured in logs and crash dumps, so it warns when used. It is deliberately retained rather than removed, so existing automation keeps working. A SecureString is always returned. Callers that must hand a secret to a native executable or an HTTP header should convert it at that boundary and not before, so the plaintext lives for as short a time as possible. The secret value is never written to output, verbose, warning, or error text. Failures report only the secret's name and which sources were tried. .PARAMETER Name Name of the secret. Used as the Credential Manager target, the SecretManagement secret name, and the environment variable name. .PARAMETER Source Restricts resolution to a single source instead of trying all three in order. Useful when a caller must guarantee a secret is not being read from an environment variable. .PARAMETER AsPlainText Returns the secret as plain text instead of a SecureString. Use this only at an unavoidable native or HTTP boundary, and do not store the result. .EXAMPLE $token = Get-PSUSecret -Name 'GITHUB_TOKEN' Resolves GITHUB_TOKEN from Credential Manager, then SecretManagement, then the environment variable, and returns it as a SecureString. .EXAMPLE Get-PSUSecret -Name 'GITHUB_TOKEN' -Source CredentialManager Resolves only from Windows Credential Manager, and throws if it is not stored there. .OUTPUTS [System.Security.SecureString], or [System.String] when -AsPlainText is supplied. .NOTES Author: Lakshmanachari Panuganti Created: 18th August 2026 .LINK https://github.com/lakshmanachari-panuganti/OMG.PSUtilities/tree/main/OMG.PSUtilities.Core #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The sources this command reads from - Windows Credential Manager and environment variables - hand back plaintext. Converting that plaintext into a SecureString is the protective step, not a leak; it is what allows callers to receive a SecureString rather than a bare string. The plaintext is not persisted, and it is only returned when the caller explicitly asks with -AsPlainText for a native or HTTP boundary.' )] [OutputType([System.Security.SecureString])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter()] [ValidateSet('CredentialManager', 'SecretManagement', 'EnvironmentVariable')] [string]$Source, [Parameter()] [switch]$AsPlainText ) begin { # Ordered so the most secure source wins. EnvironmentVariable is last and only exists # for compatibility with pre-existing configuration. $resolutionOrder = if ($PSBoundParameters.ContainsKey('Source')) { @($Source) } else { @('CredentialManager', 'SecretManagement', 'EnvironmentVariable') } } process { $attempted = [System.Collections.Generic.List[string]]::new() foreach ($tier in $resolutionOrder) { $attempted.Add($tier) $plain = $null switch ($tier) { 'CredentialManager' { try { $plain = [CredentialManager.CredMan]::GetPassword($Name) } catch { # An unreadable store must not mask the remaining tiers. Write-Verbose "[$($MyInvocation.MyCommand.Name)] Credential Manager lookup for '$Name' failed: $($_.Exception.Message)" } } 'SecretManagement' { $getSecret = Get-Command -Name 'Get-Secret' -Module 'Microsoft.PowerShell.SecretManagement' -ErrorAction SilentlyContinue if (-not $getSecret) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] SecretManagement is not installed; skipping." break } try { $secureValue = & $getSecret -Name $Name -ErrorAction Stop if ($secureValue -is [System.Security.SecureString]) { if (-not $AsPlainText) { return $secureValue } # Only unprotect at the caller's explicit request, and free the # unmanaged buffer immediately afterwards. $pointer = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureValue) try { return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($pointer) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer) } } $plain = [string]$secureValue } catch { Write-Verbose "[$($MyInvocation.MyCommand.Name)] SecretManagement lookup for '$Name' failed: $($_.Exception.Message)" } } 'EnvironmentVariable' { $plain = [Environment]::GetEnvironmentVariable($Name) if (-not [string]::IsNullOrWhiteSpace($plain)) { Write-Warning "Secret '$Name' was read from an environment variable. Environment variables are readable by any process in this session and are often captured in logs. Store it with Set-PSUCredentialToManager instead." } } } if (-not [string]::IsNullOrWhiteSpace($plain)) { Write-Verbose "[$($MyInvocation.MyCommand.Name)] Resolved '$Name' from $tier." if ($AsPlainText) { return $plain } return (ConvertTo-SecureString -String $plain -AsPlainText -Force) } } throw "Secret '$Name' was not found. Tried: $($attempted -join ', '). Configure it with: Set-PSUCredentialToManager -Target '$Name', or install Microsoft.PowerShell.SecretManagement and store it there." } } |