module AttrEncryption

Adds attr_accessors that encrypt and decrypt an object's attributes

Public Instance Methods

attr_encrypted(*attributes) click to toggle source

Generates attr_accessors that encrypt and decrypt attributes transparently

Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)

:attribute        => The name of the referenced encrypted attribute. For example
                     <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
                     attribute named 'ee' to store the encrypted email. This is useful when defining
                     one attribute to encrypt at a time or when the :prefix and :suffix options
                     aren't enough. Defaults to nil.

:type             => The data type of the value to be encrypted/decrypted. Can be 'date', 'datetime', 'binary', 'text' or json.
                     When encrypting, all values will use their string value (value.to_s). When decrypting,
                     the type of the value will determine what is returned. For example:

                       type = 'date': Date.parse(decrypted_value)
                       type = 'time': DateTime.parse(decrypted_value)
                       type = 'binary': decrypted_value
                       type = 'text': decrypted_value.force_encoding('utf-8')
                       type = 'json': JSON.parse(decrypted_value)

:prefix           => A prefix used to generate the name of the referenced encrypted attributes.
                     For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
                     generate attributes named 'crypted_email' and 'crypted_password' to store the
                     encrypted email and password. Defaults to ''.

:suffix           => A suffix used to generate the name of the referenced encrypted attributes.
                     For example <tt>attr_accessor :email, :password, :suffix => '_encrypted'</tt>
                     would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
                     encrypted email. Defaults to '_enc'.

:preencrypt       => The symbol identifying a method that should be run on an attribute immediately prior
                     to marshalling and encrypting. This could be used for things like stripping white-space
                     from values or other sorts of pre-processing. Defaults to nil.

:key              => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
                     a symbol representing an instance method then the :key option will be replaced with the result of the
                     method before being passed to the encryptor. Objects that respond to :call are evaluated as well (including procs).
                     Any other key types will be passed directly to the encryptor. TODO (DJS): We'll see if we need this.

:encode           => If set to true, attributes will be encoded as well as encrypted. This is useful if you're
                     planning on storing the encrypted attributes in a database. The default encoding is 'm' (base64),
                     however this can be overwritten by setting the :encode option to some other encoding string instead of
                     just 'true'. See http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding directives.
                     Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel. TODO(DJS): We'll see if we need this.

:default_encoding => Defaults to 'm' (base64). TODO(DJS): Hmmm. See above

:marshal          => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
                     on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord
                     or DataMapper. TODO(DJS): Don't want to use this by default in our encryption since we want to be able to query...

:marshaler        => The object to use for marshaling. Defaults to Marshal.

:dump_method      => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.

:load_method      => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.

:encryptor        => The object to use for encrypting. Defaults to Encryptor. TODO(DJS): Need to changed this to indicate our encryptor

:encrypt_method   => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'encrypt'. TODO(DJS): Verify this.

:decrypt_method   => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'decrypt'. TODO(DJS): Verify this.

:if               => Attributes are only encrypted if this option evaluates to true. If you pass a symbol representing an instance
                     method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
                     Defaults to true.

:unless           => Attributes are only encrypted if this option evaluates to false. If you pass a symbol representing an instance
                     method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
                     Defaults to false.

You can specify your own default options

TODO(DJS): Need to rework the examples.

class User
  # now all attributes will be encoded and marshaled by default
  attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
  attr_encrypted :configuration, :key => 'my secret key'
end

Example

class User
  attr_encrypted :email, :credit_card, :key => 'some secret key'
  attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
end

@user = User.new
@user.encrypted_email # nil
@user.email? # false
@user.email = 'test@example.com'
@user.email? # true
@user.encrypted_email # returns the encrypted version of 'test@example.com'

@user.configuration = { :time_zone => 'UTC' }
@user.encrypted_configuration # returns the encrypted version of configuration

