Private/New-RotationPassword.ps1

function New-RotationPassword {
    <#
    .SYNOPSIS
        Generates a random password with a uniform character distribution.

    .DESCRIPTION
        Uses rejection sampling instead of a plain modulo over random bytes. A modulo
        maps 256 byte values onto an alphabet that does not divide 256 evenly, which
        makes the first few characters of the alphabet slightly more likely. The bias
        is small, but it is free to avoid and it is the first thing a reader checks.

        Complexity is satisfied by drawing at least one character from each required
        class and shuffling, rather than by inserting known characters at fixed
        positions.

        Excluded from the alphabet: quote, backslash, backtick, dollar and space.
        Those survive badly through ARM templates, JSON payloads and shells, and the
        VMAccess extension passes the value through several of them.

    .OUTPUTS
        System.Security.SecureString
    #>

    [CmdletBinding()]
    [OutputType([securestring])]
    param(
        [ValidateRange(12, 123)]
        [int]$Length = 24
    )

    $classes = @(
        'abcdefghijkmnopqrstuvwxyz'      # no l
        'ABCDEFGHJKLMNPQRSTUVWXYZ'       # no I, no O
        '23456789'                       # no 0, no 1
        '!#%&()*+,-./:;<=>?@[]^_{|}~'
    )
    $alphabet = -join $classes

    $chars = [System.Collections.Generic.List[char]]::new()

    # One character from each class guarantees complexity without a fixed position.
    foreach ($class in $classes) {
        $chars.Add((Get-UniformChar -Alphabet $class))
    }
    while ($chars.Count -lt $Length) {
        $chars.Add((Get-UniformChar -Alphabet $alphabet))
    }

    # Fisher-Yates with a cryptographic source, so the guaranteed characters do not
    # sit at predictable offsets.
    for ($i = $chars.Count - 1; $i -gt 0; $i--) {
        $j = [System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $i + 1)
        $tmp = $chars[$i]; $chars[$i] = $chars[$j]; $chars[$j] = $tmp
    }

    $secure = [securestring]::new()
    foreach ($c in $chars) { $secure.AppendChar($c) }
    $secure.MakeReadOnly()

    # Clear the plaintext characters we still hold.
    for ($i = 0; $i -lt $chars.Count; $i++) { $chars[$i] = [char]0 }
    $chars.Clear()

    return $secure
}

function Get-UniformChar {
    <#
    .SYNOPSIS
        Draws a single character from an alphabet without modulo bias.
    #>

    [CmdletBinding()]
    [OutputType([char])]
    param(
        [Parameter(Mandatory)]
        [string]$Alphabet
    )

    return $Alphabet[[System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $Alphabet.Length)]
}