source/public/New-PGenRandomPassphrase.ps1

function New-PGenRandomPassphrase {
    <#
.SYNOPSIS
Generates a cryptographically secure random passphrase.
 
.DESCRIPTION
Generates a random passphrase from the EFF Large Word List loaded by the
PasswordGen.PS module.
 
Words are selected using a cryptographically secure random number generator
and words are not repeated within a single passphrase.
 
Optional features include word capitalization, numeric suffix generation,
clipboard copying, SecureString output, and metadata output.
 
The default passphrase contains four randomly selected words separated by
a hyphen (-).
 
.PARAMETER WordCount
Specifies the number of words to include in the generated passphrase.
 
Default: 4
 
.PARAMETER Separator
Specifies the separator inserted between words.
 
Default: -
 
Examples:
 
-
_
.
@
 
.PARAMETER CapitalizeWords
Converts each selected word to title case.
 
Example:
 
Forest-Lantern-Silver-Ocean
 
.PARAMETER AddNumber
Appends a randomly generated numeric component to the passphrase.
 
Example:
 
Forest-Lantern-Silver-Ocean-42
 
.PARAMETER NumberLength
Specifies the number of digits to generate when AddNumber is specified.
 
Default: 2
 
.PARAMETER CopyToClipboard
Copies the generated passphrase to the clipboard.
 
The passphrase is still returned to the pipeline.
 
.PARAMETER AsSecureString
Returns the generated passphrase as a SecureString.
 
This parameter cannot be used together with PassThruObject.
 
.PARAMETER PassThruObject
Returns a PSCustomObject containing the generated passphrase and
additional metadata.
 
This parameter cannot be used together with AsSecureString.
 
.OUTPUTS
System.String
 
Returned by default.
 
.OUTPUTS
System.Security.SecureString
 
Returned when AsSecureString is specified.
 
.OUTPUTS
System.Management.Automation.PSCustomObject
 
Returned when PassThruObject is specified.
 
.EXAMPLE
New-PGenRandomPassphrase
 
Example output:
 
forest-lantern-silver-ocean
 
.EXAMPLE
New-PGenRandomPassphrase -WordCount 6
 
Example output:
 
forest-lantern-silver-ocean-river-anchor
 
.EXAMPLE
New-PGenRandomPassphrase -CapitalizeWords
 
Example output:
 
Forest-Lantern-Silver-Ocean
 
.EXAMPLE
New-PGenRandomPassphrase `
    -Separator '_'
 
Example output:
 
forest_lantern_silver_ocean
 
.EXAMPLE
New-PGenRandomPassphrase `
    -AddNumber
 
Example output:
 
forest-lantern-silver-ocean-42
 
.EXAMPLE
New-PGenRandomPassphrase `
    -AddNumber `
    -NumberLength 4
 
Example output:
 
forest-lantern-silver-ocean-4829
 
.EXAMPLE
New-PGenRandomPassphrase `
    -CapitalizeWords `
    -AddNumber `
    -PassThruObject
 
Returns an object similar to:
 
Passphrase : Forest-Lantern-Silver-Ocean-42
Length : 32
WordCount : 4
Words : {Forest, Lantern, Silver, Ocean}
Separator : -
WordsCapitalized : True
NumberAdded : True
NumberLength : 2
NumberValue : 42
WordListSize : 7776
WordListSource : EFF Large Word List
EstimatedEntropyBits : 58.34
CopiedToClipboard : False
 
.EXAMPLE
New-PGenRandomPassphrase `
    -CopyToClipboard
 
Generates a passphrase and copies it to the clipboard.
 
.EXAMPLE
New-PGenRandomPassphrase `
    -AsSecureString
 
Generates a passphrase and returns it as a SecureString.
 
.NOTES
Words are selected from the EFF Large Word List loaded during module
import.
 
Passphrases are generated using
System.Security.Cryptography.RandomNumberGenerator.
 
Compatible with:
 
- Windows PowerShell 5.1
- PowerShell 7+
 
.LINK
https://github.com/junecastillote/PasswordGen.PS
 
#>

    [CmdletBinding()]
    [OutputType(
        [string],
        [System.Security.SecureString],
        [System.Management.Automation.PSCustomObject]
    )]
    param (
        [Parameter()]
        [ValidateRange(2, 50)]
        [int]$WordCount = 4,

        [Parameter()]
        [AllowEmptyString()]
        [string]$Separator = '-',

        [Parameter()]
        [switch]$CapitalizeWords,

        [Parameter()]
        [switch]$AddNumber,

        [Parameter()]
        [ValidateRange(1, 32)]
        [int]$NumberLength = 2,

        [Parameter()]
        [switch]$CopyToClipboard,

        [Parameter()]
        [switch]$AsSecureString,

        [Parameter()]
        [switch]$PassThruObject
    )

    begin {
        if ($AsSecureString -and $PassThruObject) {
            throw 'AsSecureString and PassThruObject cannot be used together.'
        }

        $words = $script:PGenWordList

        if ($null -eq $words -or $words.Count -eq 0) {

            throw (
                'The module word list was not loaded or has (0) usable words.'
            )
        }

        if ($words.Count -lt $WordCount) {
            throw (
                "The word list contains only $($words.Count) unique words, " +
                "but WordCount requires $WordCount unique words."
            )
        }

        $randomNumberGenerator =
        [System.Security.Cryptography.RandomNumberGenerator]::Create()

        function Get-CryptoRandomIndex {
            [CmdletBinding()]
            [OutputType([int])]
            param (
                [Parameter(Mandatory)]
                [ValidateRange(1, 2147483647)]
                [int]$UpperBound
            )

            if ($UpperBound -eq 1) {
                return 0
            }

            $byteBuffer = New-Object 'byte[]' 4

            $range =
            [uint64]1 + [uint64][uint32]::MaxValue

            $acceptedRange =
            $range - ($range % [uint64]$UpperBound)

            do {
                $randomNumberGenerator.GetBytes($byteBuffer)

                $randomValue = [uint64][BitConverter]::ToUInt32(
                    $byteBuffer,
                    0
                )
            }
            while ($randomValue -ge $acceptedRange)

            return $randomValue % [uint64]$UpperBound

        }

        function Get-CryptoRandomCharacter {
            [CmdletBinding()]
            [OutputType([char])]
            param (
                [Parameter(Mandatory)]
                [ValidateNotNullOrEmpty()]
                [string]$CharacterPool
            )

            $randomIndex = Get-CryptoRandomIndex `
                -UpperBound $CharacterPool.Length

            return $CharacterPool[$randomIndex]
        }
    }

    process {
        try {
            $availableWords =
            New-Object 'System.Collections.Generic.List[string]'

            foreach ($word in $words) {
                $availableWords.Add($word)
            }

            $selectedWords =
            New-Object 'System.Collections.Generic.List[string]'

            for (
                $wordIndex = 0
                $wordIndex -lt $WordCount
                $wordIndex++
            ) {
                $selectedIndex = Get-CryptoRandomIndex `
                    -UpperBound $availableWords.Count

                $selectedWord = $availableWords[$selectedIndex]

                if ($CapitalizeWords) {
                    $selectedWord = (
                        [System.Globalization.CultureInfo]::InvariantCulture
                    ).TextInfo.ToTitleCase(
                        $selectedWord.ToLowerInvariant()
                    )
                }

                $selectedWords.Add($selectedWord)

                # Remove the selected word so that words cannot repeat.
                $availableWords.RemoveAt($selectedIndex)
            }

            $passphraseParts =
            New-Object 'System.Collections.Generic.List[string]'

            foreach ($selectedWord in $selectedWords) {
                $passphraseParts.Add($selectedWord)
            }

            $numberValue = $null
            $numberWordIndex = $null

            if ($AddNumber) {
                $numberCharacters =
                New-Object 'System.Collections.Generic.List[char]'

                for (
                    $numberIndex = 0
                    $numberIndex -lt $NumberLength
                    $numberIndex++
                ) {
                    $randomNumberCharacter = Get-CryptoRandomCharacter `
                        -CharacterPool '0123456789'

                    $numberCharacters.Add($randomNumberCharacter)
                }

                $numberValue = -join $numberCharacters

                # Select one of the passphrase words and append the number.
                $numberWordIndex = Get-CryptoRandomIndex `
                    -UpperBound $passphraseParts.Count

                $passphraseParts[$numberWordIndex] = (
                    $passphraseParts[$numberWordIndex] +
                    $numberValue
                )
            }

            $passphrase = $passphraseParts -join $Separator

            if ($CopyToClipboard) {
                $setClipboardCommand = Get-Command `
                    -Name 'Set-Clipboard' `
                    -ErrorAction SilentlyContinue

                if (-not $setClipboardCommand) {
                    throw @'
CopyToClipboard was specified, but Set-Clipboard is not available in the
current PowerShell session.
'@

                }

                Set-Clipboard `
                    -Value $passphrase `
                    -ErrorAction Stop
            }

            if ($AsSecureString) {
                return ConvertTo-SecureString `
                    -String $passphrase `
                    -AsPlainText `
                    -Force
            }

            if ($PassThruObject) {
                $wordEntropyBits = (
                    $WordCount *
                    [System.Math]::Log($words.Count, 2)
                )

                $numberEntropyBits = 0

                if ($AddNumber) {
                    $numberEntropyBits = (
                        $NumberLength *
                        [System.Math]::Log(10, 2)
                    )
                }

                $estimatedEntropyBits = [System.Math]::Round(
                    (
                        $wordEntropyBits +
                        $numberEntropyBits
                    ),
                    2
                )

                $effectiveNumberLength = 0

                if ($AddNumber) {
                    $effectiveNumberLength = $NumberLength
                }

                $numberWordPosition = $null

                if ($AddNumber) {
                    $numberWordPosition = $numberWordIndex + 1
                }

                return [pscustomobject][ordered]@{
                    Passphrase           = $passphrase
                    Length               = $passphrase.Length
                    WordCount            = $selectedWords.Count
                    Words                = $selectedWords.ToArray()
                    PassphraseParts      = $passphraseParts.ToArray()
                    Separator            = $Separator
                    WordsCapitalized     = [bool]$CapitalizeWords
                    NumberAdded          = [bool]$AddNumber
                    NumberLength         = $effectiveNumberLength
                    NumberValue          = $numberValue
                    NumberWordIndex      = $numberWordIndex
                    NumberWordPosition   = $numberWordPosition
                    WordListSize         = $words.Count
                    WordListSource       = 'EFF Large Word List'
                    EstimatedEntropyBits = $estimatedEntropyBits
                    CopiedToClipboard    = [bool]$CopyToClipboard
                }
            }

            return $passphrase
        }
        finally {
            $passphrase = $null
            $numberValue = $null
        }
    }

    end {
        if ($null -ne $randomNumberGenerator) {
            $randomNumberGenerator.Dispose()
        }
    }
}