module Lockbox

Ideally encryption and decryption would happen at the blob/service level. However, Active Storage < 6.1 only supports a single service (per environment). This means all attachments need to be encrypted or none of them, which is often not practical.

Active Storage 6.1 adds support for multiple services, which changes this. We could have a Lockbox service:

lockbox:

service: Lockbox
backend: local    # delegate to another service, like mirror service
key:     ...      # Lockbox options

However, the checksum is computed *and stored on the blob* before the file is passed to the service. We don't want the MD5 checksum of the plaintext stored in the database.

Instead, we encrypt and decrypt at the attachment level, and we define encryption settings at the model level.

Constants

VERSION

Attributes

default_options[RW]
master_key[W]

Public Class Methods

attribute_key(table:, attribute:, master_key: nil, encode: true) click to toggle source
# File lib/lockbox.rb, line 94
def self.attribute_key(table:, attribute:, master_key: nil, encode: true)
  master_key ||= Lockbox.master_key
  raise ArgumentError, "Missing master key" unless master_key

  key = Lockbox::KeyGenerator.new(master_key).attribute_key(table: table, attribute: attribute)
  key = to_hex(key) if encode
  key
end
encrypts_action_text_body(**options) click to toggle source
# File lib/lockbox.rb, line 111
def self.encrypts_action_text_body(**options)
  ActiveSupport.on_load(:action_text_rich_text) do
    ActionText::RichText.lockbox_encrypts :body, **options
  end
end
generate_key() click to toggle source
# File lib/lockbox.rb, line 76
def self.generate_key
  SecureRandom.hex(32)
end
generate_key_pair() click to toggle source
# File lib/lockbox.rb, line 80
def self.generate_key_pair
  require "rbnacl"
  # encryption and decryption servers exchange public keys
  # this produces smaller ciphertext than sealed box
  alice = RbNaCl::PrivateKey.generate
  bob = RbNaCl::PrivateKey.generate
  # alice is sending message to bob
  # use bob first in both cases to prevent keys being swappable
  {
    encryption_key: to_hex(bob.public_key.to_bytes + alice.to_bytes),
    decryption_key: to_hex(bob.to_bytes + alice.public_key.to_bytes)
  }
end
master_key() click to toggle source
# File lib/lockbox.rb, line 64
def self.master_key
  @master_key ||= ENV["LOCKBOX_MASTER_KEY"]
end
migrate(relation, batch_size: 1000, restart: false) click to toggle source
# File lib/lockbox.rb, line 68
def self.migrate(relation, batch_size: 1000, restart: false)
  Migrator.new(relation, batch_size: batch_size).migrate(restart: restart)
end
new(**options) click to toggle source
# File lib/lockbox.rb, line 107
def self.new(**options)
  Encryptor.new(**options)
end
rotate(relation, batch_size: 1000, attributes:) click to toggle source
# File lib/lockbox.rb, line 72
def self.rotate(relation, batch_size: 1000, attributes:)
  Migrator.new(relation, batch_size: batch_size).rotate(attributes: attributes)
end
to_hex(str) click to toggle source
# File lib/lockbox.rb, line 103
def self.to_hex(str)
  str.unpack("H*").first
end