module ActiveRecord::Encryption::EncryptableRecord

This is the concern mixed in Active Record models to make them encryptable. It adds the encrypts attribute declaration, as well as the API to encrypt and decrypt records.

Constants

ORIGINAL_ATTRIBUTE_PREFIX

Public Instance Methods

add_length_validation_for_encrypted_columns() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 129
def add_length_validation_for_encrypted_columns
  encrypted_attributes&.each do |attribute_name|
    validate_column_size attribute_name
  end
end
ciphertext_for(attribute_name) click to toggle source

Returns the ciphertext for attribute_name.

# File lib/active_record/encryption/encryptable_record.rb, line 148
def ciphertext_for(attribute_name)
  read_attribute_before_type_cast(attribute_name)
end
decrypt() click to toggle source

Decrypts all the encryptable attributes and saves the changes.

# File lib/active_record/encryption/encryptable_record.rb, line 158
def decrypt
  decrypt_attributes if has_encrypted_attributes?
end
deterministic_encrypted_attributes() click to toggle source

Returns the list of deterministic encryptable attributes in the model class.

# File lib/active_record/encryption/encryptable_record.rb, line 58
def deterministic_encrypted_attributes
  @deterministic_encrypted_attributes ||= encrypted_attributes&.find_all do |attribute_name|
    type_for_attribute(attribute_name).deterministic?
  end
end
encrypt() click to toggle source

Encrypts all the encryptable attributes and saves the changes.

# File lib/active_record/encryption/encryptable_record.rb, line 153
def encrypt
  encrypt_attributes if has_encrypted_attributes?
end
encrypt_attribute(name, attribute_scheme) click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 84
def encrypt_attribute(name, attribute_scheme)
  encrypted_attributes << name.to_sym

  attribute name do |cast_type|
    ActiveRecord::Encryption::EncryptedAttributeType.new scheme: attribute_scheme, cast_type: cast_type
  end

  preserve_original_encrypted(name) if attribute_scheme.ignore_case?
  ActiveRecord::Encryption.encrypted_attribute_was_declared(self, name)
end
encrypted_attribute?(attribute_name) click to toggle source

Returns whether a given attribute is encrypted or not.

# File lib/active_record/encryption/encryptable_record.rb, line 143
def encrypted_attribute?(attribute_name)
  ActiveRecord::Encryption.encryptor.encrypted? ciphertext_for(attribute_name)
end
encrypts(*names, key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties) click to toggle source

Encrypts the name attribute.

