Implementing Key Wrapping in Elixir with KMS
Recently, I was part of a team implementing a payments system in Elixir deployed on AWS. A payments system has to handle sensitive data that should not be stored in plaintext but needs to be accessed in plaintext after it is first stored. For example, credit card numbers, bank account numbers should not be stored in plaintext but the system needs access to the plaintext in order to process transactions. Based on our team’s experience building payments systems in other languages, we wanted to implement a data encryption scheme which provided key-wrapping of data-encryption keys and easy automated rotation of data encryption keys.
Key wrapping (also referred to as Envelope encryption) is the practice of using a hierarchy of keys to encrypt your systems data. One key, the data-encryption key is used to encrypt sensitive data in your system. This data-encryption key is stored encrypted alongside the data it encrypts. A second key, the key-encryption key is used to decrypt the data-encryption key before an application can use it. The key-encryption key is stored in a key management system and can’t be accessed directly. The key management service itself can be subject to a higher level of security and audit than individual applications.1
Key rotation is the practice of re-encrypting sensitive data using new encryption keys on a regular basis. Regular rotation of keys produces two main benefits. When encryption keys are regularly rotated, the quantity of data compromised by the leak of any single data-encryption key is minimized. Additionally, the regular rotation of keys limits the amount of encrypted data generated by any single key. The more encrypted data an attacker can gather, the more able the attacker is to uncover the encryption key through cryptanalysis.
We found that by combining tools provided by AWS and tools in the the Elixir ecosystem, we had the right building blocks to implement this encryption scheme. However, we had to tie them together in ways that weren’t already documented. I hope this post can serve as a resource to others looking to implement such a scheme and perhaps as a motivation to the maintainers of the Elixir encryption ecosystem to make it easier for users to implement stronger encryption systems.
To implement our database encryption scheme, we made use of the AWS Key
Management Service (KMS)2 and a couple of Elixir libraries.
is an Elixir library that uses
cloak4, an Elixir encryption library,
to provide column level encryption to
ecto, an Elixir toolkit for interacting
cloak_ecto provided many of the features we wanted, but did
not suppport key-wrapping out of the box. 5 We also made use of
ex_aws_kms7, Elixir libraries for interacting with AWS APIs.
cloak_ecto allows specifying any
cloak vault8 to be used to encrypt and
decrypt data. We implemented our own vault that would pull its keys from a
KeySource was a behaviour we specified as follows:
Our custom vault would pull keys from a
KeySource depending on how our
application was configured.
In non-deployed environments, we didn’t want to rely on AWS being available so
we used a static decrypted data-encryption key. This led to a very simple
In our deployed environments, we had a more complicated
KeySource. In these
KeySource queried the datbase for encrypted data-encryption
keys and decrypted them using AWS KMS. These keys could then be provided for use
cloak vault. In addition, this
KeySource determined if a new key
needed to be created, either because no data encryption keys exist or it is time
to rotate data encryption keys.
We were able to configure the lifespan of any single data-encryption key by
max_data_encryption_key_age_in_seconds. In our non-production
deployed environments, we configured this value to
3600 or one hour. This
enabled us to exercise key rotation frequently in our non-production environment
to verify it worked as intended. In our production environment, key rotation did
not happen as frequently.
We were quite pleased with the security and ergonomics of this solution. In particular, we were pleased that we didn’t need to manually intervene to do key rotation. That being said, this system was not run in production for a great deal of time so there may be dragons lurking.
All of the libraries that we used seemed to work quite well and provided the
building blocks we needed to implement this encryption schema. However, in
particular, I feel that
cloak_ecto should do more to educate their users.
cloak_ecto does document how you can inject a data-encryption key through the
runtime environment. Doing this would allow you to implement key-wrapping, but I
would like to see a bit more guidance to users about how to properly handle key
data. The handling of the data-encryption key is probably one of the most
important parts of designing your applications encryption scheme, and
cloak_ecto should give more guidance.
One can certainly implement key-wrapping using
cloak_ecto, as this blog post details, and indeed one can implement key-wrapping by following the patterns that
cloak_ectodocuments and recommends. I do think that
cloak_ectodocumentation should be slightly more robust and will make that case later on. ↩