Lockbox
:lock: 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
Check out this post for more info on its design, and this post for more info on securing sensitive data with Rails
Installation
Add this line to your application’s Gemfile:
gem 'lockbox'
Key Generation
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
Create a migration with:
class AddEmailCiphertextToUsers < ActiveRecord::Migration[5.2]
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: "[email protected]")
If you need to query encrypted fields, check out Blind Index.
Files
Active Storage
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
Note: With Rails 6, attachments are not encrypted with:
User.create!(avatar: params[:avatar])
Until this is addressed, use:
user = User.new
user.attach(params[:avatar])
user.save!
CarrierWave
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
Local Files
Read the file as a binary string
= File.binread("file.txt")
Then follow the instructions for encrypting a string below.
Strings
Create a box
box = Lockbox.new(key: key)
Encrypt
ciphertext = box.encrypt()
Decrypt
box.decrypt(ciphertext)
Migrating Existing Data
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.
Key Rotation
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}])
Fixtures
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.
Algorithms
AES-GCM
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.
XSalsa20
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. = {algorithm: "xsalsa20"}
You can also pass an algorithm to previous_versions
for key rotation.
XSalsa20 Deployment
Heroku
Heroku comes with libsodium preinstalled.
Ubuntu
For Ubuntu 16.04, use:
sudo apt-get install libsodium18
For Ubuntu 18.04, use:
sudo apt-get install libsodium23
Travis CI
On Xenial, add to .travis.yml
:
addons:
apt:
packages:
- libsodium18
CircleCI
Add a step to .circleci/config.yml
:
- run:
name: install Libsodium
command: |
sudo apt-get install -y libsodium18
Hybrid Cryptography
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-Poly1305 for encryption.
Key Separation
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
Key Management
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.
Compatibility
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.
Migrating from Another Library
Lockbox makes it easy to migrate from another library. 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[5.2]
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[5.2]
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
Upgrading
0.2.0
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
.
Optional
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
History
View the changelog
Contributing
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