Home A functional password generator
Post
Cancel

A functional password generator

Passwords are something we often don’t think enough about. It’s common to reuse them or to write them down in places that aren’t as secure (hello sticky note under the keyboard).
Creating a truly random, unique password can be a difficult task for humans. Even subconsciously we tend to include things we know like names or birthdays and use repeated patterns. Luckily humans can write code to make the job a lot easier (other than facemashing the keyboard).

I’m not interested. Skip to the full function

Desktop View Behold, a random password is created

How do we generate a password?

We could use something like the oneliner I described in the previous post but it doesn’t give us easy control over the length of the password or the inclusion of special characters, numbers, etc. We have to write a function.

Parameters

The best way to give us these options is to start with a param block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
param (
    [Parameter(Mandatory = $true)]
    [ValidateRange(12,256)] # Default minimum and maximum length in AzureAD 
    [int]$length,

    [Parameter(Mandatory = $false)]
    [bool]$includeUppercase = $true,

    [Parameter(Mandatory = $false)]
    [bool]$includeLowercase = $true,

    [Parameter(Mandatory = $false)]
    [bool]$includeNumbers = $true,

    [Parameter(Mandatory = $false)]
    [bool]$includeSymbols = $true
)

Note that by default we will be setting all the parameters except for the length to a default $true value. This is because most modern services will require passwords that include all these options. We’re also validating a range for the $length parameter. This helps us generate passwords that aren’t too short or too long.

Character sets

First we want to define the character sets our function can use to generate a password. We’re defining these as separate sets so that we have the choice to include or not include them if we want to. Hardcoding them instead of using ranges allows us to be selective where we want to be in which characters we include. First we create an array, and then we have an if statement for each of our charsets/parameters which will add that charset to the array.

1
2
3
4
5
6
# Set up the character types to include in the password
$charTypes = @()
if ($includeUppercase) { $charTypes += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }
if ($includeLowercase) { $charTypes += 'abcdefghijklmnopqrstuvwxyz' }
if ($includeNumbers) { $charTypes += '0123456789' }
if ($includeSymbols) { $charTypes += '!@#$%^&*(){}[]=<>,.' }

Of course it’s possible that someone mistakenly sets all our parameters to $false which would create a zero character password so we validate the array to see if the number of charsets is greater than 0.

1
2
# If no character types are selected, throw an error
if ($charTypes.Count -eq 0) { throw 'Set at least one of the parameters to true.' }
Generate the password

To generate the password we use a for loop. This will repeatedly pick a random character and add it to $password until $i matches $length.

1
2
3
4
5
6
# Generate the password by selecting a random character from each character type
$password = ''
for ($i = 0; $i -lt $length; $i++) {
    $charType = $charTypes[(Get-Random -Minimum 0 -Maximum $charTypes.Count)]
    $password += $charType[(Get-Random -Minimum 0 -Maximum $charType.Length)]
}
Does the password match our expectations?

As we saw previously, in rare cases the generated password could fail to include characters from a charset we wanted to be included. To remedy that we want to check if all the params/charsets that are set to $true are included. If we’re missing one we recursively call the function again to generate a new password which in most cases will match the params/charsets that were set to $true, if it doesn’t match it will keep calling itself until we have a valid password.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Check that the password includes at least one character from each specified character type
$missingCharTypes = @()
if ($includeUppercase -and ($password -notmatch '\p{Lu}')) { $missingCharTypes += 'uppercase' }
if ($includeLowercase -and ($password -notmatch '\p{Ll}')) { $missingCharTypes += 'lowercase' }
if ($includeNumbers -and ($password -notmatch '\p{Nd}')) { $missingCharTypes += 'numbers' }
if ($includeSymbols -and ($password -notmatch '[`!@#$%^&*()_+\-=\[\]{}\\|,.<>\/?~]')) { $missingCharTypes += 'symbols' }

# If any character types are missing, regenerate the password
if ($missingCharTypes.Count -gt 0) {
    # Because we are nesting another call of this function in here it will essentially loop until a password is generated that has all CharTypes
    Write-Warning "The generated password does not include any of the following character types: $($missingCharTypes -join ', '). Regenerating password..."
    $password = New-Password -length $length -includeUppercase $includeUppercase -includeLowercase $includeLowercase -includeNumbers $includeNumbers -includeSymbols $includeSymbols
}

Full Function

Once we have all these building blocks we can build the completed function. (for a comprehensive explanation of Powershell functions have a look at this post by Adam Bertram)

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
function New-Password {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateRange(12,256)] # Default minimum and maximum length in AzureAD 
        [int]$length,

        [Parameter(Mandatory = $false)]
        [bool]$includeUppercase = $true,

        [Parameter(Mandatory = $false)]
        [bool]$includeLowercase = $true,

        [Parameter(Mandatory = $false)]
        [bool]$includeNumbers = $true,

        [Parameter(Mandatory = $false)]
        [bool]$includeSymbols = $true
    )

    # Set up the character types to include in the password
    $charTypes = @()
    if ($includeUppercase) { $charTypes += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' }
    if ($includeLowercase) { $charTypes += 'abcdefghijklmnopqrstuvwxyz' }
    if ($includeNumbers) { $charTypes += '0123456789' }
    if ($includeSymbols) { $charTypes += '!@#$%^&*(){}[]=<>,.' }

    # If no character types are selected, throw an error
    if ($charTypes.Count -eq 0) { throw 'Set at least one of the parameters to true.' }

    # Generate the password by selecting a random character from each character type
    $password = ''
    for ($i = 0; $i -lt $length; $i++) {
        $charType = $charTypes[(Get-Random -Minimum 0 -Maximum $charTypes.Count)]
        $password += $charType[(Get-Random -Minimum 0 -Maximum $charType.Length)]
    }

    # Check that the password includes at least one character from each specified character type
    $missingCharTypes = @()
    if ($includeUppercase -and ($password -notmatch '\p{Lu}')) { $missingCharTypes += 'uppercase' }
    if ($includeLowercase -and ($password -notmatch '\p{Ll}')) { $missingCharTypes += 'lowercase' }
    if ($includeNumbers -and ($password -notmatch '\p{Nd}')) { $missingCharTypes += 'numbers' }
    if ($includeSymbols -and ($password -notmatch '[`!@#$%^&*()_+\-=\[\]{}\\|,.<>\/?~]')) { $missingCharTypes += 'symbols' }

    # If any character types are missing, regenerate the password
    if ($missingCharTypes.Count -gt 0) {
        # Because we are nesting another call of this function in here it will essentially loop until a password is generated that has all CharTypes
        Write-Warning "The generated password does not include any of the following character types: $($missingCharTypes -join ', '). Regenerating password..."
        $password = New-Password -length $length -includeUppercase $includeUppercase -includeLowercase $includeLowercase -includeNumbers $includeNumbers -includeSymbols $includeSymbols
    }

    return $password
}

We can then call the function using New-Password -length 12. Which will generate a 12 character password for us.

1
2
PS C:\Users\RoelvanderWegen> New-Password -length 12
3f}29nN[4A[M
Bonus

A Javascript version of this function is live on my password generator website.

This post is licensed under CC BY 4.0 by the author.