Public/Get-DecryptedAnsibleVault.ps1

# Copyright: (c) 2018, Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)

Function Get-DecryptedAnsibleVault {
    <#
    .SYNOPSIS
    Decrypt an Ansible Vault string and return the plaintext as a string.
    
    .DESCRIPTION
    This cmdlet will take in a Ansible Vault string and return the decrypted
    value.
    
    .PARAMETER Path
    [String] The path to a file whose contents will be decrypted. This is
    mutually exclusive to the Value parameter.
    
    .PARAMETER Value
    [String] The string value to decrypt. This is mutually exclusive to the
    Path parameter.
    
    .PARAMETER Password
    [SecureString] The password used to decrypt the value with

    .PARAMETER Encoding
    [System.Text.Encoding] The string encoding of the decrypted bytes returned
    by this cmdlet. By default will be UTF8 but if the original plaintext was
    read as a different encoding type then this can override the encoding to
    what is needed.

    .INPUTS
    [String] You can pipe the encrypted vault string to decrypt.

    .OUTPUTS
    [String] The decrypted vault string.
    
    .EXAMPLE
    # Create the secure string that stores the vault password
    $password = Read-Host -AsSecureString

    # get the decrypted vault string from file contents
    Get-DecryptedAnsibleVault -Path C:\temp\vault.txt -Password $password

    # create a vault string from a string
    Get-DecryptedAnsibleVault -Value $vault_text -Password $password

    # send the string to encrypt as a pipeline input
    $vault_Text | Get-DecryptedAnsibleVault -Password $password

    # decrypt vault that had the original plaintext encoded as UTF-16
    Get-DecryptedAnsibleVault -Value $vault_text -Password $password -Encoding ([System.Text.Encoding]::Unicode)
    
    .NOTES
    This only supports the vault versions 1.1 and 1.2. These version are mostly
    identical but 1.2 is used when the Id parameter is specified. This should
    be interoperable with the ansible-vault code used by Ansible itself.
    #>

    [CmdletBinding(DefaultParameterSetName="ByPath")]
    [OutputType([String])]
    param(
        [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByPath")] [String]$Path,
        [Parameter(Position=0, Mandatory=$true, ParameterSetName="ByValue", ValueFromPipeline, ValueFromPipelineByPropertyName)] [String]$Value,
        [Parameter(Position=1, Mandatory=$true)] [SecureString]$Password,
        [Parameter()] [System.Text.Encoding]$Encoding = [System.Text.Encoding]::UTF8
    )

    $vault_text = switch ($PSCmdlet.ParameterSetName) {
        ByPath {
            $pwd_path = Join-Path -Path $pwd -ChildPath $Path
            if (Test-Path -Path $pwd_path -PathType Leaf) {
                [System.IO.File]::ReadAllText($pwd_path)
            } else {
                [System.IO.File]::ReadAllText($Path)
            }
        }
        ByValue { $Value }
    }

    if ($null -eq $vault_text) {
        throw [System.ArgumentException]"Failed to get vault text to decrypt"
    }
    if (-not $vault_text.StartsWith('$ANSIBLE_VAULT;')) {
        throw [System.ArgumentException]"Vault text does not start with the header `$ANSIBLE_VAULT;"
    }

    $version, $cipher, $id, $cipher_bytes = Get-VaultHeader -Value $vault_text

    # The salt, hmac and encrypted bytes value are split by \n, we need to
    # split by that char to get the actual values
    $salt, $hmac, $encrypted_bytes = Split-Byte -Value $cipher_bytes -Char ([char]"`n") -MaxSplit 2

    $salt = Convert-HexToByte -Value ([System.Text.Encoding]::UTF8.GetString($salt))
    $expected_hmac = [System.Text.Encoding]::UTF8.GetString($hmac)
    $encrypted_bytes = Convert-HexToByte -Value ([System.Text.Encoding]::UTF8.GetString($encrypted_bytes))

    $cipher_key, $hmac_key, $nonce = New-VaultKey -Password $password -Salt $salt

    $actual_hmac = Get-HMACValue -Value $encrypted_bytes -Key $hmac_key
    if ($actual_hmac -ne $expected_hmac) {
        throw [System.ArgumentException]"HMAC verification failed, was the wrong password entered?"
    }

    $decrypted_bytes = Invoke-AESCTRCycle -Value $encrypted_bytes -Key $cipher_key -Nonce $nonce

    # Need to manually remove the padding as AES CTR has no concept of padding
    # it is a stream mode
    $unpadded_bytes = Remove-Pkcs7Padding -Value $decrypted_bytes -BlockSize 128
    $decrypted_string = $Encoding.GetString($unpadded_bytes)

    return $decrypted_string
}