Options

  • :key_provider - Configure a KeyProvider for serving the keys to encrypt and decrypt this attribute. If not provided, it will default to ActiveRecord::Encryption.key_provider.

  • :key - A password to derive the key from. It’s a shorthand for a :key_provider that serves derivated keys. Both options can’t be used at the same time.

  • :key_provider - Set a :key_provider to provide encryption and decryption keys. If not provided, it will default to the key provider set with ‘config.key_provider`.

  • :deterministic - By default, encryption is not deterministic. It will use a random initialization vector for each encryption operation. This means that encrypting the same content with the same key twice will generate different ciphertexts. When set to true, it will generate the initialization vector based on the encrypted content. This means that the same content will generate the same ciphertexts. This enables querying encrypted text with Active Record. Deterministic encryption will use the oldest encryption scheme to encrypt new data by default. You can change this by setting +deterministic: { fixed: false }+. That will make it use the newest encryption scheme for encrypting new data.

  • :downcase - When true, it converts the encrypted content to downcase automatically. This allows to effectively ignore case when querying data. Notice that the case is lost. Use :ignore_case if you are interested in preserving it.

  • :ignore_case - When true, it behaves like :downcase but, it also preserves the original case in a specially designated column +original_<name>+. When reading the encrypted content, the version with the original case is served. But you can still execute queries that will ignore the case. This option can only be used when :deterministic is true.

  • :context_properties - Additional properties that will override Context settings when this attribute is encrypted and decrypted. E.g: encryptor:, cipher:, message_serializer:, etc.

  • :previous - List of previous encryption schemes. When provided, they will be used in order when trying to read the attribute. Each entry of the list can contain the properties supported by encrypts. Also, when deterministic encryption is used, they will be used to generate additional ciphertexts to check in the queries.

# File lib/active_record/encryption/encryptable_record.rb, line 47
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties)
  self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
  scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, downcase: downcase, \
      ignore_case: ignore_case, previous: previous, **context_properties

  names.each do |name|
    encrypt_attribute name, scheme
  end
end
global_previous_schemes_for(scheme) click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 78
def global_previous_schemes_for(scheme)
  ActiveRecord::Encryption.config.previous_schemes.collect do |previous_scheme|
    scheme.merge(previous_scheme)
  end
end
load_schema!() click to toggle source
Calls superclass method
# File lib/active_record/encryption/encryptable_record.rb, line 123
def load_schema!
  super

  add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size
end
override_accessors_to_preserve_original(name, original_attribute_name) click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 106
def override_accessors_to_preserve_original(name, original_attribute_name)
  include(Module.new do
    define_method name do
      if ((value = super()) && encrypted_attribute?(name)) || !ActiveRecord::Encryption.config.support_unencrypted_data
        send(original_attribute_name)
      else
        value
      end
    end

    define_method "#{name}=" do |value|
      self.send "#{original_attribute_name}=", value
      super(value)
    end
  end)
end
preserve_original_encrypted(name) click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 95
def preserve_original_encrypted(name)
  original_attribute_name = "#{ORIGINAL_ATTRIBUTE_PREFIX}#{name}".to_sym

  if !ActiveRecord::Encryption.config.support_unencrypted_data && !column_names.include?(original_attribute_name.to_s)
    raise Errors::Configuration, "To use :ignore_case for '#{name}' you must create an additional column named '#{original_attribute_name}'"
  end

  encrypts original_attribute_name
  override_accessors_to_preserve_original name, original_attribute_name
end
scheme_for(key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties) click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 70
def scheme_for(key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties)
  ActiveRecord::Encryption::Scheme.new(key_provider: key_provider, key: key, deterministic: deterministic,
                                       downcase: downcase, ignore_case: ignore_case, **context_properties).tap do |scheme|
    scheme.previous_schemes = global_previous_schemes_for(scheme) +
      Array.wrap(previous).collect { |scheme_config| ActiveRecord::Encryption::Scheme.new(**scheme_config) }
  end
end
source_attribute_from_preserved_attribute(attribute_name) click to toggle source

Given a attribute name, it returns the name of the source attribute when it’s a preserved one.

# File lib/active_record/encryption/encryptable_record.rb, line 65
def source_attribute_from_preserved_attribute(attribute_name)
  attribute_name.to_s.sub(ORIGINAL_ATTRIBUTE_PREFIX, "") if /^#{ORIGINAL_ATTRIBUTE_PREFIX}/.match?(attribute_name)
end
validate_column_size(attribute_name) click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 135
def validate_column_size(attribute_name)
  if limit = columns_hash[attribute_name.to_s]&.limit
    validates_length_of attribute_name, maximum: limit
  end
end

Private Instance Methods

build_decrypt_attribute_assignments() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 192
def build_decrypt_attribute_assignments
  Array(self.class.encrypted_attributes).collect do |attribute_name|
    type = type_for_attribute(attribute_name)
    encrypted_value = ciphertext_for(attribute_name)
    new_value = type.deserialize(encrypted_value)
    [attribute_name, new_value]
  end.to_h
end
build_encrypt_attribute_assignments() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 186
def build_encrypt_attribute_assignments
  Array(self.class.encrypted_attributes).index_with do |attribute_name|
    self[attribute_name]
  end
end
cant_modify_encrypted_attributes_when_frozen() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 201
def cant_modify_encrypted_attributes_when_frozen
  self.class&.encrypted_attributes.each do |attribute|
    errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute)
  end
end
decrypt_attributes() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 171
def decrypt_attributes
  validate_encryption_allowed

  decrypt_attribute_assignments = build_decrypt_attribute_assignments
  ActiveRecord::Encryption.without_encryption { update_columns decrypt_attribute_assignments }
end
encrypt_attributes() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 165
def encrypt_attributes
  validate_encryption_allowed

  update_columns build_encrypt_attribute_assignments
end
has_encrypted_attributes?() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 182
def has_encrypted_attributes?
  self.class.encrypted_attributes.present?
end
validate_encryption_allowed() click to toggle source
# File lib/active_record/encryption/encryptable_record.rb, line 178
def validate_encryption_allowed
  raise ActiveRecord::Encryption::Errors::Configuration, "can't be modified because it is encrypted" if ActiveRecord::Encryption.context.frozen_encryption?
end