module AttrEncrypted::InstanceMethods

Public Instance Methods

decrypt(attribute, encrypted_value) click to toggle source

Decrypts a value for the attribute specified using options evaluated in the current object's scope

Example

class User
  attr_accessor :secret_key
  attr_encrypted :email, key: :secret_key

  def initialize(secret_key)
    self.secret_key = secret_key
  end
end

@user = User.new('some-secret-key')
@user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
# File lib/attr_encrypted.rb, line 328
def decrypt(attribute, encrypted_value)
  encrypted_attributes[attribute.to_sym][:operation] = :decrypting
  encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
  begin
    self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
  rescue OpenSSL::Cipher::CipherError => e
    # When decryption fails with `key:` and the attribute is
    # configured to attempt a key rotation, let's try to decrypt
    # with the `old_key:` then rotate the value using the
    # `rotation_handler:`
    options = evaluated_attr_encrypted_options_for(attribute)
    raise e if nil == options[:old_key] || nil == options[:rotation_handler]

    # but even this may fail if the column's data is encrypted with
    # neither of these keys, or is corrupted in some way. We need to
    # catch this scenario and optionally give the host application
    # the ability to handle unrecoverable data
    begin
      value = self.class.decrypt(
        attribute,
        encrypted_value,
        options.merge(
          key: options[:old_key],
          iv: options[:old_iv]
        )
      )

      handler = options[:rotation_handler]
      handler.new(self, attribute, value, encrypted_value, options).call

      value
    rescue OpenSSL::Cipher::CipherError => e
      raise e unless options[:rotation_error_handler].present?

      error_handler = options[:rotation_error_handler]
      error_handler.new(self, attribute, e, encrypted_value, options).call
    end
  end
end
encrypt(attribute, value) click to toggle source

Encrypts a value for the attribute specified using options evaluated in the current object's scope

Example

class User
  attr_accessor :secret_key
  attr_encrypted :email, key: :secret_key

  def initialize(secret_key)
    self.secret_key = secret_key
  end
end

@user = User.new('some-secret-key')
@user.encrypt(:email, 'test@example.com')
# File lib/attr_encrypted.rb, line 383
def encrypt(attribute, value)
  encrypted_attributes[attribute.to_sym][:operation] = :encrypting
  encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
  self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
end
encrypted_attributes() click to toggle source

Copies the class level hash of encrypted attributes with virtual attribute names as keys and their corresponding options as values to the instance

# File lib/attr_encrypted.rb, line 392
def encrypted_attributes
  @encrypted_attributes ||= begin
    duplicated= {}
    self.class.encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
    duplicated
  end
end

Protected Instance Methods

decode_salt_if_encoded(salt, encoding) click to toggle source
# File lib/attr_encrypted.rb, line 488
def decode_salt_if_encoded(salt, encoding)
  prefix = '_'
  salt.slice(0).eql?(prefix) ? salt.slice(1..-1).unpack(encoding).first : salt
end
evaluate_attr_encrypted_option(option) click to toggle source

Evaluates symbol (method reference) or proc (responds to call) options

If the option is not a symbol or proc then the original option is returned

# File lib/attr_encrypted.rb, line 434
def evaluate_attr_encrypted_option(option)
  if option.is_a?(Symbol) && respond_to?(option, true)
    send(option)
  elsif option.respond_to?(:call)
    option.call(self)
  else
    option
  end
end
evaluated_attr_encrypted_options_for(attribute) click to toggle source

Returns attr_encrypted options evaluated in the current object's scope for the attribute specified

# File lib/attr_encrypted.rb, line 403
def evaluated_attr_encrypted_options_for(attribute)
  evaluated_options = Hash.new
  attributes = encrypted_attributes[attribute.to_sym]
  attribute_option_value = attributes[:attribute]

  [:if, :unless, :value_present, :allow_empty_value].each do |option|
    evaluated_options[option] = evaluate_attr_encrypted_option(attributes[option])
  end

  evaluated_options[:attribute] = attribute_option_value

  evaluated_options.tap do |options|
    if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
      (attributes.keys - evaluated_options.keys).each do |option|
        options[option] = evaluate_attr_encrypted_option(attributes[option])
      end

      unless options[:mode] == :single_iv_and_salt
        load_iv_for_attribute(attribute, options)
      end

      if options[:mode] == :per_attribute_iv_and_salt
        load_salt_for_attribute(attribute, options)
      end
    end
  end
end
generate_iv(algorithm) click to toggle source
# File lib/attr_encrypted.rb, line 462
def generate_iv(algorithm)
  algo = OpenSSL::Cipher.new(algorithm)
  algo.encrypt
  algo.random_iv
end
load_iv_for_attribute(attribute, options) click to toggle source
# File lib/attr_encrypted.rb, line 444
def load_iv_for_attribute(attribute, options)
  encrypted_attribute_name = options[:attribute]
  encode_iv = options[:encode_iv]
  iv = options[:iv] || send("#{encrypted_attribute_name}_iv")
  if options[:operation] == :encrypting
    begin
      iv = generate_iv(options[:algorithm])
      iv = [iv].pack(encode_iv) if encode_iv
      send("#{encrypted_attribute_name}_iv=", iv)
    rescue RuntimeError
    end
  end
  if iv && !iv.empty?
    iv = iv.unpack(encode_iv).first if encode_iv
    options[:iv] = iv
  end
end
load_salt_for_attribute(attribute, options) click to toggle source
# File lib/attr_encrypted.rb, line 468
def load_salt_for_attribute(attribute, options)
  encrypted_attribute_name = options[:attribute]
  encode_salt = options[:encode_salt]
  salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
  if options[:operation] == :encrypting
    salt = SecureRandom.random_bytes
    salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
    send("#{encrypted_attribute_name}_salt=", salt)
  end
  if salt && !salt.empty?
    salt = decode_salt_if_encoded(salt, encode_salt) if encode_salt
    options[:salt] = salt
  end
end
prefix_and_encode_salt(salt, encoding) click to toggle source
# File lib/attr_encrypted.rb, line 483
def prefix_and_encode_salt(salt, encoding)
  prefix = '_'
  prefix + [salt].pack(encoding)
end