Public/New-sqmRandomSaPassword.ps1

<#
.SYNOPSIS
    Generiert ein zufaelliges, richtlinienkonformes SA-Passwort.

.DESCRIPTION
    Erstellt ein kryptografisch sicheres Passwort das die SQL Server
    Passwort-Richtlinie erfuellt:
        - Mindestlaenge konfigurierbar (Standard: 20 Zeichen)
        - Mindestens 1 Grossbuchstabe (A-Z)
        - Mindestens 1 Kleinbuchstabe (a-z)
        - Mindestens 1 Ziffer (0-9)
        - Mindestens 1 Sonderzeichen aus dem definierten Set

    Optionaler Datei-Export: Passwort wird DPAPI-verschluesselt in eine
    Textdatei geschrieben (ConvertFrom-SecureString).
    Nur der Benutzer/Computer der exportiert hat kann die Datei wieder lesen.

.PARAMETER Length
    Laenge des Passworts. Standard: 20. Minimum: 12.

.PARAMETER ExportPath
    Optionaler Pfad fuer die verschluesselte Passwort-Datei.
    Z.B.: C:\System\Passwords\sa_password.txt
    Wenn leer: kein Export.

.OUTPUTS
    [SecureString] - Das generierte Passwort als SecureString.
    (Klartext wird nicht ausgegeben - nur auf expliziten Wunsch via [PSCredential].)

.EXAMPLE
    $pwd = New-sqmRandomSaPassword
    # Gibt SecureString zurueck

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

.EXAMPLE
    # Klartext anzeigen (nur fuer Debugging):
    $pwd = New-sqmRandomSaPassword
    $cred = New-Object PSCredential('sa', $pwd)
    $cred.GetNetworkCredential().Password

.NOTES
    DPAPI-Export: Die Datei kann nur vom selben Windows-Benutzerprofil
    auf demselben Computer entschluesselt werden.
    Fuer produktive Umgebungen: Key Vault oder CyberArk verwenden.
#>

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() }
    }
}