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

Encrypting Secrets using PowerShell and AWS KMS

AWS Key Management Service (KMS) is an Amazon managed service that makes it easy for you to create and control encryption keys that you can then use to encrypt data.  A lot of the AWS services natively integrate with KMS e.g. S3, but I wanted to use a KMS key to encrypt a secret (e.g. a password) that I could store inside a configuration file and decrypt it when required.

To do this I created a two PowerShell functions, one for encryption and one for decryption, that I can embed in scripts.  The encryption function will securely transfer your plaintext to KMS, KMS will encrypt the data and return an encrypted memory stream which I convert to a base64 string to make it easy to store in text/XML/JSON.

The decrypt function takes a previously encrypted base64 string, converts and sends it to KMS to decrypt (note you don’t have to tell KMS which key is required to decrypt) and KMS returns a plaintext memory stream which I convert back to a UTF8 encoded string.

I generally use these functions during userdata execution (boot time) on an AWS EC2 instance to decrypt secrets that I need to configure the instance and/or applications, but you could use this on any windows machine.  To support the use of IAM Roles on EC2 instances, I have made the access/secret key parameters optional i.e. if you don’t pass an access/secret key the function will attempt to use the privileges provided by the IAM role applied to the EC2 instance, assuming you are running the function on EC2.

Function to Encrypt

function Invoke-KMSEncryptText
(
	[Parameter(Mandatory=$true,Position=1,HelpMessage='PlainText to Encrypt')]
	[string]$plainText,
	[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
)
{
	# memory stream
	[byte[]]$byteArray = [System.Text.Encoding]::UTF8.GetBytes($plainText)
	$memoryStream = New-Object System.IO.MemoryStream($byteArray,0,$byteArray.Length)
	# splat
	$splat = @{Plaintext=$memoryStream; KeyId=$keyID; Region=$Region;}
	if(![string]::IsNullOrEmpty($AccessKey)){$splat += @{AccessKey=$AccessKey;}}
	if(![string]::IsNullOrEmpty($SecretKey)){$splat += @{SecretKey=$SecretKey;}}
	# encrypt
	$encryptedMemoryStream = Invoke-KMSEncrypt @splat
	$base64encrypted = [System.Convert]::ToBase64String($encryptedMemoryStream.CiphertextBlob.ToArray())
	return $base64encrypted
}

Function to Decrypt

function Invoke-KMSDecryptText
(
	[Parameter(Mandatory=$true,Position=1,HelpMessage='CipherText base64 string to decrypt')]
	[string]$cipherText,
	[Parameter(Mandatory=$true,Position=2)]
	[string]$region,
	[Parameter(Position=3)]
	[string]$AccessKey,
	[Parameter(Position=4)]
	[string]$SecretKey
)
{
	# memory stream
	$encryptedBytes = [System.Convert]::FromBase64String($cipherText)
	$encryptedMemoryStreamToDecrypt = New-Object System.IO.MemoryStream($encryptedBytes,0,$encryptedBytes.Length)
	# splat
	$splat = @{CiphertextBlob=$encryptedMemoryStreamToDecrypt; Region=$Region;}
	if(![string]::IsNullOrEmpty($AccessKey)){$splat += @{AccessKey=$AccessKey;}}
	if(![string]::IsNullOrEmpty($SecretKey)){$splat += @{SecretKey=$SecretKey;}}
	# decrypt
	$decryptedMemoryStream = Invoke-KMSDecrypt @splat
	$plainText = [System.Text.Encoding]::UTF8.GetString($decryptedMemoryStream.Plaintext.ToArray())
	return $plainText
}

Below is some sample code that makes use of the functions, simply fill in the access/secret keys, the KMS Master key you want to use for encryption and the region where the key is stored.  Obviously you should consider handling your plaintext more securely than I am here, but this serves as a simple test.

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 = ''
$plainText = 'Secret'

# Encrypt some plain text and write to host
$cipherText = Invoke-KMSEncryptText -plainText $plainText -keyID $keyID -Region $Region -AccessKey $AccessKey -SecretKey $SecretKey
Write-host $cipherText

# Decrypt the cipher text and write to host
$plainText = Invoke-KMSDecryptText -cipherText $cipherText -Region $Region -AccessKey $AccessKey -SecretKey $SecretKey
Write-host $plainText

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