Lockbox Documentation

repository·master·Indexed 23 days ago

https://github.com/ankane/lockbox

A modern encryption library for Ruby and Rails used to encrypt database fields, strings, and files. It provides integration for Active Record, Action Text, Active Storage, CarrierWave, and Shrine. Key features include support for AES-GCM and XSalsa20 algorithms, hybrid cryptography, key rotation, data padding to prevent length-based leakage, and built-in auditing for sensitive data access.

Tokens
8.8K
Snippets
18
Records
45
Agent score
81%

What's inside Lockbox

  1. Configure encryption keys (Master, Per-Field, and Per-Record)

    master

    Lockbox supports three levels of key configuration:

    1. Master Key

    By default, Lockbox uses a master key to derive unique keys for each field/uploader based on the table name and attribute name.

    If you rename a table or column, you must tell Lockbox the original name to maintain access to the derived key:

    # If table was renamed
    class User < ApplicationRecord
      has_encrypted :email, key_table: "original_table"
    end
    
    # If column was renamed
    class User < ApplicationRecord
      has_encrypted :email, key_attribute: "original_column"
    end

    2. Per Field/Uploader

    Set a specific key for a single attribute using a string or a proc:

    class User < ApplicationRecord
      has_encrypted :email, key: ENV["USER_EMAIL_ENCRYPTION_KEY"]
      # or
      has_encrypted :email, key: -> { some_method }
    end

    3. Per Record

    Use a symbol or a proc to call a method on the record instance to retrieve a unique key:

    class User < ApplicationRecord
      has_encrypted :email, key: :some_method
      # or
      has_encrypted :email, key: -> { some_method }
    end
    # Table rename
    class User < ApplicationRecord
      has_encrypted :email, key_table: "original_table"
    end
    
    # Column rename
    class User < ApplicationRecord
      has_encrypted :email, key_attribute: "original_column"
    end
    
    # Per field (String/Env)
    class User < ApplicationRecord
      has_encrypted :email, key: ENV["USER_EMAIL_ENCRYPTION_KEY"]
    end
    
    # Per field (Proc)
    class User < ApplicationRecord
      has_encrypted :email, key: -> { code }
    end
    
    # Per record (Symbol)
    class User < ApplicationRecord
      has_encrypted :email, key: :some_method
    end
    
    # Per record (Proc)
    class User < ApplicationRecord
      has_encrypted :email, key: -> { some_method }
    end
  2. Implement Hybrid Cryptography

    master

    Hybrid cryptography allows servers to encrypt data without having the ability to decrypt it. This is useful for architectures where a processing server should never see the plaintext.

    1. Install Libsodium and add rbnacl to your Gemfile.
    2. Generate a key pair:
      Lockbox.generate_key_pair
    3. Configure the model using the generated keys. Ensure the decryption_key is nil on servers that should only perform encryption:
    class User < ApplicationRecord
      has_encrypted :email, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
    end

    This implementation uses X25519 for key exchange and XSalsa20 for encryption.

  3. Encrypt Action Text body

    master

    Lockbox can encrypt the body field of ActionText::RichText. Note that this only encrypts the database field; direct file uploads used by Action Text cannot be encrypted with application-level encryption.

    1. Add a migration for action_text_rich_texts.body_ciphertext (type text).
    2. Configure the initializer:
      Lockbox.encrypts_action_text_body(migrating: true)
    3. Migrate data: Lockbox.migrate(ActionText::RichText).
    4. Update initializer to remove migrating: true and drop the unencrypted column.
    # Migration
    class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[8.1]
      def change
        add_column :action_text_rich_texts, :body_ciphertext, :text
      end
    end
    
    # Initializer
    Lockbox.encrypts_action_text_body(migrating: true)
    
    # Migration command
    Lockbox.migrate(ActionText::RichText)
  4. Set up auditing for sensitive data access

    master

    Lockbox provides a built-in way to track when sensitive data is accessed using Active Record.

    1. Generate the audit migration:
    rails generate lockbox:audits
    rails db:migrate
    1. Create an audit record in your controller or service whenever data is viewed:
    LockboxAudit.create!(
      subject: @user,
      viewer: current_user,
      data: ["name", "email"],
      context: "#{controller_name}##{action_name}",
      ip: request.remote_ip
    )
    1. Query audits using LockboxAudit.last(n).

    Warning: This is a convenience feature for application-level auditing and is not a substitute for infrastructure-level security, as users with database access can bypass these logs.

    rails generate lockbox:audits
    rails db:migrate
    
    # Example usage in a controller
    LockboxAudit.create!(
      subject: @user,
      viewer: current_user,
      data: ["name", "email"],
      context: "#{controller_name}##{action_name}",
      ip: request.remote_ip
    )
  5. Encrypt Active Storage attachments

    master

    To encrypt files attached via Active Storage:

    1. Use encrypts_attached in your model:
      class User < ApplicationRecord
        has_one_attached :license
        encrypts_attached :license
      end

    Limitations:

    • Variants and previews are not supported.
    • Metadata (e.g., image width/height) is not extracted.
    • Direct uploads cannot be encrypted with Lockbox (use server-side encryption instead).

    Serving encrypted files: You must use a controller action to download and decrypt the file:

    def license
      user = User.find(params[:id])
      send_data user.license.download, type: user.license.content_type
    end
    class User < ApplicationRecord
      has_one_attached :license
      encrypts_attached :license
    end
  6. Rotate encryption keys

    master

    To rotate keys without downtime, you can provide previous keys that Lockbox should attempt to use for decryption.

    Global Configuration

    Set previous_versions in a Lockbox initializer to apply to all fields globally:

    Lockbox.default_options[:previous_versions] = [{master_key: previous_key}]

    Individual Fields

    Pass previous_versions directly to the has_encrypted declaration in your model:

    class User < ApplicationRecord
      has_encrypted :email, previous_versions: [{master_key: previous_key}]
    end

    Local Objects

    For standalone Lockbox instances (strings or files):

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

    Bulk Rotation Tasks

    Once keys are configured, use the following methods to migrate existing data:

    • Active Record & Mongoid: Lockbox.rotate(Model, attributes: [:attr1, :attr2])
    • Action Text: Lockbox.rotate(ActionText::RichText, attributes: [:body])
    • Active Storage: Iterate through records and call rotate_encryption! on the attachment.
    • CarrierWave: Iterate through records and call rotate_encryption! on the file/uploader.

    After all data is migrated, remove the previous_versions configuration.

  7. Encrypt files with Shrine

    master

    To encrypt files using Shrine, you manually encrypt the IO object before passing it to Shrine, or decrypt it when reading.

    Manual Encryption/Decryption:

    # Encrypting
    lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
    user.license = lockbox.encrypt_io(params.require(:user).fetch(:license))
    
    # Decrypting/Serving
    def license
      user = User.find(params[:id])
      lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
      send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
    end
  8. Decrypt Lockbox data in Python

    master

    To decrypt Lockbox data in Python, install the cryptography package and use the AESGCM primitive.

    Important Requirements:

    1. Use the attribute key, not the master key.
    2. For files, skip the Base64 decoding step for the ciphertext.
    3. The ciphertext format is: nonce (12 bytes) + actual ciphertext + auth tag (16 bytes). In the decrypt method, the nonce is passed separately, and the remaining bytes (including the tag) are passed as the ciphertext.
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM
    from base64 import b64decode
    
    key = '61e6ba4a3a2498e3a8fdcd047eff0cd9864016f2c83c34599a3257a57ce6f7fb'
    ciphertext = 'Uv/+Sgar0kM216AvVlBH5Gt8vIwtQGfPysl539WY2DER62AoJg=='
    
    key = bytes.fromhex(key)
    ciphertext = b64decode(ciphertext) # skip for files
    
    aesgcm = AESGCM(key)
    plaintext = aesgcm.decrypt(ciphertext[:12], ciphertext[12:], b'')
    
    print(plaintext)
  9. Use padding to prevent data leakage via message length

    master

    Because ciphertext length is proportional to plaintext length, an attacker can infer information about the data (e.g., distinguishing between 'fail' and 'consider' based on byte size).

    To prevent this, enable padding to ensure all ciphertexts have a uniform length.

    # Enable padding for a specific instance
    lockbox = Lockbox.new(key: key, padding: true)
    
    # Change the block size (default is 16 bytes)
    lockbox = Lockbox.new(padding: 32)

    Lockbox uses ISO/IEC 7816-4 padding. Note that if a status is larger than block_size - 1, it will result in a larger ciphertext block.

  10. Migrate existing Active Record data to encrypted fields

    master

    To encrypt an existing column without downtime:

    1. Add a new [attribute]_ciphertext column.
    2. Update the model to use migrating: true:
      has_encrypted :email, migrating: true
    3. Backfill the data in the Rails console:
      Lockbox.migrate(User)
    4. Once backfilled, remove the migrating: true option and add self.ignored_columns += ["original_column_name"] to the model to prevent it from trying to read the unencrypted column.
    5. Drop the original unencrypted column.
    # Step 2: Model update
    class User < ApplicationRecord
      has_encrypted :email, migrating: true
    end
    
    # Step 3: Backfill
    Lockbox.migrate(User)
    
    # Step 4: Final state
    class User < ApplicationRecord
      has_encrypted :email
    
      # remove this line after dropping email column
      self.ignored_columns += ["email"]
    end
  11. Decrypt Lockbox data in Rust

    master

    To decrypt Lockbox data in Rust, add the aes-gcm, base64, and hex crates to your Cargo.toml.

    Important Requirements:

    1. Use the attribute key, not the master key.
    2. For files, skip the Base64 decoding step for the ciphertext.
    3. The ciphertext format is: nonce (12 bytes) + actual ciphertext + auth tag (16 bytes).
    [dependencies]
    aes-gcm = "0.10.3"
    base64 = "0.22.1"
    hex = "0.4.3"
    use aes_gcm::aead::{generic_array::GenericArray, Aead};
    use aes_gcm::{Aes256Gcm, Key, KeyInit};
    use base64::prelude::*;
    
    fn main() {
        let key = hex::decode("61e6ba4a3a2498e3a8fdcd047eff0cd9864016f2c83c34599a3257a57ce6f7fb").expect("decode failure!");
        let ciphertext = BASE64_STANDARD.decode("Uv/+Sgar0kM216AvVlBH5Gt8vIwtQGfPysl539WY2DER62AoJg==").expect("decode failure!");
    
        let key = Key::<Aes256Gcm>::from_slice(&key);
        let aead = Aes256Gcm::new(key);
        let nonce = GenericArray::from_slice(&ciphertext[..12]);
        let plaintext_bytes = aead.decrypt(nonce, &ciphertext[12..]).expect("decryption failure!");
        let plaintext = String::from_utf8(plaintext_bytes).expect("utf8 failure!");
    
        println!("{:?}", plaintext);
    }