module ActiveRecord #:nodoc:
class Base
class << self # Class methods
# Drop in replacement for attr_encrypted gem, except that it uses
# SymmetricEncryption for managing the encryption key
#
# Parameters:
# * Symbolic names of each method to create which has a corresponding
# method already defined in rails starting with: encrypted_
# * Followed by an optional hash:
# :random_iv [true|false]
# Whether the encrypted value should use a random IV every time the
# field is encrypted.
# It is recommended to set this to true where feasible. If the encrypted
# value could be used as part of a SQL where clause, or as part
# of any lookup, then it must be false.
# Setting random_iv to true will result in a different encrypted output for
# the same input string.
# Note: Only set to true if the field will never be used as part of
# the where clause in an SQL query.
# Note: When random_iv is true it will add a 8 byte header, plus the bytes
# to store the random IV in every returned encrypted string, prior to the
# encoding if any.
# Default: false
# Highly Recommended where feasible: true
#
# :type [Symbol]
# The type for this field, #see SymmetricEncryption::COERCION_TYPES
# Default: :string
#
# :compress [true|false]
# Whether to compress str before encryption
# Should only be used for large strings since compression overhead and
# the overhead of adding the 'magic' header may exceed any benefits of
# compression
# Note: Adds a 6 byte header prior to encoding, only if :random_iv is false
# Default: false
def attr_encrypted(*params)
# Ensure ActiveRecord has created all its methods first
# Ignore failures since the table may not yet actually exist
define_attribute_methods rescue nil
options = params.last.is_a?(Hash) ? params.pop.dup : {}
params.each do |attribute|
SymmetricEncryption::Generator.generate_decrypted_accessors(self, attribute, "encrypted_#{attribute}", options)
encrypted_attributes[attribute.to_sym] = "encrypted_#{attribute}".to_sym
end
end
# Contains a hash of encrypted attributes with virtual attribute names as keys and real attribute
# names as values
#
# Example
#
# class User < ActiveRecord::Base
# attr_encrypted :email
# end
#
# User.encrypted_attributes => { email: encrypted_email }
def encrypted_attributes
@encrypted_attributes ||= superclass.respond_to?(:encrypted_attributes) ? superclass.encrypted_attributes.dup : {}
end
# Return the name of all encrypted virtual attributes as an Array of symbols
# Example: [:email, :password]
def encrypted_keys
@encrypted_keys ||= encrypted_attributes.keys
end
# Return the name of all encrypted columns as an Array of symbols
# Example: [:encrypted_email, :encrypted_password]
def encrypted_columns
@encrypted_columns ||= encrypted_attributes.values
end
# Returns whether an attribute has been configured to be encrypted
#
# Example
#
# class User < ActiveRecord::Base
# attr_accessor :name
# attr_encrypted :email
# end
#
# User.encrypted_attribute?(:name) # false
# User.encrypted_attribute?(:email) # true
def encrypted_attribute?(attribute)
encrypted_keys.include?(attribute)
end
# Returns whether the attribute is the database column to hold the
# encrypted data for a matching encrypted attribute
#
# Example
#
# class User < ActiveRecord::Base
# attr_accessor :name
# attr_encrypted :email
# end
#
# User.encrypted_column?(:encrypted_name) # false
# User.encrypted_column?(:encrypted_email) # true
def encrypted_column?(attribute)
encrypted_columns.include?(attribute)
end
protected
# Allows you to use dynamic methods like find_by_email or scoped_by_email for
# encrypted attributes
#
# This is useful for encrypting fields like email addresses. Your user's email addresses
# are encrypted in the database, but you can still look up a user by email for logging in
#
# Example
#
# class User < ActiveRecord::Base
# attr_encrypted :email
# end
#
# User.find_by_email_and_password('test@example.com', 'testing')
# # results in a call to
# User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing')
def method_missing_with_attr_encrypted(method, *args, &block)
if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
attribute_names = match.captures.last.split('_and_')
attribute_names.each_with_index do |attribute, index|
encrypted_name = "encrypted_#{attribute}"
if method_defined? encrypted_name.to_sym
args[index] = ::SymmetricEncryption.encrypt(args[index])
attribute_names[index] = encrypted_name
end
end
method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
end
method_missing_without_attr_encrypted(method, *args, &block)
end
# Dynamic finders dropped in Rails 4.1
if ActiveRecord::VERSION::STRING.to_f < 4.1
alias_method_chain :method_missing, :attr_encrypted
end
end
end
end