source/public/New-PGenRandomPassword.ps1
|
function New-PGenRandomPassword { <# .SYNOPSIS Generates a cryptographically secure random password. .DESCRIPTION Generates a random password using configurable character sets and cryptographically secure random number generation provided by System.Security.Cryptography.RandomNumberGenerator. By default, uppercase letters, lowercase letters, numbers, and special characters are enabled. The function guarantees that the resulting password contains at least one character from every enabled character set. Additional minimum numeric and special character requirements may also be specified. The generated password can optionally be copied to the clipboard, returned as a SecureString, or returned as a metadata object containing password statistics and generation details. .PARAMETER Length Specifies the total length of the password. The minimum allowable length is determined dynamically based on the enabled character sets and any minimum character requirements. Default: 16 .PARAMETER Uppercase Enables uppercase letters (A-Z). Default: $true .PARAMETER Lowercase Enables lowercase letters (a-z). Default: $true .PARAMETER Numbers Enables numeric characters (0-9). Default: $true If MinimumNumbers is greater than zero, the numeric character set is automatically enabled regardless of this setting. .PARAMETER Special Enables special characters. Default: $true If MinimumSpecial is greater than zero, the special character set is automatically enabled regardless of this setting. .PARAMETER MinimumNumbers Specifies the minimum number of numeric characters required in the generated password. A value greater than zero automatically enables the Numbers character set. Default: 0 .PARAMETER MinimumSpecial Specifies the minimum number of special characters required in the generated password. A value greater than zero automatically enables the Special character set. Default: 0 .PARAMETER SpecialCharacters Specifies the set of special characters that may be used when the Special character set is enabled. Duplicate characters are automatically removed. Default: !@#$%^&* .PARAMETER NoAmbiguousCharacters Excludes commonly confused characters from the generated password. Excluded characters: Uppercase: I O Lowercase: l Numbers: 0 1 .PARAMETER CopyToClipboard Copies the generated plaintext password to the system clipboard. The password is still returned to the pipeline. .PARAMETER AsSecureString Returns the generated password as a SecureString. This parameter cannot be used together with PassThruObject. .PARAMETER PassThruObject Returns a PSCustomObject containing the generated password and additional metadata including character counts, entropy estimate, and generation options. This parameter cannot be used together with AsSecureString. .OUTPUTS System.String Returned by default. .OUTPUTS System.Security.SecureString Returned when AsSecureString is specified. .OUTPUTS System.Management.Automation.PSCustomObject Returned when PassThruObject is specified. .EXAMPLE New-PGenRandomPassword Generates a 16-character password using all default character sets. .EXAMPLE New-PGenRandomPassword -Length 24 Generates a 24-character password using all default character sets. .EXAMPLE New-PGenRandomPassword ` -Length 20 ` -MinimumNumbers 3 ` -MinimumSpecial 2 Generates a 20-character password containing at least three numbers and at least two special characters. .EXAMPLE New-PGenRandomPassword ` -Length 20 ` -NoAmbiguousCharacters Generates a password that excludes visually ambiguous characters such as I, O, l, 0, and 1. .EXAMPLE New-PGenRandomPassword ` -Uppercase:$false ` -Lowercase:$true ` -Numbers:$true ` -Special:$false ` -Length 16 Generates a password using only lowercase letters and numbers. .EXAMPLE New-PGenRandomPassword ` -Length 32 ` -CopyToClipboard Generates a password and copies it to the clipboard. .EXAMPLE New-PGenRandomPassword ` -Length 32 ` -AsSecureString Generates a password and returns it as a SecureString. .EXAMPLE New-PGenRandomPassword ` -Length 24 ` -MinimumNumbers 2 ` -MinimumSpecial 2 ` -PassThruObject Returns an object similar to: Password : Tm!4qN3@Lc2hRw8#Jf6DpXaK Length : 24 ContainsUppercase : True ContainsLowercase : True ContainsNumbers : True ContainsSpecial : True UppercaseCount : 9 LowercaseCount : 8 NumberCount : 4 SpecialCount : 3 MinimumNumbers : 2 MinimumSpecial : 2 AmbiguousCharsExcluded : False CharacterPoolSize : 70 PoolBasedEntropyBits : 147.09 CopiedToClipboard : False .NOTES Character requirements are guaranteed through explicit character allocation followed by a cryptographically secure Fisher-Yates shuffle. Random values are generated using System.Security.Cryptography.RandomNumberGenerator rather than Get-Random. Compatible with: - Windows PowerShell 5.1 - PowerShell 7+ .LINK https://github.com/junecastillote/PasswordGen.PS #> [CmdletBinding()] [OutputType( [string], [System.Security.SecureString], [System.Management.Automation.PSCustomObject] )] param ( [Parameter()] [ValidateRange(1, 4096)] [int]$Length = 16, [Parameter()] [bool]$Uppercase = $true, [Parameter()] [bool]$Lowercase = $true, [Parameter()] [bool]$Numbers = $true, [Parameter()] [bool]$Special = $true, [Parameter()] [ValidateRange(0, 4096)] [int]$MinimumNumbers = 0, [Parameter()] [ValidateRange(0, 4096)] [int]$MinimumSpecial = 0, [Parameter()] [ValidateNotNullOrEmpty()] [string]$SpecialCharacters = '!@#$%^&*', [Parameter()] [switch]$NoAmbiguousCharacters, [Parameter()] [switch]$CopyToClipboard, [Parameter()] [switch]$AsSecureString, [Parameter()] [switch]$PassThruObject ) begin { if ($AsSecureString -and $PassThruObject) { throw 'AsSecureString and PassThruObject cannot be used together.' } # A positive minimum automatically enables its corresponding set. $numbersEnabled = $Numbers -or ($MinimumNumbers -gt 0) $specialEnabled = $Special -or ($MinimumSpecial -gt 0) # Remove duplicate special characters to avoid accidental weighting. $normalizedSpecialCharacters = -join ( $SpecialCharacters.ToCharArray() | Select-Object -Unique ) $characterSets = [ordered]@{ Uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' Lowercase = 'abcdefghijklmnopqrstuvwxyz' Numbers = '0123456789' Special = $normalizedSpecialCharacters } if ($NoAmbiguousCharacters) { $characterSets.Uppercase = $characterSets.Uppercase -replace '[IO]', '' $characterSets.Lowercase = $characterSets.Lowercase -replace 'l', '' $characterSets.Numbers = $characterSets.Numbers -replace '[01]', '' } # Keep character categories distinct. This ensures that one character # cannot satisfy both a special-character requirement and an # alphanumeric requirement. if ($specialEnabled) { $nonSpecialCharacters = ( $characterSets.Uppercase + $characterSets.Lowercase + $characterSets.Numbers ).ToCharArray() $overlappingSpecialCharacters = @( $characterSets.Special.ToCharArray() | Where-Object { $nonSpecialCharacters -contains $_ } | Select-Object -Unique ) if ($overlappingSpecialCharacters.Count -gt 0) { $overlapText = -join $overlappingSpecialCharacters throw ( 'SpecialCharacters cannot contain characters from the ' + 'uppercase, lowercase, or numeric sets. ' + "Overlapping characters: $overlapText" ) } } $enabledSets = New-Object 'System.Collections.Generic.List[object]' if ($Uppercase) { $enabledSets.Add( [pscustomobject]@{ Name = 'Uppercase' Characters = $characterSets.Uppercase Minimum = 1 } ) } if ($Lowercase) { $enabledSets.Add( [pscustomobject]@{ Name = 'Lowercase' Characters = $characterSets.Lowercase Minimum = 1 } ) } if ($numbersEnabled) { $enabledSets.Add( [pscustomobject]@{ Name = 'Numbers' Characters = $characterSets.Numbers Minimum = [System.Math]::Max( 1, $MinimumNumbers ) } ) } if ($specialEnabled) { $enabledSets.Add( [pscustomobject]@{ Name = 'Special' Characters = $characterSets.Special Minimum = [System.Math]::Max( 1, $MinimumSpecial ) } ) } if ($enabledSets.Count -eq 0) { throw @' At least one character set must be enabled. Enable Uppercase, Lowercase, Numbers, or Special. '@ } foreach ($set in $enabledSets) { if ([string]::IsNullOrEmpty($set.Characters)) { throw "The enabled '$($set.Name)' character set is empty." } } $minimumRequiredLength = 0 foreach ($set in $enabledSets) { $minimumRequiredLength += $set.Minimum } if ($Length -lt $minimumRequiredLength) { $requirementSummary = @( $enabledSets | ForEach-Object { '{0}={1}' -f $_.Name, $_.Minimum } ) $message = @( "Length must be at least $minimumRequiredLength for the current requirements." "Current requirements: $($requirementSummary -join ', ')." ) -join ' ' throw $message } $combinedPool = -join ( $enabledSets | ForEach-Object { $_.Characters } ) # Remove duplicate characters from the final combined pool. $combinedPool = -join ( $combinedPool.ToCharArray() | Select-Object -Unique ) if ([string]::IsNullOrEmpty($combinedPool)) { throw 'The combined character pool is empty.' } $randomNumberGenerator = [System.Security.Cryptography.RandomNumberGenerator]::Create() function Get-CryptoRandomIndex { [CmdletBinding()] [OutputType([int])] param ( [Parameter(Mandatory)] [ValidateRange(1, 2147483647)] [int]$UpperBound ) if ($UpperBound -eq 1) { return 0 } # Use rejection sampling to avoid modulo bias. $byteBuffer = New-Object 'byte[]' 4 $range = [uint64]1 + [uint64][uint32]::MaxValue $acceptedRange = $range - ($range % [uint64]$UpperBound) do { $randomNumberGenerator.GetBytes($byteBuffer) $randomValue = [uint64][BitConverter]::ToUInt32( $byteBuffer, 0 ) } while ($randomValue -ge $acceptedRange) return $randomValue % [uint64]$UpperBound } function Get-CryptoRandomCharacter { [CmdletBinding()] [OutputType([char])] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CharacterPool ) $randomIndex = Get-CryptoRandomIndex ` -UpperBound $CharacterPool.Length return $CharacterPool[$randomIndex] } } process { try { $passwordCharacters = New-Object 'System.Collections.Generic.List[char]' # Add the required number of characters from each enabled set. foreach ($set in $enabledSets) { for ($index = 0; $index -lt $set.Minimum; $index++) { $randomCharacter = Get-CryptoRandomCharacter ` -CharacterPool $set.Characters $passwordCharacters.Add($randomCharacter) } } # Fill remaining positions from the complete enabled pool. while ($passwordCharacters.Count -lt $Length) { $randomCharacter = Get-CryptoRandomCharacter -CharacterPool $combinedPool $passwordCharacters.Add($randomCharacter) } # Apply a cryptographically secure Fisher-Yates shuffle. for ( $currentIndex = $passwordCharacters.Count - 1 $currentIndex -gt 0 $currentIndex-- ) { $swapIndex = Get-CryptoRandomIndex ` -UpperBound ($currentIndex + 1) $temporaryCharacter = $passwordCharacters[$currentIndex] $passwordCharacters[$currentIndex] = $passwordCharacters[$swapIndex] $passwordCharacters[$swapIndex] = $temporaryCharacter } $password = -join $passwordCharacters if ($CopyToClipboard) { $setClipboardCommand = Get-Command ` -Name 'Set-Clipboard' ` -ErrorAction SilentlyContinue if (-not $setClipboardCommand) { throw @' CopyToClipboard was specified, but Set-Clipboard is not available in the current PowerShell session. '@ } Set-Clipboard ` -Value $password ` -ErrorAction Stop } if ($AsSecureString) { return ConvertTo-SecureString ` -String $password ` -AsPlainText ` -Force } if ($PassThruObject) { $poolBasedEntropy = [System.Math]::Round( $Length * [System.Math]::Log( $combinedPool.Length, 2 ), 2 ) $actualUppercaseCount = 0 $actualLowercaseCount = 0 $actualNumberCount = 0 $actualSpecialCount = 0 foreach ($passwordCharacter in $password.ToCharArray()) { if ( $characterSets.Uppercase.IndexOf( $passwordCharacter ) -ge 0 ) { $actualUppercaseCount++ } elseif ( $characterSets.Lowercase.IndexOf( $passwordCharacter ) -ge 0 ) { $actualLowercaseCount++ } elseif ( $characterSets.Numbers.IndexOf( $passwordCharacter ) -ge 0 ) { $actualNumberCount++ } elseif ( $characterSets.Special.IndexOf( $passwordCharacter ) -ge 0 ) { $actualSpecialCount++ } } $effectiveMinimumNumbers = 0 if ($numbersEnabled) { $effectiveMinimumNumbers = [System.Math]::Max( 1, $MinimumNumbers ) } $effectiveMinimumSpecial = 0 if ($specialEnabled) { $effectiveMinimumSpecial = [System.Math]::Max( 1, $MinimumSpecial ) } return [pscustomobject][ordered]@{ Password = $password Length = $password.Length ContainsUppercase = ($actualUppercaseCount -gt 0) ContainsLowercase = ($actualLowercaseCount -gt 0) ContainsNumbers = ($actualNumberCount -gt 0) ContainsSpecial = ($actualSpecialCount -gt 0) UppercaseCount = $actualUppercaseCount LowercaseCount = $actualLowercaseCount NumberCount = $actualNumberCount SpecialCount = $actualSpecialCount MinimumNumbers = $effectiveMinimumNumbers MinimumSpecial = $effectiveMinimumSpecial AmbiguousCharsExcluded = [bool]$NoAmbiguousCharacters CharacterPoolSize = $combinedPool.Length PoolBasedEntropyBits = $poolBasedEntropy CopiedToClipboard = [bool]$CopyToClipboard } } return $password } finally { # This clears the local reference but cannot guarantee removal # of every immutable string copy from process memory. $password = $null } } end { if ($null -ne $randomNumberGenerator) { $randomNumberGenerator.Dispose() } } } |