README.md in lockbox-0.3.1 vs README.md in lockbox-0.3.2

- old
+ new

@@ -2,16 +2,16 @@ :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 (with the option to have more) - Makes migrating existing data and key rotation easy -Learn [the principles behind it](https://ankane.org/modern-encryption-rails), [how to secure emails](https://ankane.org/securing-user-emails-lockbox), and [how to secure sensitive data in Rails](https://ankane.org/sensitive-data-rails) +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](https://ankane.org/modern-encryption-rails), [how to secure emails with Devise](https://ankane.org/securing-user-emails-lockbox), and [how to secure sensitive data in Rails](https://ankane.org/sensitive-data-rails). + [![Build Status](https://travis-ci.org/ankane/lockbox.svg?branch=master)](https://travis-ci.org/ankane/lockbox) ## Installation Add this line to your application’s Gemfile: @@ -20,11 +20,11 @@ gem 'lockbox' ``` ## Key Generation -Generate an encryption key +Generate a key ```ruby Lockbox.generate_key ``` @@ -40,34 +40,30 @@ ```ruby Lockbox.master_key = Rails.application.credentials.lockbox_master_key ``` -Alternatively, you can use a [key management service](#key-management) to manage your keys. +Then follow the instructions below for the data you want to encrypt. -## Instructions +#### Database Fields -Database fields - - [Active Record](#active-record) - [Mongoid](#mongoid) -Files +#### Files - [Active Storage](#active-storage) - [CarrierWave](#carrierwave) - [Shrine](#shrine) - [Local Files](#local-files) -Other +#### Other - [Strings](#strings) -## Database Fields +## Active Record -### Active Record - Create a migration with: ```ruby class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0] def change @@ -92,11 +88,11 @@ If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index). #### Types -Specify the type of a field with: +Fields are strings by default. Specify the type of a field with: ```ruby class User < ApplicationRecord encrypts :born_on, type: :date encrypts :signed_at, type: :datetime @@ -108,11 +104,11 @@ 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 +**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. ```ruby class User < ApplicationRecord @@ -138,12 +134,60 @@ #### Validations Validations work as expected with the exception of uniqueness. Uniqueness validations require a [blind index](https://github.com/ankane/blind_index). -### Mongoid +#### Fixtures +You can use encrypted attributes in fixtures with: + +```yml +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. + +#### Migrating Existing Data + +Lockbox makes it easy to encrypt an existing column. Add a new column for the ciphertext, then add to your model: + +```ruby +class User < ApplicationRecord + encrypts :email, migrating: true +end +``` + +Backfill the data in the Rails console: + +```ruby +Lockbox.migrate(User) +``` + +Then update the model to the desired state: + +```ruby +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, Lockbox can migrate them at the same time. + +```ruby +class User < ApplicationRecord + blind_index :email, migrating: true +end +``` + +## Mongoid + Add to your model: ```ruby class User field :email_ciphertext, type: String @@ -158,14 +202,12 @@ User.create!(email: "hi@example.org") ``` If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index). -## Files +## Active Storage -### Active Storage - Add to your model: ```ruby class User < ApplicationRecord has_one_attached :license @@ -189,15 +231,16 @@ To serve encrypted files, use a controller action. ```ruby def license - send_data @user.license.download, type: @user.license.content_type + user = User.find(params[:id]) + send_data user.license.download, type: user.license.content_type end ``` -### CarrierWave +## CarrierWave Add to your uploader: ```ruby class LicenseUploader < CarrierWave::Uploader::Base @@ -205,54 +248,80 @@ end ``` Encryption is applied to all versions after processing. +You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration: + +```ruby +class AddLicenseToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :license, :string + end +end +``` + +And updating the model: + +```ruby +class User < ApplicationRecord + mount_uploader :license, LicenseUploader +end +``` + To serve encrypted files, use a controller action. ```ruby def license - send_data @user.license.read, type: @user.license.content_type + user = User.find(params[:id]) + send_data user.license.read, type: user.license.content_type end ``` -### Shrine +## Shrine -Create a box +Generate a key ```ruby -box = Lockbox.new(key: key) +key = Lockbox.generate_key ``` +Create a lockbox + +```ruby +lockbox = Lockbox.new(key: key) +``` + Encrypt files before passing them to Shrine ```ruby -LicenseUploader.upload(box.encrypt_io(file), :store) +LicenseUploader.upload(lockbox.encrypt_io(file), :store) ``` And decrypt them after reading ```ruby -box.decrypt(uploaded_file.read) +lockbox.decrypt(uploaded_file.read) ``` For models, encrypt with: ```ruby license = params.require(:user).fetch(:license) -@user.license = box.encrypt_io(license) +user.license = lockbox.encrypt_io(license) ``` To serve encrypted files, use a controller action. ```ruby def license - send_data box.decrypt(@user.license.read), type: @user.license.mime_type + user = User.find(params[:id]) + send_data box.decrypt(user.license.read), type: user.license.mime_type end ``` -### Local Files +## Local Files Read the file as a binary string ```ruby message = File.binread("file.txt") @@ -260,156 +329,180 @@ Then follow the instructions for encrypting a string below. ## Strings -Create a box +Generate a key ```ruby -box = Lockbox.new(key: key) +key = Lockbox.generate_key ``` -Encrypt +Create a lockbox ```ruby -ciphertext = box.encrypt(message) +lockbox = Lockbox.new(key: key, encode: true) ``` -Decrypt +Encrypt ```ruby -box.decrypt(ciphertext) +ciphertext = lockbox.encrypt("hello") ``` -Decrypt and return UTF-8 instead of binary +Decrypt ```ruby -box.decrypt_str(ciphertext) +lockbox.decrypt(ciphertext) ``` -## Migrating Existing Data +Use `decrypt_str` get the value as UTF-8 -Lockbox makes it easy to encrypt an existing column. Add a new column for the ciphertext, then add to your model: - -```ruby -class User < ApplicationRecord - encrypts :email, migrating: true -end -``` - -Backfill the data in the Rails console: - -```ruby -Lockbox.migrate(User) -``` - -Then update the model to the desired state: - -```ruby -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. ### Active Record -For Active Record, use: +Update your model: ```ruby class User < ApplicationRecord encrypts :email, previous_versions: [{key: previous_key}] end ``` -To rotate, use: +Use `master_key` instead of `key` if passing the master key. +To rotate existing records, use: + ```ruby -user.update!(email: user.email) +Lockbox.rotate(User, attributes: [:email]) ``` +Once all records are rotated, you can remove `previous_versions` from the model. + ### Mongoid -For Mongoid, use: +Update your model: ```ruby class User encrypts :email, previous_versions: [{key: previous_key}] end ``` -To rotate, use: +Use `master_key` instead of `key` if passing the master key. +To rotate existing records, use: + ```ruby -user.update!(email: user.email) +Lockbox.rotate(User, attributes: [:email]) ``` +Once all records are rotated, you can remove `previous_versions` from the model. + ### Active Storage -For Active Storage use: +Update your model: ```ruby class User < ApplicationRecord encrypts_attached :license, previous_versions: [{key: previous_key}] end ``` +Use `master_key` instead of `key` if passing the master key. + To rotate existing files, use: ```ruby -user.license.rotate_encryption! +User.find_each do |user| + user.license.rotate_encryption! +end ``` +Once all files are rotated, you can remove `previous_versions` from the model. + ### CarrierWave -For CarrierWave, use: +Update your model: ```ruby class LicenseUploader < CarrierWave::Uploader::Base encrypt previous_versions: [{key: previous_key}] end ``` +Use `master_key` instead of `key` if passing the master key. + To rotate existing files, use: ```ruby -user.license.rotate_encryption! +User.find_each do |user| + user.license.rotate_encryption! +end ``` +Once all files are rotated, you can remove `previous_versions` from the model. + ### Strings For strings, use: ```ruby Lockbox.new(key: key, previous_versions: [{key: previous_key}]) ``` -## Fixtures +## Auditing -You can use encrypted attributes in fixtures with: +It’s a good idea to track user and employee access to sensitive data. Lockbox provides a convenient way to do this with Active Record, but you can use a similar pattern to write audits to any location. -```yml -test_user: - email_ciphertext: <%= User.generate_email_ciphertext("secret").inspect %> +```sh +rails generate lockbox:audits +rails db:migrate ``` -Be sure to include the `inspect` at the end or it won’t be encoded properly in YAML. +Then create an audit wherever a user can view data: +```ruby +class UsersController < ApplicationController + def show + @user = User.find(params[:id]) + + LockboxAudit.create!( + subject: @user, + viewer: current_user, + data: ["email", "dob"], + context: "#{controller_name}##{action_name}", + ip: request.remote_ip + ) + end +end +``` + +Query audits with: + +```ruby +LockboxAudit.last(100) +``` + +**Note:** This approach is not intended to be used in the event of a breach or insider attack, as it’s trivial for someone with access to your infrastructure to bypass. + ## Algorithms ### AES-GCM -This is the default algorithm. Rotate the key every 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the key. +This is the default algorithm. It’s: +- well-studied +- NIST recommended +- an IETF standard +- fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set) + +**For users who do a lot of encryptions:** You should rotate an individual key after 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the key. Each database field and file uploader use a different key (derived from the master key) to extend this window. + ### XSalsa20 You can also use XSalsa20, which uses an extended nonce so you don’t have to worry about nonce collisions. First, [install Libsodium](https://github.com/crypto-rb/rbnacl/wiki/Installing-libsodium). For Homebrew, use: ```sh @@ -548,37 +641,68 @@ 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 +## Data Leakage +While encryption hides the content of a message, an attacker can still get the length of the message (since the length of the ciphertext is the length of the message plus a constant number of bytes). + +Let’s say you want to encrypt the status of a candidate’s background check. Valid statuses are `clear`, `consider`, and `fail`. Even with the data encrypted, it’s trivial to map the ciphertext to a status. + +```ruby +lockbox = Lockbox.new(key: key) +lockbox.encrypt("fail").bytesize # 32 +lockbox.encrypt("clear").bytesize # 33 +lockbox.encrypt("consider").bytesize # 36 +``` + Add padding to conceal the exact length of messages. ```ruby -Lockbox.new(padding: true) +lockbox = Lockbox.new(key: key, padding: true) +lockbox.encrypt("fail").bytesize # 44 +lockbox.encrypt("clear").bytesize # 44 +lockbox.encrypt("consider").bytesize # 44 ``` -The block size for padding is 16 bytes by default. Change this with: +The block size for padding is 16 bytes by default. If we have a status larger than 15 bytes, it will have a different length than the others. ```ruby +box.encrypt("length15status!").bytesize # 44 +box.encrypt("length16status!!").bytesize # 60 +``` + +Change the block size with: + +```ruby Lockbox.new(padding: 32) # bytes ``` -## Reference +## Binary Columns -Set default options in an initializer with: +You can use `binary` columns for the ciphertext instead of `text` columns to save space. ```ruby -Lockbox.default_options = {algorithm: "xsalsa20"} +class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :email_ciphertext, :binary + end +end ``` -For database fields, encrypted data is encoded in Base64. If you use `binary` columns instead of `text` columns, set: +You should disable Base64 encoding if you do this. ```ruby class User < ApplicationRecord encrypts :email, encode: false end +``` + +or set it globally: + +```ruby +Lockbox.default_options = {encode: false} ``` ## Compatibility It’s easy to read encrypted data in another language if needed.