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.
We can thus write a little class that wraps all this up:
class Encryptor
def initialize(key, salt)
passphrase = ActiveSupport::KeyGenerator.new(key).generate_key(salt)
@encryptor = ActiveSupport::MessageEncryptor.new(passphrase)
end
def encrypt(plaintext)
@encryptor.encrypt_and_sign(plaintext)
end
def decrypt(encrypted_data)
@encryptor.decrypt_and_verify(encrypted_data)
end
end
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:
encryptor = Encryptor.new("my_secret_key", "my_secret_salt")
first_value = encryptor.encrypt("secret message") #=> "TnQvV2p0MTlTWXA2SzZ3Rk5IVi8wQjVtcldwMFZJZ0pTSHRIZ2J6bkZaST0tLVkxZVhuR2dERXo0eDU1clBBcTBXZFE9PQ==--7f56992cc41378ec8df2c74342b9ef1b68a40673"
second_value = encryptor.encrypt("secret message") #=> "ZFRnMVhYT3IxVW84MTFxQ0t4NEd6bW5GdUpxQXN2Q1ZvT3pCL3hvN2o0ND0tLXhXenlpbS85R2JnSVExSEEyREN0WWc9PQ==--9a2ef4de0b0afa04bbbe370b7c887efa4fa9873b"
encryptor.decrypt(first_value) #=> "secret message"
encryptor.decrypt(second_value) #=> "secret message"
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):
def generate_random_values
10.times.each_with_object({}) do |number, acc|
index = sprintf "%02d", number
acc[index] = SecureRandom.hex(64)
end
end
This method can then be used to generate a local YAML file for your development/test environments, or for ENV variables in production.
Development/test
As a one-time task, you’ll need to write out a YAML file for your keys/salts:
["keys", "salts"].each do |filename|
my_secrets = {
development: generate_random_values,
test: generate_random_values
}
File.write Rails.root.join("config", "secret_#{filename}.yml"), my_secrets.to_yaml
end
We follow the database.yml
format of having a YAML file “namespaced” by environment:
development:
00: 022b5940ca52e6d45e7f...
01: 8f2938d4d3ac844e1c3a...
# (snip)
test:
00: 273681232d8fe0c9ec9f...
01: ee01cc498888607c59c1...
# (snip)
This format means we can use a neat Rails method for loading these values into our app:
# config/initializers/load_secret_stuff.rb
SECRET_KEYS = Rails.application.config_for("secret_keys")
SECRET_SALTS = Rails.application.config_for("secret_salts")
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:
encryption_type = "KEY" # or "SALT"
puts 10.times.each_with_object({}) do |number, acc|
index = sprintf "%02d", number
acc[index] = %Q{<%= ENV["MY_SECRET_#{encryption_type}_#{index}\"] %>}
end.to_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:
require "platform-api" # gem for the Heroku API
task :send_secrets_to_heroku do
# See https://github.com/heroku/platform-api#a-real-world-example for instructions on how to
# obtain an OAuth key from Heroku
heroku = PlatformAPI.connect_oauth("your_heroku_oauth_key_here") # don't commit this key!
# Keys
generate_random_values.each_with_index do |key, index|
puts "Uploading key #{index}: #{key}"
heroku.config_var.update("name_of_your_app_on_heroku", "MY_SECRET_KEY_#{index}" => key)
end
# Salts
generate_random_values.each_with_index do |salt, index|
puts "Uploading salt #{index}: #{salt}"
heroku.config_var.update("name_of_your_app_on_heroku", "MY_SECRET_SALT_#{index}" => salt)
end
end
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.
Image by Rama on Wikimedia Commons