Encrypting secrets with Rails

Our new project Digestive uses the APIs of various services to construct progress reports for web agencies to send to their clients. In order to access said APIs, we need to store customers’ third-party API keys, and since these keys allow access to potentially sensitive information, we didn’t want to store them in plain text in the database. This meant we needed to encrypt them.

CAVEAT: Always refer to established best practice when doing anything related to security, and use well-established encryption libraries/software – don’t try to roll your own!

ActiveSupport::MessageEncryptor

Fortunately, Rails has a useful helper class for just this purpose: ActiveSupport::MessageEncryptor (backed by Ruby’s OpenSSL bindings), which takes a key and uses it to encrypt/decrypt a string. So that you can avoid using the same key every time you encrypt something, Rails also has a ActiveSupport::KeyGenerator, which can generate keys based on a base secret and a salt.

Since ActiveSupport::MessageEncryptor uses a random initialisation vector, the encrypted data produced will be different every time – this is something to keep in mind when testing your code – but will always decrypt to the same value:

Key/salt generation

Ideally, you would get the key for the encryption from the user – this is generally how two-factor authentication works – but since that’s unfeasible if we want to access APIs without the user’s permission every time, we need to store the key somewhere. That somewhere needs to different from where the encrypted values are stored, meaning that an attacker would have to compromise two sources to get all the information needed (in this case, the database plus whenever the secret is stored).

Storing production keys in your codebase (Git repo) is a big no-no, so most people go for storing keys in the Unix environment. In our case, we wanted a variety of keys and salts, so that not every API key was encrypted with the same key/salt, giving us no. of keys * no. of salts combinations to encrypt with.

As an example, here’s a little method for generating keys/salts (in this case, we create 10 different values):

The #config_for method takes the name of a YAML file in your config directory, runs it through ERB, then returns the values under the environment key. This method was only introduced in Rails 4.2.0, but the functionality isn’t too hard to reproduce for earlier versions.

Production

For production environments, we want to retrieve the keys/salts from the ENV, so we need to modify our secret_{keys,salts}.yml files to use good ol’ ERB interpolation:

# secret_{keys,salts}.yml# Append this to the bottom:production:00:<%= ENV["MY_SECRET_{KEY, SALT}_00"] %>01:<%= ENV["MY_SECRET_{KEY, SALT}_01"] %># ...and so on

To save typing this out by hand, here’s a quick script to generate the necessary YAML:

Now that we’ve got a way to load the keys/salts from the environment, we need to generate them and add them to production. We’re using Heroku, so we can use their API to upload the keys/salts to our app:

Bear in mind that Heroku limits you to 16KB of ENV data, so you can’t go crazy and store thousands of key/salt pairs. The idea is to have enough that you have a reasonable number of combinations to choose from (for example, 10 salts and 10 keys gives you 100 different combinations). How you choose which key/salt to use will depend on your app, but you want something that always gives you the same key/salt pair. Something like the created_at timestamp of a model (which doesn’t change), would be a good choice.

You probably want to keep a copy of these keys in a very safe place in case Heroku goes down spectacularly and leaves you unable to decrypt any information, such as an encrypted volume not available to the internet, a bank vault, down an abandoned mineshaft etc.

Conclusion

There’s no such thing as perfect security, but at least separating the storage of your keys/secrets from your encrypted data gives you a little more piece of mind that an SQL injection attack won’t walk off with all your customers’ sensitive data.

Security is also a moving target, and so we’re always looking for ways to make our app more secure. We make sure we make full use of Rails’ SQL-escaping for all queries, and audit Digestive on every push and pull request with CodeClimate to help us catch any obvious security holes we may have missed.

If you’re interested in finding out more about Digestive, head over to Digestive.io.