After building an encryption/key-rotation scheme to allow the safe storage of bank account information in Reverb, I thought I’d share some of the things I’ve learned. Let me preface by saying that storing sensitive user data in your database is a topic that shouldn’t be taken lightly. Please consult a security specialist before implementing anything discussed here in your live product. With that out of the way, let’s dig in!

Encryption with AES and attr_encrypted

Encrypted data is obfuscated and retrieved by a secret digital key, usually a sufficiently long, random string. AES-256 is a highly recommend algorithm for this. Here’s an example of encrypting and decrypting some data using the aes gem:

Using encryption like this in Rails is made simple with the attr_encrypted gem. For my example, I’m going to make a table called “encrypted_fields” that will be a general table for any encrypted data we have. This will make it easier for us to later deal with key rotation.

The IV and salt columns are random bits of data that give added protection against brute force attacks. Without them, once a hacker knows how one piece of data was encrypted, they can decrypt ALL the data (read more about IVs and salts). attr_encrypted supports the use of these with the ‘per_attribute_iv_and_salt’ mode.

You’ll notice that the above table has a foreign key to another table I’ve called ‘data_encryption_keys’. In this scheme there are in fact 2 keys that you need to unlock the data. The Data Encryption Key (DEK) encrypts your data, and a second key — the Key Encryption Key (KEK) in turn encrypts the DEK. These 2 keys should be stored in separate locations as an added layer of security. Here’s the schema for the DEK:

Again, we can encrypt the key with attr_encrypted. The boolean ‘primary’ flag indicates which key should be used for any new data that needs encryption. This time, we use the KEK for encryption, which is stored outside of the database.

Let’s generate a primary DEK and encrypt some data!

Key Rotation

With the ability to store encrypted blobs in our database, we still need a way to “rotate” our keys. This means creating a new key and re-encrypting all of the old key’s data with it. There are generally 2 schedules for rotating keys:

KEK — Rotate whenever someone with access leaves your company, or you suspect database information has been leaked. This is the quickest rotation to perform because you only need to re-encrypt at most a few DEKs. DEK — Rotate once a year. This may take longer to perform if you have a lot of encrypted data.

Let’s take a look at our DEK rotation. Our strategy is as follows:

Generate a new DEK Promote the new key as the new primary Re-encrypt all EncryptedFields with the new key Cleanup any unused keys

Now in code:

Since each encrypted field has a foreign key to a DEK, if our rotation task blows up when we’ve only re-encrypted half of our fields, we are still able to retrieve all of our data, and we can simply re-run a new rotation at any time. This benefit could also be applied to KEK rotation by giving our DEK’s some sort of unique representation of the KEK used to encrypt it (a name, for example). Rotating your KEK would be a very similar process and would depend on your storage, so I won’t cover it here.

Bonus Round — Encrypt Something!

We’ve got all the tools we need, let’s look at how we might use our encrypted fields in our app. For this example, let’s try to encrypt our users’s SSNs. Since we have a separate table for our encrypted data, we’ll need a foreign key on our users table.

With this, we can treat ‘ssn’ as any other attribute on our user, and it will automatically be encrypted and stored in our encrypted_fields table. Voila!

Of course with a little metaprogramming this could be wrapped into a neat module for use across your app, but I’ll leave that as an exercise for the reader :)

EDIT: User Technion commented that the default CBC mode utilized by attr_encrypted can be susceptible to certain attacks when not using message authentication. See this stackoverflow post for more info. Note that the encryption algorithm can be easily be configured for this gem (docs).

—

Joe Kurleto, Reverb.com