Public/Read-Keytab.ps1
|
<#
SPDX-License-Identifier: Apache-2.0 Copyright (c) 2025 Stefan Ploch #> function Read-Keytab { <# .SYNOPSIS Parse a keytab file into structured entries. .DESCRIPTION Robust parser for MIT keytab v0x0502. Can reveal raw key bytes for merge scenarios (sensitive). Returns entry objects with properties: Realm, Components, NameType, TimestampUtc, Kvno, EtypeId, EtypeName, KeyLength, Key (masked by default), and RawKey (when -RevealKeys is used). .PARAMETER Path Path to the keytab file (Pos 1). .PARAMETER RevealKeys Include raw key bytes in each entry's RawKey property (sensitive). .PARAMETER MaxKeyHex Max length of the displayed hex string for masked key preview. .INPUTS System.String (file path) or objects with FilePath/FullName properties. .OUTPUTS System.Object[]. Array of parsed entry objects. .EXAMPLE Read-Keytab -Path .\user.keytab Parse a keytab and return entries. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position=0)] [Alias('FullName','PSPath','FilePath')] [ValidateNotNullOrEmpty()] [string]$Path, [switch]$RevealKeys, [int]$MaxKeyHex = 64 ) begin { if ($RevealKeys) { Write-Warning 'RevealKeys is sensitive: raw key bytes will be returned in entries.' } $in = Resolve-PathUniversal -Path $Path -Purpose Input } process { $bytes = [IO.File]::ReadAllBytes($in) if ($bytes.Length -lt 2 -or $bytes[0] -ne 0x05 -or $bytes[1] -ne 0x02) { throw "'$in' is not a valid keytab (expected 0x05 0x02 at start)" } $pos = 2 $list = New-Object System.Collections.Generic.List[object] while ($pos -lt $bytes.Length) { if ($pos + 4 -gt $bytes.Length) { throw "Unexpected end of file at position $pos in '$in'" } $entryLength = [int]( ($bytes[$pos] -shl 24) -bor ($bytes[$pos+1] -shl 16) -bor ($bytes[$pos+2] -shl 8) -bor ($bytes[$pos+3]) ) $pos += 4 if ($entryLength -lt 0) { $entryLength = -$entryLength } # MIT quirk if ($entryLength -lt 0) { throw "Invalid entry length at position $pos in '$Path'" } if ($pos + $entryLength -gt $bytes.Length) { $misMatchLength = $entryLength - ($bytes.Length - $pos) throw "Declared entry exceeds file length by $misMatchLength bytes at position $pos in '$Path'" } $entry = $bytes[$pos..($pos+$entryLength-1)] $pos += $entryLength # Use a single cursor ($iref.Value) for all reads to avoid desync $index = 0 $iref = [ref]$index # compCount, realm length, realm $compCount = ReadUInt16 $entry $iref $realmLength = ReadUInt16 $entry $iref $idx = [int]$iref.Value [byte[]]$realmBytesSlice = $entry[$idx..($idx+$realmLength-1)] $realm = [Text.Encoding]::ASCII.GetString($realmBytesSlice) $iref.Value = $idx + $realmLength # components (repeat compCount times) $components = @() for ($c = 0; $c -lt $compCount; $c++) { $len = ReadUInt16 $entry $iref $idx = [int]$iref.Value [byte[]]$compSlice = $entry[$idx..($idx+$len-1)] $components += ,([Text.Encoding]::ASCII.GetString($compSlice)) $iref.Value = $idx + $len } # nameType, timestamp $nameType = ReadUInt32 $entry $iref $timestamp = ReadUInt32 $entry $iref # kvno8 (1 byte) $idx = [int]$iref.Value $kvno8 = [int]$entry[$idx] $idx++ $iref.Value = $idx # etype (UInt16), keyLength (UInt16), key bytes $etype = ReadUInt16 $entry $iref $keyLength = ReadUInt16 $entry $iref $idx = [int]$iref.Value $keyBytes = $entry[$idx..($idx+$keyLength-1)] $iref.Value = $idx + $keyLength # optional kvno32 (UInt32); prefer over kvno8 if present and non-zero $kvno32 = $null if ($iref.Value + 4 -le $entry.Length) { $kvno32 = ReadUInt32 $entry $iref } $kvnoEffective = if ($kvno32 -and $kvno32 -ne 0) { [int]$kvno32 } else { $kvno8 } # build display values and add entry $keyHexFull = ($keyBytes | ForEach-Object { $_.ToString('x2') }) -join '' $keyDisplay = if ($RevealKeys) { if ($keyHexFull.Length -gt $MaxKeyHex) { $keyHexFull.Substring(0,$MaxKeyHex) + '...' } else { $keyHexFull } } else { if ($keyHexFull.Length -le 16) { $keyHexFull } else { $keyHexFull.Substring(0,8) + '...' + $keyHexFull.Substring($keyHexFull.Length-8,8) } } $rawKey = if ($RevealKeys) { $keyBytes } else { $null } $list.Add([pscustomobject]@{ Realm = $realm Components = $components NameType = [int]$nameType TimestampUtc = ([DateTimeOffset]::FromUnixTimeSeconds([int]$timestamp).UtcDateTime) Kvno = $kvnoEffective EtypeId = $etype EtypeName = Get-EtypeNameFromId $etype KeyLength = $keyLength Key = $keyDisplay RawKey = $rawKey }) } } end { return $list } } |