Public/Test-PasswordEntropy.ps1
|
function Test-PasswordEntropy { <# .SYNOPSIS Evaluates the entropy and strength of a password. .DESCRIPTION Accepts a secure string, plain string, PSCredential, or SecretManagement secret object from the pipeline or parameter and evaluates password entropy. .PARAMETER InputObject The object to evaluate, as a secure string, plain string, PSCredential, or SecretManagement .INPUTS string[], System.Security.SecureString[], System.Management.Automation.PSCredential[], SecretManagement secret wrapper objects .OUTPUTS System.Management.Automation.PSCustomObject .EXAMPLE Read-Host -AsSecureString | Test-PasswordEntropy Provide a secure string at the command line, then evaluate it. .EXAMPLE Get-Secret -Name 'SuperSecret' | Test-PasswordEntropy Retrieve a secret from a secret store, then evaluate it. .EXAMPLE Get-Credential | Test-PasswordEntropy Consume a PSCredential object from the pipeline, and evalute its password property. .NOTES While you could provide an insecure string at the command line, pipe it to `ConvertTo-SecureString`, then pipe it to the function, this is not recommended as it would expose the secret in plaintext. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [AllowNull()] [object]$InputObject ) begin { function Convert-SecureStringToPlain { param([System.Security.SecureString]$ss) if ($null -eq $ss) { return $null } $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss) try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } function Get-PlainFromPossibleSecret { param($obj) if ( $null -eq $obj ) { return $null } # If it's already a string if ( $obj -is [string] ) { return $obj } # SecureString directly if ( $obj -is [System.Security.SecureString] ) { return Convert-SecureStringToPlain -ss $obj } # PSCredential: use password (or username+password if you want) if ( $obj -is [System.Management.Automation.PSCredential] ) { return $obj.GetNetworkCredential().Password } # SecretManagement secret wrapper: many providers return an object with a property 'Secret' that is a SecureString if ($obj -is [object]) { # Try common property names foreach ( $propName in @('Secret','Password','Value') ) { if ($obj.PSObject.Properties.Match($propName)) { $val = $obj.$propName if ($val -is [System.Security.SecureString]) { return Convert-SecureStringToPlain -ss $val } elseif ($val -is [string]) { return $val } elseif ($val -is [System.Management.Automation.PSCredential]) { return $val.GetNetworkCredential().Password } } } # If object is a wrapper and has a ToString that reveals the secret (rare), don't auto-expose; prefer explicit extraction. } return $null } function Assert-Entropy { param( [double]$entropyBits ) switch ($entropyBits) { { $_ -le 28 } { 'Very Weak'; break } { $_ -in (29..35) } { 'Weak'; break } { $_ -in (36..59) } { 'Moderate'; break } { $_ -in (60..79) } { 'Strong'; break } default { 'Very Strong' } } } } process { $plain = Get-PlainFromPossibleSecret -obj $InputObject if ([string]::IsNullOrEmpty($plain)) { Write-Verbose "No plaintext password extracted from input of type: $($InputObject.GetType().FullName)" return } # Calculate Shannon entropy per character $length = $plain.Length if ($length -eq 0) { Write-Verbose "Empty string provided." return } $charCounts = @{} foreach ($c in $plain.ToCharArray()) { if ($charCounts.ContainsKey($c)) { $charCounts[$c]++ } else { $charCounts[$c] = 1 } } $hPerChar = 0.0 foreach ($count in $charCounts.Values) { $p = $count / $length $hPerChar += -1 * $p * [Math]::Log($p, 2) } $totalEntropy = $hPerChar * $length $roundedTotal = [Math]::Round($totalEntropy, 2) $roundedPerChar = [Math]::Round($hPerChar, 4) $strength = Assert-Entropy -entropyBits $roundedTotal [PSCustomObject]@{ Length = $length EntropyPerChar = $roundedPerChar EntropyBits = $roundedTotal Strength = $strength } } end {} } |