Encrypting Files using PowerShell and AWS KMS

In my last post I explained how to encrypt secrets e.g. passwords, using AWS Key Management Service (KMS) so you could store encrypted passwords securely in config files.  The next obvious question which I have been asked is “Can you encrypt whole files using KMS?”.

The first answer to this question is yes, very easily if you store them in S3; however your use case may not allow you to store your files in S3 and you need a way to store the files encrypted using KMS but not in S3, this is where a method call “Envelope Encryption” can be used.

Envelope encryption works by using a combination of two keys, your master KMS key and a randomly generated Data key that the KMS service creates on-demand for you.  When requesting a Data key from KMS, you are supplied with both an unencrypted and an encrypted (with the KMS master-key) version of the Data key.

The Data key is a symmetric encryption key that is used to encrypt files locally on your machine, symmetric encryption is very fast but as the same key is used to encrypt and decrypt your data you need to protect the symmetric key somehow; in this case the Data key is protected by encrypting it with the Master key.  Once you have encrypted the local file with the unencrypted Data key, you throw it away and store the encrypted data key with the encrypted file.

To decrypt the file, it is a two-step process use: KMS to decrypt the encrypted Data key and then use the decrypted Data key to decrypt the file.  There is a detailed explanation of how envelope encryption works here.

Below are the two functions required to encrypt and decrypt a file using KMS envelope encryption.  I been able to use them to encrypt a file, confirmed the contents is unreadable, and then decypt the file to get it back to it’s original state.

Function to Encrypt

function Invoke-KMSEncryptFile
(
[Parameter(Mandatory=$true,Position=1,HelpMessage='PlainText to Encrypt')]
[ValidateScript({(Test-Path $_) -eq $true})]
[string]$filePath,
[Parameter(Mandatory=$true,Position=2,HelpMessage='GUID of Encryption Key in KMS')]
[string]$keyID,
[Parameter(Mandatory=$true,Position=3)]
[string]$region,
[Parameter(Position=4)]
[string]$AccessKey,
[Parameter(Position=5)]
[string]$SecretKey,
[Parameter(Position=6,HelpMessage='Name of output file')]
[ValidateScript({(Test-Path $_) -eq $false})]
[string]$outPath,
[Parameter(Position=7,HelpMessage='Encryption Strength')]
[ValidateSet('AES_128','AES_256')]
[string]$KeySpec = 'AES_128'
)
{
# Unecrypted Byte Array
$byteArray = [System.IO.File]::ReadAllBytes($filePath)

# memory stream for unencrypted file
$memoryStream = New-Object System.IO.MemoryStream

# splat
$splat = @{keyID=$keyID; Region=$Region; KeySpec=$KeySpec}
if(![string]::IsNullOrEmpty($AccessKey)){$splat += @{AccessKey=$AccessKey;}}
if(![string]::IsNullOrEmpty($SecretKey)){$splat += @{SecretKey=$SecretKey;}}
# Get KMS DataKey
try {
$DataKey = New-KMSDataKey @splat
}
catch {
throw "Failed to get DataKey from KMS key"
}
$encryptedDataKey = $DataKey.CiphertextBlob.ToArray()

# Symetric encryption of file
$cryptor = New-Object -TypeName System.Security.Cryptography.AesManaged
$cryptor.Mode = [System.Security.Cryptography.CipherMode]::CBC
$cryptor.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$cryptor.KeySize = 128
$cryptor.BlockSize = 128

$iv = $cryptor.IV

$cs = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $cryptor.CreateEncryptor($DataKey.PlainText.ToArray(),$iv), [System.Security.Cryptography.CryptoStreamMode]::Write)
$cs.Write($byteArray,0,$byteArray.Length)
$cs.FlushFinalBlock()
$encryptedContent = $memoryStream.ToArray()

# Create new byte array that should contain both unencrypted iv, encrypted data, encrypted KMS key
$result = New-Object Byte[] ($encryptedDataKey.length + $iv.Length + $encryptedContent.Length)

# copy Data Key, IV and encrypted file arrays into single array
$currentPosition = 0

[System.Buffer]::BlockCopy($encryptedDataKey, 0, $result, $currentPosition, $encryptedDataKey.Length)
$currentPosition += $encryptedDataKey.Length

[System.Buffer]::BlockCopy($iv, 0, $result, $currentPosition, $iv.Length)
$currentPosition += $iv.Length

[System.Buffer]::BlockCopy($encryptedContent, 0, $result, $currentPosition, $encryptedContent.Length)
$currentPosition += $encryptedContent.Length

if ([string]::IsNullOrEmpty($outPath))
{
$finalOutPath = $filePath + '.enc'
}
else
{
$finalOutPath = $outPath
}

# Write bytes to file
[System.IO.File]::WriteAllBytes($finalOutPath,$result)

return $finalOutPath;
}

Function to Decrypt

