permissions.ps1
|
Set-StrictMode -Version Latest function Test-Elevation { <# .SYNOPSIS Test-Elevation - Returns whether the current process has administrator privileges. .DESCRIPTION Checks the Windows principal of the current identity for membership in the built-in Administrators role. Returns $true when the process holds an elevated (administrator) token, $false otherwise. This is a pure predicate: it makes no changes and never elevates. Use Request-AdministratorPrivilege to actually elevate. .OUTPUTS System.Boolean .EXAMPLE PS> if (-not (Test-Elevation)) { throw 'Run me elevated.' } .LINK https://github.com/adnoctem/winkit/lib/permissions.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] [OutputType([bool])] param () # winkit is Windows-only; fail clearly rather than throwing a cryptic type # error if this is ever invoked on a non-Windows host (the [Security.Principal] # Windows types are present on PS7/Windows but the call throws elsewhere). if ([Environment]::OSVersion.Platform -ne 'Win32NT') { throw 'Test-Elevation is only supported on Windows.' } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Backwards-compatible alias for the original name. Remove once callers are # migrated to Test-Elevation. # CHANGE-NOTE: drop this alias after updating references. Set-Alias -Name Read-ProcessElevation -Value Test-Elevation function Request-AdministratorPrivilege { <# .SYNOPSIS Request-AdministratorPrivilege - Ensures the current script runs elevated, re-launching it under UAC if necessary. .DESCRIPTION Intended to be called once near the top of an entry-point script (e.g. Invoke-Optimizer.ps1, Invoke-Bootstrap.ps1) as a self-elevation block. Behaviour: * If already elevated, returns immediately and the script continues. * If not elevated, re-launches the SAME PowerShell host with the SAME script and arguments under the RunAs verb (triggering UAC), waits for the elevated copy to finish, and exits the current (non-elevated) process with the elevated copy's exit code. Hardening over a naive self-elevation block: * Re-launches the actual current host (pwsh vs powershell), not a hardcoded powershell.exe. * Preserves the original working directory across the RunAs boundary (RunAs otherwise starts the child in system32, breaking relative paths). * Faithfully reconstructs the original invocation (including bound parameters and switches) so -Profile, -WhatIf, paths with spaces, etc. survive intact. * Propagates the elevated child's exit code to the original caller, so cmd.exe launchers checking %ERRORLEVEL% and CI see the real result. * Handles UAC denial gracefully (clear message + non-zero exit) instead of an unhandled Win32Exception. * Guards against an infinite re-launch loop via an environment marker, in case elevation "succeeds" but the token still isn't elevated (rare UAC/GPO configurations). NOTE: this function calls exit on the non-elevated path. It is designed for entry-point scripts, not for dot-sourced library use — calling it from an interactive session would terminate that session when not elevated. .PARAMETER ScriptPath Path to the script to re-launch. Defaults to the caller's own path ($PSCommandPath of the calling script). Override only for unusual hosting. .PARAMETER BoundParameters The calling script's $PSBoundParameters, used to faithfully reconstruct named parameters and switches for the elevated re-launch. Strongly recommended; pass $PSBoundParameters from the caller. .PARAMETER ArgumentList Any unbound/positional arguments ($args from the caller) to append after the reconstructed bound parameters. .PARAMETER IsElevatedRelaunch Loop guard. The caller passes this as $true only when its own param block received the re-launch marker, indicating THIS process is already the elevated child. If set and the process is still not elevated, the function aborts instead of spawning again. See the usage example for the pattern. .OUTPUTS None. Either returns (already elevated) or exits the process (re-launched). .EXAMPLE PS> # At the top of Invoke-Optimizer.ps1, whose param block includes a PS> # hidden [switch]$Elevated used purely as the re-launch marker: PS> Request-AdministratorPrivilege ` PS> -BoundParameters $PSBoundParameters ` PS> -ArgumentList $args ` PS> -IsElevatedRelaunch:$Elevated .LINK https://github.com/adnoctem/winkit/lib/permissions.ps1 .LINK https://michael-casey.com/blog/self-elevating-powershell-and-batch-scripts/ The common naive self-elevation pattern (Start-Process -Verb RunAs + hardcoded powershell, no parameter forwarding). Evaluated and deliberately superseded: this implementation adds host preservation, working-directory preservation, faithful bound-parameter/switch reconstruction, exit-code propagation, UAC-denial handling, and a re-launch loop guard. .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] [OutputType([void])] param ( [string]$ScriptPath = $MyInvocation.PSCommandPath, [System.Collections.IDictionary]$BoundParameters, [object[]]$ArgumentList, [switch]$IsElevatedRelaunch ) if ([Environment]::OSVersion.Platform -ne 'Win32NT') { throw 'Request-AdministratorPrivilege is only supported on Windows.' } # Already elevated — nothing to do. if (Test-Elevation) { return } # Loop guard: if the caller told us this is already the elevated re-launch # (via its own -Elevated marker switch) and we're STILL not elevated, stop # rather than spawning forever. We pass the marker as an explicit parameter # rather than an environment variable (which doesn't survive the RunAs # boundary) or a bare argument token (which a CmdletBinding param block would # reject) — an explicit switch the caller declares is unambiguous. if ($IsElevatedRelaunch) { Write-Error ('Elevation was attempted but the process is still not elevated. ' + 'Check UAC / Group Policy settings (e.g. "Run all administrators in Admin Approval Mode"). ' + 'Aborting to avoid a re-launch loop.') exit 1 } # RunAs minimum: Windows Vista (build 6000). Earlier versions can't elevate. $build = [int](Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop | Select-Object -ExpandProperty BuildNumber) if ($build -lt 6000) { throw "Self-elevation requires Windows Vista or later (build >= 6000); detected build $build." } # Resolve the script to re-launch. if ([string]::IsNullOrWhiteSpace($ScriptPath)) { throw 'Could not determine the script path to re-launch. Pass -ScriptPath explicitly.' } $ScriptPath = (Resolve-Path -LiteralPath $ScriptPath -ErrorAction Stop).Path # Re-launch the SAME host (pwsh.exe or powershell.exe), not a hardcoded one. $hostExe = (Get-Process -Id $PID).Path if ([string]::IsNullOrWhiteSpace($hostExe)) { throw 'Could not determine the current PowerShell host executable path.' } # Faithfully reconstruct the argument list for the elevated re-launch. $reArgs = [System.Collections.Generic.List[string]]::new() $reArgs.Add('-NoProfile') $reArgs.Add('-ExecutionPolicy'); $reArgs.Add('Bypass') $reArgs.Add('-File'); $reArgs.Add($ScriptPath) if ($BoundParameters) { foreach ($name in $BoundParameters.Keys) { $value = $BoundParameters[$name] if ($value -is [System.Management.Automation.SwitchParameter]) { # Switches: include the flag only when present/true. Use -Name:$true form # so the elevated copy receives an explicit value, avoiding ambiguity. if ($value.IsPresent) { $reArgs.Add("-$name") } } elseif ($value -is [bool]) { $reArgs.Add("-$name`:$([bool]$value)") } elseif ($null -ne $value) { # Arrays -> repeat the parameter for each element so [string[]] params # round-trip correctly (e.g. -VCRedistVersions 14.0,12.0). foreach ($item in @($value)) { $reArgs.Add("-$name") $reArgs.Add([string]$item) } } } } if ($ArgumentList) { foreach ($a in $ArgumentList) { $reArgs.Add([string]$a) } } # Inject the elevation marker switch so the re-launched child can pass it back # into this function as -IsElevatedRelaunch and trip the loop guard if needed. # The caller's param block must declare a [switch]$Elevated for this to bind. if ($BoundParameters -and -not $BoundParameters.Contains('Elevated')) { $reArgs.Add('-Elevated') } elseif (-not $BoundParameters) { $reArgs.Add('-Elevated') } # Preserve the working directory across the RunAs boundary. Start-Process # -Verb RunAs otherwise launches in system32, breaking relative paths the # elevated script might use. $workingDir = (Get-Location -PSProvider FileSystem).ProviderPath $startInfo = @{ FilePath = $hostExe ArgumentList = $reArgs.ToArray() Verb = 'RunAs' WorkingDirectory = $workingDir PassThru = $true Wait = $true } try { $proc = Start-Process @startInfo # Propagate the elevated child's exit code to our caller. exit $proc.ExitCode } catch [System.ComponentModel.Win32Exception] { # 1223 = ERROR_CANCELLED — user clicked "No" on the UAC prompt. if ($_.Exception.NativeErrorCode -eq 1223) { Write-Error 'Elevation was cancelled by the user. Administrator privileges are required to continue.' exit 1223 } throw } } # Shared native method - compiled once, reused by Set-RegistryOwner if ($null -eq ('OwnershipNative' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public class OwnershipNative { [DllImport("ntdll.dll")] public static extern int RtlAdjustPrivilege(ulong Privilege, bool Enable, bool CurrentThread, ref bool Enabled); } '@ -ErrorAction Stop } function Set-RegistryOwner { <# .SYNOPSIS Set-RegistryOwner - Takes ownership of a registry key, optionally including all subkeys. .DESCRIPTION Takes ownership of the specified registry key (owned by e.g. TrustedInstaller or a vendor installer) and grants the owning account FullControl. Uses RtlAdjustPrivilege P/Invoke to acquire SeTakeOwnership, SeBackup, and SeRestore, then manipulates the key ACL via System.Security.AccessControl.RegistrySecurity. Ownership is granted to the current user by default, or to any account via -SID. Requires elevation: the process must hold an administrator token, otherwise the privilege adjustment and ACL writes will fail. Recurse is on by default: subkeys are taken over recursively, with access rules propagated. .PARAMETER Hive Registry hive, e.g. HKLM, HKCU, HKCR, HKCC, HKU (or HKEY_* long forms). .PARAMETER Key Key path below the hive, e.g. 'SOFTWARE\WOW6432Node\Classes\CLSID\{abc}'. .PARAMETER SID SID of the account to grant ownership to. Defaults to the current user. .PARAMETER Recurse Take ownership of the key and all subkeys. Defaults to $true. .OUTPUTS PSCustomObject - New-OperationResult-shaped result (Target, Source, Action, Status). .EXAMPLE PS> Set-RegistryOwner -Hive 'HKLM' -Key 'SOFTWARE\MyApp' .LINK https://github.com/adnoctem/winkit/lib/permissions.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '', Justification = 'The reference behaviour (and the downstream requirements doc) mandate -Recurse defaulting to $true for ownership takeover.')] [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Hive, [Parameter(Mandatory = $true, Position = 1)] [string] $Key, [Parameter(Mandatory = $false)] [System.Security.Principal.SecurityIdentifier] $SID, [Parameter(Mandatory = $false)] [switch] $Recurse = $true ) if (-not (Test-Elevation)) { throw 'Set-RegistryOwner requires an elevated process (administrator token).' } if ($null -eq $SID) { $SID = Get-UserSID -UserName $env:USERNAME if ([string]::IsNullOrWhiteSpace($SID)) { throw "Could not resolve a SID for '$env:USERNAME'." } $SID = [System.Security.Principal.SecurityIdentifier]::new($SID) } switch -Regex ($Hive) { '^(HKCU|HKEY_CURRENT_USER)$' { $root = 'CurrentUser'; break } '^(HKLM|HKEY_LOCAL_MACHINE)$' { $root = 'LocalMachine'; break } '^(HKCR|HKEY_CLASSES_ROOT)$' { $root = 'ClassesRoot'; break } '^(HKCC|HKEY_CURRENT_CONFIG)$' { $root = 'CurrentConfig'; break } '^(HKU|HKEY_USERS)$' { $root = 'Users'; break } default { throw "Unsupported hive '$Hive'. Use HKCU, HKLM, HKCR, HKCC, or HKU." } } # Acquire SeTakeOwnership (9), SeBackup (17), and SeRestore (18). foreach ($privilegeId in @(9, 17, 18)) { $enabled = $false $null = [OwnershipNative]::RtlAdjustPrivilege($privilegeId, $true, $false, [ref]$enabled) } function Invoke-RegistryOwnershipTakeover { param ( [string]$Root, [string]$KeyPath, [int]$RecurseLevel, [bool]$Recurse ) $item = [Microsoft.Win32.Registry]::$Root.OpenSubKey($KeyPath, 'ReadWriteSubTree', 'TakeOwnership') if ($null -eq $item) { Write-Verbose "Key not found: $Hive\$KeyPath" return } try { $acl = [System.Security.AccessControl.RegistrySecurity]::new() $acl.SetOwner($SID) if ($PSCmdlet.ShouldProcess("$Hive\$KeyPath", 'Take ownership')) { $item.SetAccessControl($acl) # Enable inheritance of permissions (not ownership) from the parent key. $acl.SetAccessRuleProtection($false, $false) $item.SetAccessControl($acl) } if ($RecurseLevel -eq 0) { # Top-level key: grant FullControl, propagated to all subkeys. $rule = [System.Security.AccessControl.RegistryAccessRule]::new( $SID, 'FullControl', 'ContainerInherit', 'None', 'Allow') $acl.ResetAccessRule($rule) $changeItem = $item.OpenSubKey('', 'ReadWriteSubTree', 'ChangePermissions') try { if ($PSCmdlet.ShouldProcess("$Hive\$KeyPath", 'Set FullControl access')) { $changeItem.SetAccessControl($acl) } } finally { $changeItem.Dispose() } } if ($Recurse) { foreach ($subKeyName in $item.OpenSubKey('').GetSubKeyNames()) { Invoke-RegistryOwnershipTakeover -Root $Root -KeyPath "$KeyPath\$subKeyName" -RecurseLevel ($RecurseLevel + 1) -Recurse $Recurse } } } finally { $item.Dispose() } } Invoke-RegistryOwnershipTakeover -Root $root -KeyPath $Key -RecurseLevel 0 -Recurse $Recurse if ($WhatIfPreference) { New-OperationResult -Target "$Hive\$Key" -Source 'Registry' -Action 'TakeOwnership' -Status 'DryRun' -Detail 'No changes applied.' return } New-OperationResult -Target "$Hive\$Key" -Source 'Registry' -Action 'TakeOwnership' -Status 'Completed' -Detail "Ownership granted to $SID." -Property @{ SID = $SID.Value } } function Set-ItemOwner { <# .SYNOPSIS Set-ItemOwner - Takes ownership of a file or folder via takeown/icacls. .DESCRIPTION Takes ownership of the specified filesystem object (file or directory) and grants FullControl to the specified group, using takeown.exe and icacls.exe shell-outs. For directories the operation recurses through all children. When takeown/icacls fail (e.g. genuinely stuck items), a robocopy /purge fallback is attempted against an empty staging folder - note that this DELETES the target's contents and is a last-resort path. Requires elevation. .PARAMETER Path Full path of the file or directory to take ownership of. .PARAMETER Group Group to grant FullControl to. Defaults to 'administrators'. .OUTPUTS PSCustomObject - New-OperationResult-shaped result (Target, Source, Action, Status). .EXAMPLE PS> Set-ItemOwner -Path 'C:\Program Files\VendorApp' .LINK https://github.com/adnoctem/winkit/lib/permissions.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Path, [Parameter(Mandatory = $false)] [string] $Group = 'administrators' ) if (-not (Test-Elevation)) { throw 'Set-ItemOwner requires an elevated process (administrator token).' } if (-not (Test-Path -LiteralPath $Path)) { New-OperationResult -Target $Path -Source 'FileSystem' -Action 'TakeOwnership' -Status 'Failed' -ErrorMessage 'Path does not exist.' return } $isDirectory = (Get-Item -LiteralPath $Path) -is [System.IO.DirectoryInfo] if (-not $PSCmdlet.ShouldProcess($Path, 'Take ownership')) { if ($WhatIfPreference) { New-OperationResult -Target $Path -Source 'FileSystem' -Action 'TakeOwnership' -Status 'DryRun' -Detail 'No changes applied.' } return } $purgeFallback = $false if ($isDirectory) { # /F path, /R recurse, /A grant admins, /D Y answer 'yes' to prompts & "$env:SystemRoot\System32\takeown.exe" /F $Path /R /A /D Y 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { & "$env:SystemRoot\System32\icacls.exe" $Path /grant "${Group}:f" /t /q 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { $purgeFallback = $true } } else { $purgeFallback = $true } } else { & "$env:SystemRoot\System32\takeown.exe" /F $Path /A 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { & "$env:SystemRoot\System32\icacls.exe" $Path /grant "${Group}:f" /q 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { $purgeFallback = $true } } else { $purgeFallback = $true } } $fallbackRecovered = $false if ($purgeFallback) { Write-Verbose "takeown/icacls failed; attempting robocopy /purge fallback for: $Path" $emptyDir = Join-Path -Path $env:TEMP -ChildPath 'psf-empty-takeown' if (-not (Test-Path -LiteralPath $emptyDir)) { $null = New-Item -Path $emptyDir -ItemType Directory -Force } try { & "$env:SystemRoot\System32\robocopy.exe" $emptyDir $Path /purge 2>&1 | Out-Null # Robocopy exit codes 0-7 are success variants. if ($LASTEXITCODE -le 7) { $fallbackRecovered = $true } } finally { if (Test-Path -LiteralPath $emptyDir) { Remove-Item -LiteralPath $emptyDir -Recurse -Force -ErrorAction SilentlyContinue } } } if ($fallbackRecovered) { New-OperationResult -Target $Path -Source 'FileSystem' -Action 'TakeOwnership' -Status 'Completed' -Detail "Item recovered via robocopy /purge fallback; FullControl granted to '$Group'." } elseif ($purgeFallback) { New-OperationResult -Target $Path -Source 'FileSystem' -Action 'TakeOwnership' -Status 'Failed' -ErrorMessage 'takeown/icacls failed and the robocopy /purge fallback could not recover the item.' } else { New-OperationResult -Target $Path -Source 'FileSystem' -Action 'TakeOwnership' -Status 'Completed' -Detail "FullControl granted to '$Group'." } } function New-EncryptedCredentialFile { <# .SYNOPSIS New-EncryptedCredentialFile - Stores a credential in a DPAPI-independent encrypted file. .DESCRIPTION Encrypts a credential's password with a freshly generated random AES key (ConvertFrom-SecureString -Key) and writes the key and the encrypted blob to separate files. Plain ConvertFrom-SecureString without -Key uses Windows DPAPI, which binds the encrypted blob to a specific user and machine. The -Key variant is the correct approach when a scheduled task or automation script running as a different account or machine must decrypt it later. File format: the credential file holds the username in plaintext on the first line (usernames are not secrets) and the encrypted password blob on the second. The key file holds the key as base64. SECURITY CAVEAT: this pattern is only as secure as protecting the key file separately from the encrypted blob. The key file is locked down to the creating user's account, but you should still store it somewhere with tighter ACLs than the credential file - or use a secrets-management mechanism when one is available. Anyone holding both files can decrypt the credential. .PARAMETER Path Path of the credential file to write. .PARAMETER KeyPath Path of the key file to write. .PARAMETER Credential The credential to store. Required unless -UserName is used. .PARAMETER UserName Username to store; prompts for the password interactively (Read-Host -AsSecureString). Thin interactive wrapper around the programmatic -Credential path. .OUTPUTS PSCustomObject - New-OperationResult-shaped result. .EXAMPLE PS> New-EncryptedCredentialFile -Path '.\svc-cred.bin' -KeyPath '.\svc-cred.key' -Credential (Get-Credential) .EXAMPLE PS> New-EncryptedCredentialFile -Path '.\svc-cred.bin' -KeyPath '.\svc-cred.key' -UserName 'DOMAIN\svc-backup' .LINK https://github.com/adnoctem/winkit/lib/permissions.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([PSCustomObject])] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $KeyPath, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $false)] [string] $UserName ) if ($null -eq $Credential -and -not $UserName) { throw 'Supply -Credential (programmatic) or -UserName (interactive password prompt).' } if ($null -eq $Credential) { $securePassword = Read-Host -Prompt "Password for '$UserName'" -AsSecureString $Credential = [System.Management.Automation.PSCredential]::new($UserName, $securePassword) } if (-not $PSCmdlet.ShouldProcess("$Path / $KeyPath", 'Write encrypted credential files')) { if ($WhatIfPreference) { New-OperationResult -Target $Path -Source 'CredentialFile' -Action 'Write' -Status 'DryRun' -Detail 'No files written.' } return } $key = [byte[]]::new(32) $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try { $rng.GetBytes($key) } finally { $rng.Dispose() } $encryptedBlob = $Credential.Password | ConvertFrom-SecureString -Key $key $keyBase64 = [Convert]::ToBase64String($key) Set-Content -LiteralPath $Path -Value @($Credential.UserName, $encryptedBlob) -Encoding ASCII Set-Content -LiteralPath $KeyPath -Value $keyBase64 -Encoding ASCII try { $acl = Get-Acl -LiteralPath $KeyPath $acl.SetAccessRuleProtection($true, $false) $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().User $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($currentUser, 'FullControl', 'Allow') $acl.SetAccessRule($rule) Set-Acl -LiteralPath $KeyPath -AclObject $acl Write-Verbose 'Restricted the key file to the current user account.' } catch { Write-Warning "Could not restrict ACLs on the key file '$KeyPath': $($_.Exception.Message)" } New-OperationResult -Target $Path -Source 'CredentialFile' -Action 'Write' -Status 'Completed' -Detail 'Credential stored with a random AES key; key file written separately.' } function Get-EncryptedCredentialFile { <# .SYNOPSIS Get-EncryptedCredentialFile - Reads a credential stored by New-EncryptedCredentialFile. .DESCRIPTION Reads the key file and the encrypted credential file created by New-EncryptedCredentialFile and returns the decrypted PSCredential. File format: the credential file holds the username in plaintext on the first line and the encrypted password blob on the second; the key file holds the key as base64. .PARAMETER Path Path of the credential file. .PARAMETER KeyPath Path of the key file. .OUTPUTS System.Management.Automation.PSCredential .EXAMPLE PS> Get-EncryptedCredentialFile -Path '.\svc-cred.bin' -KeyPath '.\svc-cred.key' .LINK https://github.com/adnoctem/winkit/lib/permissions.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([System.Management.Automation.PSCredential])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $KeyPath ) if (-not (Test-Path -LiteralPath $Path)) { throw "Credential file not found: $Path" } if (-not (Test-Path -LiteralPath $KeyPath)) { throw "Key file not found: $KeyPath" } $lines = Get-Content -LiteralPath $Path if ($lines.Count -lt 2) { throw "Credential file '$Path' is malformed (expected a username line and an encrypted blob line)." } $userName = $lines[0] $encryptedBlob = ($lines[1..($lines.Count - 1)] -join '') $key = [Convert]::FromBase64String((Get-Content -LiteralPath $KeyPath -Raw).Trim()) $securePassword = $encryptedBlob | ConvertTo-SecureString -Key $key [System.Management.Automation.PSCredential]::new($userName, $securePassword) } |