Lockbox

:package: 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

Build Status

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.

Types

Specify the type of a field with:

class User < ApplicationRecord
  encrypts :born_on, type: :date
  encrypts :signed_at, type: :datetime
  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 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
  encrypts :properties
end

Validations

Validations work as expected with the exception of uniqueness. Uniqueness validations require a 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

message = 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(message)

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.default_options = {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 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.

Padding

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

Reference

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

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 of encrypts_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: