PwGen.psm1
|
#Requires -Version 5.1 <# PwGen - a PowerShell port of pwgen, the pronounceable password generator. Copyright (C) 2026 Eran Based on pwgen, Copyright (C) 2001-2014 Theodore Ts'o <https://github.com/tytso/pwgen> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. #> Set-StrictMode -Version Latest # Character sets (same as pwgen) $script:Digits = '0123456789' $script:Uppers = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' $script:Lowers = 'abcdefghijklmnopqrstuvwxyz' $script:SymbolSet = '!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~' $script:Ambiguous = 'B8G6I1l0OQDS5Z2' $script:VowelSet = '01aeiouyAEIOUY' # Phoneme element flags $script:F_CONSONANT = 1 $script:F_VOWEL = 2 $script:F_DIPHTHONG = 4 $script:F_NOTFIRST = 8 $C = $script:F_CONSONANT; $V = $script:F_VOWEL; $D = $script:F_DIPHTHONG; $NF = $script:F_NOTFIRST $elements = [ordered]@{ a = $V; ae = $V -bor $D; ah = $V -bor $D; ai = $V -bor $D b = $C; c = $C; ch = $C -bor $D; d = $C e = $V; ee = $V -bor $D; ei = $V -bor $D f = $C; g = $C; gh = $C -bor $D -bor $NF; h = $C i = $V; ie = $V -bor $D j = $C; k = $C; l = $C; m = $C; n = $C; ng = $C -bor $D -bor $NF o = $V; oh = $V -bor $D; oo = $V -bor $D p = $C; ph = $C -bor $D; qu = $C -bor $D; r = $C; s = $C; sh = $C -bor $D t = $C; th = $C -bor $D u = $V; v = $C; w = $C; x = $C; y = $C; z = $C } $script:ElemStr = [string[]]@($elements.Keys) $script:ElemFlags = [int[]]@($elements.Values) Remove-Variable C, V, D, NF, elements # Unbiased CSPRNG integer in [0, max). RandomNumberGenerator.GetInt32 only exists on # .NET Core 3+, so use rejection sampling over GetBytes to also run on Windows PowerShell 5.1. class PwGenRng { static [System.Security.Cryptography.RandomNumberGenerator] $Rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() static [byte[]] $Buf = [byte[]]::new(4) static [int] GetInt32([int]$Max) { if ($Max -le 1) { return 0 } $range = [long]4294967296 $limit = $range - ($range % $Max) while ($true) { [PwGenRng]::Rng.GetBytes([PwGenRng]::Buf) $v = [long][BitConverter]::ToUInt32([PwGenRng]::Buf, 0) if ($v -lt $limit) { return [int]($v % $Max) } } return 0 } } function script:Get-PhonemePassword { param([int]$Length, [bool]$Upper, [bool]$Digit, [bool]$Symbol, [bool]$NoAmbiguous) $ambChars = $script:Ambiguous.ToCharArray() $n = $script:ElemStr.Length while ($true) { $sb = [System.Text.StringBuilder]::new($Length + 1) $needUpper = $Upper; $needDigit = $Digit; $needSymbol = $Symbol $prev = 0 $first = $true $shouldBe = if ([PwGenRng]::GetInt32(2)) { $script:F_VOWEL } else { $script:F_CONSONANT } while ($sb.Length -lt $Length) { $i = [PwGenRng]::GetInt32($n) $str = $script:ElemStr[$i] $flags = $script:ElemFlags[$i] if (-not ($flags -band $shouldBe)) { continue } if ($first -and ($flags -band $script:F_NOTFIRST)) { continue } # Don't allow a vowel followed by a vowel/diphthong pair if (($prev -band $script:F_VOWEL) -and ($flags -band $script:F_VOWEL) -and ($flags -band $script:F_DIPHTHONG)) { continue } if ($str.Length -gt ($Length - $sb.Length)) { continue } if ($NoAmbiguous -and $str.IndexOfAny($ambChars) -ge 0) { continue } if ($Upper -and ($first -or ($flags -band $script:F_CONSONANT)) -and [PwGenRng]::GetInt32(10) -lt 2) { $up = [char]::ToUpperInvariant($str[0]) if (-not ($NoAmbiguous -and $script:Ambiguous.Contains($up))) { $str = $up + $str.Substring(1) $needUpper = $false } } [void]$sb.Append($str) if ($sb.Length -ge $Length) { break } if ($Digit -and -not $first -and [PwGenRng]::GetInt32(10) -lt 3) { do { $ch = $script:Digits[[PwGenRng]::GetInt32(10)] } while ($NoAmbiguous -and $script:Ambiguous.Contains($ch)) [void]$sb.Append($ch) $needDigit = $false # Restart the phoneme sequence after a digit $first = $true $prev = 0 $shouldBe = if ([PwGenRng]::GetInt32(2)) { $script:F_VOWEL } else { $script:F_CONSONANT } continue } if ($Symbol -and -not $first -and [PwGenRng]::GetInt32(10) -lt 2) { do { $ch = $script:SymbolSet[[PwGenRng]::GetInt32($script:SymbolSet.Length)] } while ($NoAmbiguous -and $script:Ambiguous.Contains($ch)) [void]$sb.Append($ch) $needSymbol = $false } if ($shouldBe -eq $script:F_CONSONANT) { $shouldBe = $script:F_VOWEL } elseif (($prev -band $script:F_VOWEL) -or ($flags -band $script:F_DIPHTHONG) -or [PwGenRng]::GetInt32(10) -gt 3) { $shouldBe = $script:F_CONSONANT } else { $shouldBe = $script:F_VOWEL } $prev = $flags $first = $false } if (-not ($needUpper -or $needDigit -or $needSymbol)) { return $sb.ToString() } } } function script:Get-RandomPassword { param([int]$Length, [bool]$Upper, [bool]$Digit, [bool]$Symbol, [bool]$NoAmbiguous, [bool]$NoVowels, [string]$RemoveChars) $exclude = [System.Collections.Generic.HashSet[char]]::new() if ($NoAmbiguous) { $script:Ambiguous.ToCharArray() | ForEach-Object { [void]$exclude.Add($_) } } if ($NoVowels) { $script:VowelSet.ToCharArray() | ForEach-Object { [void]$exclude.Add($_) } } if ($RemoveChars) { $RemoveChars.ToCharArray() | ForEach-Object { [void]$exclude.Add($_) } } $filter = { param($set) -join ($set.ToCharArray() | Where-Object { -not $exclude.Contains($_) }) } # Each class that is enabled and still non-empty must appear at least once $classes = [System.Collections.Generic.List[string]]::new() $classes.Add((& $filter $script:Lowers)) if ($Upper) { $classes.Add((& $filter $script:Uppers)) } if ($Digit) { $classes.Add((& $filter $script:Digits)) } if ($Symbol) { $classes.Add((& $filter $script:SymbolSet)) } $required = @($classes | Where-Object { $_.Length -gt 0 }) $charset = -join $required if ($charset.Length -eq 0) { throw 'No characters left to generate a password from (check -RemoveChars / -NoVowels / -NoAmbiguous).' } if ($required.Count -gt $Length) { $required = @() } # too short to satisfy every class while ($true) { $chars = [char[]]::new($Length) for ($i = 0; $i -lt $Length; $i++) { $chars[$i] = $charset[[PwGenRng]::GetInt32($charset.Length)] } $pw = [string]::new($chars) $ok = $true foreach ($cls in $required) { if ($pw.IndexOfAny($cls.ToCharArray()) -lt 0) { $ok = $false; break } } if ($ok) { return $pw } } } function script:New-PwGenBatch { param([int]$Length, [int]$Count, [bool]$Secure, [bool]$Symbols, [bool]$NoCapitalize, [bool]$NoNumerals, [bool]$NoAmbiguous, [bool]$NoVowels, [string]$RemoveChars) $upper = -not $NoCapitalize $digit = -not $NoNumerals # Same short-length rules as pwgen if ($Length -le 2) { $upper = $false } if ($Length -le 1) { $digit = $false } # Like pwgen: -v, -r and very short lengths use the fully random generator $useRandom = $Secure -or $NoVowels -or $RemoveChars -or $Length -lt 5 for ($k = 0; $k -lt $Count; $k++) { if ($useRandom) { Get-RandomPassword -Length $Length -Upper $upper -Digit $digit -Symbol $Symbols ` -NoAmbiguous $NoAmbiguous -NoVowels $NoVowels -RemoveChars $RemoveChars } else { Get-PhonemePassword -Length $Length -Upper $upper -Digit $digit -Symbol $Symbols -NoAmbiguous $NoAmbiguous } } } function script:ConvertTo-PwGenNumber([string]$Value, [string]$What, [int]$Max) { $n = 0 if (-not [int]::TryParse($Value, [ref]$n) -or $n -lt 1 -or $n -gt $Max) { throw "pwgen: invalid $What '$Value' (must be 1-$Max). Try 'pwgen --help'." } $n } $script:PwgenUsage = @' Usage: pwgen [ OPTIONS ] [ pw_length ] [ num_pw ] Options supported by pwgen: -c or --capitalize Include at least one capital letter in the password (default) -A or --no-capitalize Don't include capital letters in the password -n or --numerals Include at least one number in the password (default) -0 or --no-numerals Don't include numbers in the password -y or --symbols Include at least one special symbol in the password -r <chars> or --remove-chars=<chars> Remove characters from the set of characters to generate passwords -s or --secure Generate completely random passwords -B or --ambiguous Don't include ambiguous characters in the password -h or --help Print a help message -N <num> or --num-passwords=<num> Number of passwords to generate -C Print the generated passwords in columns -1 Don't print the generated passwords in columns -v or --no-vowels Do not use any vowels so as to avoid accidental nasty words PowerShell extras: --clip Copy the password(s) to the clipboard instead of printing them --secure-string Output [securestring] objects instead of text (implies -1) Randomness comes from System.Security.Cryptography.RandomNumberGenerator. The -H option (seed from a file's SHA1) is not supported. '@ function pwgen { <# .SYNOPSIS Generates pronounceable or fully random passwords, like the Linux pwgen utility. .DESCRIPTION A PowerShell port of pwgen. Accepts the same options as pwgen, including combined short flags (-sy1B) and GNU-style long options (--remove-chars=xyz). By default it generates pronounceable passwords of 8 characters containing at least one capital letter and one digit. When writing to the console it prints a screen full of passwords in columns; when piped or assigned it outputs one password per line. Run 'pwgen --help' for the full list of options. .EXAMPLE pwgen Prints a screen full of 8-character pronounceable passwords. .EXAMPLE pwgen -sy1B 20 3 Three fully random 20-character passwords with symbols and no ambiguous characters. .EXAMPLE pwgen -s 24 --clip Copies one random 24-character password to the clipboard. .EXAMPLE $cred = [pscredential]::new('svc-user', (pwgen -s 24 --secure-string)) Creates a credential without the password ever being stored as plain text in a variable. .LINK https://github.com/eran132/pwgen-powershell #> # Intentionally a simple (non-advanced) function so that raw pwgen-style # flags such as -sy, -0, -1, --remove-chars=xyz arrive untouched in $args. $p = @{ Secure = $false; Symbols = $false; NoCapitalize = $false; NoNumerals = $false NoAmbiguous = $false; NoVowels = $false; RemoveChars = '' } $columns = $null $clip = $false $secureString = $false $count = $null $positional = [System.Collections.Generic.List[string]]::new() $argv = @($args | ForEach-Object { "$_" }) for ($i = 0; $i -lt $argv.Count; $i++) { $a = $argv[$i] if ($a -eq '--') { if ($i + 1 -lt $argv.Count) { $positional.AddRange([string[]]$argv[($i + 1)..($argv.Count - 1)]) } break } if ($a.StartsWith('--')) { $name, $val = $a.Substring(2) -split '=', 2 $needsValue = $name -in 'remove-chars', 'num-passwords' if ($needsValue -and $null -eq $val) { if ($i + 1 -ge $argv.Count) { throw "pwgen: option '--$name' requires an argument" } $val = $argv[++$i] } elseif (-not $needsValue -and $null -ne $val) { throw "pwgen: option '--$name' doesn't allow an argument" } switch ($name) { 'capitalize' { $p.NoCapitalize = $false } 'no-capitalize' { $p.NoCapitalize = $true } 'numerals' { $p.NoNumerals = $false } 'no-numerals' { $p.NoNumerals = $true } 'symbols' { $p.Symbols = $true } 'secure' { $p.Secure = $true } 'ambiguous' { $p.NoAmbiguous = $true } 'no-vowels' { $p.NoVowels = $true } 'remove-chars' { $p.RemoveChars = $val } 'num-passwords' { $count = ConvertTo-PwGenNumber $val 'number of passwords' 1000000 } 'clip' { $clip = $true } 'secure-string' { $secureString = $true } 'help' { return $script:PwgenUsage } default { throw "pwgen: unrecognized option '--$name'. Try 'pwgen --help'." } } continue } if ($a.Length -gt 1 -and $a[0] -eq '-') { for ($j = 1; $j -lt $a.Length; $j++) { $o = $a[$j] $rest = $a.Substring($j + 1) switch -CaseSensitive ($o) { 'c' { $p.NoCapitalize = $false } 'A' { $p.NoCapitalize = $true } 'n' { $p.NoNumerals = $false } '0' { $p.NoNumerals = $true } 'y' { $p.Symbols = $true } 's' { $p.Secure = $true } 'B' { $p.NoAmbiguous = $true } 'v' { $p.NoVowels = $true } 'C' { $columns = $true } '1' { $columns = $false } 'h' { return $script:PwgenUsage } 'H' { Write-Warning 'pwgen: -H (sha1 seed) is not supported; using cryptographic randomness.' if (-not $rest) { $i++ } $j = $a.Length } { $_ -ceq 'r' -or $_ -ceq 'N' } { $v = if ($rest) { $rest } elseif ($i + 1 -lt $argv.Count) { $argv[++$i] } else { throw "pwgen: option requires an argument -- '$o'" } if ($o -ceq 'r') { $p.RemoveChars = $v } else { $count = ConvertTo-PwGenNumber $v 'number of passwords' 1000000 } $j = $a.Length } default { throw "pwgen: invalid option -- '$o'. Try 'pwgen --help'." } } } continue } $positional.Add($a) } if ($positional.Count -gt 2) { throw "pwgen: too many arguments. Try 'pwgen --help'." } $length = if ($positional.Count -ge 1) { ConvertTo-PwGenNumber $positional[0] 'password length' 4096 } else { 8 } if ($positional.Count -ge 2) { $count = ConvertTo-PwGenNumber $positional[1] 'number of passwords' 1000000 } # Like pwgen: columns by default when writing straight to an interactive console if ($null -eq $columns) { $columns = -not $clip -and -not $secureString -and -not [Console]::IsOutputRedirected -and $MyInvocation.PipelinePosition -eq $MyInvocation.PipelineLength } if ($secureString) { $columns = $false } $width = 80 try { if ($Host.UI.RawUI.WindowSize.Width -gt 0) { $width = $Host.UI.RawUI.WindowSize.Width } } catch { Write-Debug "pwgen: console width unavailable, using $width columns" } $perLine = [Math]::Max(1, [Math]::Floor(($width - 1) / ($length + 1))) if ($null -eq $count) { $count = if ($columns) { $perLine * 20 } else { 1 } } $pws = @(New-PwGenBatch -Length $length -Count $count @p) if ($clip) { Set-Clipboard -Value $pws Write-Information "Copied $count password(s) to the clipboard." -InformationAction Continue return } if ($secureString) { foreach ($pw in $pws) { $ss = [securestring]::new() foreach ($ch in $pw.ToCharArray()) { $ss.AppendChar($ch) } $ss.MakeReadOnly() $ss } return } if ($columns) { for ($k = 0; $k -lt $pws.Count; $k += $perLine) { $pws[$k..([Math]::Min($k + $perLine, $pws.Count) - 1)] -join ' ' } } else { $pws } } Export-ModuleMember -Function pwgen |