A Simple Way to Encrypt Data in Rails without Gem

 
Rails secure data encryption and decryption is represented by a safe. Photo by Gabriel Wasylko on Unsplash

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
Make sure to store both 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
ActiveRecord Team model must have an encrypted_api_token database column

Reusable 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.



Back to index