Public/Protect-Keytab.ps1
|
<#
SPDX-License-Identifier: Apache-2.0 Copyright (c) 2025 Stefan Ploch #> function Protect-Keytab { <# .SYNOPSIS Protect a keytab file at rest using Windows DPAPI. .DESCRIPTION Uses DPAPI (CurrentUser or LocalMachine scope) to encrypt a keytab file. Optional additional entropy can be provided. Can restrict ACL on the output and delete the plaintext original after successful protection. LocalMachine scope is not portable across machines. .PARAMETER Path Path to the plaintext keytab file to protect. .PARAMETER OutputPath Destination path for the protected file. Defaults to <Path>.dpapi when not specified. .PARAMETER Scope DPAPI scope: CurrentUser (default) or LocalMachine. LocalMachine scope binds decryption to the computer and is not portable. .PARAMETER Entropy Optional additional entropy string to bind to the protection. .PARAMETER Force Overwrite OutputPath if it exists. .PARAMETER DeletePlaintext Remove the original plaintext file after successful protection. .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 Protect-Keytab -Path .\user.keytab -OutputPath .\user.keytab.dpapi -Scope CurrentUser -RestrictAcl Protect a keytab with DPAPI in the current-user scope and set a restrictive ACL. #> [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]$DeletePlaintext, [switch]$RestrictAcl ) begin { $in = Resolve-PathUniversal -Path $InputPath -Purpose Input if ($OutputPath) { $out = Resolve-PathUniversal -Path $OutputPath -Purpose Output } else { $out = Resolve-OutputPath -InputPath $in -Extension '.dpapi' -AppendExtension -CreateDirectory } } process { if ($PSCmdlet.ShouldProcess($in, 'Protect keytab (DPAPI)')) { if ((Test-Path -LiteralPath $out) -and -not $Force) { throw "Output already exists: '$out'. 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 { $protected = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $entropyBytes, $scopeEnum) [IO.File]::WriteAllBytes($out, $protected) if ($RestrictAcl) { Set-UserOnlyAcl -Path $out } } finally { if ($bytes) { [Array]::Clear($bytes, 0, $bytes.Length) } if ($protected) { [Array]::Clear($protected, 0, $protected.Length) } if ($entropyBytes) { [Array]::Clear($entropyBytes, 0, $entropyBytes.Length) } } } } end { if ($DeletePlaintext) { try { Remove-Item -LiteralPath $in -Force } catch { Write-Warning "Failed to delete plaintext '$in': $($_.Exception.Message)" } } return $out } } |