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.