Storing Secrets in Git using PowerShell

Storing secrets (passwords, API keys and so on) in Git is a bad idea. We all know that. But somehow, we still end up doing it anyway. Some people have come up with decent solutions to this problem. They are using asymmetric encryption to store their secrets. Notable examples include git-crypt, git-secret, or blackbox. But all three tools have one thing in common: they are bash tools, which leaves the Windows folks out in the rain. I know that WSL can get around this limitation, but I wanted something that I can use in PowerShell. So I came up with a rather stupid and simple solution: using 7-zip to AES encrypt secrets.

Here's an example of an encryption function:

function Read-Password {
    [CmdletBinding()]
    param()

    $secure1 = Read-Host -Prompt 'Enter password' -AsSecureString
    $secure2 = Read-Host -Prompt 'Re-enter password' -AsSecureString

    $plain1 = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure1))
    $plain2 = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure2))

    if ($plain1 -ne $plain2) {
        throw 'The provided passwords do not match'
    }

    $secure1
}

function Compress-Secret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo[]]$Files,
        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo]$DestinationPath,
        [Parameter(Mandatory = $true)]
        [System.Security.SecureString]$Password
    )

    if (Test-Path $DestinationPath) {
        Remove-Item $DestinationPath -Confirm
    }

    $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password))

    7z a $DestinationPath -p"$plain" $Files -spf | Write-Verbose

    if ($LASTEXITCODE -ne 0) {
        throw "Error while trying to create '$DestinationPath'"
    }
}

Which could be called like this:

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'

Compress-Secret -Files (Get-Content '.secrets') -DestinationPath 'secrets.7z' -Password (Read-Password)

The .secrets file should contain a list of secret files, e.g:

config/some-api-key.conf
config/license-key.txt
another-secret.json

And here's the decryption function:

function Expand-Secret {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.IO.FileInfo]$SourcePath,
        [System.Security.SecureString]$Password = (Read-Host -Prompt 'Enter password' -AsSecureString)
    )

    $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password))

    7z x $SourcePath -p"$plain" -y | Write-Verbose

    if ($LASTEXITCODE -ne 0) {
        throw "Error while trying to decrypt '$SourcePath'"
    }
}

Which we can call like this:

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'

Expand-Secret -SourcePath 'secrets.7z'

The above snippets are neither the most sophisticated, nor the most secure code, but it's a starting point.

A few final notes:

Published: 2019-08-23