Encrypting ActiveRecords with Lockbox in Rails 6.1

Before I dive in, I will give you a bit of context.

I am currently working on an application that uses the Stellar blockchain behind the scenes to open up access to capital to farmers in developing countries like Zimbabwe.

The purpose of the blockchain is to provide transparency from the moment the investor parts with their hard-earned money, to the time the farmer starts sharing profits with investors. By nature, blockchain is very complex and the average farmer can not be bothered to worry about setting up wallets, ensuring their keys are safely secured, and all the jargon that is associated with blockchain technologies.

It is essential that we hide the complexity from the end-users, and make their experiences as seamless as possible. Sort of how when we browse websites, we do not even think about entangled mess of TCP/IP connections happening, or worse still the 1s and 0s being executed by CPUs in data centers all over the world. This is how we apply conceptual compression to make our systems easier and delightful to use.

This application I am talking about is built on Ruby on Rails. And we will be the custodians of the crypto wallets of both the farmers and the investors. However, this is fraught with risk as crypto exchanges are prone to hacking, but it is a risk we acknowledge and are working on adding layers of security to mitigate that risk.

The first one being, Encryption! It would be very irresponsible to store seed phrases (a.k.a secret keys) to the crypto wallets in plain text. Hackers would have one hell of a tea party and thank the heavens for such an easy hack. When I saw ActiveRecord encrypted attributes I was happy because Rails has always been the battery included framework that came with everything you needed out of the box. However, I was disappointed to discover it was only available for Rails 7, which is yet to be officially released.

So I decided to go on with lockbox, a modern encryption gem for Ruby and Rails, created by one highly reputable Andrew Kane.

Install Lockbox

Add the gem to your Gemfile.

# Gemfile.rb

gem 'lockbox'

Then run bundle install to complete your installation.

Setup your application to encrypt using Lockbox

It's very important to try this on your local machine first, then on a staging server, then finally on your production server, to avoid loss of (sometimes irrecoverable) data - @kudapara

Generate a key

In the project directory run rails console and run the following code to generate the key that will be used to encrypt your records.

Lockbox.generate_key

# "daa846ef907a5b44a6b83ac6991a9ff63126b869b2f97afc43d8329b46328abe"

Copy the generated key and use it in your preferred way to handle secrets in your Rails application, either

1. rails credentials

Add your generated key to your credentials for each environment (rails credentials:edit --environment for Rails 6+)

lockbox:
  master_key: daa846ef907a5b44a6b83ac6991a9ff63126b869b2f97afc43d8329b46328abe

2. rails credentials without environment specific master_key

or create config/initializers/lockbox.rb with something like

lockbox_master_key = Rails.application.credentials.lockbox[:master_key]
Lockbox.master_key = lockbox_master_key

The lockbox_master_key could be coming from rails credentials or a key management service like VaultProject.

3. Use LOCKBOX_MASTER_KEY environment variable (not recommended in production)

LOCKBOX_MASTER_KEY="daa846ef907a5b44a6b83ac6991a9ff63126b869b2f97afc43d8329b46328abe"

Migrate the model you want to encrypt attributes for

class AddSeedCiphertextToAccounts < ActiveRecord::Migration[6.1]
  def change
    add_column :accounts, :seed_ciphertext, :text

    # add this line if you are encrypting an existing column
    Lockbox.migrate(User)
  end
end

and add the following code to your model

class Account < ApplicationRecord
  encrypts :seed, migrating: true

  # for existing columns. remove this line after dropping seed column
  self.ignored_columns = ["seed"]
end

Take note of the migrating:true. That is to ensure you encrypt an existing column without downtime. If you are adding a new sensitive column to your model, you can ignore it.

Commit your changes and run the migrations. Confirm that your secret fields have been encrypted properly and everything is working without issue.

Once you are satisfied that all is well, you can now add the migration to drop the original column that stored the data in plaintext.

class DropSeedFromAccounts < ActiveRecord::Migration[6.1]
  def change
    remove_column :accounts, :seed
  end
end

Once you run this migration, there is no turning back, that original unencrypted data is gone forever, please, please, I beg, verify 10 times if possible before running this migration.

Then you're good to go. No need to change anywhere else in your codebase. It still works the same as before, so code like

current_user.accounts.create(address: blockchain_account.address, seed: blockchain_account.seed, account_type: 'regular')

will still work, and the seed will be automatically encrypted.

or

Stellar::KeyPair.from_seed(current_user.accounts.regular.first.seed)

will still work and the seed will be automatically decrypted.

This is the first step in the right direction. We are still exploring more ways we can secure this system from internal threats, but for now, this is good to go.

πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯

Thank you for reading, If you enjoyed this content you can donate some Stellar Lumens (XLM) or USDC to my stellar wallet

28