Import-Clixml Error - The data is invalid


> $PSVersionTable

Name Value


PSVersion 4.0
WSManStackVersion 3.0
SerializationVersion 1.1.0.1
CLRVersion 4.0.30319.34209
BuildVersion 6.3.9600.16394
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0}
PSRemotingProtocolVersion 2.2

We maintain credential objects in clixml files (using Export-Clixml) and we import them back into scripts using Import-Clixml in order to connect to various resources. However, over the last couple months, we’ve had a handful of times where something seems to affect the encryption that prevents Import-Clixml from working for the user who created the file and we start getting errors when trying to import credentials:


> whoami
[ad domain]\scrptAcct
> $cred = Import-Clixml .\credentials\svcAcct1_for_scrptAcct.clixml
Import-clixml : The data is invalid.
At line:1 char:9

  • $cred = Import-clixml .\credentials\svcAcct1_for_scrptAcct.clixml
  •     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (:slight_smile: [Import-Clixml], CryptographicException
    • FullyQualifiedErrorId : System.Security.Cryptography.CryptographicException,Microsoft.PowerShell.Commands.ImportClixmlCommand

> $cred = Import-Clixml .\credentials\svcAcct2_for_scrptAcct.clixml
Import-clixml : The data is invalid.
At line:1 char:9

  • $cred = Import-clixml .\credentials\svcAcct2_for_scrptAcct.clixml
  •     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (:slight_smile: [Import-Clixml], CryptographicException
    • FullyQualifiedErrorId : System.Security.Cryptography.CryptographicException,Microsoft.PowerShell.Commands.ImportClixmlCommand

This is an Active Directory (I’m not an AD admin) user account that we use to execute the scripts on our scripting host and connects to various remote resources. Again, this appears to impact every credential file created by a single user. Other credential files on that machine are fine. So perhaps something about the user is getting changed…? Now, we can simply recreate all the clixml credential files and they begin to work again, but this issue breaks scheduled tasks (which is what usually lets us know something has broke) and is generally a PITA.

I couldn’t find much information that seems related to this when searching for that error. I’ve also been trying to find some information on either Export-Clixml, Get-Credential or just ConvertTo-SecureString to see how it encrypts the Password to tie the object to the user that created it. Does it use the SID (or some other user element) as a seed/key? Does that mean that the user SID (or whatever data element) is somehow getting changed and thus breaking the decrypting of the credential object?

Any help would be appreciated.

It uses the Data Protection API. The encryption keys are randomly generated (and rotated on a schedule), and stored in the user profile directory. (Depending on how your AD is set up, copies of the keys may also exist there, but that’s not turned on by default.)

If the user profile was deleted, then you’d lose the keys and start getting those errors when attempting to decrypt the old data.

Wait, rotated? Meaning what it uses today to encrypt something may be different next month (or whatever)? So how does decryption take place? How does it know how to reverse the process if the key is changing? Doesn’t there have to be something static it uses to decrypt the SecureString?

And no, it doesn’t appear that the user profile has been deleted.

Thank you for the help. I’m going to start reading on that API a bit.

I am curious about your “scripting host.” The SecureString is encrypted using DPAPI. And unless you use an explicit key (which opens up another whole level of key management issues), then the two major constraints are:

  • The key can only be decrypted in the same user context it was encrypted in. That is, Bob can't use a key that Jane generated when she was logged in.
  • The key can only be decrypted on the same machine it was encrypted on originally. So if your scripting host moved, the SecureString would no longer be valid.

Right, that was my understanding as well Bob. As far as I know, as I mentioned to Dave, the user profile hasn’t been deleted (nor do we have a [username].[DOMAIN] user profile folder now) and the script host itself hasn’t had anything significant happen to it - especially between the days when it went from working to not working. It wasn’t renamed, removed/added to AD, re-IP’d… It didn’t have Windows Updates applied… It didn’t even have a reboot that night. So I’m kind of scratching my head looking for variables that would have changed from one day to the next.

And no, I am not setting any explicit key to be used.

Dave mentioned that a copy of the keys, could (potentially) be stored in AD. However, if that was the case, would that remove the copy on the local server?

This is the feature I was referring to: https://technet.microsoft.com/en-us/library/cc771348.aspx . It doesn’t remove the local copy of the keys from your computer, so far as I know; it just downloads them from AD if you don’t already have one locally.

The old keys are still kept, so you can decrypt old data, but every so often a new DPAPI key is generated and used for any new encryption. The keys are stored in "$env:APPDATA\Microsoft\Protect[User SID]", and you’ll need to use the -Force switch on Get-ChildItem to see them.

I just poked around a bit, and it looks like the GUID of the master key for a particular encrypted piece of data is stored in bytes 24…39 of the encrypted blob, so you can extract that and know for sure if the proper key exists in the user’s profile:

function Get-DPAPIMasterKey
{
    param (
        [Parameter(Mandatory = $true)]
        [ValidateScript({
            if ($_.Length % 2 -ne 0 -or $_ -notmatch '^[\da-f]+$')
            {
                throw "'$_' is not a valid DPAPI-encrypted secure string."
            }

            return $true
        })]
        [string] $EncryptedSecureString
    )

    $encryptedByteStrings = [string[]]($EncryptedSecureString -split '(?<=\G.{2})' -match '\S')
    $encryptedBytes = [byte[]]($encryptedByteStrings | ForEach-Object { [Convert]::ToByte($_, 16) })
    $guidBytes = [byte[]]($encryptedBytes[24..39])

    return (New-Object Guid (,$guidBytes)).Guid
}

The EncryptedSecureString value could be copied right out of your XML file; it’ll be between the [SS] and [/SS] tags. (Have to use square brackets here; our forum software treats XML like HTML and eats it.)