DEV Community

Samuel Kling
Samuel Kling

Posted on • Edited on

4

Use Windows Data Protection API with Python for handling credentials.

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

PS C:\> Get-Credential | Export-Clixml -Path "cred.xml"
Enter fullscreen mode Exit fullscreen mode

Import the credentials from a file to a PSCredential object.

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
Enter fullscreen mode Exit fullscreen mode

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

The Export-Clixml cmdlet encrypts credential objects by using the Windows Data Protection API . 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

.
├─ company1-sftp-script
│  ├─ download-files-newer-than-1-day.ps1
│  └─ credentials.xml
└─ company2-sftp-script
   ├─ download-files-newer-than-1-day.ps1
   └─ credentials.xml
Enter fullscreen mode Exit fullscreen mode

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.

PS C:\> pip install pywin32
Enter fullscreen mode Exit fullscreen mode

And then we need to create two files in 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]))

Enter fullscreen mode Exit fullscreen mode
# 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]))

Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

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

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
Enter fullscreen mode Exit fullscreen mode

How cool is that!

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more