Lockbox
:lock: File encryption for Ruby and Rails
- Supports Active Storage and CarrierWave
- Uses AES-GCM by default for authenticated encryption
- Makes key rotation easy
Check out this post for more info on securing sensitive data with Rails
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. = {algorithm: "xchacha20"}
You can also pass an algorithm to previous_versions
for key rotation.
Hybrid Cryptography
Hybrid cryptography allows servers to encrypt data without being able to decrypt it.
Install Libsodium and add rbnacl to your application’s Gemfile:
gem 'rbnacl'
Generate a key pair with:
Lockbox.generate_key_pair
Store the keys with your other secrets. Then use:
# 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
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 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.
Database Fields
Lockbox can also be used with attr_encrypted for database fields. This gives you:
- Easy key rotation
- XChaCha20
- Hybrid cryptography
- No need for separate IV columns
Add to your Gemfile:
gem 'attr_encrypted'
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.
class AddEncryptedPhoneToUsers < ActiveRecord::Migration[5.2]
def change
add_column :users, :encrypted_phone, :string
end
end
All Lockbox options are supported.
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
end
For hybrid cryptography, use:
class User < ApplicationRecord
attr_encrypted :phone, encryptor: Lockbox::Encryptor, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
attribute :encrypted_phone_iv # prevent attr_encrypted error
end
Reference
Pass associated data to encryption and decryption
box.encrypt(, 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:
- Report bugs
- Fix bugs and submit pull requests
- Write, clarify, or fix documentation
- Suggest or add new features