See README for more examples
# File lib/attr_encryption.rb, line 116
def attr_encrypted(*attributes)
  options = {
    :prefix           => '',
    :suffix           => '_enc',
    :if               => true,
    :unless           => false,
    :encode           => false,
    :key              => $encryption_key,
    :type             => 'text',
    :default_encoding => 'm',
    :preenrypt        => nil,
    :marshal          => false,
    :marshaler        => Marshal,
    :dump_method      => 'dump',
    :load_method      => 'load',
    :encryptor        => MySQLEncryptor.instance,
    :encrypt_method   => 'encrypt',
    :decrypt_method   => 'decrypt'
  }.merge!(attr_encrypted_options).merge!(attributes.last.is_a?(Hash) ? attributes.pop : {})

  options[:encode] = options[:default_encoding] if options[:encode] == true

  attributes.each do |attribute|
    encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym

    instance_methods_as_symbols = instance_methods.collect { |method| method.to_sym }
    attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
    attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")

    define_method(attribute) do
      cached_value = instance_variable_get("@#{attribute}")
      if cached_value
        case options[:type]
        when 'date'
          value = cached_value.is_a?(Date) ? cached_value : nil
        when 'json'
          value = cached_value.is_a?(Hash) || cached_value.is_a?(Array) ? cached_value : nil
        else
          value = cached_value
        end
      else
        value = nil
      end
      value || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
    end

    define_method("#{attribute}=") do |value|
      value_to_encrypt = options[:type] == 'json' ? value.to_json : value
      
      send("#{encrypted_attribute_name}=", encrypt(attribute, value_to_encrypt))
      instance_variable_set("@#{attribute}", value)
    end

    define_method("#{attribute}?") do
      value = send(attribute)
      value.respond_to?(:empty?) ? !value.empty? : !!value
    end

    encrypted_attributes[attribute.to_sym] = options.merge(:attribute => encrypted_attribute_name)
  end
end
Also aliased as: attr_encryptor
attr_encrypted?(attribute) click to toggle source

Checks if an attribute is configured with attr_encrypted

Example

class User
  attr_accessor :name
  attr_encrypted :email
end

User.attr_encrypted?(:name)  # false
User.attr_encrypted?(:email) # true
# File lib/attr_encryption.rb, line 197
def attr_encrypted?(attribute)
  encrypted_attributes.has_key?(attribute.to_sym)
end
attr_encrypted_options() click to toggle source

Default options to use with calls to attr_encrypted

It will inherit existing options from its superclass

# File lib/attr_encryption.rb, line 182
def attr_encrypted_options
  @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
end
attr_encryptor(*attributes)
Alias for: attr_encrypted
cleanse_value(value, options) click to toggle source

Cleans up the value to ensure we don't get empty strings the db when we should be getting nils.

# File lib/attr_encryption.rb, line 245
def cleanse_value(value, options)
  return nil if value.is_a?(String) && value.empty? && options[:type] == 'date'
  value
end
decrypt(attribute, encrypted_value, options = {}) click to toggle source

Decrypts a value for the attribute specified

Example

class User
  attr_encrypted :email
end

email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
# File lib/attr_encryption.rb, line 210
def decrypt(attribute, encrypted_value, options = {})
  options = encrypted_attributes[attribute.to_sym].merge(options)
  if options[:if] && !options[:unless] && !encrypted_value.nil? && !(encrypted_value.is_a?(String) && encrypted_value.empty?)
    encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
    value = options[:encryptor].send(options[:decrypt_method], options.merge!(:value => encrypted_value))
    value = options[:marshaler].send(options[:load_method], value) if options[:marshal]
    value
  else
    encrypted_value
  end
end
encrypt(attribute, value, options = {}) click to toggle source

Encrypts a value for the attribute specified

Example

class User
  attr_encrypted :email
end

encrypted_email = User.encrypt(:email, 'test@example.com')
# File lib/attr_encryption.rb, line 231
def encrypt(attribute, value, options = {})
  options = encrypted_attributes[attribute.to_sym].merge(options)
  if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
    value = options[:preencrypt] ? (value.is_a?(String) ? value.send(options[:preencrypt]) : value) : value
    value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
    encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(:value => value))
    encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
    encrypted_value
  else
    cleanse_value value, options
  end
end
encrypted_attributes() click to toggle source

Contains a hash of encrypted attributes with virtual attribute names as keys and their corresponding options as values

Example

class User
  attr_encrypted :email, :key => 'my secret key'
end

User.encrypted_attributes # { :email => { :attribute => 'encrypted_email', :key => 'my secret key' } }
# File lib/attr_encryption.rb, line 260
def encrypted_attributes
  @encrypted_attributes ||= superclass.encrypted_attributes.dup
end
method_missing(method, *arguments, &block) click to toggle source

Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method if attribute was configured with attr_encrypted

Example

class User
  attr_encrypted :email, :key => 'my secret key'
end

User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
Calls superclass method
# File lib/attr_encryption.rb, line 274
def method_missing(method, *arguments, &block)
  if method.to_s =~ /\A((en|de)crypt)_(.+)\z/ && attr_encrypted?($3)
    send($1, $3, *arguments)
  else
    super
  end
end