📦 Modern encryption for Rails
- Uses state-of-the-art algorithms
- Works with database fields, files, and strings
- Stores encrypted data in a single field
- Requires you to only manage a single encryption key
- Makes migrating existing data and key rotation easy
Learn the principles behind it, how to secure emails, and how to secure sensitive data in Rails
Add this line to your application’s Gemfile:
gem 'lockbox'
Generate an encryption 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
Alternatively, you can use a key management service to manage your keys.
Database fields
Files
Other
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.
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
end
Note: Always use a text
or binary
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 # [master]
encrypts :properties, :settings, :configuration
end
For StoreModel, use: [master]
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.
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.
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
send_data @user.license.download, type: @user.license.content_type
end
Add to your uploader:
class LicenseUploader < CarrierWave::Uploader::Base
encrypt
end
Encryption is applied to all versions after processing.
To serve encrypted files, use a controller action.
def license
send_data @user.license.read, type: @user.license.content_type
end
Create a box
box = Lockbox.new(key: key)
Encrypt files before passing them to Shrine
LicenseUploader.upload(box.encrypt_io(file), :store)
And decrypt them after reading
box.decrypt(uploaded_file.read)
For models, encrypt with:
license = params.require(:user).fetch(:license)
@user.license = box.encrypt_io(license)
To serve encrypted files, use a controller action.
def license
send_data box.decrypt(@user.license.read), type: @user.license.mime_type
end
Read the file as a binary string
message = File.binread("file.txt")
Then follow the instructions for encrypting a string below.
Create a box
box = Lockbox.new(key: key)
Encrypt
ciphertext = box.encrypt(message)
Decrypt
box.decrypt(ciphertext)
Decrypt and return UTF-8 instead of binary
box.decrypt_str(ciphertext)
Lockbox makes it easy to encrypt an existing column. 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.
To make key rotation easy, you can pass previous versions of keys that can decrypt.
For Active Record, use:
class User < ApplicationRecord
encrypts :email, previous_versions: [{key: previous_key}]
end
To rotate, use:
user.update!(email: user.email)
For Active Storage use:
class User < ApplicationRecord
encrypts_attached :license, previous_versions: [{key: previous_key}]
end
To rotate existing files, use:
user.license.rotate_encryption!
For CarrierWave, use:
class LicenseUploader < CarrierWave::Uploader::Base
encrypt previous_versions: [{key: previous_key}]
end
To rotate existing files, use:
user.license.rotate_encryption!
For strings, use:
Lockbox.new(key: key, previous_versions: [{key: previous_key}])
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.
This is the default algorithm. Rotate the key every 2 billion encryptions to minimize the chance of a nonce collision, which will expose the key.
You can also use XSalsa20, which uses an extended nonce so you don’t have to worry about nonce collisions. First, install Libsodium. For Homebrew, use:
brew install libsodium
And add to your Gemfile:
gem 'rbnacl'
Then add to your model:
class User < ApplicationRecord
encrypts :email, algorithm: "xsalsa20"
end
Make it the default with:
Lockbox.default_options = {algorithm: "xsalsa20"}
You can also pass an algorithm to previous_versions
for key rotation.
Heroku comes with libsodium preinstalled.
For Ubuntu 18.04, use:
sudo apt-get install libsodium23
For Ubuntu 16.04, use:
sudo apt-get install libsodium18
On Bionic, add to .travis.yml
:
addons:
apt:
packages:
- libsodium23
On Xenial, add to .travis.yml
:
addons:
apt:
packages:
- libsodium18
Add a step to .circleci/config.yml
:
- run:
name: install Libsodium
command: |
sudo apt-get install -y libsodium18
Hybrid cryptography allows servers to encrypt data without being able to decrypt it.
Follow the instructions above for installing Libsodium and including rbnacl
in your Gemfile.
Generate a key pair with:
Lockbox.generate_key_pair
Store the keys with your other secrets. Then use:
class User < ApplicationRecord
encrypts :email, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
end
Make sure decryption_key
is nil
on servers that shouldn’t decrypt.
This uses X25519 for key exchange and XSalsa20 for encryption.
The master key is used to generate unique keys for each column. This technique comes from CipherSweet. The table name and column name are both used in this process. If you need to rename a table with encrypted columns, or an encrypted column itself, get the key:
Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
And set it directly before renaming:
class User < ApplicationRecord
encrypts :email, key: ENV["USER_EMAIL_ENCRYPTION_KEY"]
end
You can use a key management service to manage your keys with KMS Encrypted.
class User < ApplicationRecord
encrypts :email, key: :kms_key
end
For CarrierWave, use:
class LicenseUploader < CarrierWave::Uploader::Base
encrypt key: -> { model.kms_key }
end
Note: KMS Encrypted’s key rotation does not know to rotate encrypted files, so avoid calling record.rotate_kms_key!
on models with file uploads for now.
Add padding to conceal the exact length of messages.
Lockbox.new(padding: true)
The block size for padding is 16 bytes by default. Change this with:
Lockbox.new(padding: 32) # bytes
Set default options in an initializer with:
Lockbox.default_options = {algorithm: "xsalsa20"}
For database fields, encrypted data is encoded in Base64. If you use binary
columns instead of text
columns, set:
class User < ApplicationRecord
encrypts :email, encode: false
end
It’s easy to read encrypted data in another language if needed.
For AES-GCM, the format is:
- nonce (IV) - 12 bytes
- ciphertext - variable length
- authentication tag - 16 bytes
Here are some examples.
For XSalsa20, use the appropriate Libsodium library.
Lockbox makes it easy to migrate from another library without downtime. The example below uses attr_encrypted
but the same approach should work for any library.
Let’s suppose your model looks like this:
class User < ApplicationRecord
attr_encrypted :name, key: key
attr_encrypted :email, key: key
end
Create a migration with:
class MigrateToLockbox < ActiveRecord::Migration[6.0]
def change
add_column :users, :name_ciphertext, :text
add_column :users, :email_ciphertext, :text
end
end
And add encrypts
to your model with the migrating
option:
class User < ApplicationRecord
encrypts :name, :email, migrating: true
end
Then run:
Lockbox.migrate(User)
Once all records are migrated, remove the migrating
option and the previous model code (the attr_encrypted
methods in this example).
class User < ApplicationRecord
encrypts :name, :email
end
Then remove the previous gem from your Gemfile and drop its columns.
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[6.0]
def change
remove_column :users, :encrypted_name, :text
remove_column :users, :encrypted_name_iv, :text
remove_column :users, :encrypted_email, :text
remove_column :users, :encrypted_email_iv, :text
end
end
0.2.0 brings a number of improvements. Here are a few to be aware of:
- Added
encrypts
method for database fields - Added support for XSalsa20
attached_encrypted
is deprecated in favor ofencrypts_attached
.
To switch to a master key, generate a key:
Lockbox.generate_key
And set ENV["LOCKBOX_MASTER_KEY"]
or Lockbox.master_key
.
Update your model:
class User < ApplicationRecord
encrypts_attached :license, previous_versions: [{key: key}]
end
New uploads will be encrypted with the new key.
You can rotate existing records with:
User.unscoped.find_each do |user|
user.license.rotate_encryption!
end
Once that’s complete, update your model:
class User < ApplicationRecord
encrypts_attached :license
end
View the changelog
Everyone is encouraged to help improve this project. Here are a few ways you can help:
- Report bugs
- Fix bugs and submit pull requests
- Write, clarify, or fix documentation
- Suggest or add new features
To get started with development and testing:
git clone https://github.com/ankane/lockbox.git
cd lockbox
bundle install
bundle exec rake test