Src/Public/New-Passphrase.ps1

<#
.NOTES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Module: PSPassPhrase
Function: New-Passphrase
Author: Martin Cooper (@mc1903)
Date: 03-05-2025
GitHub Repo: https://github.com/mc1903/PSpassPhrase
Version: 2.0.1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
.SYNOPSIS
Generates one or more passphrases composed of random words, numbers, and optional special characters.
 
.DESCRIPTION
The New-Passphrase function generates passphrases by selecting a specified number of random words from a word list. These words can be separated by spaces, hyphens, or underscores.
 
The passphrase may optionally include a random number and a special character at the end. The function allows customisation of the word length, capitalisation, and special characters used.
 
.PARAMETER passPhraseCount
Specifies the number of passphrases to generate. Default is 1.
 
.PARAMETER wordsPerPassPhrase
Specifies the number of words to use in each passphrase. Default is 3.
 
.PARAMETER minWordLength
Specifies the minimum length of words to be included in the passphrase. Default is 5.
 
.PARAMETER maxWordLength
Specifies the maximum length of words to be included in the passphrase. Default is 8.
 
.PARAMETER firstCapitalOnly
If specified, only the first letter of the passphrase will be capitalised. Otherwise, the first letter of each word in the passphrase will be capitalised.
 
.PARAMETER delimiterChar
Specifies the character to use as the delimiter between each word in the passphrase.
Valid values are: 'none', 'space', 'hyphen', or 'underscore'.
- 'none' separates words with a space (e.g. "AlphaBetaGamma").
- 'space' separates words with a space (e.g. "Alpha Beta Gamma").
- 'hyphen' separates words with a hyphen (e.g. "Alpha-Beta-Gamma").
- 'underscore' separates words with an underscore (e.g. "Alpha_Beta_Gamma").
The delimiter character will also be inserted before the random number at the end.
The default is 'none'.
 
.PARAMETER noSpaceBetween
**Deprecated**: This switch made the passphrase words run together with nothing in between.
It is retained for backward compatibility.
Please use the `-delimiterChar ""` or omit the parameter for default behaviour.
 
.PARAMETER randomNumberLength
Specifies the length of the random number to append to each passphrase. Default is 3.
 
.PARAMETER noLastSpecialChar
If specified, no special character will be appended to the end of the passphrase.
 
.PARAMETER specialCharacterList
Specifies the list of special characters to choose from when appending a special character to the passphrase. Default is a list containing "!", "$", "*", and "&".
 
.PARAMETER wordListPath
Specifies the path to the word list file to use for generating passphrases. If not specified, a default list will be used.
 
.EXAMPLE
# Generate a single passphrase with the default settings
New-Passphrase
 
.EXAMPLE
# Generate 3 passphrases, each composed of 4 words, with a minimum word length of 6 and maximum word length of 10
New-Passphrase -passPhraseCount 3 -wordsPerPassPhrase 4 -minWordLength 6 -maxWordLength 10
 
.EXAMPLE
# Generate a passphrase with words separated by hyphens, a 5-digit random number, and no special character at the end
New-Passphrase -delimiterChar hyphen -randomNumberLength 5 -noLastSpecialChar
 
#>

Function New-Passphrase {
    [CmdletBinding()]
    Param (
        [int]$passPhraseCount = 1,
        [int]$wordsPerPassPhrase = 3,
        [int]$minWordLength = 5,
        [int]$maxWordLength = 8,
        [ValidateSet('none','space','hyphen','underscore')]
        [string]$delimiterChar = 'none',
        [switch]$firstCapitalOnly,
        [int]$randomNumberLength = 3,
        [switch]$noLastSpecialChar,
        [string[]]$specialCharacterList = @("!", "$", "*", "&"),
        [string]$wordListPath,
        [switch]$noSpaceBetween # Deprecated, remains for compatibility. Prefer -delimiterChar.
    )

    # Deprecated Parameters Warning
    if ($PSBoundParameters.ContainsKey('noSpaceBetween')) {
        Write-Warning -Message "The 'noSpaceBetween' parameter is deprecated and will be removed in future releases. Please use the 'delimiterChar' parameter instead."
    }

    if ($minWordLength -gt $maxWordLength) {
        throw "The minimum word length ($minWordLength) cannot be greater than the maximum word length ($maxWordLength)"
    }
    
    if (-not $wordListPath) {
        try {
            $modulePath = Get-ModulePath
            $wordListPath = Join-Path -Path $modulePath -ChildPath 'src\private\DefaultWordList.txt'
        }
        catch {
            throw "The default word list could not be found. Please specify the word list location using the -wordListPath parameter."
        }
    }

    if (-not (Test-Path -Path $wordListPath)) {
        throw "Word list file not found at path: $wordListPath"
    }

    Write-Verbose "Loading word list from $($wordListPath)"
    $wordList = Get-Content -Path $wordListPath

    Write-Verbose "The word list contains $($wordList.count) entries"

    Write-Verbose "Filtering word list and selecting only the words between $($minWordLength) and $($maxWordLength) characters"
    $pattern = '^[a-zA-Z]+$'
    $filteredWordList = $wordList | Where-Object { $_ -match $pattern -and $_.Length -ge $minWordLength -and $_.Length -le $maxWordLength }

    # Translate delimiter string to actual character
    switch ($delimiterChar) {
        'space'      { $delimiter = " " }
        'hyphen'     { $delimiter = "-" }
        'underscore' { $delimiter = "_" }
        'none'          { $delimiter = "" }
        default      { $delimiter = "" }
    }

    $passPhrases = @()
    for ($i = 0; $i -lt $passPhraseCount; $i++) {
        Write-Verbose "Generating passPhrase $($i+1) of $($passPhraseCount)"
        $passPhraseWords = @()
        for ($j = 0; $j -lt $wordsPerPassPhrase; $j++) {
            $random = Get-Random -InputObject $filteredWordList
            $passPhraseWords += $random
        }
    
        # Compose the passphrase with word formatting
        if ($firstCapitalOnly) {
            $concatWords = ($passPhraseWords | ForEach-Object { $_.ToLower() }) -join $delimiter
            $passPhrase = $concatWords.Substring(0,1).ToUpper() + $concatWords.Substring(1)
        }
        else {
            $concatWords = ($passPhraseWords | ForEach-Object { $_.Substring(0,1).ToUpper() + $_.Substring(1).ToLower() }) -join $delimiter
            $passPhrase = $concatWords
        }

        # Add the delimiter before the random number at the end
        $passPhrase += $delimiter + (-join ((0..9) | Get-Random -Count $randomNumberLength))

        if (!$noLastSpecialChar) {
            $randomSpecialChar = Get-Random -InputObject $specialCharacterList
            $passPhrase += $randomSpecialChar
        }
        $passPhrases += @{
            Index      = ($i + 1)
            PassPhrase = $passPhrase
        }
    }
    Write-Output "Index PassPhrase"
    Write-Output "----- ----------"
    foreach ($entry in $passPhrases) {
        $formatString = "{0,-5} {1}"
        Write-Output ($formatString -f $entry.Index, $entry.PassPhrase)
    }
}