function Invoke-KMSDecryptFile
(
 [Parameter(Mandatory=$true,Position=1,HelpMessage='File to decrypt')]
 [ValidateScript({(Test-Path $_) -eq $true})]
 [string]$filePath,
 [Parameter(Mandatory=$true,Position=2)]
 [string]$region,
 [Parameter(Position=3)]
 [string]$AccessKey,
 [Parameter(Position=4)]
 [string]$SecretKey,
 [Parameter(Position=5,HelpMessage='Name of output file')]
 [ValidateScript({(Test-Path $_) -eq $false})]
 [string]$outPath,
 [Parameter(Position=6,HelpMessage='Encryption Strength')]
 [ValidateSet('AES_128','AES_256')]
 [string]$KeySpec = 'AES_128'
)
{
 # read encrypted file to Byte Array
 $cipherTextArray = [System.IO.File]::ReadAllBytes($filePath)
# setup byte arrays
 $DataKeyLength = switch ($KeySpec)
 {
 'AES_128' {151}
 'AES_256' {167}
 }
 $encryptedDataKey = New-Object Byte[] $DataKeyLength
 $iv = New-Object Byte[] 16
 $encryptedContent = New-Object Byte[] ($cipherTextArray.length - ($DataKeyLength + 16))

 # split file array into 3 to retrieve encrypted Key, IV and Data
 $currentPosition = 0

 [System.Buffer]::BlockCopy($cipherTextArray, $currentPosition, $encryptedDataKey, 0, $encryptedDataKey.length)
 $currentPosition += $encryptedDataKey.Length

 [System.Buffer]::BlockCopy($cipherTextArray, $currentPosition, $iv, 0, $iv.length)
 $currentPosition += $iv.Length

 [System.Buffer]::BlockCopy($cipherTextArray, $currentPosition, $encryptedContent, 0, $encryptedContent.Length)
 $currentPosition += $encryptedContent.Length

 # memory stream for Datakey
 $encryptedMemoryStreamToDecrypt = New-Object System.IO.MemoryStream($encryptedDataKey,0,$encryptedDataKey.Length)

 # splat
 $splat = @{CiphertextBlob=$encryptedMemoryStreamToDecrypt; Region=$Region;}
 if(![string]::IsNullOrEmpty($AccessKey)){$splat += @{AccessKey=$AccessKey;}}
 if(![string]::IsNullOrEmpty($SecretKey)){$splat += @{SecretKey=$SecretKey;}}

 # decrypt key
 try {
 $decryptedMemoryStream = Invoke-KMSDecrypt @splat
 }
 catch {
 throw "Failed to decrypt KMS key"
 }
 $DataKey = $decryptedMemoryStream.Plaintext.ToArray()

 # memory stream for file
 $memoryStream = New-Object System.IO.MemoryStream
# Symmetric decryption of file
 $cryptor = New-Object -TypeName System.Security.Cryptography.AesManaged
 $cryptor.Mode = [System.Security.Cryptography.CipherMode]::CBC
 $cryptor.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
 $cryptor.KeySize = 128
 $cryptor.BlockSize = 128

 $cs = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $cryptor.CreateDecryptor($DataKey,$iv), [System.Security.Cryptography.CryptoStreamMode]::Write)
 $cs.Write($encryptedContent,0,$encryptedContent.Length)
 $cs.FlushFinalBlock()
 $decryptedArray = $memoryStream.ToArray()

 if ([string]::IsNullOrEmpty($outPath))
 {
 $finalOutPath = $filePath + '.dec'
 }
 else
 {
 $finalOutPath = $outPath
 }

 # Write bytes to file
 [System.IO.File]::WriteAllBytes($finalOutPath,$decryptedArray)

 Return $finalOutPath
}

Below is some sample code that makes use of the functions, simply fill in the locations of the files, the encryption strength (AES_128 or AES_256), the access/secret keys, the KMS Master key you want to use for encryption and the region where the key is stored.

Import-Module awspowershell
# set your credentials to access AWS, key you want to encrypt with, and the region the key is stored
$AccessKey = ''
$SecretKey = ''
$Region = 'eu-west-1'
$keyID = '' # GUID
$KeySpec = 'AES_128' # AES_128 or AES_256
# Encrypt File
$encryptedFile = Invoke-KMSEncryptFile -filePath "C:\temp\testfile.txt" -keyID $keyID -KeySpec $KeySpec -Region $Region -AccessKey $AccessKey -SecretKey $SecretKey
Write-Host $encryptedFile
# Decrypt File
$decryptedFile = Invoke-KMSDecryptFile -filePath "C:\temp\testfile.txt.enc" -outPath "C:\temp\testfile2.txt" -KeySpec $KeySpec -Region $Region -AccessKey $AccessKey -SecretKey $SecretKey
Write-Host $decryptedFile

THIS POSTING AND CODE RELATED TO IT ARE PROVIDED “AS IS” AND INFERS NO WARRANTIES OR RIGHTS, USE AT YOUR OWN RISK