Private/21.Crypto.S2K.ps1

<#
SPDX-License-Identifier: Apache-2.0
Copyright (c) 2025 Stefan Ploch
#>



#region AES S2K for MIT/Heimdal/Windows salt policies
# ---------------------------------------------------------------------- #
# AES S2K for MIT/Heimdal/Windows salt policies
# using custom PDKDF2-HMACSHA1.
#
# ---------------------------------------------------------------------- #

function Normalize-PrincipalForSalt {
    <#
        .SYNOPSIS
        Normalize a principal descriptor for use as a salt in key derivation.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('MIT','Heimdal','Windows')] [string]$Compatibility,
        [Parameter(Mandatory)][object]$PrincipalDescriptor # { Components, Realm, NameType }
    )

    $realm = if ($Compatibility -eq 'Windows') {
        $PrincipalDescriptor.Realm.ToUpperInvariant()
    } else {
        $PrincipalDescriptor.Realm
    }
    $components = [string[]]$PrincipalDescriptor.Components.Clone()

    # KRB_NT_SRV_HST (3): lowercase service and host for Windows flavor
    if ($Compatibility -eq 'Windows' -and $PrincipalDescriptor.NameType -eq 3) {
        if ($components.Count -ge 1) { $components[0] = $components[0].ToLowerInvariant() }
        if ($components.Count -ge 2) { $components[1] = $components[1].ToLowerInvariant() }
    }

    [pscustomobject]@{
        Components = $components
        Realm      = $realm
        NameType   = $PrincipalDescriptor.NameType
    }
}

function Get-DefaultSalt {
    <#
    .SYNOPSIS
    Get the default salt for a given principal descriptor and compatibility.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Mit','Heimdal','Windows')] [string]$Compatibility,
        [Parameter(Mandatory)][object]$PrincipalDescriptor
    )

    $principal = Normalize-PrincipalForSalt -Compatibility $Compatibility -PrincipalDescriptor $PrincipalDescriptor
    $saltStr = $principal.Realm + ($principal.Components -join '')
    [Text.Encoding]::UTF8.GetBytes($saltStr)
}

function ConvertTo-BigEndianUint32Bytes {
    <#
    .SYNOPSIS
    Convert a 32-bit unsigned integer to a big-endian byte array.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][uint32]$Value
    )

    $bytes = [BitConverter]::GetBytes([uint32]$Value)
    if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes) }
    $bytes
}

function Invoke-PBKDF2Hmac {
    <#
    .SYNOPSIS
    Invoke PBKDF2-HMAC-SHA1 key derivation.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][byte[]]$PasswordBytes,
        [Parameter(Mandatory)][byte[]]$SaltBytes,
        [Parameter(Mandatory)][int]$Iterations,
        [Parameter(Mandatory)][int]$DerivedKeyLength,
        [Parameter(Mandatory)][System.Security.Cryptography.HMAC]$HmacAlgorithm
    )

    if ($Iterations -lt 1) { throw "PDKDF2 iterations must be >= 1" }

    # Set HMAC key from password bytes
    $HmacAlgorithm.Key = $PasswordBytes

    $hashlength     = $HmacAlgorithm.HashSize / 8 # bits to bytes
    $blocks         = [math]::Ceiling($DerivedKeyLength / [double]$hashlength)
    $derivedKey     = New-Object byte[]($DerivedKeyLength)
    $offset         = 0

    try {
        for ($i = 1; $i -le $blocks; $i++) {                                # for every block
            $iterBytes = ConvertTo-BigEndianUint32Bytes -Value $i           # Block index (1-based)
            $msg = New-Object byte[] ($SaltBytes.Length +4)                 # Salt + Block index
            [Array]::Copy($SaltBytes, 0, $msg, 0, $SaltBytes.Length)        # Copy SaltBytes to <msg>
            [Array]::Copy($iterBytes, 0, $msg, $SaltBytes.Length, 4)        # Copy Block index to <msg>

            $curIter = $HmacAlgorithm.ComputeHash($msg)                     # Compute initial hash
            $curIterHash = [byte[]]$curIter.Clone()                         # Clone initial hash

            for ($j = 2; $j -le $Iterations; $j++) {                        # for each iteration
                $curIter = $HmacAlgorithm.ComputeHash($curIter)             # Compute subsequent hash
                for ($k = 0; $k -lt $curIterHash.Length; $k++) {            # for each byte in the hash
                    $curIterHash[$k] = $curIterHash[$k] -bxor $curIter[$k]  # XOR with current iteration
                }
            }

            $cpyBytes = [Math]::Min($hashLength, $DerivedKeyLength - $offset)   # copy bytes of current block
            [Array]::Copy($curIterHash, 0, $derivedKey, $offset, $cpyBytes)     # to <derivedKey>
            $offset += $cpyBytes

            [Array]::Clear($msg, 0, $msg.Length)                                # Clear message buffer
            [Array]::Clear($iterBytes, 0, $iterBytes.Length)                    # Clear iteration bytes
            [Array]::Clear($curIterHash, 0, $curIterHash.Length)                # Clear current iteration hash
            [Array]::Clear($curIter, 0, $curIter.Length)                        # Clear current iteration
        }
    } finally { }
    $derivedKey
}

function ConvertFrom-SecureStringToPlain {
    <#
    .SYNOPSIS
    Convert a SecureString to a plain text string.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][SecureString]$Secure
    )

    $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure)
    try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) }
    finally { if ($ptr -ne [intPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } }
}

function Derive-AesKeyWithPbkdf2 {
    <#
    .SYNOPSIS
    Derive an AES key using PBKDF2 with HMAC-SHA1.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][int]$Etype,
        [Parameter(Mandatory)][string]$PasswordPlain,
        [Parameter(Mandatory)][byte[]]$SaltBytes,
        [int]$Iterations
    )

    switch ($Etype) {
        17 { # AES-128_CTS_HMAC_SHA1_96
            $keyLength = 16
            $hmac = [System.Security.Cryptography.HMACSHA1]::New()
            if (-not $Iterations) { $Iterations = 4096 } # RFC 2898
        }
        18 { # AES-256_CTS_HMAC_SHA1_96
            $keyLength = 32
            $hmac = [System.Security.Cryptography.HMACSHA1]::New()
            if (-not $Iterations) { $Iterations = 4096 } # RFC 2898
        }
        19 { # AES-128_CTS_HMAC_SHA256_128
            $keyLength = 16
            $hmac = [System.Security.Cryptography.HMACSHA256]::New()
            if (-not $Iterations) { $Iterations = 32768 } # RFC 8009
        }
        20 { # AES-256_CTS_HMAC_SHA384_192
            $keyLength = 32
            $hmac = [System.Security.Cryptography.HMACSHA384]::New()
            if (-not $Iterations) { $Iterations = 32768 } # RFC 8009
        }
        default { throw "Unsupported Etype: $Etype" }
    }

    $passBytes = [Text.Encoding]::UTF8.GetBytes($PasswordPlain)
    $saltLocal = [byte[]]$SaltBytes.Clone()

    try {
        return Invoke-PBKDF2Hmac -PasswordBytes $passBytes -SaltBytes $saltLocal -Iterations $Iterations -DerivedKeyLength $keyLength -HmacAlgorithm $hmac
    }
    finally {
        if ($hmac) { $hmac.Dispose() }
        if ($saltLocal) { [Array]::Clear($saltLocal, 0, $saltLocal.Length) }
        if ($passBytes) { [Array]::Clear($passBytes, 0, $passBytes.Length)}
    }
}
#endregion