/mar 1, 2015

Crypto Bliss with AWS KMS

By Jason Nichols

So you've got a last minute requirement to secure your customer data by encrypting it at the database level? Don't panic. Take a deep breath, keep calm, and read on...

Previously, I discussed some of the higher level concerns and pitfalls with attempting to roll your own key management. In today's post I'm going to dive into the details about how to do it without reinventing the wheel.

Let's start out by describing how the system will work. KMS offers x things that makes it easy to secure data:

  1. Generate both Master and Data keys
  2. Encrypt/decrypt small blocks of data using the master key.
  3. Generate random data

These three crypto primitives go along way in getting us to our endgame. Let's start with keys.

Storing the key with the data

As I said above, AWS allows you encrypt/decrypt small amounts of data (up to 4k in size). This is described within the API. At first glance, you may be thinking "What good is that, my data is much larger!"

Fear not, we'll use a technique called envelope encryption which is both secure and bandwidth efficient. Basically we're going to store an encrypted key along with our data. Let's lay out a formula using the following setup:

  • p = the plaintext (data) you wish to encrypt
  • c = the ciphertext (encrypted data)
  • Kd = the key used to encrypt the data
  • Cd = Kd in ciphertext form
  • Km = your AWS master key
  • e() = the encryption algorithm
  • d() = the decryption algorithm

With envelope encryption, we encrypt the data with Kd, then encrypt the key itself using the master key Km. Then both data key Kd and c are stored together until decryption.

As a formula it would look as such

c = e(p, Kd)

So the ciphertext is a function of the encryption algorithm, the plaintext, and the key. That's crypto 101. But instead of trying to keep Kd stashed somewhere hidden, let's encrypt that with our AWS master key (which resides on AWS hardware within an HSM, and is never revealed):

Cd = e(Kd, Km)

With Kd now safely encypted as Cd, we can store that along with our cipertext in the database. KMS makes this quite easy, as we can use a single API call to generate Kd, and return it to us with a ciphertext version already encrypted with Km:

GenerateDataKeyRequest dataKeyRequest = new GenerateDataKeyRequest();

GenerateDataKeyResult dataKeyResult = kmsClient.generateDataKey(dataKeyRequest);
ByteBuffer plaintextKey = dataKeyResult.getPlaintext();
ByteBuffer encryptedKey = dataKeyResult.getCiphertextBlob();

AWS_ALGORITHM is a String constant set to AES_256.

At this point we have our data key Kd in both plaintext and ciphertext form and we're almost ready to encrypt our data with it. We have one more task to deal with first: We need an initialization vector! We have two ways to get random data, use Java's SecureRandom or the KMS API. Unfortunately, SecureRandom is not without its pitfalls, for this reason we'll use the KMS API to generate random data (random data supplied by the KMS HSM should be quite random):

// A 128 byte IV supplied by Amazon KMS
GenerateRandomRequest randomRequest = new GenerateRandomRequest().withNumberOfBytes(16);
GenerateRandomResult randomResult = kmsClient.generateRandom(randomRequest);
byte[] iv = toArray(randomResult.getPlaintext());

With our plaintext data key, our randomly generated IV, and our data, let's set up the AES cipher, encrypt the data, and then save it as a hex string. This is all straight Java crypto:

// Set up the Cipher using the specified algorihtm and the AWS generated IV.
SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, ALGORITHM);

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));

byte[] cipherText = cipher.doFinal(data);
String cipherTextStr = Hex.encodeHexString(cipherText);

We use a hex String to store our ciphertext because in our case we're serializing to JSON.

If you're paying attention, you may realize that using this setup, every single row in the database will be encrypted with a different data key! This is correct and helps insulate each row's data from each other.

When storing the ciphertext in a DB, you'll want to store the encryptedKey ByteBuffer and the IV along with it. The encrypted key actually has basic key metadata with it which is used when it's time to decrypt the key.

Decryption is straightforward:

// Start with decrypting the data key
DecryptRequest decryptRequest = new DecryptRequest().withCiphertextBlob(ByteBuffer.wrap(encryptedKey));
DecryptResult decryptResult = kmsClient.decrypt(decryptRequest);
byte[] aesKey = toArray(decryptResult.getPlainText());

Decrypt the actual data:

// Set up the Cipher using the specified algorihtm and the AWS generated IV.
SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, ALGORITHM);

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));

byte[] cipherText = cipher.doFinal(Hex.decodeHex(cipherTextStr.toCharArray()));

Important: ensure that you now zero our any sensitive data (such as your aesKey):

* Zero out a byte array, overwriting its previous contents.
* @param b The byte array to zero out.  This parameter may be null.
private void zero(byte[] b) {
  if (b != null) {
    for (int i = 0 ; i < b.length ; i++) {
      b[i] = 0;

You don't want to leave unencrypted keys sitting around in memory and visible in a debugger or heap dump!

Lastly, we use a utility method to convert ByteBuffers to byte[] values (such as in aesKey):

private byte[] toArray(ByteBuffer bb) {
  byte[] b = new byte[bb.remaining()];
  return b;

With this, your data is now encrypted. There is one large piece missing though: we aren't guaranteeing message integrity with an HMAC! We're relying on the AES decryption to fail (eg: give us invalid data) in the event that the ciphertext is altered (either maliciously or accidentally). In my next blog post I'll demonstrate how to incorporate this into your setup.

Happy coding!


Related Posts

By Jason Nichols