Use Windows Data Protection API with Python for handling credentials.
Tags: python, powershell
Published: 2022-01-12 21:01:15

As a IT Tech, I do alot of automation scripting, both in Powershell and Python against various systems and Ive never liked how I had to handle credentials in scripts.

I dont want to have any credentials or API-keys in plain text on a server that others have access to, so I saved it as environment variables like most of us do.
That works fine, but when you have hundreds of variables for email accounts, API-keys and SFTP accounts it can get messy. 

When I looked for a better solution I found this gem for Powershell!

**All commands below are run from PowerShell.**

Prompt for Username and Password and save to file
```bash
PS C:\> Get-Credential | Export-Clixml -Path "cred.xml"
```

Import the credentials from a file to a PSCredential object.
```bash
PS C:\> $cred = Import-Clixml -Path "cred.xml"
PS C:\> $cred

UserName                     Password
--------                     --------
samkling System.Security.SecureString
PS C:\> ConvertFrom-SecureString $cred.password -AsPlainText

password
```
*ConvertFrom-SecureString -AsPlainText requires PowerShell 7.0.
Export-Clixml only exports encrypted credentials on Windows.*

The [Export-Clixml](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/export-clixml?view=powershell-7.1)  cmdlet encrypts credential objects by using the [Windows Data Protection API](https://docs.microsoft.com/en-us/previous-versions/windows/apps/hh464970(v=win.10)) . The encryption ensures that only your user account on only that computer can decrypt the contents of the credential object. The exported CLIXML file can't be used on a different computer or by a different user.


I now store the credentials neatly, and secure in a credentials.xml file in the same directory as the actual script. Anyone can access the credential file and the script, but they wont be able to decrypt 

```bash
.
├─ company1-sftp-script
│  ├─ download-files-newer-than-1-day.ps1
│  └─ credentials.xml
└─ company2-sftp-script
   ├─ download-files-newer-than-1-day.ps1
   └─ credentials.xml
```


## How can we use the same method in Python? 

Sadly there is no Export-Clixml/Import-Clixml equalent for Python so we will have to build it ourselves.

First we need to have access Windows Data Protection API. There are several ways, I use [pywin32](https://pypi.org/project/pywin32/).

```bash
PS C:\> pip install pywin32
```

And then we need to create two files in python.

```python
# export_clixml.py
import win32crypt
import binascii
import sys


def export_clixml(username, password):

    # encrypt the password with DPAPI.
    crypted_password = win32crypt.CryptProtectData(
        password.encode("utf-16-le"), None, None, None, None, 0
    )

    # Do some magic to return the password in the exact same format as if you would use Powershell.
    password_secure_string = binascii.hexlify(crypted_password).decode()

    # Use the same xml format as for powershells Export-Clixml, just replace values for username and password.
    xml = f"""<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
    <Obj RefId="0">
        <TN RefId="0">
        <T>System.Management.Automation.PSCredential</T>
        <T>System.Object</T>
        </TN>
        <ToString>System.Management.Automation.PSCredential</ToString>
        <Props>
        <S N="UserName">{username}</S>
        <SS N="Password">{password_secure_string}</SS>
        </Props>
    </Obj>
    </Objs>"""

    return xml


if __name__ == "__main__":
    if len(sys.argv) == 3:
        # Dont do this, It's just so that we can pipe the output to file to mimic the powershells version.
        print(export_clixml(sys.argv[1], sys.argv[2]))

```

```python
# import_clixml.py
import win32crypt
import binascii
import sys


def import_clixml(filename):

    with open(filename, "r", encoding="utf-8") as f:
        xml = f.read()

        # Extract username and password from the XML since thats all we care about.
        username = xml.split('<S N="UserName">')[1].split("</S>")[0]
        password_secure_string = xml.split('<SS N="Password">')[1].split("</SS>")[0]

        # CryptUnprotectDate returns two values, description and the password, 
        # we dont care about the description, so we use _ as variable name.
        _, decrypted_password_string = win32crypt.CryptUnprotectData(
            binascii.unhexlify(password_secure_string), None, None, None, 0
        )

        return f"{username}, {decrypted_password_string.decode()}"


if __name__ == "__main__":
    # We use sys args just to mimic the powershell version.
    if len(sys.argv) == 2:
        print(import_clixml(sys.argv[1]))

```

Lets try it out and see if we can use Powershells Export-Clixml and Pythons Import-Clixml.

```bash
PS C:\> Get-Credential | Export-Clixml -Path powershell_cred.xml 

PowerShell credential request
Enter your credentials.
User: samkling
Password: password

PS C:\> py .\Import_Clixml.py .\powershell_cred.xml

samkling, password
```

Sweet, so it returned the correct username and password decrypted.

What about the other way around? From Python to Powershell?

```bash
PS C:\> $cred = Import-Clixml -Path .\python_cred.xml
PS C:\> $cred

UserName                     Password
--------                     --------
samkling System.Security.SecureString

PS C:\> ConvertFrom-SecureString $cred.password -AsPlainText

password
```

How cool is that!