The idea with this script is to provide a way to work with configuration (xml) files for PowerShell scripts. These files may contain credential information which needs to be secured; those would be held under a special element name, Credential
. Someone setting up the configuration file could set it up with the password in plaintext. On the script’s first run it would read this plaintext password and encrypt it, updating the config to flag it as encrypted. This saves the effort of manually working out the encrypted value each time the value’s changed; rather set it and then run the script to have it automatically protect itself.
Get-Config
reads the configuration file. It essentially just imports the XML, only finding any credential elements and converting them to PSCredentials. To achieve that, it also needs to convert the config data from being stored in XML format (i.e. which can’t hold a PSCredential) to a Hashtable (which can).
Protect-Config
is used to update the configuration file, finding any unencrypted passwords and ensuring they get encrypted (saving the config file again once done). An additional -PassThru
parameter can be supplied to have this return the configuration (i.e. to save having to call Get-Config
in addition to Protect-Config
at the start of every script).
function Protect-Config { [CmdletBinding()] Param ( [Parameter(Mandatory = $ true)] [ValidateScript({Test-Path -Path $ _ -PathType 'Leaf'})] [string]$ Path , [Parameter(Mandatory = $ false)] [switch]$ PassThru ) Process { [bool]$ amended = $ false [string]$ safePath = Resolve-Path -Path $ Path | Select-Object -ExpandProperty 'ProviderPath' #ensure the path is valid for .net as well as PS write-verbose $ safePath $ config = [xml](Get-Content -Path $ safePath -Raw) $ config.SelectNodes("//*/Credential/Password[not(./@IsEncrypted = 'true') and (./text())]") | %{ Write-Verbose "Unencrypted data found; encrypting" $ amended = $ true $ attribute = $ config.CreateAttribute("IsEncrypted") $ attribute.Value = 'true' $ _.Attributes.Append($ attribute) $ _.InnerText = $ _.InnerText | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString } if ($ amended) {$ config.Save($ safePath)} if ($ PassThru.IsPresent) {Get-Config -Path $ safePath} } } function Get-Config { [CmdletBinding(DefaultParameterSetName='ByPath')] Param ( [Parameter(Mandatory = $ true, ParameterSetName='ByPath')] [ValidateScript({Test-Path -Path $ _ -PathType 'Leaf'})] [string]$ Path , [Parameter(Mandatory = $ true, ValueFromPipeline = $ true, ParameterSetName='ByElement')] [System.Xml.XmlElement]$ Element ) Process { $ result = @{} if ($ PSCmdlet.ParameterSetName -eq 'ByPath') { $ config = [xml](Get-Content -Path $ Path -Raw) $ result = $ config.DocumentElement | Get-Config } else { if ($ Element.LocalName -eq 'Credential') { #Credentials have special handling rules to convert them to PSCredential objects [string]$ u = $ Element.SelectSingleNode('./Username/text()').Value [string]$ p = $ Element.SelectSingleNode('./Password/text()').Value if ($ p) { $ result[$ Element.LocalName] = [System.Management.Automation.PSCredential]::new($ u, ($ p | ConvertTo-SecureString)) } else { if ($ u) { $ result[$ Element.LocalName] = [System.Management.Automation.PSCredential]::new($ u, ([System.Security.SecureString]::new())) } else { $ result[$ Element.LocalName] = [System.Management.Automation.PSCredential]::Empty } } } else { $ childElements = $ Element.ChildNodes | Where-Object {$ _.GetType().ToString() -eq 'System.Xml.XmlElement'} if ($ childElements.Count -gt 0) { $ result[$ Element.LocalName] = $ childElements | Get-Config } else { $ result[$ Element.LocalName] = $ Element.SelectSingleNode('./text()').Value } } } $ result } }
A configuration file would look something like this:
<?xml version="1.0" encoding="utf-8"?> <Config> <MyDatabase> <Credential> <Username>MyUsername</Username> <Password>MyUnencryptedPassword</Password> </Credential> <DbInstance>MyServer\MyInstance</DbInstance> <DbCatalog>MyCatalog</DbCatalog> </MyDatabase> <Path>\server\share\subfolder</Path> </Config>
Once the first run occurs, this would be updated to something like:
<?xml version="1.0" encoding="utf-8"?> <Config> <MyDatabase> <Credential> <Username>MyUsername</Username> <Password IsEncrypted="true">01000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef9876543201000000abcdef98765432</Password> </Credential> <DbInstance>MyServer\MyInstance</DbInstance> <DbCatalog>MyCatalog</DbCatalog> </MyDatabase> <Path>\server\share\subfolder</Path> </Config>
Saving the passwords in plaintext is obviously a bad idea; the idea here would be for the admin to be aware that they need to immediately run the script after amending any credentials, to ensure this information is not visible on the file system for long.
This script is written for PowerShell v5. My scenario didn’t require support for older versions, so this implementation’s not taken those into account.