Public/Unprotect-Keytab.ps1

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



function Unprotect-Keytab {
    <#
    .SYNOPSIS
    Decrypt a DPAPI-protected keytab file.
 
    .DESCRIPTION
    Uses DPAPI to decrypt a previously protected keytab file. Defaults output name by
    stripping the .dpapi suffix when present. If additional entropy was used during protection,
    the same Entropy value must be provided for decryption. Can restrict ACL on the output.
 
       .PARAMETER Path
        Path to the DPAPI-protected input file.
 
        .PARAMETER OutputPath
        Destination for the decrypted keytab. Defaults to removing the .dpapi extension when not specified.
 
        .PARAMETER Scope
        DPAPI scope used for decryption: CurrentUser (default) or LocalMachine.
 
        .PARAMETER Entropy
        Additional entropy string that was used during protection (if any).
 
        .PARAMETER Force
        Overwrite OutputPath if it exists.
 
        .PARAMETER RestrictAcl
        Apply a user-only ACL to the output file.
 
        .INPUTS
        System.String (file path) or objects with FilePath/FullName properties.
 
        .OUTPUTS
        System.String. Returns the OutputPath written.
 
        .EXAMPLE
        Unprotect-Keytab -Path .\user.keytab.dpapi -OutputPath .\user.keytab -Scope CurrentUser
        Decrypt a DPAPI-protected keytab into a plaintext keytab.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position=0)]
        [Alias('Path','In','FullName','FilePath')]
        [ValidateNotNullOrEmpty()]
        [string]$InputPath,

        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position=1)]
        [Alias('Out','Output', 'OutFile')]
        [string]$OutputPath,

        [ValidateSet('CurrentUser','LocalMachine')]
        [string]$Scope = 'CurrentUser',

        [string]$Entropy,
        [SecureString]$EntropySecure,
        [switch]$Force,
        [switch]$RestrictAcl
    )
    begin {
        $in = Resolve-PathUniversal -Path $InputPath -Purpose Input
        $base = [IO.Path]::GetFileName($in)
        if ($OutputPath) {
            $out = Resolve-PathUniversal -Path $OutputPath -Purpose Output
        } else {
            $baseName = if ($base -like '*.dpapi') { $base.Substring(0, $base.Length - 6) } else { $base }
            $out = Resolve-OutputPath -InputPath $in -BaseName $baseName -CreateDirectory
        }
        $plain = $null
    }
    process {
        if ($PSCmdlet.ShouldProcess($in, 'Unprotecting keytab (DPAPI)')) {
            if ((Test-Path -LiteralPath $out) -and -not $Force) {
                throw "Output file '$out' already exists. Use -Force to overwrite."
            }

            $bytes = [IO.File]::ReadAllBytes($in)
            $entropyBytes = $null
            try {
                if ($EntropySecure) {
                    $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($EntropySecure)
                    try {
                        $plainEntropy = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                        if ($plainEntropy) { $entropyBytes = [Text.Encoding]::UTF8.GetBytes($plainEntropy) }
                    } finally {
                        if ($bstr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
                    }
                } elseif ($Entropy) {
                    $entropyBytes = [Text.Encoding]::UTF8.GetBytes($Entropy)
                }
            } catch {
                Write-Warning ("Failed to materialize entropy: {0}" -f $_.Exception.Message)
            }
            $scopeEnum = if ($Scope -eq 'LocalMachine') {
                [System.Security.Cryptography.DataProtectionScope]::LocalMachine
            } else {
                [System.Security.Cryptography.DataProtectionScope]::CurrentUser
            }

            try {
                $plain = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $entropyBytes, $scopeEnum)
                [IO.File]::WriteAllBytes($out, $plain)
                if ($RestrictAcl) { Set-UserOnlyAcl -Path $out }
            } finally {
                if ($bytes) { [Array]::Clear($bytes, 0, $bytes.Length) }
                if ($plain) { [Array]::Clear($plain, 0, $plain.Length) }
                if ($entropyBytes) { [Array]::Clear($entropyBytes, 0, $entropyBytes.Length) }
            }
        }
    }
    end {
        return $out
    }
}