Encrypting Data in .NET apps Using AWS Key Management Service
Let’s be honest, cryptography can be a dense and intimidating topic for most. When contemplating your cryptography strategy, there are so many questions that need to be answered: symmetric or asymmetric encryption? Which algorithm should you use? What will the key management story be? etc. – etc….
Utilizing a key management service like AWS Key Management Service (or KMS) is a great way to simplify your cryptography strategy. AWS KMS supports customer managed keys as well as keys managed by AWS, key rotation, symmetric or asymmetric encryption, et. al.
The Solution
In this tutorial, we’ll go over a frequently used strategy utilizing AWS KMS for key management, utilizing symmetric keys for data encryption. In this strategy, KMS Keys are stored in AWS. These Keys, are used to generate “data” keys. We will then encrypt our data with the data key and then send our encrypted data across the wire along side the encrypted data key. On the other end of the wire we will use the same AWS Key to decrypt the data key and then decrypt the payload with the decrypted data key.
Remember, for any example solution from AWS with .NET, we focus on the code that exemplifies the problem we are trying to solve. We don’t include logging, input validation, exception handling, etc., and we embed the configuration data within classes instead of using environment variables, configuration files, key/value stores and the like. These items should not be skipped for proper solutions.
Prerequisites
To complete this solution, you will need the .NET CLI which is included in the .NET SDK. In addition, you will need to download the AWS CLI and configure your environment. You will also need to create an AWS KMS Key and create an IAM user with programmatic access with the appropriate permissions to generate data keys and to decrypt.
Warning: some AWS services may have fees associated with them.
Create Common Abstractions
Let’s start out by creating a project named Abstractions that will contain common interfaces used across multiple projects. We’ll create the project using the following command:
$ dotnet new classlib --name Abstractions
The Abstractions project will have the following interfaces.
IEncrypter: defines one method named Encrypt that takes a string to encrypt as input and returns a task of type IEncryptionPackage.
public interface IEncrypter
{
Task<IEncryptionPackage> Encrypt(string plainText);
}
IDecrypter: defines one method that takes an IEncryptionPackage based class and returns a task of type string.
public interface IDecrypter
{
Task<string> Decrypt(IEncryptionPackage encryptionPackage);
}
IEncryptionPackage: defines two properties, CipherText and EncryptedKey.
public interface IEncryptionPackage
{
string CipherText { get; set; }
string EncryptedKey { get; set; }
}
Crypto Implementations
With the abstractions in place, let’s now create a few implementations that will allow us to encrypt and decrypt data. These implementations will be created in a project named Crypto and we can create the project at the CLI like so:
$ dotnet new classlib --name Crypto
The Crypto project is going to require one package and we can add it like so:
$ dotnet add Crypto/ package AWSSDK.KeyManagementService
We also need to add a dependency on our Abstractions project.
$ dotnet add Crypto/ reference Abstractions/
Encryption Package
The first implementation will be the EncryptionPackage which implements IEncryptionPackage. The purpose of this class is to be a vessel for the cipher text and the encrypted key.
using Abstractions;
namespace Crypto
{
public class EncryptionPackage : IEncryptionPackage
{
public string CipherText { get; set; }
public string EncryptedKey { get; set; }
}
}
For this solution we will be using AES, or Advanced Encryption Standard, to encrypt and decrypt data. AES uses initialization vectors which are usually unique and/or randomly generated. In this example solution we will be “hard coding” the initialization vector on both the encrypting and decrypting portions of this solution. You can get more information on initialization vectors here.
Let’s first take a look at how we will encrypt the data.
AES Encrypter
Let’s create a class in the Crypto project named AESEncrypter that implements IEncrypter and in that class we’ll create a few fields.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Abstractions;
using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
namespace Crypto
{
public class AESEncrypter : IEncrypter
{
private readonly string _keyId = "<your-key>";
private readonly byte[] _iv =
new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
public async Task<IEncryptionPackage> Encrypt(string plainText)
{
}
}
}
Let’s take a look at those fields.
_keyId: should be set to the ARN of the KMS Key that you created during the prerequisites.
_iv: represents the initialization vector for AES encryption. This field will be set to a byte array of: { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }.
Let’s now create an Encrypt method. First, we create a local variable to hold the encrypted data. Next, we create a request to KMS for a data key. Then, once the response is received, we create local variables for the plain text key and the encrypted key and set the variables to values pulled from the data key response.
byte[] encryptedData;
AmazonKeyManagementServiceClient kmsClient = new AmazonKeyManagementServiceClient();
GenerateDataKeyRequest dataKeyRequest = new GenerateDataKeyRequest()
{
KeyId = _keyId,
KeySpec = DataKeySpec.AES_256
};
GenerateDataKeyResponse dataKeyResponse = await kmsClient.GenerateDataKeyAsync(dataKeyRequest);
byte[] encryptedDataKey = dataKeyResponse.CiphertextBlob.ToArray();
byte[] plainTextKey = dataKeyResponse.Plaintext.ToArray();
The following block essentially reads the plain text into a StreamWriter. That stream then gets encrypted by the CryptoStream and then written to the MemoryStream. As a last step we convert the MemoryStream to a byte array.
Notice the using statements. Using statements are used so that the unmanaged resources are correctly disposed of.
using (Aes aes = Aes.Create())
{
ICryptoTransform cryptoTransform = aes.CreateEncryptor(plainTextKey, _iv);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream =
new CryptoStream(
memoryStream,
cryptoTransform,
CryptoStreamMode.Write)
)
{
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
await streamWriter.WriteAsync(plainText);
}
encryptedData = memoryStream.ToArray();
}
}
}
The last step is to convert our encrypted data to a string and package it inside an IEncryptionPackage based object along with the encrypted data key. Then, we finally return the IEncryptionPackage.
string encryptedString = Convert.ToBase64String(encryptedData);
IEncryptionPackage encryptionPackage = new EncryptionPackage{
CipherText = encryptedString,
EncryptedKey = Convert.ToBase64String(encryptedDataKey)
};
return encryptionPackage;
Let’s take a look at the complete Encrypt method.
public async Task<IEncryptionPackage> Encrypt(string plainText)
{
byte[] encryptedData;
GenerateDataKeyRequest dataKeyRequest = new GenerateDataKeyRequest()
{
KeyId = _keyId,
KeySpec = DataKeySpec.AES_256
};
GenerateDataKeyResponse dataKeyResponse =
await _kmsClient.GenerateDataKeyAsync(dataKeyRequest);
byte[] encryptedDataKey = dataKeyResponse.CiphertextBlob.ToArray();
byte[] plainTextKey = dataKeyResponse.Plaintext.ToArray();
using(Aes aes = Aes.Create()){
ICryptoTransform cryptoTransform = aes.CreateEncryptor(plainTextKey, _iv);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream,
cryptoTransform, CryptoStreamMode.Write))
{
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
await streamWriter.WriteAsync(plainText);
}
encryptedData = memoryStream.ToArray();
}
}
}
string encryptedString = Convert.ToBase64String(encryptedData);
IEncryptionPackage encryptionPackage = new EncryptionPackage{
CipherText = encryptedString,
EncryptedKey = Convert.ToBase64String(encryptedDataKey)
};
return encryptionPackage;
}
And, let’s take a look at the complete AESEncrypter class.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Abstractions;
using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
namespace Crypto
{
public class AESEncrypter : IEncrypter
{
private readonly string _keyId = "<your-key>";
private readonly byte[] _iv =
new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
public async Task<IEncryptionPackage> Encrypt(string plainText)
{
byte[] encryptedData;
AmazonKeyManagementServiceClient kmsClient =
new AmazonKeyManagementServiceClient();
GenerateDataKeyRequest dataKeyRequest = new GenerateDataKeyRequest()
{
KeyId = _keyId,
KeySpec = DataKeySpec.AES_256
};
GenerateDataKeyResponse dataKeyResponse =
await kmsClient.GenerateDataKeyAsync(dataKeyRequest);
byte[] encryptedDataKey = dataKeyResponse.CiphertextBlob.ToArray();
byte[] plainTextKey = dataKeyResponse.Plaintext.ToArray();
using (Aes aes = Aes.Create())
{
ICryptoTransform cryptoTransform =
aes.CreateEncryptor(plainTextKey, _iv);
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream =
new CryptoStream(
memoryStream,
cryptoTransform,
CryptoStreamMode.Write)
)
{
using (
StreamWriter streamWriter =
new StreamWriter(cryptoStream)
)
{
await streamWriter.WriteAsync(plainText);
}
encryptedData = memoryStream.ToArray();
}
}
}
string encryptedString = Convert.ToBase64String(encryptedData);
IEncryptionPackage encryptionPackage = new EncryptionPackage
{
CipherText = encryptedString,
EncryptedKey = Convert.ToBase64String(encryptedDataKey)
};
return encryptionPackage;
}
}
}
AES Decrypter
Now, we need to create an IDecrypter implementation. Let’s create a class named AESDecrypter in the Crypto project and again create a couple fields to work with. These fields represent the same data as in the AESEncrypter.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Abstractions;
using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
namespace Crypto
{
public class AESDecrypter : IDecrypter
{
private readonly string _keyId = "<your-key>";
private readonly byte[] _iv =
new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
}
}
Let’s now take a look at the AESDecrypter. Here, the Decrypt method does all the work. Let’s fill out a Decrypt method. The first step is to Decrypt the encrypted data key by making a call to the KMS SDK.
AmazonKeyManagementServiceClient kmsClient = new AmazonKeyManagementServiceClient();
MemoryStream ciphertextBlob = new MemoryStream(Convert.FromBase64String((encryptionPackage.EncryptedKey)));
DecryptRequest decryptRequest = new DecryptRequest()
{
CiphertextBlob = ciphertextBlob,
KeyId = _keyId
};
DecryptResponse decryptResponse = await kmsClient.DecryptAsync(decryptRequest);
byte[] key = decryptResponse.Plaintext.ToArray();
With the data key decrypted, we can now decrypt the cipher text.
The following block may look similar to what we saw in the AESEncrypter. However, there are a few big differences. Here we first convert the textual data to a byte array. We then instantiate an Aes object, and then a new MemoryStream, passing in the byte array that we just created. We then instantiate a CryptoStream passing in the MemoryStream, an ICryptoTransform (the value returned from the AES Decryptor method) and the CryptoStreamMode.Read enum value. We then create a StreamReader to read the cipher text into plain text and then return the plain text.
string plainText = String.Empty;
byte[] byteData = Convert.FromBase64String(encryptionPackage.CipherText);
using (Aes aes = Aes.Create())
{
using (MemoryStream memoryStream = new MemoryStream(byteData))
{
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream, aes.CreateDecryptor(key, _iv), CryptoStreamMode.Read))
{
using (StreamReader streamReader = new StreamReader(cryptoStream))
{
plainText = streamReader.ReadToEnd();
}
}
}
}
return plainText;
Here’s the Decrypt method in its entirety.
public async Task<string> Decrypt(IEncryptionPackage encryptionPackage)
{
AmazonKeyManagementServiceClient kmsClient = new AmazonKeyManagementServiceClient();
MemoryStream ciphertextBlob = new
MemoryStream(Convert.FromBase64String((encryptionPackage.EncryptedKey)));
DecryptRequest decryptRequest = new DecryptRequest()
{
CiphertextBlob = ciphertextBlob,
KeyId = _keyId
};
DecryptResponse decryptResponse = await kmsClient.DecryptAsync(decryptRequest);
byte[] key = decryptResponse.Plaintext.ToArray();
string plainText = String.Empty;
byte[] byteData = Convert.FromBase64String(encryptionPackage.CipherText);
using (Aes aes = Aes.Create())
{
using (MemoryStream memoryStream = new MemoryStream(byteData))
{
using (CryptoStream cryptoStream =
new CryptoStream(
memoryStream,
aes.CreateDecryptor(key, _iv),
CryptoStreamMode.Read)
)
{
using (StreamReader streamReader = new StreamReader(cryptoStream))
{
plainText = streamReader.ReadToEnd();
}
}
}
}
return plainText;
}
And, here’s the complete AESDecrypter class.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Abstractions;
using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
namespace Crypto
{
public class AESDecrypter : IDecrypter
{
private readonly string _keyId = "<your-key>";
private readonly byte[] _iv =
new byte[] { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
public async Task<string> Decrypt(IEncryptionPackage encryptionPackage)
{
AmazonKeyManagementServiceClient kmsClient =
new AmazonKeyManagementServiceClient();
MemoryStream ciphertextBlob =
new MemoryStream(
Convert.FromBase64String((encryptionPackage.EncryptedKey))
);
DecryptRequest decryptRequest = new DecryptRequest()
{
CiphertextBlob = ciphertextBlob,
KeyId = _keyId
};
DecryptResponse decryptResponse =
await kmsClient.DecryptAsync(decryptRequest);
byte[] key = decryptResponse.Plaintext.ToArray();
string plainText = String.Empty;
byte[] byteData = Convert.FromBase64String(encryptionPackage.CipherText);
using (Aes aes = Aes.Create())
{
using (MemoryStream memoryStream = new MemoryStream(byteData))
{
using (CryptoStream cryptoStream =
new CryptoStream(
memoryStream,
aes.CreateDecryptor(key, _iv),
CryptoStreamMode.Read)
)
{
using (
StreamReader streamReader =
new StreamReader(cryptoStream)
)
{
plainText = streamReader.ReadToEnd();
}
}
}
}
return plainText;
}
}
}
.NET Test Applications
Now that we have our abstractions and implementations created, let’s create a couple test apps.
Encryption App
With the following command, let’s create a .NET console application to test out the encryption side of things.
$ dotnet new console --name Client
With the new app created, let’s add the dependencies.
$ dotnet add Client/ reference Abstractions/ Crypto/
Within Program.cs, let’s modify the Main method by following these steps:
- Write to the console asking the user for input.
- Read from the console and capture the text entered into a variable. This text is the text we’ll encrypt and send across the wire.
- New up an AESEncrypter object.
- Create a local IEncryptionPackage variable and assign the value of a call to the AESEncrypter.Encrypt method, passing in the text we just captured.
- Send the IEncryptionPackage based object across the wire by serializing it into JSON encoded text, then using an HttpClient, send it to the API app. More on that below.
private const string RequestUri = "http://localhost:5000/Message";
static async Task Main(string[] args)
{
Console.WriteLine("Enter message:");
var message = Console.ReadLine();
Console.WriteLine("Original:" + message);
IEncrypter encrypter = new AESEncrypter();
IEncryptionPackage encryptionPackage = await encrypter.Encrypt(message);
string jsonString = JsonSerializer.Serialize(encryptionPackage);
using (HttpClient httpClient = new HttpClient())
{
var httpContent = new StringContent(
jsonString,
Encoding.UTF8,
"application/json"
);
httpClient.DefaultRequestHeaders.Accept.Clear();
httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("text/plain")
);
HttpResponseMessage response =
await httpClient.PostAsync(_requestUri, httpContent);
string decryptedString = await response.Content.ReadAsStringAsync();
Console.Write("Decrypted: " + decryptedString);
}
}
Decryption App
For Decryption we’ll use a simple API as the reference application. Let’s create it with the command below:
$ dotnet new webapi --name Api
Like the .NET console application, the API project needs to take a dependency on the Abstractions and Crypto projects and we can add those dependencies with the following command:
$ dotnet add Api/ reference Abstractions/ Crypto/
We’ll create a MessageController class with one method, ReceiveMessage, that we can send data to. The ReceiveMessage method will have the responsibility of using the AESDecrypter class to decrypt the data it receives. The code breaks down like so:
[HttpPost]
public async Task<IActionResult> ReceiveMessage(EncryptionPackage encryptionPackage){
if(encryptionPackage == null ||
encryptionPackage.CipherText == null ||
encryptionPackage.EncryptedKey == null){
return BadRequest();
}
IDecrypter decrypter = new AESDecrypter();
string plainText = await decrypter.Decrypt(encryptionPackage);
Console.WriteLine("Decrypted:" + plainText);
return Ok(plainText);
}
Now that we have all the code and dependencies in place, let’s create a solution file.
$ dotnet new sln --name kms
And, let’s add our projects to the solution file like this:
$ dotnet sln add Abstractions/ Crypto/ Api/ Client/
And then, let’s build the solution.
$ dotnet build
Great, everything builds. Let’s run the API application at the CLI.
$ dotnet run --project Api/
You should see something like this:
Building…
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: /kms/Api
Now, let’s open a new CLI and run the .NET console app, “Client”, with a similar command.
$ dotnet run --project Client/
With both the API and Client Apps running, let’s give everything a test. In the “Client” app, enter the text, “test” and hit enter, and you should see the following result:
Enter message:
test
Original: test
Decrypted: test
Challenges
Now that you have a working solution, let’s take things a little further…
- Modify the Api application to use configuration and dependency injection.
- Modify the Client and Api apps to use different encryption techniques.
- Debug through the Client and Api apps to see the change in the data.
You have completed the solution, learning to create an AWS KMS Key from the CLI. You also learned how to generate data keys and encrypt and decrypt data in .NET using KMS generated data keys.