src/PSPasswordGenerator.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<#
    PSPasswordGenerator.psm1, code for the PSPasswordGenerator module
    Copyright (C) 2016-2022 Colin Cogle <colin@colincogle.name>
    Online at <https://github.com/rhymeswithmogul/PSPasswordGenerator>
 
    This program is free software: you can redistribute it and/or modify it
    under the terms of the GNU Affero General Public License as published by the
    Free Software Foundation, either version 3 of the License, or (at your
    option) any later version.
 
    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 Affero General Public License
    for more details.
 
    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>.
#>


#Requires -Version 3.0

# .ExternalHelp PSPasswordGenerator-help.xml
Function Get-RandomPassword {
    [CmdletBinding(DefaultParameterSetName='RandomSecurely')]
    [OutputType([String], ParameterSetName='RandomInsecurely')]
    [OutputType([String], ParameterSetName='WordsInsecurely')]
    [OutputType([Security.SecureString], ParameterSetName='RandomSecurely')]
    [OutputType([Security.SecureString], ParameterSetName='WordsSecurely')]
    [Alias('New-RandomPassword')]
    Param(
        [Parameter(ParameterSetName='RandomInsecurely')]
        [Parameter(ParameterSetName='RandomSecurely')]
        [ValidateRange(1, [UInt32]::MaxValue)]
        [Alias('Count', 'MinLength', 'Size')]
        [UInt32] $Length = 16,

        [Parameter(ParameterSetName='RandomInsecurely')]
        [Parameter(ParameterSetName='RandomSecurely')]
        [Switch] $StartWithLetter,

        [Parameter(ParameterSetName='WordsInsecurely')]
        [Parameter(ParameterSetName='WordsSecurely')]
        [ValidateRange(1, [UInt32]::MaxValue)]
        [UInt32] $Words = 3,

        [Parameter(ParameterSetName='WordsInsecurely', Mandatory)]
        [Parameter(ParameterSetName='WordsSecurely', Mandatory)]
        [ValidateNotNullOrEmpty()]
        [IO.FileInfo] $WordList,

        [Parameter(ParameterSetName='RandomInsecurely', Mandatory)]
        [Parameter(ParameterSetName='WordsInsecurely', Mandatory)]
        [Switch] $AsPlainText,

        [Switch] $NoSymbols,

        [Switch] $UseAmbiguousCharacters,

        [Switch] $UseExtendedAscii
    )

    # Warn the user if they've specified mutually-exclusive options.
    If ($NoSymbols -and $UseExtendedAscii) {
        Write-Warning 'The -NoSymbols parameter was also specified. No extended ASCII characters will be used.'
    }

    $ret = ""
    If ($PSCmdlet.ParameterSetName -Like 'Random*') {
        For ($i = 0; $i -lt $Length; $i++) {
            Do {
                Do {
                    Do {
                        Do {
                            $x = Get-Random -Minimum 33 -Maximum 254
                            Write-Debug "Considering character: $([char]$x)"
                        } While ($x -eq 127 -Or (-Not $UseExtendedAscii -And $x -gt 127))
                        # The above Do..While loop does this:
                        # 1. Don't allow ASCII 127 (delete).
                        # 2. Don't allow extended ASCII, unless the user wants it.

                    } While (-Not $UseAmbiguousCharacters -And ($x -In @(49,73,108,124,48,79)))
                    # The above loop disallows 1 (ASCII 49), I (73), l (108),
                    # | (124), 0 (48) or O (79) -- unless the user wants those.

                } While ($NoSymbols -And ($x -lt 48 -Or ($x -gt 57 -And $x -lt 65) -Or ($x -gt 90 -And $x -lt 97) -Or $x -gt 122))
                # If the -NoSymbols parameter was specified, this loop will ensure
                # that the character is neither a symbol nor in the extended ASCII
                # character set.
                
            } While ($i -eq 0 -And $StartWithLetter -And -Not (($x -ge 65 -And $x -le 90) -Or ($x -ge 97 -And $x -le 122)))
            # If the -StartWithLetter parameter was specified, this loop will make
            # sure that the first character is an upper- or lower-case letter.

            Write-Debug "SUCCESS: Adding character: $([char]$x)"
            $ret += [char]$x
        }
    }

    # If we're generating random words:
    Else {
        # There is DEFINITELY room for improvement here. Loading an entire
        # wordlist into memory can be quite cumbersome.
        $allWords = Get-Content -LiteralPath $WordList -ErrorAction Stop
        $culture  = (Get-Culture).TextInfo

        $ret = ''
        For ($i = 0; $i -lt $Words; $i++) {
            # Pick a random word from the list.
            $word = Get-Random $allWords

            # Randomly capitalize the first letter of the word.
            If ((Get-Random) % 2) {
                $word = $culture.ToTitleCase($word)
            }

            # Stick something in between the words.
            # Letters are 65-90 (caps) and 97-122 (lower)
            $separator = 0
            Do {
                $ch = (Get-RandomPassword -Length 1 -NoSymbols:$NoSymbols -AsPlainText -UseExtendedAscii:$UseExtendedAscii)
                Write-Debug "Trying separator $ch."
                $separator = [Convert]::ToByte([Char]$ch)
            } While (
                ($separator -ge 65 -and $separator -le 90)                <# No uppercase letters #> `
                -or ($separator -ge 97 -and $separator -le 122)            <# No lowercase letters #> `
                -or ($separator -ge 128 -and $separator -le 165)        <# No accented letters #> `
                -or ($separator -gt 165 -and -Not $UseExtendedAscii)    <# Unwanted extended ASCII #> `
            )

            Write-Debug "WORD=`"$word`", SEP=`"$([Char]$separator)`""
            $ret += $word
            $ret += [Char]$separator
        }

        # Chop off the final separator.
        $ret = $ret.Substring(0, $ret.Length - 1)
    }

    If ($AsPlainText) {
        Return $ret
    } Else {
        $ss = ConvertTo-SecureString -AsPlainText -Force -String $ret
        Remove-Variable -Name 'ret' -ErrorAction SilentlyContinue
        Return $ss
    }
}