This must be freshman programming again.  I’m redoing CredLocker and want the way to parametrically generate passwords with minimum character set counts.

This is also a good example of New-Object System.Random and [char[]]”Casting a string to char array”, as well as an interesting possible coding interview question:

For a given number that is greater than 0, return 0..<number> in random order.



function New-Password
{
    #region header

    <#
    .synopsis
    Yet another password generator
    
    .description
    For given sets of characters (upper, lower, digits, and symbols) generate string with at least that many of each set, then randomize string.
    
    .parameter Length
    Password length in characters.  Defaults to 16. If parameter value is less than 8, it will be set to 8.

    .parameter Symbol
    Minimum symbol count.  Defaults to 3. If parameter value is less than zero, it will be set to zero. If parameter value is zero, none of this character set will be in the password.
    
    .parameter Capital
    Minimum capital letter count.  Defaults to 3. If parameter value is less than zero, it will be set to zero. If parameter value is zero, none of this character set will be in the password.
    
    .parameter Digit
    Minimum digit count.  Defaults to 3. If parameter value is less than zero, it will be set to zero. If parameter value is zero, none of this character set will be in the password.
    
    .parameter Lower
    Minimum lowercase letter count.  Defaults to 3. If parameter value is less than zero, it will be set to zero. If parameter value is zero, none of this character set will be in the password.
    
    .notes
    Written for Credlocker to generate passwords.
    
    Three-phase approach.
    
    First phase ensures minimums for each character set is satisfied, set by set.   The buffer has the character sets in order.
    
    Second phase fills the buffer with characters from any character set that is not explictly excluded (parameter value set to 0).
    
    Third phase randomizes the buffer.
    
    #>

    param (
        [int]$Length = 16,
        [int]$Symbol = 3,
        [int]$Capital = 3,
        [int]$Digit = 3,
        [int]$Lower = 3
    );

    #endregion
    #region functions

    function Get-RandomCharFromSet
    {
        <#
        .synopsis
        Return <n> random characters from a given set.
        
        .parameter CharSet
        Set of characters from which to randomly select <n> characters.
        
        .parameter Count
        Number of characters to select.
        
        .param Noops
        Maximum number of random numbers to 'burn' prior to each selection.
        
        .notes
        Prior to each selection, function 'burns' a random number of random numbers from the randomizer.
        
        .outputs
        [char] array.

        #>

        param (
            [char[]]$CharSet,
            [int]$Count = 3,
            [int]$Noops = 16
        )

        if (($Count -gt 0) -and $CharSet)
        {
            $randomChar = New-Object System.Random;
            $randomLoop = New-Object System.Random;

            if ($Noops -lt 1) { $Noops = 1; }
            
            0 .. ($count - 1) |
            % {
                # attempt to initialize random number generator each time
                0 .. $randomLoop.Next(0, $Noops) | %{ $randomChar.Next(0, $CharSet.Count) | Out-Null; }

                $charSet[$randomChar.Next(0, $CharSet.Count)];
            } # 0 .. ($count - 1) |

        } # if ($Count)

    } # function Get-RandomCharFromSet

    function Get-RandomizedNumberSequence
    {
        <#
        .synopsis
        Return 0 .. <n> in random order.
        
        .parameter Count
        Number of numbers to return.
        
        .param Noops
        Maximum number of random numbers to 'burn' prior to each selection.
        
        .notes
        Prior to each selection, function 'burns' a random number of random numbers from the randomizer.
        
        .outputs
        [char] array.
        
        #>

        param (
            [int]$Count = 16,
            [int]$Noops = 16
        )

        if ($Count -gt 0)
        {
            $hash = @{};
            $i = 0;

            $random     = New-Object System.Random;
            $randomLoop = New-Object System.Random;

            if ($Noops -lt 1) { $Noops = 1; }
            
            while ($hash.Keys.Count -lt $Count)
            {
                # attempt to initialize random number generator each time
                0 .. $randomLoop.Next(0, $Noops) | %{ $random.Next(0, $Count) | Out-Null; }

                $number = $random.Next(0,$count);

                # if we haven't hit this number yet, add it to the hash and increment $i
                if (!$hash.ContainsKey($number))
                {
                    $hash.$number = $i;
                    $i += 1;

                } # if (!$hash.ContainsKey($number))

            } # while ($hash.Keys.Count -lt $count)

            # dump the keys in sorted order which means $i is randomized
            $hash.Keys | Sort-Object | % { $hash.$_; }

        } # if ($count)

    } # function Get-RandomizedNumberSequence

    $noops = 16;

    #endregion
    #region parameter verification

    # -Length value is at least 8
    if ($Length -lt 8)
    {
        Write-Warning "$($MyInvocation.MyCommand.Name) -Length $Length will be set to 8.";
        $Length = 8;

    } # if ($Length -lt 8)
    
    # -Symbol, -Capital, -Digit, and -Lower values are 0 or greater
    ('Symbol', 'Capital', 'Digit', 'Lower') |
    % {
        if ((Get-Variable -Name $_ -ValueOnly) -lt 0)
        {
            Write-Warning "$($MyInvocation.MyCommand.Name) -$_ $(Get-Variable -Name $_ -ValueOnly) will be set to 0."
            Set-Variable -Name $_ -Value 0;

        } # if ((Get-Variable -Name $_ -ValueOnly) -lt 0)

    } # ('Symbol', 'Capital', 'Digit', 'Lower') |

    $sum = $Symbol + $Capital + $Digit + $Lower;

    # At least one set of characters is permitted
    if (!$sum)
    {
        Write-Warning "$($MyInvocation.MyCommand.Name) -Symbol $Symbol -Captial $Capital -Digit $Digit -Lower $Lower has no characters to choose from.";
        return;

    } # if (!$sum)

    # -Symbol, -Capital, -Digit, and -Lower values exceed -Length
    if ($sum -gt $Length)
    {
        Write-Warning "$($MyInvocation.MyCommand.Name) -Symbol $Symbol -Captial $Capital -Digit $Digit -Lower $Lower summed is longer than -Length $Length.  -Length will be set to $sum."
        $Length = $sum;

    } # if ($sum -gt $Length)

    #endregion
    #region set up
    
    # define individual character sets.  Note that $symbolChars does not include the space character.
    [string]$symbolChars  = '''``~!@#$%^&*()_-+={[}]|\:;"<,>.?/';
    [string]$capitalChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    [string]$digitChars   = '01234567890';
    [string]$lowerChars   = 'abcdefghijklmnopqrstuvwxyz'

    # handle the difference between the required password length and sum of minimum of each character set
    [string]$otherChars = '';
    if ($Symbol ) { $otherChars += $symbolChars;  }
    if ($Capital) { $otherChars += $capitalChars; }
    if ($Digit  ) { $otherChars += $digitChars;   }
    if ($Lower  ) { $otherChars += $lowerChars;   }

    #endregion
    #region generate the password

    [char[]]$buffer = @();

    # get the minimum count of each character set.  however, $buffer is ordered by set.
    $buffer += Get-RandomCharFromSet -CharSet $symbolChars  -Noops $noops -Count $Symbol;
    $buffer += Get-RandomCharFromSet -CharSet $capitalChars -Noops $noops -Count $Capital;
    $buffer += Get-RandomCharFromSet -CharSet $digitChars   -Noops $noops -Count $Digit;
    $buffer += Get-RandomCharFromSet -CharSet $lowerChars   -Noops $noops -Count $Lower;
    $buffer += Get-RandomCharFromSet -CharSet $otherChars   -Noops $noops -Count ($Length - $sum);

    # jumble the characters, return as string.
    [string]::Join($null,(& {
            Get-RandomizedNumberSequence -Count $Length -Noops $noops | % { $buffer[$_]; }
        }
    ));

    #endregion
    
} # function New-Password