require 'encrypted_strings'
require 'encrypted_attributes/sha_cipher'
module EncryptedAttributes
module MacroMethods
# Encrypts the given attribute.
#
# Configuration options:
# * :mode - The mode of encryption to use. Default is :sha.
# See EncryptedStrings for other possible modes.
# * :to - The attribute to write the encrypted value to. Default
# is the same attribute being encrypted.
# * :before - The callback to invoke every time *before* the
# attribute is encrypted
# * :after - The callback to invoke every time *after* the
# attribute is encrypted
# * :on - The ActiveRecord callback to use when triggering the
# encryption. By default, this will encrypt on before_validation.
# See ActiveRecord::Callbacks for a list of possible callbacks.
# * :if - Specifies a method, proc or string to call to determine
# if the encryption should occur. The method, proc or string should return
# or evaluate to a true or false value.
# * :unless - Specifies a method, proc or string to call to
# determine if the encryption should not occur. The method, proc or string
# should return or evaluate to a true or false value.
#
# For additional configuration options used during the actual encryption,
# see the individual cipher class for the specified mode.
#
# == Encryption timeline
#
# By default, attributes are encrypted immediately before a record is
# validated. This means that you can still validate the presence of the
# encrypted attribute, but other things like password length cannot be
# validated without either (a) decrypting the value first or (b) using a
# different encryption target. For example,
#
# class User < ActiveRecord::Base
# encrypts :password, :to => :crypted_password
#
# validates_presence_of :password, :crypted_password
# validates_length_of :password, :maximum => 16
# end
#
# In the above example, the actual encrypted password will be stored in
# the +crypted_password+ attribute. This means that validations can
# still run against the model for the original password value.
#
# user = User.new(:password => 'secret')
# user.password # => "secret"
# user.crypted_password # => nil
# user.valid? # => true
# user.crypted_password # => "8152bc582f58c854f580cb101d3182813dec4afe"
#
# user = User.new(:password => 'longer_than_the_maximum_allowed')
# user.valid? # => false
# user.crypted_password # => "e80a709f25798f87d9ca8005a7f64a645964d7c2"
# user.errors[:password] # => "is too long (maximum is 16 characters)"
#
# == Encryption mode examples
#
# SHA encryption:
#
# class User < ActiveRecord::Base
# encrypts :password
# # encrypts :password, :salt => 'secret'
# end
#
# Symmetric encryption:
#
# class User < ActiveRecord::Base
# encrypts :password, :mode => :symmetric
# # encrypts :password, :mode => :symmetric, :key => 'custom'
# end
#
# Asymmetric encryption:
#
# class User < ActiveRecord::Base
# encrypts :password, :mode => :asymmetric
# # encrypts :password, :mode => :asymmetric, :public_key_file => '/keys/public', :private_key_file => '/keys/private'
# end
#
# == Dynamic configuration
#
# For better security, the encryption options (such as the salt value)
# can be based on values in each individual record. In order to
# dynamically configure the encryption options so that individual records
# can be referenced, an optional block can be specified.
#
# For example,
#
# class User < ActiveRecord::Base
# encrypts :password, :mode => :sha, :before => :create_salt do |user|
# {:salt => user.salt}
# end
#
# private
# def create_salt
# self.salt = "#{login}-#{Time.now}"
# end
# end
#
# In the above example, the SHA encryption's salt is configured
# dynamically based on the user's login and the time at which it was
# encrypted. This helps improve the security of the user's password.
def encrypts(*attr_names, &config)
base_options = attr_names.last.is_a?(Hash) ? attr_names.pop : {}
attr_names.each do |attr_name|
options = base_options.dup
attr_name = attr_name.to_s
to_attr_name = (options.delete(:to) || attr_name).to_s
# Figure out what cipher is being configured for the attribute
mode = options.delete(:mode) || :sha
class_name = "#{mode.to_s.classify}Cipher"
if EncryptedAttributes.const_defined?(class_name)
cipher_class = EncryptedAttributes.const_get(class_name)
else
cipher_class = EncryptedStrings.const_get(class_name)
end
# Define encryption hooks
define_callbacks("before_encrypt_#{attr_name}", "after_encrypt_#{attr_name}")
send("before_encrypt_#{attr_name}", options.delete(:before)) if options.include?(:before)
send("after_encrypt_#{attr_name}", options.delete(:after)) if options.include?(:after)
# Set the encrypted value on the configured callback
callback = options.delete(:on) || :before_validation
# Create a callback method to execute on the callback event
send(callback, :if => options.delete(:if), :unless => options.delete(:unless)) do |record|
record.send(:write_encrypted_attribute, attr_name, to_attr_name, cipher_class, config || options)
true
end
# Define virtual source attribute
if attr_name != to_attr_name && !column_names.include?(attr_name)
attr_reader attr_name unless method_defined?(attr_name)
attr_writer attr_name unless method_defined?("#{attr_name}=")
end
# Define the reader when reading the encrypted attribute from the database
define_method(to_attr_name) do
read_encrypted_attribute(to_attr_name, cipher_class, config || options)
end
unless included_modules.include?(EncryptedAttributes::InstanceMethods)
include EncryptedAttributes::InstanceMethods
end
end
end
end
module InstanceMethods #:nodoc:
private
# Encrypts the given attribute to a target location using the encryption
# options configured for that attribute
def write_encrypted_attribute(attr_name, to_attr_name, cipher_class, options)
value = send(attr_name)
# Only encrypt values that actually have content and have not already
# been encrypted
unless value.blank? || value.encrypted?
callback("before_encrypt_#{attr_name}")
# Create the cipher configured for this attribute
cipher = create_cipher(cipher_class, options, value)
# Encrypt the value
value = cipher.encrypt(value)
value.cipher = cipher
# Update the value based on the target attribute
send("#{to_attr_name}=", value)
callback("after_encrypt_#{attr_name}")
end
end
# Reads the given attribute from the database, adding contextual
# information about how it was encrypted so that equality comparisons
# can be used
def read_encrypted_attribute(to_attr_name, cipher_class, options)
value = read_attribute(to_attr_name)
# Make sure we set the cipher for equality comparison when reading
# from the database. This should only be done if the value is *not*
# blank, is *not* encrypted, and hasn't changed since it was read from
# the database. The dirty checking is important when the encypted value
# is written to the same attribute as the unencrypted value (i.e. you
# don't want to encrypt when a new value has been set)
unless value.blank? || value.encrypted? || attribute_changed?(to_attr_name)
# Create the cipher configured for this attribute
value.cipher = create_cipher(cipher_class, options, value)
end
value
end
# Creates a new cipher with the given configuration options
def create_cipher(klass, options, value)
options = options.is_a?(Proc) ? options.call(self) : options.dup
# Only use the contextual information for this plugin's ciphers
klass.parent == EncryptedAttributes ? klass.new(value, options) : klass.new(options)
end
end
end
ActiveRecord::Base.class_eval do
extend EncryptedAttributes::MacroMethods
end