I wonder if there are any implicit assumptions that I've taken that may make the code malfunction?
There is a reason I want to avoid using Import-Clixml cmdlet? Hence, I've developed an alternative, i.e. a sequence of command that is aimed to extract username and password from CliXml file created with Export-Clixml. It works by now but I'm not sure if for instance the splitting solution is reliable.
$credFileUriBld = [UriBuilder]::New('file','localhost',-1,"MyCredentials.xml"))
$credFile = [Xml.XMLDocument]::New()
$nsMgr4ps1xml = [Xml.XmlNamespaceManager]::New($credFile.NameTable)
$nsMgr4ps1xml.AddNamespace('ps1xml','http://schemas.microsoft.com/powershell/2004/04')
$credFile.Load($credFileUriBld.Path)
$netCredInfo = [System.Net.NetworkCredential]::New($credFile.SelectSingleNode('/ps1xml:Objs/ps1xml:Obj/ps1xml:Props/ps1xml:S[@N=''UserName'']/text()',$nsMgr4ps1xml).Get_Value(),
($credFile.SelectSingleNode('/ps1xml:Objs/ps1xml:Obj/ps1xml:Props/ps1xml:SS[@N=''Password'']/text()',$nsMgr4ps1xml).Get_Value().Split('00') |
ForEach-Object { if([String]::IsNullOrEmpty($_)) { } else { $_.Trim() } } |
ForEach-Object { [convert]::ToInt32($_,16) } |
ForEach-Object { [convert]::ToChar($_) } |
ForEach-Object -Begin { $ss=[SecureString]::New() } -Process {$ss.AppendChar($_)} -End { $ss }))
$netCredInfo.UserName
$netCredInfo.Password
May you take a glimpse and advise if there are any assumptions that make the code unreliable?
Your approach only works in PowerShell Core on Unix-like platforms (macOS, Linux), but it shouldn't be used there for security reasons - it doesn't work on Windows (neither in Windows PowerShell nor in PowerShell Core), because the passwords there are - sensibly - truly encrypted, whereas your code assumes non-encrypted password storage.
Security Warning:
[securestring]
on Unix-like platforms offers NO protection - the characters are stored unencrypted - the encryption underlying [securestring]
on Windows only relies on the Windows-only DPAPI (Data Protection API).
[securestring]
is generally not recommended for new code - see this Roslyn analyzer recommendation.If you save a [securestring]
instance to a file via Export-CliXml
on a Unix-like platform - e.g. with Get-Credential | Export-CliXml MyCredentials.xml
- the "secure" data (password) can trivially be retrieved by anyone who can read the file. By contrast, on Windows a DPAPI-encrypted representation is stored that can only be decrypted by the same user on the same machine.
As your code demonstrates, on Unix a persisted [securestring]
instance is simply a "byte string" that contains the Unicode code points of the characters making up the plain-text content; for instance, a [securestring]
containing string 'test'
is persisted as '7400650073007400'
, which can be constructed as follows:
-join [Text.Encoding]::Unicode.GetBytes('test').ForEach({ $_.Tostring('x2') })
...and converted back with:
[Text.Encoding]::Unicode.GetString([byte[]] ('7400650073007400' -split '(..)' -ne '' -replace '^', '0x'))
In short: On Unix-like platforms (PowerShell Core), do NOT use Get-Credential | Export-CliXml
to persist credentials - they will be stored UNENCRYPTED. To provide any protection at all you'd have to deny everyone else read access to the file via file permissions.
For use on Windows only, if you do need to avoid Import-CliXml
, here's a greatly simplified solution that should also perform better.
While this code would technically also work on Unix-like platforms, it offers no protection whatsoever, as discussed above.
Do note that it requires the use of the ConvertTo-SecureString
cmdlet in order to convert the DPAPI-encrypted password representation in the CLIXML file to a secure string ([securestring]
instance).
# Load the CLIXML file into a [System.Xml.XmlDocument] ([xml]) instance.
($credXml = [xml]::new()).Load($PWD.ProviderPath + '\MyCredentials.xml')
# Take an XPath shortcut that avoids having to deal with namespaces.
# This should be safe, if you know your XML file to have been created with
# Get-Credential | Export-CliXml MyCredentials.xml
$username, $encryptedPassword =
$credXml.SelectNodes('//*[@N="UserName" or @N="Password"]').'#text'
$networkCred = [System.Net.NetworkCredential]::new(
$username,
(ConvertTo-SecureString $encryptedPassword)
)
$networkCred.UserName
# $networkCred.Password # CAUTION: This would echo the plain-text password.