Private/20.Keytab.IO.ps1

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



#region Keytab Writer / Parser
# ---------------------------------------------------------------------- #
#
# Keytab Writer / Parser
#
# ---------------------------------------------------------------------- #

function Write-UInt16BE([IO.BinaryWriter]$binaryWriter,[int]$Value) {
    [byte[]]$b = [BitConverter]::GetBytes([uint16]$Value)
    if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($b) }
    $binaryWriter.Write($b,0,2)
}

function Write-UInt32BE([IO.BinaryWriter]$binaryWriter,[System.UInt32]$Value) {
    [byte[]]$b = [BitConverter]::GetBytes([uint32]$Value)
    if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($b) }
    $binaryWriter.Write($b,0,4)
}

function Write-Int32BE([IO.BinaryWriter]$BinaryWriter,[int]$Value) {
    Write-UInt32BE $BinaryWriter ([System.UInt32]$Value)
}

function ReadUInt16([byte[]]$bytes,[ref]$i) {
    $idx = [int]$i.Value
    [byte[]]$b = $bytes[$idx..($idx+1)]
    if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($b) }
    $i.Value = $idx + 2
    return [uint16]([BitConverter]::ToUInt16($b,0))
}

function ReadUInt32([byte[]]$bytes,[ref]$i) {
    $idx = [int]$i.Value
    [byte[]]$b = $bytes[$idx..($idx+3)]
    if ([BitConverter]::IsLittleEndian) { [Array]::Reverse($b) }
    $i.Value = $idx + 4
    return [uint32]([BitConverter]::ToUInt32($b,0))
}


function New-KeytabEntry {
    <#
        .SYNOPSIS
        Builds a single MIT keytab entry bytes for a principal/etype/kvno.
 
        .DESCRIPTION
        Encodes realm, components, name type, timestamp, kvno (8/32-bit), and key material
        into the on-disk keytab entry format. Used internally by New-KeytabFile.
    #>

    param(
        [Parameter(Mandatory)][object]$PrincipalDescriptor,
        [Parameter(Mandatory)][int]$EncryptionType,
        [Parameter(Mandatory)][byte[]]$Key,
        [Parameter(Mandatory)][int]$Kvno,
        [int]$Timestamp = [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
    )

    $enc = [Text.Encoding]::ASCII
    $realmBytes = $enc.GetBytes($PrincipalDescriptor.Realm)

    $memStream    = New-Object IO.MemoryStream # write in memory first, then to disk when done
    $binaryWriter = New-Object IO.BinaryWriter($memStream)

    Write-UInt16BE $binaryWriter $PrincipalDescriptor.Components.Count
    # realm
    Write-UInt16BE $binaryWriter $realmBytes.Length; $BinaryWriter.Write($realmBytes)
    #components
    foreach ($comp in $PrincipalDescriptor.Components) {
        [byte[]]$compBytes = $enc.GetBytes($comp)
        Write-UInt16BE $binaryWriter $compBytes.Length
        $binaryWriter.Write($compBytes)
    }

    Write-UInt32BE $binaryWriter ([uint32]$PrincipalDescriptor.NameType)
    Write-UInt32BE $binaryWriter ([uint32]$Timestamp)
    $binaryWriter.Write([byte]($Kvno -band 0xFF))
    Write-UInt16BE $binaryWriter $EncryptionType
    Write-UInt16BE $binaryWriter $Key.Length
    $binaryWriter.Write($Key)
    # 32 bit kvno extension
    Write-UInt32BE $binaryWriter ([uint32]$Kvno)

    $binaryWriter.Flush()
    $memStream.ToArray()
}


function New-KeytabFile {
    <#
        .SYNOPSIS
        Writes a complete MIT keytab file (v0x0502) from principal descriptors and key sets.
 
        .DESCRIPTION
        Iterates key sets (by KVNO) and encryption types, generating entries for each principal.
        Supports deterministic timestamps via -FixedTimestampUtc and optionally restricts ACLs
        to the current user.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][object[]]$PrincipalDescriptors,
        [Parameter(Mandatory)][object[]]$KeySets, # list of {Kvno: Keys}
        [int[]]$EtypeFilter,
        [switch]$RestrictAcl,
        [datetime]$FixedTimestampUtc
    )

    $full = if ([IO.Path]::IsPathRooted($Path)) { [IO.Path]::GetFullPath($Path) } else { [IO.Path]::GetFullPath((Join-Path (Get-Location) $Path)) }
    $tmp = "$full.tmp"
    if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force }

    $memStream = New-Object IO.MemoryStream
    $binaryWriter = New-Object IO.BinaryWriter($memStream)
    $entryCount = 0

    try {
        # keytab v2 header
        $binaryWriter.Write([byte]0x05)
        $binaryWriter.Write([byte]0x02)
        $tsArg = @{}
        $timestampSec =
        if ($PSBoundParameters.ContainsKey('FixedTimestampUtc') -and $FixedTimestampUtc) {
            [int][DateTimeOffset]::new(($FixedTimestampUtc.ToUniversalTime())).ToUnixTimeSeconds()
        } else {
            [int][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
        }

        if ($PSBoundParameters.ContainsKey('FixedTimestampUtc') -and $FixedTimestampUtc) {
        $tsArg = @{ Timestamp = $timestampSec }
        }

        foreach ($keySet in $KeySets | Sort-Object Kvno) {
            foreach ($etype in ($keySet.Keys.Keys | Sort-Object)) {
                if ($EtypeFilter -and ($EtypeFilter -notcontains $etype)) { continue }
                [byte[]]$bytes = $keySet.Keys[$etype]
                foreach ($pd in $PrincipalDescriptors) {
                    [byte[]]$entryBytes = New-KeytabEntry -PrincipalDescriptor $pd -EncryptionType $etype -Key $bytes -Kvno $keySet.Kvno @tsArg
                    Write-Int32BE $binaryWriter $entryBytes.Length
                    $binaryWriter.Write($entryBytes, 0, $entryBytes.Length)
                    $entryCount++
                }
            }
        }

        $binaryWriter.Flush();
        Write-Verbose "[Keytab] entries=$entryCount length=$($memStream.Length) file=$tmp"
    } finally {
        $binaryWriter.Dispose()
    }

    [IO.File]::WriteAllBytes($tmp, $memStream.ToArray())
    Move-Item -LiteralPath $tmp -Destination $full -Force
    if ($RestrictAcl) { $null = Set-UserOnlyAcl -Path $full }
    $full
}
#endregion