Encrypting ActiveRecords with Lockbox in Rails 6.1
Native ActiveRecord encryption is coming in Rails 7, but in the meantime, LockBox to the rescue.
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.
Add the gem to your Gemfile.
# Gemfile.rb gem 'lockbox'
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
config/initializers/lockbox.rb with something like
lockbox_master_key = Rails.application.credentials.lockbox[:master_key] Lockbox.master_key = lockbox_master_key
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)
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.
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.