Module: Lockbox

Extended by:
Padding
Defined in:
lib/lockbox/active_storage_extensions.rb,
lib/lockbox.rb,
lib/lockbox/io.rb,
lib/lockbox/box.rb,
lib/lockbox/model.rb,
lib/lockbox/utils.rb,
lib/lockbox/aes_gcm.rb,
lib/lockbox/padding.rb,
lib/lockbox/railtie.rb,
lib/lockbox/version.rb,
lib/lockbox/migrator.rb,
lib/lockbox/encryptor.rb,
lib/lockbox/calculations.rb,
lib/lockbox/key_generator.rb,
lib/lockbox/log_subscriber.rb,
lib/lockbox/carrier_wave_extensions.rb,
lib/generators/lockbox/audits_generator.rb

Overview

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.

Defined Under Namespace

Modules: ActiveStorageExtensions, Calculations, CarrierWaveExtensions, Generators, Model, Padding Classes: AES_GCM, Box, DecryptionError, Encryptor, Error, IO, KeyGenerator, LogSubscriber, Migrator, PaddingError, Railtie, Utils

Constant Summary collapse

VERSION =
"1.2.0"

Constants included from Padding

Padding::PAD_FIRST_BYTE, Padding::PAD_ZERO_BYTE

Class Attribute Summary collapse

Class Method Summary collapse

Methods included from Padding

pad, pad!, unpad, unpad!

Class Attribute Details

.default_optionsObject

Returns the value of attribute default_options.



30
31
32
# File 'lib/lockbox.rb', line 30

def default_options
  @default_options
end

.encode_attributesObject

Returns the value of attribute encode_attributes.



30
31
32
# File 'lib/lockbox.rb', line 30

def encode_attributes
  @encode_attributes
end

.master_keyObject



36
37
38
# File 'lib/lockbox.rb', line 36

def self.master_key
  @master_key ||= ENV["LOCKBOX_MASTER_KEY"]
end

Class Method Details

.attribute_key(table:, attribute:, master_key: nil, encode: true) ⇒ Object

Raises:

  • (ArgumentError)


66
67
68
69
70
71
72
73
# File 'lib/lockbox.rb', line 66

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) ⇒ Object



83
84
85
86
87
# File 'lib/lockbox.rb', line 83

def self.encrypts_action_text_body(**options)
  ActiveSupport.on_load(:action_text_rich_text) do
    ActionText::RichText.has_encrypted :body, **options
  end
end

.generate_keyObject



48
49
50
# File 'lib/lockbox.rb', line 48

def self.generate_key
  SecureRandom.hex(32)
end

.generate_key_pairObject



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/lockbox.rb', line 52

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

.migrate(relation, batch_size: 1000, restart: false) ⇒ Object



40
41
42
# File 'lib/lockbox.rb', line 40

def self.migrate(relation, batch_size: 1000, restart: false)
  Migrator.new(relation, batch_size: batch_size).migrate(restart: restart)
end

.new(**options) ⇒ Object



79
80
81
# File 'lib/lockbox.rb', line 79

def self.new(**options)
  Encryptor.new(**options)
end

.rotate(relation, batch_size: 1000, attributes:) ⇒ Object



44
45
46
# File 'lib/lockbox.rb', line 44

def self.rotate(relation, batch_size: 1000, attributes:)
  Migrator.new(relation, batch_size: batch_size).rotate(attributes: attributes)
end

.to_hex(str) ⇒ Object



75
76
77
# File 'lib/lockbox.rb', line 75

def self.to_hex(str)
  str.unpack1("H*")
end