📦 Modern encryption for Rails
- Uses state-of-the-art algorithms
- Works with database fields, files, and strings
- Makes migrating existing data and key rotation easy
Lockbox aims to make encryption as friendly and intuitive as possible. Encrypted fields and files behave just like unencrypted ones for maximum compatibility with 3rd party libraries and existing code.
Learn the principles behind it, how to secure emails with Devise, and how to secure sensitive data in Rails.
Add this line to your application’s Gemfile:
gem 'lockbox'
Generate a key
Lockbox.generate_key
Store the key with your other secrets. This is typically Rails credentials or an environment variable (dotenv is great for this). Be sure to use different keys in development and production. Keys don’t need to be hex-encoded, but it’s often easier to store them this way.
Set the following environment variable with your key (you can use this one in development)
LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
or create config/initializers/lockbox.rb
with something like
Lockbox.master_key = Rails.application.credentials.lockbox_master_key
Then follow the instructions below for the data you want to encrypt.
Create a migration with:
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :email_ciphertext, :text
end
end
Add to your model:
class User < ApplicationRecord
encrypts :email
end
You can use email
just like any other attribute.
User.create!(email: "hi@example.org")
If you need to query encrypted fields, check out Blind Index.
Fields are strings by default. Specify the type of a field with:
class User < ApplicationRecord
encrypts :born_on, type: :date
encrypts :signed_at, type: :datetime
encrypts :opens_at, type: :time
encrypts :active, type: :boolean
encrypts :salary, type: :integer
encrypts :latitude, type: :float
encrypts :video, type: :binary
encrypts :properties, type: :json
encrypts :settings, type: :hash
encrypts :messages, type: :array
end
Note: Use a text
column for the ciphertext in migrations, regardless of the type
Lockbox automatically works with serialized fields for maximum compatibility with existing code and libraries.
class User < ApplicationRecord
serialize :properties, JSON
store :settings, accessors: [:color, :homepage]
attribute :configuration, CustomType.new
encrypts :properties, :settings, :configuration
end
For StoreModel, use:
class User < ApplicationRecord
encrypts :configuration, type: Configuration.to_type
after_initialize do
self.configuration ||= {}
end
end
Validations work as expected with the exception of uniqueness. Uniqueness validations require a blind index.
You can use encrypted attributes in fixtures with:
test_user:
email_ciphertext: <%= User.generate_email_ciphertext("secret").inspect %>
Be sure to include the inspect
at the end or it won’t be encoded properly in YAML.
Lockbox makes it easy to encrypt an existing column without downtime.
Add a new column for the ciphertext, then add to your model:
class User < ApplicationRecord
encrypts :email, migrating: true
end
Backfill the data in the Rails console:
Lockbox.migrate(User)
Then update the model to the desired state:
class User < ApplicationRecord
encrypts :email
# remove this line after dropping email column
self.ignored_columns = ["email"]
end
Finally, drop the unencrypted column.
If adding blind indexes, mark them as migrating
during this process as well.
class User < ApplicationRecord
blind_index :email, migrating: true
end
Create a migration with:
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[6.0]
def change
add_column :action_text_rich_texts, :body_ciphertext, :text
end
end
Create config/initializers/lockbox.rb
with:
Lockbox.encrypts_action_text_body(migrating: true)
Migrate existing data:
Lockbox.migrate(ActionText::RichText)
Update the initializer:
Lockbox.encrypts_action_text_body
And drop the unencrypted column.
Add to your model:
class User
field :email_ciphertext, type: String
encrypts :email
end
You can use email
just like any other attribute.
User.create!(email: "hi@example.org")
If you need to query encrypted fields, check out Blind Index.
You can migrate existing data similarly to Active Record.
Add to your model:
class User < ApplicationRecord
has_one_attached :license
encrypts_attached :license
end
Works with multiple attachments as well.
class User < ApplicationRecord
has_many_attached :documents
encrypts_attached :documents
end
There are a few limitations to be aware of:
- Metadata like image width and height are not extracted when encrypted
- Direct uploads cannot be encrypted
To serve encrypted files, use a controller action.
def license
user = User.find(params[:id])
send_data user.license.download, type: user.license.content_type
end
Note: This feature is experimental. Please try it in a non-production environment and share how it goes.
Lockbox makes it easy to encrypt existing files without downtime.
Add to your model:
class User < ApplicationRecord
encrypts_attached :license, migrating: true
end
Migrate existing files:
Lockbox.migrate(User)
Then update the model to the desired state:
class User < ApplicationRecord
encrypts_attached :license
end
Add to your uploader:
class LicenseUploader < CarrierWave::Uploader::Base
encrypt
end
Encryption is applied to all versions after processing.
You can mount the uploader as normal. With Active Record, this involves creating a migration:
class AddLicenseToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :license, :string
end
end
And updating the model:
class User < ApplicationRecord
mount_uploader :license, LicenseUploader
end
To serve encrypted files, use a controller action.
def license
user = User.find(params[:id])
send_data user.license.read, type: user.license.content_type
end
Encrypt existing files without downtime. Create a new encrypted uploader:
class LicenseV2Uploader < CarrierWave::Uploader::Base
encrypt key: Lockbox.attribute_key(table: "users", attribute: "license")
end
Add a new column for the uploader, then add to your model:
class User < ApplicationRecord
mount_uploader :license_v2, LicenseV2Uploader
before_save :migrate_license, if: :license_changed?
def migrate_license
self.license_v2 = license
end
end
Migrate existing files:
User.find_each do |user|
if user.license? && !user.license_v2?
user.migrate_license
user.save!
end
end
Then update the model to the desired state:
class User < ApplicationRecord
mount_uploader :license, LicenseV2Uploader, mount_on: :license_v2
end
Finally, delete the unencrypted files and drop the column for the original uploader. You can also remove the key
option from the uploader.
Generate a key
key = Lockbox.generate_key
Create a lockbox
lockbox = Lockbox.new(key: key)
Encrypt files before passing them to Shrine
LicenseUploader.upload(lockbox.encrypt_io(file), :store)
And decrypt them after reading
lockbox.decrypt(uploaded_file.read)
For models, encrypt with:
license = params.require(:user).fetch(:license)
user.license = lockbox.encrypt_io(license)
To serve encrypted files, use a controller action.
def license
user = User.find(params[:id])
send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
end
Generate a key
key = Lockbox.generate_key
Create a lockbox
lockbox = Lockbox.new(key: key)
Encrypt
ciphertext = lockbox.encrypt(File.binread("file.txt"))
Decrypt
lockbox.decrypt(ciphertext)
Generate a key
key = Lockbox.generate_key
Create a lockbox
lockbox = Lockbox.new(key: key, encode: true)
Encrypt
ciphertext = lockbox.encrypt("hello")
Decrypt
lockbox.decrypt(ciphertext)