Creating a PowerShell password generator for generating a single or entire lists of passwords [Version2]

I created the blog post Creating a PowerShell password generator for generating a single or entire lists of passwords and then uploaded the code to Reddit. I received a lot of great critique on the code and even got some bugs pointed out. I decided to make it a challenge for myself and see if I could use the input from the good Reddit people and make the code better.

This code is built on the code I showed in Creating a PowerShell password generator for generating a single or entire lists of passwords.

Some of the code I am showing here was created by the following Reddit users: /u/Szeraax and /u/ExceptionEX

You can find the updated version of the function on my Repository

The tasks

  1. In the first script i used the parameters to create two seperate code paths. One codepath for if i just wanted to create a single password. Another codepath if i wanted to create a list of multiple passwords. There where no reason to split this up into two seperate codepaths since the base code of creating the password is the exact samte.
  2. In the first script the default password provided by the password generator was a 10 character password with no symbols or uppercase characters. A password should by default generate a fairly strong password. And if you need a lesser secure password you should be able to configure it. This task forced me to reverse the parameters so instead of choosing if the password should contain, then the parameter would be used not make the password contain a type of character.
  3. In the first script i used while loops for generating the passwords, i should use for loops instead, for loops is also more effcient than while loops.
  4. The last task was to validate the passwords created. In the first script there where no validation on the passwords created, and since they are generated from multiple arrays, there is a chance that the password would not actually contain a symbol, for example.

Walking through the code

I will walk through the code and my thought process of how I built it.

The Parameters

Now I needed to create parameters for if the password does not need symbols, numbers, lowercase or uppercase letters. I also needed a parameter for the length of the password, a parameter for how many passwords should be generated if the user needs multiple, and the file path to where the list of passwords should be stored.

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)][Switch]$SkipUpperCase,
        [Parameter(Mandatory=$false)][Switch]$SkipLowerCase,
        [Parameter(Mandatory=$false)][Switch]$SkipNumbers,
        [Parameter(Mandatory=$false)][Switch]$SkipSymbols,
        [Parameter(Mandatory=$false)][Int]$PasswordLength = 16,
        [Parameter(Mandatory=$false)][Int]$NumberOfPasswords = 1,
        [Parameter(Mandatory=$false)][String]$PasswordsFilePath
    )

All parameters have the attribute Mandatory set to False. The reason for this is that if a user calls the function, without any parameters, the generator should just provide a single strong password.

I set the default value of $PasswordLength to 16. This defaults the password created to be 16 characters long. The user can always call the function with the parameter and define their own password length.

I also set the parameter $NumberOfPasswords default value to 1. The reason for this is that by default I only want a single password generated.

The Begin Block

In the begin block I start by creating an if condition stating that if a user calls all the switch parameters to skip a character type, the script should provide an error statement and exit the script. The reason for this is that if the user skips all character type no password can be created.

I then create two arrays. The first array: $CharArray will be an array of all the different characters chosen by the user.

The second array: $ValidatePass is an array where I store a switch statement depending on which switch parameters the user has chosen. For example, if a user does not select any switch parameters the $ValidatePass array will contain 1,2,3,4. If the user set the switch parameter $SkipNumbers, the array would contain 2,3,4. These switch statements are used in the Process block to run validations on the password. I will explain this deeper later in the post.

I then create four arrays containing the different character types

$LowerLetters = New-Object System.Collections.ArrayList; 97..122 | % {$LowerLetters.Add([Char]$_)} | Out-Null
$UpperLetters = New-Object System.Collections.ArrayList; 65..90 | % {$UpperLetters.Add([Char]$_)} | Out-Null
$Numbers = New-Object System.Collections.ArrayList; 0..9 | % {$Numbers.Add($_.ToString())} | Out-Null
$Symbols = New-Object System.Collections.ArrayList; 33..47 | % {$Symbols.Add([Char]$_)} | Out-Null

I then create four if conditions that add the if a switch parameter has not been set then add that type of characters to the array $CharArray, and add the corresponding number to the array $ValidatePass.

if(!$SkipNumbers.IsPresent){$CharArray.Add($Numbers) | Out-Null; $ValidatePass.Add(1) | Out-Null}
if(!$SkipLowerCase.IsPresent){$CharArray.Add($LowerLetters) | Out-Null; $ValidatePass.Add(2) | Out-Null}
if(!$SkipUpperCase.IsPresent){$CharArray.Add($UpperLetters) | Out-Null; $ValidatePass.Add(3) | Out-Null}
if(!$SkipSymbols.IsPresent){$CharArray.Add($Symbols) | Out-Null; $ValidatePass.Add(4) | Out-Null}

At the end of the Begin block, I will go through the array $CharArray and add all the characters to the string $WorkingSet

$WorkingSet = $CharArray | % {$_}

The Process Block

In the process block, I start with an if condition to see if the user wants the passwords saved to a txt file. If the user-defined the parameter $PasswordFilePath then the function will create the file.

if($PasswordsFilePath -and !(Test-Path $PasswordsFilePath)){
    New-Item $PasswordsFilePath -ItemType File
}

Then I will create a for loop. The purpose of this loop is to generate as many passwords as defined in the parameter $NumberOfPasswords. By default, this parameter is set to 1 and therefore the loop would only run once, creating a single password.

Inside the loop, a new array named $Password will be generated. And then another for loop is created. Inside the second for loop a single character will be generated randomly from the string $WorkingSet, created in the Begin block. This second loop will run as many times as stated in the parameter $PasswordLength.

