Public/New-sqmRandomSaPassword.ps1

<#
.SYNOPSIS
    Generates a random, policy-compliant SA password.

.DESCRIPTION
    Creates a cryptographically secure password that meets the SQL Server
    password policy:
        - Minimum length configurable (default: 20 characters)
        - At least 1 uppercase letter (A-Z)
        - At least 1 lowercase letter (a-z)
        - At least 1 digit (0-9)
        - At least 1 special character from the defined set

    Optional file export: the password is written to a text file, DPAPI-
    encrypted (ConvertFrom-SecureString).
    Only the user/computer that exported it can read the file again.

.PARAMETER Length
    Length of the password. Default: 20. Minimum: 12.

.PARAMETER ExportPath
    Optional path for the encrypted password file.
    E.g.: C:\System\Passwords\sa_password.txt
    If empty: no export.

.OUTPUTS
    [SecureString] - The generated password as a SecureString.
    (Plain text is not output - only on explicit request via [PSCredential].)

.EXAMPLE
    $pwd = New-sqmRandomSaPassword
    # Returns a SecureString

.EXAMPLE
    $pwd = New-sqmRandomSaPassword -Length 24 -ExportPath 'C:\System\Passwords\sa.txt'
    # SecureString + DPAPI export to C:\System\Passwords\sa.txt

.EXAMPLE
    # Show plain text (debugging only):
    $pwd = New-sqmRandomSaPassword
    $cred = New-Object PSCredential('sa', $pwd)
    $cred.GetNetworkCredential().Password

.NOTES
    DPAPI export: the file can only be decrypted from the same Windows user
    profile on the same computer.
    For production environments: use Key Vault or CyberArk instead.
#>

function New-sqmRandomSaPassword
{
    [CmdletBinding()]
    [OutputType([System.Security.SecureString])]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateRange(12, 128)]
        [int]$Length = 20,

        [Parameter(Mandatory = $false)]
        [string]$ExportPath
    )

    $functionName = $MyInvocation.MyCommand.Name

    function _Log { param([string]$Msg, [string]$Level = 'INFO')
        Write-Verbose "[$functionName] $Msg"
        try { Invoke-sqmLogging -Message $Msg -FunctionName $functionName -Level $Level } catch { }
    }

    # --- Zeichenvorrat ---
    $charUpper   = 'ABCDEFGHJKLMNPQRSTUVWXYZ'    # ohne I und O (Verwechslungsgefahr)
    $charLower   = 'abcdefghjkmnpqrstuvwxyz'      # ohne i, l, o
    $charDigits  = '23456789'                      # ohne 0 und 1
    $charSpecial = '!@#$%^&*()-_=+[]{}|;:,.<>?'

    $allChars = ($charUpper + $charLower + $charDigits + $charSpecial).ToCharArray()

    # --- Kryptografisch sicherer Zufallsgenerator ---
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()

    function _GetRandomChar {
        param([char[]]$Pool)
        $bytes = New-Object byte[] 4
        $rng.GetBytes($bytes)
        $idx = [math]::Abs([BitConverter]::ToInt32($bytes, 0)) % $Pool.Length
        return $Pool[$idx]
    }

    try
    {
        # Garantiere Zeichenklassen-Anforderungen
        $pwChars = [System.Collections.Generic.List[char]]::new()
        $pwChars.Add((_GetRandomChar $charUpper.ToCharArray()))
        $pwChars.Add((_GetRandomChar $charLower.ToCharArray()))
        $pwChars.Add((_GetRandomChar $charDigits.ToCharArray()))
        $pwChars.Add((_GetRandomChar $charSpecial.ToCharArray()))

        # Restliche Zeichen aus dem vollen Pool
        for ($i = 4; $i -lt $Length; $i++)
        {
            $pwChars.Add((_GetRandomChar $allChars))
        }

        # Fisher-Yates-Shuffle fuer kryptografisch sichere Permutation
        for ($i = $pwChars.Count - 1; $i -gt 0; $i--)
        {
            $bytes2 = New-Object byte[] 4
            $rng.GetBytes($bytes2)
            $j = [math]::Abs([BitConverter]::ToInt32($bytes2, 0)) % ($i + 1)
            $tmp           = $pwChars[$i]
            $pwChars[$i]   = $pwChars[$j]
            $pwChars[$j]   = $tmp
        }

        $plainText  = -join $pwChars
        $secureString = ConvertTo-SecureString -String $plainText -AsPlainText -Force

        _Log "Passwort generiert (Laenge: $Length Zeichen)."

        # --- Optionaler DPAPI-Export ---
        if ($ExportPath -and $ExportPath -ne '')
        {
            $exportDir = Split-Path $ExportPath -Parent
            if ($exportDir -and -not (Test-Path $exportDir))
            {
                New-Item -ItemType Directory -Path $exportDir -Force | Out-Null
            }

            $encrypted = ConvertFrom-SecureString $secureString
            Set-Content -Path $ExportPath -Value $encrypted -Encoding UTF8 -Force
            _Log "Passwort DPAPI-verschluesselt exportiert: $ExportPath"
            Write-Verbose "SA-Passwort exportiert nach: $ExportPath"
        }

        # Klartext aus dem Speicher loeschen
        $plainText = $null
        [System.GC]::Collect()

        return $secureString
    }
    finally
    {
        if ($null -ne $rng) { $rng.Dispose() }
    }
}