Encrypted Attributes & RSpec

Posted

Among new features in Rails 7, ActiveRecord Encryption seemed to garner the response, “Wait, you mean it wasn’t already there?”

BCrypt can encrypt password data, and Rails has supported encrypted credentials for a while. A myriad of libraries encrypt arbitrary data, and third-party services offer HIPAA-compliant storage of health records1.

There are good reasons to encrypt certain kinds of data directly through a Rails application versus some third-party solution, so if you are going to do it there should be a consistent way to do it.

And that’s what we now have.

Enter the Encrypt API

Having a consistent API makes bespoke solutions less error-prone for reasons that programmers often overlook:

  1. Key rotation. I put this first because it is the last thing most developers consider. It surprises me how many developers consider this as an after-thought.
  2. Algorithm selection. Aside from the strength of encryption or cross-platform library support, should it be deterministic or non-deterministic?
  3. Querying. Related to the prior point, understanding ways in which we may need to query against the encrypted data.

Example

Let’s say we have this model. The username is encrypted deterministically so that we can look up a user by username.

  class Account < ApplicationRecord
    belongs_to :user

    encrypts :username, deterministic: true
    encrypts :password

    validates_presence_of :username, :password

The test is straightforward, using encrypted_attribute?:

    describe "encryption" do
      let!(:account) { create(:account, username: "foo1", password: "bar2") }

      it "encrypts username" do
        expect(account).to satisfy { |a| a.encrypted_attribute?(:username) }
      end

      it "encrypts password" do
        expect(account).to satisfy { |a| a.encrypted_attribute?(:password) }
      end
    end

Performing searches on a username can happen due to the deterministic flag being set. It’s important to note that once this is set, it can’t be un-set without needing to migrate any existing data.

Configuration & Key Rotation

Key rotation is also straightforward. Within your encrypted credentials, this block is initialized for you:

active_record_encryption:
  primary_key: (long cipher key)
  deterministic_key: (long text)
  key_derivation_salt: (long text)

Rotating keys requires turning primary_key into an array and appending a new key, like so:

active_record_encryption:
  primary_key:
    - (long cipher key)
    - xxx... # <- new key goes here
  deterministic_key: (long text)
  key_derivation_salt: (long text)

Other aspects…

Those are just a few aspects of how it works. There are other less-used but useful pieces of the API, such as being able to execute code with encryption/decryption disabled– handy for batch processing tasks where you want “pass-through” behavior for any encrypted fields.


  1. This is to say that encryption may be a technical solution, but its roots are in user privacy, legal liability, and financial exposure. Confused? Ask your manager! ↩︎