$Password = New-Object System.Collections.ArrayList
for($y = 0; $y -le $PasswordLength; $y++){
    $Character = $WorkingSet | Get-Random
    $Password.Add($Character) | Out-Null
}

Once the $Password has been generated as an Array containing different characters, I will need to verify if the password contains symbols, numbers, lowercase, and uppercase characters or what was defined by the user.

To do this I will use the array $ValidatePass created in the Begin block.

Switch ($ValidatePass){
    1 {if(!($Password -match '\d')){$PassNotValid = $true}}
    2 {if(!($Password -cmatch "[a-z]")){$PassNotValid = $true}}
    3 {if(!($Password -cmatch "[A-Z]")){$PassNotValid = $true}}
    4 {
        $Password | % {if($Symbols -contains $_){$ContainSymbol = $true}}
        if($ContainSymbol -eq $false){$PassNotValid = $true}
        $ContainSymbol = $false
    }
}

In the switch function, I will validate if each block defined by the array $ValidatePass is true, and if it is true the variable $PassNotValid will be set to $true

I then use the if statement to chose whether the password should be outputted/saved to the file or if the password is not valid and this loop should be disregarded.

This if statement will, if the variable $PassNotValid equals $true take the loop counter $i and subtract by 1. This way the loop will run again and the total length of the password will still be what was defined in the parameter $PasswordLength.

if($PassNotValid -eq $true){$i = $i - 1; $PassNotValid = $false; continue}else {
    $Password = $Password -join ""
    if($PasswordsFilePath){Add-Content -Path $PasswordsFilePath -Value $Password}else{Return $Password}
}

Conclusion

This process was a great learning opportunity for me to try and think differently and try to improve my code. This also made it clear to me that it is always a good idea to get a second pair of eyes on your code. Sometimes you will get a task, create a script that accomplishes the task, but you might be able to improve the code or there might be some bugs not showing in your testing.

The complete script

function New-PSPasswordV2 {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)][Switch]$SkipUpperCase,
        [Parameter(Mandatory=$false)][Switch]$SkipLowerCase,
        [Parameter(Mandatory=$false)][Switch]$SkipNumbers,
        [Parameter(Mandatory=$false)][Switch]$SkipSymbols,
        [Parameter(Mandatory=$false)][Int]$PasswordLength = 16,
        [Parameter(Mandatory=$false)][Int]$NumberOfPasswords = 1,
        [Parameter(Mandatory=$false)][String]$PasswordsFilePath
    )
    
    begin {
        if($SkipUpperCase.IsPresent -and $SkipLowerCase.IsPresent -and $SkipNumbers.IsPresent -and $SkipSymbols.IsPresent){
            Write-Error "You may not skip all four types of characters at the same time, try again..."
            Exit
        }

        $CharArray = New-Object System.Collections.ArrayList
        $ValidatePass = New-Object System.Collections.ArrayList

        $LowerLetters = New-Object System.Collections.ArrayList; 97..122 | % {$LowerLetters.Add([Char]$_)} | Out-Null
        $UpperLetters = New-Object System.Collections.ArrayList; 65..90 | % {$UpperLetters.Add([Char]$_)} | Out-Null
        $Numbers = New-Object System.Collections.ArrayList; 0..9 | % {$Numbers.Add($_.ToString())} | Out-Null
        $Symbols = New-Object System.Collections.ArrayList; 33..47 | % {$Symbols.Add([Char]$_)} | Out-Null

        if(!$SkipNumbers.IsPresent){$CharArray.Add($Numbers) | Out-Null; $ValidatePass.Add(1) | Out-Null}
        if(!$SkipLowerCase.IsPresent){$CharArray.Add($LowerLetters) | Out-Null; $ValidatePass.Add(2) | Out-Null}
        if(!$SkipUpperCase.IsPresent){$CharArray.Add($UpperLetters) | Out-Null; $ValidatePass.Add(3) | Out-Null}
        if(!$SkipSymbols.IsPresent){$CharArray.Add($Symbols) | Out-Null; $ValidatePass.Add(4) | Out-Null}

        $WorkingSet = $CharArray | % {$_}
    }
    
    process {
        if($PasswordsFilePath -and !(Test-Path $PasswordsFilePath)){
            New-Item $PasswordsFilePath -ItemType File
        }

        for($i = 0; $i -le $NumberOfPasswords; $i++){
            $Password = New-Object System.Collections.ArrayList
            for($y = 0; $y -le $PasswordLength; $y++){
                $Character = $WorkingSet | Get-Random
                $Password.Add($Character) | Out-Null
            }

            Switch ($ValidatePass){
                1 {if(!($Password -match '\d')){$PassNotValid = $true}}
                2 {if(!($Password -cmatch "[a-z]")){$PassNotValid = $true}}
                3 {if(!($Password -cmatch "[A-Z]")){$PassNotValid = $true}}
                4 {
                    $Password | % {if($Symbols -contains $_){$ContainSymbol = $true}}
                    if($ContainSymbol -eq $false){$PassNotValid = $true}
                    $ContainSymbol = $false
                }
            }
            if($PassNotValid -eq $true){$i = $i - 1; $PassNotValid = $false; continue}else {
                $Password = $Password -join ""
                if($PasswordsFilePath){Add-Content -Path $PasswordsFilePath -Value $Password}else{Return $Password}
            }
        }
    }
    
    end {
        Write-Verbose -Message "Finishing function"
    }
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

This website uses cookies. By continuing to use this site, you accept our use of cookies.