Private/New-Fido2RandomPin.ps1

    <#
    .SYNOPSIS
    Generates a random FIDO2-compatible PIN (ASCII letters and digits, or digits only with -Numeric).
 
    .DESCRIPTION
    Produces a PIN suitable for YubiKey FIDO2: NFC Form C normalized, UTF-8 length at most 63 bytes
    (ASCII alphanumeric uses one byte per character). See Yubico FIDO2 PIN documentation for more details.
     
    Rejects PINs that are a single repeated character or that match a small blocklist of weak values
    in accordance with Yubico's FIDO2 PIN complexity requirements (see links below).
 
    .PARAMETER PinLength
    Number of characters. Must be between 4 and 63 inclusive.
 
    .PARAMETER Numeric
    If set, the PIN uses only ASCII digits (0-9). Still subject to weak-PIN rejection. Cannot be combined with -EnforceCharacterDiversity.
 
    .PARAMETER EnforceCharacterDiversity
    Require at least one uppercase, one lowercase, and one digit. Cannot be used with -Numeric.
 
    .OUTPUTS
    System.String
 
    .NOTES
    Internal helper; not exported from the module.
 
    .LINK
    Yubico FIDO2 PIN requirements:
    https://docs.yubico.com/yesdk/users-manual/application-fido2/fido2-pin.html
 
    .LINK
    YubiKey firmware PIN complexity rules:
    https://docs.yubico.com/hardware/yubikey/yk-tech-manual/5.7-firmware-specifics.html#pin-complexity
    #>


function New-Fido2RandomPin {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateRange(4, 63)]
        [int]$PinLength,

        [Parameter()]
        [switch]$Numeric,

        [Parameter()]
        [switch]$EnforceCharacterDiversity
    )

    if ($Numeric -and $EnforceCharacterDiversity) {
        throw "Cannot use -EnforceCharacterDiversity with -Numeric (digit-only PINs have no letter case)."
    }

    # Initialize blocklist once per session (case-insensitive for alphabetic entries)
    if (-not $script:Fido2BlockedPins) {
        $script:Fido2BlockedPins = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($entry in @(
            '123456', '123123', '654321', '123321', '112233', '121212', '123456789',
            'password', 'qwerty', '12345678', '1234567', '520520', '123654', '1234567890',
            '159753', 'qwerty123', 'abc123', 'password1', 'iloveyou', '1q2w3e4r'
        )) {
            [void]$script:Fido2BlockedPins.Add($entry)
        }
    }

    # ASCII digits only, or digits + uppercase + lowercase
    $alphabet = if ($Numeric) { '0123456789' } else { '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' }
    $alphabetLen = $alphabet.Length

    $attempt = 0
    $maxAttempts = 1000

    # Build random candidates until one satisfies checks below
    do {
        $attempt++

        if ($attempt -gt $maxAttempts) {
            throw "Failed to generate a valid FIDO2 PIN after $maxAttempts attempts."
        }

        # Generate PIN using cryptographically secure RNG
        $chars = [char[]]::new($PinLength)
        for ($i = 0; $i -lt $PinLength; $i++) {
            $chars[$i] = $alphabet[[System.Security.Cryptography.RandomNumberGenerator]::GetInt32($alphabetLen)]
        }

        $pin = -join $chars

        # NFC normalization (redundant for ASCII, kept for future-proofing)
        $pinNfc = $pin.Normalize([System.Text.NormalizationForm]::FormC)

        # Reject if normalization changed length (should never happen with ASCII)
        if ($pinNfc.Length -ne $PinLength) { continue }

        # Reject if exceeds 63 bytes UTF-8 (also impossible with ASCII but kept for correctness)
        if ([System.Text.Encoding]::UTF8.GetByteCount($pinNfc) -gt 63) { continue }

        # Reject repeated single character (e.g., "AAAAAA", "111111")
        if ($pinNfc -match '^(.)\1+$') { continue }

        # Reject known weak PINs
        if ($script:Fido2BlockedPins.Contains($pinNfc)) { continue }

        # Optional: enforce character diversity
        if ($EnforceCharacterDiversity) {
            $hasDigit = $pinNfc -match '\d'
            $hasLower = $pinNfc -cmatch '[a-z]'
            $hasUpper = $pinNfc -cmatch '[A-Z]'

            if (-not ($hasDigit -and $hasLower -and $hasUpper)) { continue }
        }

        return $pinNfc
    } while ($true)
}