File encryption for Ruby and Rails
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docs
lib
test
.gitignore
.travis.yml
CHANGELOG.md
Gemfile
LICENSE.txt
README.md
Rakefile
lockbox.gemspec

README.md

Lockbox

🔒 File encryption for Ruby and Rails

Check out this post for more info on securing sensitive data with Rails

Build Status

Installation

Add this line to your application’s Gemfile:

gem 'lockbox'

Key Generation

Generate an encryption key

SecureRandom.hex(32)

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.

Alternatively, you can use a key management service to manage your keys.

Files

Create a box

box = Lockbox.new(key: key)

Encrypt

ciphertext = box.encrypt(File.binread("license.jpg"))

Decrypt

box.decrypt(ciphertext)

Active Storage

Add to your model:

class User < ApplicationRecord
  has_one_attached :license
  attached_encrypted :license, key: key
end

Works with multiple attachments as well.

class User < ApplicationRecord
  has_many_attached :documents
  attached_encrypted :documents, key: key
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

Add to your uploader:

class LicenseUploader < CarrierWave::Uploader::Base
  encrypt key: key
end

Encryption is applied to all versions after processing.

Serving Files

To serve encrypted files, use a controller action.

def license
  send_data @user.license.download, type: @user.license.content_type
end

Use read instead of download for CarrierWave.

Key Rotation

To make key rotation easy, you can pass previous versions of keys that can decrypt.

Lockbox.new(key: key, previous_versions: [{key: previous_key}])

For Active Storage use:

class User < ApplicationRecord
  attached_encrypted :license, key: key, previous_versions: [{key: previous_key}]
end

To rotate existing files, use:

user.license.rotate_encryption!

For CarrierWave, use:

class LicenseUploader < CarrierWave::Uploader::Base
  encrypt key: key, previous_versions: [{key: previous_key}]
end

To rotate existing files, use:

user.license.rotate_encryption!

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, which will leak the key.

XChaCha20

Install Libsodium >= 1.0.12 and add rbnacl to your application’s Gemfile:

gem 'rbnacl'

Then pass the algorithm option:

# files
box = Lockbox.new(key: key, algorithm: "xchacha20")

# Active Storage
class User < ApplicationRecord
  attached_encrypted :license, key: key, algorithm: "xchacha20"
end

# CarrierWave
class LicenseUploader < CarrierWave::Uploader::Base
  encrypt key: key, algorithm: "xchacha20"
end

Make it the default with:

Lockbox.default_options = {algorithm: "xchacha20"}

You can also pass an algorithm to previous_versions for key rotation.

Key Management

You can use a key management service to manage your keys with KMS Encrypted.

For Active Storage, use:

class User < ApplicationRecord
  attached_encrypted :license, 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 files in another language if needed.

Here are some examples.

The format for AES-GCM is:

  • nonce (IV) - 12 bytes
  • ciphertext - variable length
  • authentication tag - 16 bytes

For XChaCha20, use the appropriate Libsodium library.

Reference

Pass associated data to encryption and decryption

box.encrypt(message, associated_data: "bingo")
box.decrypt(ciphertext, associated_data: "bingo")

History

View the changelog

Contributing

Everyone is encouraged to help improve this project. Here are a few ways you can help: