README.md in lockbox-0.1.1 vs README.md in lockbox-0.2.0
- old
+ new
@@ -1,14 +1,16 @@
# Lockbox
-:lock: File encryption for Ruby and Rails
+:lock: Modern encryption for Rails
-- Supports Active Storage and CarrierWave
-- Uses AES-GCM by default for [authenticated encryption](https://tonyarcieri.com/all-the-crypto-code-youve-ever-written-is-probably-broken)
-- Makes key rotation easy
+- 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](https://ankane.org/sensitive-data-rails) for more info on securing sensitive data with Rails
+Check out [this post](https://ankane.org/modern-encryption-rails) for more info on its design, and [this post](https://ankane.org/sensitive-data-rails) for more info on securing sensitive data with Rails
[![Build Status](https://travis-ci.org/ankane/lockbox.svg?branch=master)](https://travis-ci.org/ankane/lockbox)
## Installation
@@ -21,99 +23,208 @@
## Key Generation
Generate an encryption key
```ruby
-SecureRandom.hex(32)
+Lockbox.generate_key
```
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/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)
+
+```sh
+LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
+```
+
+or create `config/initializers/lockbox.rb` with something like
+
+```ruby
+Lockbox.master_key = Rails.application.credentials.lockbox_master_key
+```
+
Alternatively, you can use a [key management service](#key-management) to manage your keys.
-## Files
+## Database Fields
-Create a box
+Create a migration with:
```ruby
-box = Lockbox.new(key: key)
+class AddEmailCiphertextToUsers < ActiveRecord::Migration[5.2]
+ def change
+ add_column :users, :email_ciphertext, :text
+ end
+end
```
-Encrypt
+Add to your model:
```ruby
-ciphertext = box.encrypt(File.binread("license.jpg"))
+class User < ApplicationRecord
+ encrypts :email
+end
```
-Decrypt
+You can use `email` just like any other attribute.
```ruby
-box.decrypt(ciphertext)
+User.create!(email: "hi@example.org")
```
-## Active Storage
+If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
+## Files
+
+### Active Storage
+
Add to your model:
```ruby
class User < ApplicationRecord
has_one_attached :license
- attached_encrypted :license, key: key
+ encrypts_attached :license
end
```
Works with multiple attachments as well.
```ruby
class User < ApplicationRecord
has_many_attached :documents
- attached_encrypted :documents, key: key
+ 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
-## CarrierWave
+To serve encrypted files, use a controller action.
+```ruby
+def license
+ send_data @user.license.download, type: @user.license.content_type
+end
+```
+
+**Note:** With Rails 6, attachments are not encrypted with:
+
+```ruby
+User.create!(avatar: params[:avatar])
+```
+
+Until this is addressed, use:
+
+```ruby
+user = User.new
+user.attach(params[:avatar])
+user.save!
+```
+
+### CarrierWave
+
Add to your uploader:
```ruby
class LicenseUploader < CarrierWave::Uploader::Base
- encrypt key: key
+ encrypt
end
```
Encryption is applied to all versions after processing.
-## Serving Files
-
To serve encrypted files, use a controller action.
```ruby
def license
- send_data @user.license.download, type: @user.license.content_type
+ send_data @user.license.read, type: @user.license.content_type
end
```
-Use `read` instead of `download` for CarrierWave.
+### Local Files
+Read the file as a binary string
+
+```ruby
+message = File.binread("file.txt")
+```
+
+Then follow the instructions for encrypting a string below.
+
+## Strings
+
+Create a box
+
+```ruby
+box = Lockbox.new(key: key)
+```
+
+Encrypt
+
+```ruby
+ciphertext = box.encrypt(message)
+```
+
+Decrypt
+
+```ruby
+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:
+
+```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.
+For Active Record, use:
+
```ruby
-Lockbox.new(key: key, previous_versions: [{key: previous_key}])
+class User < ApplicationRecord
+ encrypts :email, previous_versions: [{key: previous_key}]
+end
```
+To rotate, use:
+
+```ruby
+user.update!(email: user.email)
+```
+
For Active Storage use:
```ruby
class User < ApplicationRecord
- attached_encrypted :license, key: key, previous_versions: [{key: previous_key}]
+ encrypts_attached :license, previous_versions: [{key: previous_key}]
end
```
To rotate existing files, use:
@@ -123,105 +234,163 @@
For CarrierWave, use:
```ruby
class LicenseUploader < CarrierWave::Uploader::Base
- encrypt key: key, previous_versions: [{key: previous_key}]
+ encrypt previous_versions: [{key: previous_key}]
end
```
To rotate existing files, use:
```ruby
user.license.rotate_encryption!
```
+For strings, use:
+
+```ruby
+Lockbox.new(key: key, previous_versions: [{key: previous_key}])
+```
+
+## 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.
+
## Algorithms
### AES-GCM
-The default algorithm is AES-GCM with a 256-bit key. Rotate the key every 2 billion files to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will leak the key.
+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.
-### XChaCha20
+### XSalsa20
-[Install Libsodium](https://github.com/crypto-rb/rbnacl/wiki/Installing-libsodium) >= 1.0.12 and add [rbnacl](https://github.com/crypto-rb/rbnacl) to your application’s Gemfile:
+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
+brew install libsodium
+```
+
+And add to your Gemfile:
+
```ruby
gem 'rbnacl'
```
-Then pass the `algorithm` option:
+Then add to your model:
-```ruby
-# files
-box = Lockbox.new(key: key, algorithm: "xchacha20")
-# Active Storage
+```ruby
class User < ApplicationRecord
- attached_encrypted :license, key: key, algorithm: "xchacha20"
+ encrypts :email, algorithm: "xsalsa20"
end
-
-# CarrierWave
-class LicenseUploader < CarrierWave::Uploader::Base
- encrypt key: key, algorithm: "xchacha20"
-end
```
Make it the default with:
```ruby
-Lockbox.default_options = {algorithm: "xchacha20"}
+Lockbox.default_options = {algorithm: "xsalsa20"}
```
You can also pass an algorithm to `previous_versions` for key rotation.
-## Hybrid Cryptography
+#### XSalsa20 Deployment
-[Hybrid cryptography](https://en.wikipedia.org/wiki/Hybrid_cryptosystem) allows servers to encrypt data without being able to decrypt it.
+##### Heroku
-[Install Libsodium](https://github.com/crypto-rb/rbnacl/wiki/Installing-libsodium) and add [rbnacl](https://github.com/crypto-rb/rbnacl) to your application’s Gemfile:
+Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packages) preinstalled.
-```ruby
-gem 'rbnacl'
+##### Ubuntu
+
+For Ubuntu 16.04, use:
+
+```sh
+sudo apt-get install libsodium18
```
+For Ubuntu 18.04, use:
+
+```sh
+sudo apt-get install libsodium23
+```
+
+##### Travis CI
+
+On Xenial, add to `.travis.yml`:
+
+```yml
+addons:
+ apt:
+ packages:
+ - libsodium18
+```
+
+##### CircleCI
+
+Add a step to `.circleci/config.yml`:
+
+```yml
+- run:
+ name: install Libsodium
+ command: |
+ sudo apt-get install -y libsodium18
+```
+
+## Hybrid Cryptography
+
+[Hybrid cryptography](https://en.wikipedia.org/wiki/Hybrid_cryptosystem) 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:
```ruby
Lockbox.generate_key_pair
```
Store the keys with your other secrets. Then use:
```ruby
-# files
-box = Lockbox.new(algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key)
-
-# Active Storage
class User < ApplicationRecord
- attached_encrypted :license, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
+ encrypts :email, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
end
-
-# CarrierWave
-class LicenseUploader < CarrierWave::Uploader::Base
- encrypt 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](https://ciphersweet.paragonie.com/internals/key-hierarchy). 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:
+
+```ruby
+Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
+```
+
+And set it directly before renaming:
+
+```ruby
+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](https://github.com/ankane/kms_encrypted).
-For Active Storage, use:
-
```ruby
class User < ApplicationRecord
- attached_encrypted :license, key: :kms_key
+ encrypts :email, key: :kms_key
end
```
For CarrierWave, use:
@@ -233,73 +402,124 @@
**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 files in another language if needed.
+It’s easy to read encrypted data in another language if needed.
-Here are [some examples](docs/Compatibility.md).
+For AES-GCM, the format is:
-The format for AES-GCM is:
-
- nonce (IV) - 12 bytes
- ciphertext - variable length
- authentication tag - 16 bytes
-For XChaCha20, use the appropriate [Libsodium library](https://libsodium.gitbook.io/doc/bindings_for_other_languages).
+Here are [some examples](docs/Compatibility.md).
-## Database Fields
+For XSalsa20, use the appropriate [Libsodium library](https://libsodium.gitbook.io/doc/bindings_for_other_languages).
-Lockbox can also be used with [attr_encrypted](https://github.com/attr-encrypted/attr_encrypted) for database fields. This gives you:
+## Migrating from Another Library
-1. Easy key rotation
-2. XChaCha20
-3. Hybrid cryptography
-4. No need for separate IV columns
+Lockbox makes it easy to migrate from another library. The example below uses `attr_encrypted` but the same approach should work for any library.
-Add to your Gemfile:
+Let’s suppose your model looks like this:
```ruby
-gem 'attr_encrypted'
+class User < ApplicationRecord
+ attr_encrypted :name, key: key
+ attr_encrypted :email, key: key
+end
```
-Create a migration to add a new column for the encrypted data. We don’t need a separate IV column, as this will be included in the encrypted data.
+Create a migration with:
```ruby
-class AddEncryptedPhoneToUsers < ActiveRecord::Migration[5.2]
+class MigrateToLockbox < ActiveRecord::Migration[5.2]
def change
- add_column :users, :encrypted_phone, :string
+ add_column :users, :name_ciphertext, :text
+ add_column :users, :email_ciphertext, :text
end
end
```
-All Lockbox options are supported.
+And add `encrypts` to your model with the `migrating` option:
```ruby
class User < ApplicationRecord
- attr_encrypted :phone, encryptor: Lockbox::Encryptor, key: key, algorithm: "xchacha20", previous_versions: [{key: previous_key}]
-
- attribute :encrypted_phone_iv # prevent attr_encrypted error
+ encrypts :name, :email, migrating: true
end
```
-For hybrid cryptography, use:
+Then run:
```ruby
+Lockbox.migrate(User)
+```
+
+Once all records are migrated, remove the `migrating` option and the previous model code (the `attr_encrypted` methods in this example).
+
+```ruby
class User < ApplicationRecord
- attr_encrypted :phone, encryptor: Lockbox::Encryptor, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
+ encrypts :name, :email
+end
+```
- attribute :encrypted_phone_iv # prevent attr_encrypted error
+Then remove the previous gem from your Gemfile and drop its columns.
+
+```ruby
+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
```
-## Reference
+## Upgrading
-Pass associated data to encryption and decryption
+### 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:
+
```ruby
-box.encrypt(message, associated_data: "bingo")
-box.decrypt(ciphertext, associated_data: "bingo")
+Lockbox.generate_key
+```
+
+And set `ENV["LOCKBOX_MASTER_KEY"]` or `Lockbox.master_key`.
+
+Update your model:
+
+```ruby
+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:
+
+```ruby
+User.unscoped.find_each do |user|
+ user.license.rotate_encryption!
+end
+```
+
+Once that’s complete, update your model:
+
+```ruby
+class User < ApplicationRecord
+ encrypts_attached :license
+end
```
## History
View the [changelog](https://github.com/ankane/lockbox/blob/master/CHANGELOG.md)