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

1

3

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?

Autacoid answered 21/4, 2019 at 12:46 Comment(4)
Thank you for the opinion. It's a matter of style. This snippet is a part of a bigger powershell application. More specifically, this is an element of powershell class implementing low-level infrastructure layer. I've adopted a standard that bottom/plumbing layer methods do not take advantage of higher level complex cmdlets. So, this is governance, not a technical imperative.Autacoid
I see. Do note that your code chooses the worst of both worlds: you're missing out on the convenience of high-level functionality only to implement something that not only requires much more coding effort but also performs much worse. (The performance aspect could be helped, but it would require avoiding the pipeline and cmdlets altogether.)Snowslide
Thanks for pointing directions to increase the performance of the snippet. Fully agree. Nevertheless, this code run only once when feeding Credential Cache collection. But, certainly, I'll put onto the non-functional issues queue.Autacoid
Is the whole point of this (in the actual application) to avoid having to use files (since the *-Clixml commands only operate on files)? If so, what about just using the Serialize() and Deserialize() static methods of the [System.Management.Automation.PSSerializer] class?Apostatize
S
6

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).

  • 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.
Snowslide answered 22/4, 2019 at 16:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.