A Simple Way to Encrypt Data in Rails without Gem Updated Jul 14, 2020

2 minute read

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.