
Storing sensitive data in plaintext can seriously harm your internet business if an attacker gets hold of the database. Encrypting data is also a GDPR friendly best practice. In this tutorial I will describe a simple way to securely encrypt, store, and decrypt data using built in Ruby on Rails helpers instead of external dependencies.
Avoid heavy Gem dependencies
attr_encrypted gem is a popular tool for storing encrypted data in Rails apps. The problem is that adding it to your application includes over 2k external lines of code. What’s worse is that the project has not been updated for several months at the time of writing.
Rails offers a handy ActiveSupport::MessageEncryptor
class, that hides away all the complexity of data encryption, and can be wrapped in a simple to use service object or reusable module.
Custom encryption service object
Let’s start with implementing a service object class doing the actual heavy lifting, but only exposing two straightforward public class methods encrypt
and decrypt
:
class EncryptionService
KEY = ActiveSupport::KeyGenerator.new(
ENV.fetch("SECRET_KEY_BASE")
).generate_key(
ENV.fetch("ENCRYPTION_SERVICE_SALT"),
ActiveSupport::MessageEncryptor.key_len
).freeze
private_constant :KEY
delegate :encrypt_and_sign, :decrypt_and_verify, to: :encryptor
def self.encrypt(value)
new.encrypt_and_sign(value)
end
def self.decrypt(value)
new.decrypt_and_verify(value)
end
private
def encryptor
ActiveSupport::MessageEncryptor.new(KEY)
end
end
SECRET_KEY_BASE
and ENCRYPTION_SERVICE_SALT
somewhere safe otherwise, you would not be able to decrypt your secure data!You can use this code to generate a secure ENCRYPTION_SERVICE_SALT
value:
SecureRandom.random_bytes(
ActiveSupport::MessageEncryptor.key_len
)
Now you can use the service directly in your models like that:
class Team < ApplicationRecord
...
def api_token
EncryptionService.decrypt(encrypted_api_token)
end
def api_token=(value)
self.encrypted_api_token = EncryptionService.encrypt(value)
end
end
encrypted_api_token
database columnReusable module using metaprogramming
If you want to encrypt attributes across different models, you could simplify using the encryption service with a bit of metaprogramming magic:
module Encryptable
extend ActiveSupport::Concern
class_methods do
def attr_encrypted(*attributes)
attributes.each do |attribute|
define_method("#{attribute}=".to_sym) do |value|
return if value.nil?
self.public_send(
"encrypted_#{attribute}=".to_sym,
EncryptionService.encrypt(value)
)
end
define_method(attribute) do
value = self.public_send("encrypted_#{attribute}".to_sym)
EncryptionService.decrypt(value) if value.present?
end
end
end
end
end
Now you can include it in your ActiveRecord models. You can also use it to encrypt multiple attributes of a single model as long as there is a correct corresponding database column:
class Team < ApplicationRecord
...
include Encryptable
attr_encrypted :api_token, :api_secret
end
Searching by encrypted values
One caveat when it comes to encrypting data is they it is no longer searchable by plaintext value. In theory, you could decrypt objects one by one to find a match, but that would be terribly inefficient.
The described approach generates a different hash each time, even for the same values. It means that it must not be used for attributes that you’d like to use for searching.
Summary
This post only scratches a surface of data encryption in Rails, but this simple approach should cover many of the common use cases. I am using this method to encrypt Slack API tokens in my side project Abot.
Check out the official docs for more advanced uses of ActiveSupport::MessageEncryptor
like rotating keys and data expiry. With high-level helpers built in directly in Rails, you should always think twice before relying on external dependencies for security-related features.