Module: YattrEncrypted

Defined in:
lib/yattr_encrypted.rb,
lib/yattr_encrypted/railtie.rb,
lib/yattr_encrypted/version.rb

Overview

Adds attr_accessors that encrypt and decrypt an object’s attributes

Defined Under Namespace

Classes: Railtie

Constant Summary collapse

ALGORITHM =
'aes-256-cbc'
VERSION =
'0.1.7'

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object

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)

: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, :prefix => '', :suffix =>
         '_encrypted'</tt> would generate attributes named
         'email_encrypted' and 'password_encrypted' to store the
         encrypted email.  Defaults to '_encrypted'.

:key     The encryption key. Not generally required.
         Defaults to Rails.application.config.secret_token

Example

class User
  yattr_encrypted :email
end

@user = User.new
@user.encrypted_email # nil? is true
@user.email? # false
@user.email = '[email protected]'
@user.email? # true
@user.encrypted_email # returns the encrypted version of '[email protected]'


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/yattr_encrypted.rb', line 54

def self.included(base)
  class << base
    attr_accessor :yate_encrypted_attributes

    def yattr_encrypted(*attributes)
      # construct options
      options = {
        :prefix           => '',
        :suffix           => '_encrypted',
        :key              => defined?(::Rails) ? ::Rails.application.config.secret_token : nil,
      }
      # merge specific options
      options.merge!(attributes.pop) if Hash === attributes.last

      # tell self to define instance methods from the database if they have not already been generated
      define_attribute_methods unless attribute_methods_generated?
      
      # schedule yattr_update_encrypted_values before save
      self.send :before_save, :yate_update_encrypted_values unless self.yate_encrypted_attributes

      # collect existing instance methods
      instance_methods_as_symbols = instance_methods.map { |method| method.to_sym }
 
      # iterate through attributes and create accessors, verify encryped accessors exist
      attributes.map { |x| x.to_sym }.each do |attribute|
        encrypted_attribute_name = [options[:prefix], attribute, options[:suffix]].join.to_sym

        # barf if reader and write doesn't exist for encrypted attribute
        raise ArgumentError.new("No Reader method for encrypted version of #{attribute}: #{encrypted_attribute_name}") \
            unless instance_methods_as_symbols.include?(encrypted_attribute_name)
        raise ArgumentError.new("No Write method for encrypted version of #{attribute}: #{encrypted_attribute_name}") \
            unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")

        tmp =<<-XXX
        def #{attribute}
          options = yate_encrypted_attributes[:#{attribute}]
          if @#{attribute}.nil? || @#{attribute}.empty?
            @#{attribute} = #{encrypted_attribute_name} ? \
                yate_decrypt(#{encrypted_attribute_name}, options[:key]) : \
                ''
            if options[:read_filter].respond_to? :call
                @#{attribute} = options[:read_filter].call(@#{attribute})
            elsif options[:read_filter]
              @#{attribute} = self.send options[:read_filter].to_sym, @#{attribute}
            end
            self.yate_checksums[:#{attribute}] = yate_attribute_hash_value(:#{attribute})
            self.yate_dirty[:#{attribute}] = true
          elsif options[:read_filter]
            if options[:read_filter].respond_to? :call
              @#{attribute} = options[:read_filter].call(@#{attribute})
            else
              @#{attribute} = self.send options[:read_filter].to_sym, @#{attribute}
            end
          end
          @#{attribute}
        end
        XXX
        class_eval(tmp)

        tmp =<<-XXX
        def #{attribute}= value
          options = yate_encrypted_attributes[:#{attribute}]
          if options[:write_filter].respond_to? :call
            value = options[:write_filter].call(value)
          elsif options[:write_filter]
            value = self.send options[:write_filter], value
          end
          @#{attribute} = value
          self.#{encrypted_attribute_name} = yate_encrypt(value, options[:key])
          self.yate_checksums[:#{attribute}] = yate_attribute_hash_value(:#{attribute})
          self.yate_dirty[:#{attribute}] = true
        end
        XXX
        class_eval(tmp)

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

        self.yate_encrypted_attributes ||= {}

        self.yate_encrypted_attributes[attribute.to_sym] = \
            options.merge(:attribute => encrypted_attribute_name)

        # this conditioning is a hack to allow stand alone tests to work
        # one of these days 'real soon now' I'll figure out how to
        # stub out Rails::Railtie, but until then. . .
        if defined?(Rails)
          # add to filter_parameters keep out of log
          Rails.application.config.filter_parameters += [attribute, encrypted_attribute_name]

          # protect encrypted field from mass assignment
          attr_protected encrypted_attribute_name.to_sym
       end
      end
    end
  end
end