Encryption is not an easy task. You have to manage the encryption itself, key rotation and securing the keys. In this post, I’ll describe how we managed to encrypt our database columns and enable key rotation as well. This post won’t cover the key management or the secure storage for encryption keys. It will be covered in later post where we integrate it with AWS KMS.
Finding a library
Instead of writing our own wrapper for AES-256 encryption, we just look for a gem that supports encryption for table columns but also allows transparently encrypt/decrypt without much code changes. The attr_encrypted gem suits our needs. It has simple integration setup and flexible enough for our needs. However, it is not clear on how we rotate encryption keys.
The requirement is that, we need to be able to rotate encryption keys as well as be able to change the encryption method. It is not straightforward to do that with this gem. However, it is flexible enough to allow us to write our own encryption wrapper for AES-256-CBC.
KEK and DEK
KEK (Key Encryption Key) is a key used to encrypts keys.
DEK (Data Encryption Key) is a key used to encrypt data.
What we did is to have a KEK stored securely in our system (specifically in AWS KMS). To simplify, just imagine we have a secure way to fetch our KEK which serves as the master key for all our data. Then, we generate a new encryption key (DEK) used to encrypt the data. The DEK is stored within the encrypted data, but is encrypted with our KEK.
encrypted_ssn = 'encryption_method::key_name::encrypted_dek::salt::encrypted_data
encryption_method is the name mapped to a class that handles the encryption. Currently, we have two classes, one which is a wrapper to
AES-256-CBC and another one which is a wrapper to Rails’
MessageEncryptor. MessageEncryptor has a larger encrypted data than our AES wrapper, so we used the AES wrapper as our default encryption method.
key_name refers to the key name we used to encrypt the DEK. There are multiple key names to facilitate key rotation. The key itself is secured by a key management system which is, in our case, Amazon’s KMS.
encrypted_dek is the encrypted DEK generated when the data is about to be saved. It is encrypted by the encryption method used and the KEK used.
salt serves as the encryption IV.
encrypted_data is the data we encrypt using the encryption method, DEK and salt.
Decryption is easy. We just fetch back the stored data, identify the encryption method, key name, decrypt the DEK, then decrypt the data. If the default encryption method or KEK changed, decryption should still work since all the information is present as long as the old KEK is not removed from the key storage. However, saving the data again will use the new default.
As long as the KEK is still stored in the secure key storage, you should still be able to decrypt data using the old keys. To migrate to new key, you may want to create a migration task to update all encrypted data to use the new key.
User.all.each do |user| user.update!(ssn: user.ssn) end
What about attr_encrypted?
Well, attr_encrypted did a good job at wrapping this all together to allow us encrypt/decrypt the way we want without much changes to our existing models. We used the gem, but used a custom encryptor.
attr_encrypted :ssn, encryptor: OurOwnEncryptionService