powershellcredentialssecurestringnetworkcredentialspowershell-core

Are there any implicit assumptions in Import-Clixml when importing credentials?


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?


Solution

  • 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:

    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.