module Vault::EncryptedModel

Public Instance Methods

__vault_generate_context(context) click to toggle source

Generates an Vault Transit encryption context for use on derived keys.

# File lib/vault/encrypted_model.rb, line 380
def __vault_generate_context(context)
  case context
  when String
    context
  when Symbol
    send(context)
  when Proc
    context.call(self)
  else
    nil
  end
end
__vault_initialize_attributes!() click to toggle source

Decrypt all the attributes from Vault. @return [true]

# File lib/vault/encrypted_model.rb, line 230
def __vault_initialize_attributes!
  if self.class.vault_lazy_decrypt
    @__vault_loaded = false
    return
  end

  __vault_load_attributes!
end
__vault_load_attribute!(attribute, options) click to toggle source

Decrypt and load a single attribute from Vault.

# File lib/vault/encrypted_model.rb, line 254
def __vault_load_attribute!(attribute, options)
  key        = options[:key]
  path       = options[:path]
  serializer = options[:serializer]
  column     = options[:encrypted_column]
  context    = options[:context]
  default    = options[:default]
  transform  = options[:transform_secret]

  # Load the ciphertext
  ciphertext = read_attribute(column)

  # If the user provided a value for the attribute, do not try to load
  # it from Vault
  if attributes[attribute.to_s]
    return
  end

  # Generate context if needed
  generated_context = __vault_generate_context(context)

  if transform
    # If this is a secret encrypted with FPE, we do not need to decrypt with vault
    # This prevents a double encryption via standard vault encryption and FPE.
    # FPE is decrypted later as part of the serializer
    plaintext = ciphertext
  else
    # Load the plaintext value
    plaintext = Vault::Rails.decrypt(
      path, key, ciphertext,
      context: generated_context
    )
  end

  # Deserialize the plaintext value, if a serializer exists
  if serializer
    plaintext = serializer.decode(plaintext)
  end

  # Set to default if needed
  if default && plaintext == nil
    plaintext = default
  end

  # Write the virtual attribute with the plaintext value
  instance_variable_set("@#{attribute}", plaintext)
  @attributes.write_from_database attribute.to_s, plaintext
end
__vault_load_attributes!(attribute_to_read = nil) click to toggle source
# File lib/vault/encrypted_model.rb, line 239
def __vault_load_attributes!(attribute_to_read = nil)
  self.class.__vault_attributes.each do |attribute, options|
    # skip loading certain keys in one of two cases:
    # 1- the attribute has already been loaded
    # 2- the single decrypt option is set AND this is not the attribute we're requesting to decrypt
    next if instance_variable_get("@#{attribute}") || (self.class.vault_single_decrypt && attribute_to_read != attribute)
    self.__vault_load_attribute!(attribute, options)
  end

  @__vault_loaded = self.class.__vault_attributes.all? { |attribute, __| instance_variable_defined?("@#{attribute}") }

  return true
end
__vault_persist_attribute!(attribute, options) click to toggle source

Encrypt a single attribute using Vault and persist back onto the encrypted attribute value.

# File lib/vault/encrypted_model.rb, line 327
def __vault_persist_attribute!(attribute, options)
  key        = options[:key]
  path       = options[:path]
  serializer = options[:serializer]
  column     = options[:encrypted_column]
  context    = options[:context]
  transform  = options[:transform_secret]

  # Only persist changed attributes to minimize requests - this helps
  # minimize the number of requests to Vault.
  if ActiveRecord.gem_version >= Gem::Version.new("6.0")
    return unless previous_changes.include?(attribute)
  elsif ActiveRecord.gem_version >= Gem::Version.new("5.2")
    return unless previous_changes_include?(attribute)
  elsif ActiveRecord.gem_version >= Gem::Version.new("5.1")
    return unless saved_change_to_attribute?(attribute.to_s)
  else
    return unless attribute_changed?(attribute)
  end

  # Get the current value of the plaintext attribute
  plaintext = attributes[attribute.to_s]

  # Apply the serialize to the plaintext value, if one exists
  if serializer
    plaintext = serializer.encode(plaintext)
  end

  # Generate context if needed
  generated_context = __vault_generate_context(context)

  if transform
    # If this is a secret encrypted with FPE, we should not encrypt it in vault
    # This prevents a double encryption via standard vault encryption and FPE.
    # FPE was performed earlier as part of the serialization process.
    ciphertext = plaintext
  else
    # Generate the ciphertext and store it back as an attribute
    ciphertext = Vault::Rails.encrypt(
      path, key, plaintext,
      context: generated_context
    )
  end

  # Write the attribute back, so that we don't have to reload the record
  # to get the ciphertext
  write_attribute(column, ciphertext)

  # Return the updated column so we can save
  { column => ciphertext }
end
__vault_persist_attributes!() click to toggle source

Encrypt all the attributes using Vault and set the encrypted values back on this model. @return [true]

# File lib/vault/encrypted_model.rb, line 306
def __vault_persist_attributes!
  changes = {}

  self.class.__vault_attributes.each do |attribute, options|
    if c = self.__vault_persist_attribute!(attribute, options)
      changes.merge!(c)
    end
  end

  # If there are any changes to the model, update them all at once,
  # skipping any callbacks and validation. This is okay, because we are
  # already in a transaction due to the callback.
  if !changes.empty?
    self.update_columns(changes)
  end

  return true
end
reload(*) click to toggle source

Override the reload method to reload the Vault attributes. This will ensure that we always have the most recent data from Vault when we reload a record from the database.

Calls superclass method
# File lib/vault/encrypted_model.rb, line 396
def reload(*)
  super.tap do
    # Unset all the instance variables to force the new data to be pulled
    # from Vault
    self.class.__vault_attributes.each do |attribute, _|
      self.instance_variable_set("@#{attribute}", nil)
      @attributes.write_from_database attribute.to_s, nil
    end

    self.__vault_initialize_attributes!
  end
end