jborean93/PowerShell-AnsibleVault

Add -OutPath option to cmdlet to set the vault output to a file

swilliams-dev opened this issue · 2 comments

Get-EncryptedAnsibleVault produces a valid ansible-vault file, however when that file is decrypted via ansible-vault or Get-DecryptedAnsibleVault it has a zero-length whitespace character prepended to the file. I've attempted various encoding options on Get-EncryptedAnsibleVault as well as used -Path and -Value... seems to happen regardless.

Test script:

$path = 'test.txt'
$key = 'test' | ConvertTo-SecureString -AsPlainText -Force
$content = Get-Content -Path $path
Write-Output "'$($content)'"
Write-Output "'$($content.length)'"

#Encrypt
$content = Get-EncryptedAnsibleVault -Path $path -Password $key
$content | Out-File -encoding UTF8 "$($path)" -NoNewline
#Decrypt
$content = Get-DecryptedAnsibleVault -Path $path -Password $key
$content | Out-File -encoding UTF8 "$($path)" -NoNewline

$content = Get-Content -Path $path
Write-Output "'$($content)'"
Write-Output "'$($content.length)'"

Output:

PS C:\Users\slwillia\Desktop> C:\Users\slwillia\Desktop\vault.ps1
'testContent'
'11'
'testContent'
'12'

The issue here is due to the UTF-8 BOM character that appears in front of the file which is placed there when Windows uses -Encoding UTF8 on Out-File. What this BOM character does is tell programs that the encoding of this file is in UTF-8 and to parse it accordingly. Having a BOM for UTF-8 is not actually recommended but Microsoft does it anyway https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom. To prove this, you can run;

$plaintext_path = "C:\path\to\test.txt"
$bytes = [System.IO.File]::ReadAllBytes($plaintext_path)
$bytes[0..3]

This should output 239, 187, 191 which is the decimal representation of the UTF-8 BOM bytes \xEF\xBB\xBF.

Unless the BOM is needed, rarely is, you should be creating your initial plaintext file without the UTF-8 BOM. Usually you can just specify the -Encoding ASCII with Out-File but if you need UTF-8 without the BOM you can't use that cmdlet. There are 3 easy ways to get this going if UTF-8 is required;

# First way is to copy the file as is from `ansible-vault`, unix doesn't usually write a BOM for UTF-8

# Second way is to set the initial plaintext file as UTF-8 but without the BOM
$encoding = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false
$plaintext_bytes = $encoding.GetBytes("testContent")
[System.IO.File]::WriteAllBytes("C:\path\to\test.txt", $plaintext_bytes)

# Third way is to pass in the file contents using the -Value parameter and bypass files altogether
$plaintext = Get-Content -Path C:\path\to\test.txt -Raw
Get-EncryptedAnsibleVault -Value $plaintext -password $key

The reason why this adds an extra char to your decrypted string is that Get-EncryptedAnsibleVault will read the file specified by the -Path parameter as raw bytes and doesn't need to worry about the encoding. This allows people to encrypt binary files or a file that is not UTF-8 encoded. In your example, it will read the file with BOM at the beginning and encrypt that. The cipher text will contain the BOM, just encrypted, and is the reason why Get-DecryptedAnsibleVault as well as ansible-vault will have that extra char at the beginning. Based on this, Get-DecryptedAnsibleVault is decrypted the vault exactly the way it should be.

This unfortunately doesn't help you too much as there is no -OutPath for that cmdlet and it currently needs to return a string. In your case, making sure the file has no BOM in the beginning will solve your case. In the meantime I will look at adding an -OutPath parameter which will allow you to output the decrypted vault file straight from the byte source without worrying about the encoding.

Thank you for the swift response, the following works for me:

# Credit to jborean93 for use of his library:
# https://github.com/jborean93/PowerShell-AnsibleVault
# You may need to run:
# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
# Optional Usage:
# .\vault.ps1 -path 'secret.txt' -key 'somesecret' -decrypt

param (
	[Parameter(Mandatory=$true)][string]$path,
	[string]$key,
	[switch]$decrypt = $False # Defaults to Encrypting mode
)

if (-not (Get-Module "AnsibleVault")) {
    Write-Host "AnsibleVault Module needs to be installed:"
    Write-Host "Install-Module -Name AnsibleVault"
}

if ($key){
    $securekey = $key | ConvertTo-SecureString -AsPlainText -Force
} else {
    $securekey = Read-Host -AsSecureString "Provide encryption key"
}

$content = Get-Content -Path $path -Raw

if (-not $content) {
    Write-Host "The path: '$($path)' could not be read."
} else {
    if($decrypt){
         Write-Host "Decrypting content of '$($path)'"
        $content = Get-DecryptedAnsibleVault -Value $content -Password $securekey
    } else {
        Write-Host "Encrypting content of '$($path)'"
        $content = Get-EncryptedAnsibleVault -Value $content -Password $securekey
    }
    $content | Out-File "$($path)" -NoNewline
    Write-Host "Wrote content to '$($path)'"
}