honkster-attr_encrypted 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Sean Huber - shuber@huberry.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,302 @@
1
+ = attr_encrypted
2
+
3
+ Generates attr_accessors that encrypt and decrypt attributes transparently
4
+
5
+ It works with ANY class, however, you get a few extra features when you're using it with <tt>ActiveRecord</tt>, <tt>DataMapper</tt>, or <tt>Sequel</tt>
6
+
7
+
8
+ == Installation
9
+
10
+ gem install attr_encrypted
11
+
12
+
13
+ == Usage
14
+
15
+ === Basic
16
+
17
+ Encrypting attributes has never been easier:
18
+
19
+ class User
20
+ attr_accessor :name
21
+ attr_encrypted :ssn, :key => 'a secret key'
22
+
23
+ def load
24
+ # loads the stored data
25
+ end
26
+
27
+ def save
28
+ # saves the :name and :encrypted_ssn attributes somewhere (e.g. filesystem, database, etc)
29
+ end
30
+ end
31
+
32
+ @user = User.new
33
+ @user.ssn = '123-45-6789'
34
+ @user.encrypted_ssn # returns the encrypted version of :ssn
35
+ @user.save
36
+
37
+ @user = User.load
38
+ @user.ssn # decrypts :encrypted_ssn and returns '123-45-6789'
39
+
40
+ The <tt>attr_encrypted</tt> method is also aliased as <tt>attr_encryptor</tt> to conform to Ruby's <tt>attr_</tt> naming conventions. I should have called this project <tt>attr_encryptor</tt> but it was too late when I realized it ='(.
41
+
42
+
43
+ === Specifying the encrypted attribute name
44
+
45
+ By default, the encrypted attribute name is <tt>encrypted_#{attribute}</tt> (e.g. <tt>attr_encrypted :email</tt> would create an attribute named <tt>encrypted_email</tt>). So, if you're storing the encrypted attribute in the database, you need to make sure the <tt>encrypted_#{attribute}</tt> field exists in your table. You have a couple of options if you want to name your attribute something else.
46
+
47
+
48
+ ==== The <tt>:attribute</tt> option
49
+
50
+ You can simply pass the name of the encrypted attribute as the <tt>:attribute</tt> option:
51
+
52
+ class User
53
+ attr_encrypted :email, :key => 'a secret key', :attribute => 'email_encrypted'
54
+ end
55
+
56
+ This would generate an attribute named <tt>email_encrypted</tt>
57
+
58
+
59
+ ==== The <tt>:prefix</tt> and <tt>:suffix</tt> options
60
+
61
+ If you're planning on encrypting a few different attributes and you don't like the <tt>encrypted_#{attribute}</tt> naming convention then you can specify your own:
62
+
63
+ class User
64
+ attr_encrypted :email, :credit_card, :ssn, :key => 'a secret key', :prefix => 'secret_', :suffix => '_crypted'
65
+ end
66
+
67
+ This would generate the following attributes: <tt>secret_email_crypted</tt>, <tt>secret_credit_card_crypted</tt>, and <tt>secret_ssn_crypted</tt>.
68
+
69
+
70
+ === Encryption keys
71
+
72
+ Although a <tt>:key</tt> option may not be required (see custom encryptor below), it has a few special features
73
+
74
+
75
+ ==== Unique keys for each attribute
76
+
77
+ You can specify unique keys for each attribute if you'd like:
78
+
79
+ class User
80
+ attr_encrypted :email, :key => 'a secret key'
81
+ attr_encrypted :ssn, :key => 'a different secret key'
82
+ end
83
+
84
+
85
+ ==== Symbols representing instance methods as keys
86
+
87
+ If your class has an instance method that determines the encryption key to use, simply pass a symbol representing it like so:
88
+
89
+ class User
90
+ attr_encrypted :email, :key => :encryption_key
91
+
92
+ def encryption_key
93
+ # does some fancy logic and returns an encryption key
94
+ end
95
+ end
96
+
97
+
98
+ ==== Procs as keys
99
+
100
+ You can pass a proc/lambda object as the <tt>:key</tt> option as well:
101
+
102
+ class User
103
+ attr_encrypted :email, :key => proc { |user| user.key }
104
+ end
105
+
106
+ This can be used to create asymmetrical encryption by requiring users to provide their own encryption keys.
107
+
108
+
109
+ === Conditional encrypting
110
+
111
+ There may be times that you want to only encrypt when certain conditions are met. For example maybe you're using rails and you don't want to encrypt
112
+ attributes when you're in development mode. You can specify conditions like this:
113
+
114
+ class User < ActiveRecord::Base
115
+ attr_encrypted :email, :key => 'a secret key', :unless => Rails.env.development?
116
+ end
117
+
118
+ You can specify both <tt>:if</tt> and <tt>:unless</tt> options. 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.
119
+
120
+
121
+ === Custom encryptor
122
+
123
+ The <tt>Encryptor</tt> (see http://github.com/shuber/encryptor) class is used by default. You may use your own custom encryptor by specifying
124
+ the <tt>:encryptor</tt>, <tt>:encrypt_method</tt>, and <tt>:decrypt_method</tt> options
125
+
126
+ Lets suppose you'd like to use this custom encryptor class:
127
+
128
+ class SillyEncryptor
129
+ def self.silly_encrypt(options)
130
+ (options[:value] + options[:secret_key]).reverse
131
+ end
132
+
133
+ def self.silly_decrypt(options)
134
+ options[:value].reverse.gsub(/#{options[:secret_key]}$/, '')
135
+ end
136
+ end
137
+
138
+ Simply set up your class like so:
139
+
140
+ class User
141
+ attr_encrypted :email, :secret_key => 'a secret key', :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt
142
+ end
143
+
144
+ Any options that you pass to <tt>attr_encrypted</tt> will be passed to the encryptor along with the <tt>:value</tt> option which contains the string to encrypt/decrypt. Notice it uses <tt>:secret_key</tt> instead of <tt>:key</tt>.
145
+
146
+
147
+ === Custom algorithms
148
+
149
+ The default <tt>Encryptor</tt> uses the standard ruby OpenSSL library. It's default algorithm is <tt>aes-256-cbc</tt>. You can modify this by passing the <tt>:algorithm</tt> option to the <tt>attr_encrypted</tt> call like so:
150
+
151
+ class User
152
+ attr_encrypted :email, :key => 'a secret key', :algorithm => 'bf'
153
+ end
154
+
155
+ Run <tt>openssl list-cipher-commands</tt> to view a list of algorithms supported on your platform. See http://github.com/shuber/encryptor for more information.
156
+
157
+ aes-128-cbc
158
+ aes-128-ecb
159
+ aes-192-cbc
160
+ aes-192-ecb
161
+ aes-256-cbc
162
+ aes-256-ecb
163
+ base64
164
+ bf
165
+ bf-cbc
166
+ bf-cfb
167
+ bf-ecb
168
+ bf-ofb
169
+ cast
170
+ cast-cbc
171
+ cast5-cbc
172
+ cast5-cfb
173
+ cast5-ecb
174
+ cast5-ofb
175
+ des
176
+ des-cbc
177
+ des-cfb
178
+ des-ecb
179
+ des-ede
180
+ des-ede-cbc
181
+ des-ede-cfb
182
+ des-ede-ofb
183
+ des-ede3
184
+ des-ede3-cbc
185
+ des-ede3-cfb
186
+ des-ede3-ofb
187
+ des-ofb
188
+ des3
189
+ desx
190
+ idea
191
+ idea-cbc
192
+ idea-cfb
193
+ idea-ecb
194
+ idea-ofb
195
+ rc2
196
+ rc2-40-cbc
197
+ rc2-64-cbc
198
+ rc2-cbc
199
+ rc2-cfb
200
+ rc2-ecb
201
+ rc2-ofb
202
+ rc4
203
+ rc4-40
204
+
205
+
206
+ === Default options
207
+
208
+ Let's imagine that you have a few attributes that you want to encrypt with different keys, but you don't like the <tt>encrypted_#{attribute}</tt> naming convention. Instead of having to define your class like this:
209
+
210
+ class User
211
+ attr_encrypted :email, :key => 'a secret key', :prefix => '', :suffix => '_crypted'
212
+ attr_encrypted :ssn, :key => 'a different secret key', :prefix => '', :suffix => '_crypted'
213
+ attr_encrypted :credit_card, :key => 'another secret key', :prefix => '', :suffix => '_crypted'
214
+ end
215
+
216
+ You can simply define some default options like so:
217
+
218
+ class User
219
+ attr_encrypted_options.merge!(:prefix => '', :suffix => '_crypted')
220
+ attr_encrypted :email, :key => 'a secret key'
221
+ attr_encrypted :ssn, :key => 'a different secret key'
222
+ attr_encrypted :credit_card, :key => 'another secret key'
223
+ end
224
+
225
+ This should help keep your classes clean and DRY.
226
+
227
+
228
+ === Encoding
229
+
230
+ You're probably going to be storing your encrypted attributes somehow (e.g. filesystem, database, etc) and may run into some issues trying to store a weird
231
+ encrypted string. I've had this problem myself using MySQL. You can simply pass the <tt>:encode</tt> option to automatically encode/decode when encrypting/decrypting.
232
+
233
+ class User
234
+ attr_encrypted :email, :key => 'some secret key', :encode => true
235
+ end
236
+
237
+ The default encoding is <tt>m*</tt> (base64). You can change this by setting <tt>:encode => 'some encoding'</tt>. See the <tt>Array#pack</tt> method at http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding options.
238
+
239
+
240
+ === Marshaling
241
+
242
+ You may want to encrypt objects other than strings (e.g. hashes, arrays, etc). If this is the case, simply pass the <tt>:marshal</tt> option to automatically marshal when encrypting/decrypting.
243
+
244
+ class User
245
+ attr_encrypted :credentials, :key => 'some secret key', :marshal => true
246
+ end
247
+
248
+ You may also optionally specify <tt>:marshaler</tt>, <tt>:dump_method</tt>, and <tt>:load_method</tt> if you want to use something other than the default <tt>Marshal</tt> object.
249
+
250
+
251
+ === Encrypt/decrypt attribute methods
252
+
253
+ If you use the same key to encrypt every record (per attribute) like this:
254
+
255
+ class User
256
+ attr_encrypted :email, :key => 'a secret key'
257
+ end
258
+
259
+ Then you'll have these two class methods available for each attribute: <tt>User.encrypt_email(email_to_encrypt)</tt> and <tt>User.decrypt_email(email_to_decrypt)</tt>. This can be useful when you're using <tt>ActiveRecord</tt> (see below).
260
+
261
+
262
+ === ActiveRecord
263
+
264
+ If you're using this gem with <tt>ActiveRecord</tt>, you get a few extra features:
265
+
266
+
267
+ ==== Default options
268
+
269
+ For your convenience, the <tt>:encode</tt> option is set to true by default since you'll be storing everything in a database.
270
+
271
+
272
+ ==== Dynamic find_by_ and scoped_by_ methods
273
+
274
+ Let's say you'd like to encrypt your user's email addresses, but you also need a way for them to login. Simply set up your class like so:
275
+
276
+ class User < ActiveRecord::Base
277
+ attr_encrypted :email, :key => 'a secret key'
278
+ attr_encrypted :password, :key => 'some other secret key'
279
+ end
280
+
281
+ You can now lookup and login users like so:
282
+
283
+ User.find_by_email_and_password('test@example.com', 'testing')
284
+
285
+ The call to <tt>find_by_email_and_password</tt> is intercepted and modified to <tt>find_by_encrypted_email_and_encrypted_password('ENCRYPTED EMAIL', 'ENCRYPTED PASSWORD')</tt>. The dynamic scope methods like <tt>scoped_by_email_and_password</tt> work the same way.
286
+
287
+ NOTE: This only works if all records are encrypted with the same encryption key (per attribute).
288
+
289
+
290
+ === DataMapper and Sequel
291
+
292
+ Just like the default options for <tt>ActiveRecord</tt>, the <tt>:encode</tt> option is set to true by default since you'll be storing everything in a database.
293
+
294
+
295
+ == Note on Patches/Pull Requests
296
+
297
+ * Fork the project.
298
+ * Make your feature addition or bug fix.
299
+ * Add tests for it. This is important so I don't break it in a
300
+ future version unintentionally.
301
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
302
+ * Send me a pull request. Bonus points for topic branches.
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the attr_encrypted gem.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the attr_encrypted gem.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'attr_encrypted'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README*')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
@@ -0,0 +1,327 @@
1
+ require 'encryptor'
2
+
3
+ # Adds attr_accessors that encrypt and decrypt an object's attributes
4
+ module AttrEncrypted
5
+ autoload :Version, 'attr_encrypted/version'
6
+
7
+ def self.extended(base) # :nodoc:
8
+ base.class_eval do
9
+ include InstanceMethods
10
+ attr_writer :attr_encrypted_options
11
+ @attr_encrypted_options, @encrypted_attributes = {}, {}
12
+ end
13
+ end
14
+
15
+ # Generates attr_accessors that encrypt and decrypt attributes transparently
16
+ #
17
+ # Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)
18
+ #
19
+ # :attribute => The name of the referenced encrypted attribute. For example
20
+ # <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
21
+ # attribute named 'ee' to store the encrypted email. This is useful when defining
22
+ # one attribute to encrypt at a time or when the :prefix and :suffix options
23
+ # aren't enough. Defaults to nil.
24
+ #
25
+ # :prefix => A prefix used to generate the name of the referenced encrypted attributes.
26
+ # For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
27
+ # generate attributes named 'crypted_email' and 'crypted_password' to store the
28
+ # encrypted email and password. Defaults to 'encrypted_'.
29
+ #
30
+ # :suffix => A suffix used to generate the name of the referenced encrypted attributes.
31
+ # For example <tt>attr_accessor :email, :password, :prefix => '', :suffix => '_encrypted'</tt>
32
+ # would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
33
+ # encrypted email. Defaults to ''.
34
+ #
35
+ # :key => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
36
+ # a symbol representing an instance method then the :key option will be replaced with the result of the
37
+ # method before being passed to the encryptor. Objects that respond to :call are evaluated as well (including procs).
38
+ # Any other key types will be passed directly to the encryptor.
39
+ #
40
+ # :encode => If set to true, attributes will be encoded as well as encrypted. This is useful if you're
41
+ # planning on storing the encrypted attributes in a database. The default encoding is 'm' (base64),
42
+ # however this can be overwritten by setting the :encode option to some other encoding string instead of
43
+ # just 'true'. See http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding directives.
44
+ # Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
45
+ #
46
+ # :default_encoding => Defaults to 'm' (base64).
47
+ #
48
+ # :marshal => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
49
+ # on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord
50
+ # or DataMapper.
51
+ #
52
+ # :marshaler => The object to use for marshaling. Defaults to Marshal.
53
+ #
54
+ # :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
55
+ #
56
+ # :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
57
+ #
58
+ # :encryptor => The object to use for encrypting. Defaults to Encryptor.
59
+ #
60
+ # :encrypt_method => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'encrypt'.
61
+ #
62
+ # :decrypt_method => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'decrypt'.
63
+ #
64
+ # :if => Attributes are only encrypted if this option evaluates to true. If you pass a symbol representing an instance
65
+ # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
66
+ # Defaults to true.
67
+ #
68
+ # :unless => Attributes are only encrypted if this option evaluates to false. If you pass a symbol representing an instance
69
+ # method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
70
+ # Defaults to false.
71
+ #
72
+ # You can specify your own default options
73
+ #
74
+ # class User
75
+ # # now all attributes will be encoded and marshaled by default
76
+ # attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
77
+ # attr_encrypted :configuration, :key => 'my secret key'
78
+ # end
79
+ #
80
+ #
81
+ # Example
82
+ #
83
+ # class User
84
+ # attr_encrypted :email, :credit_card, :key => 'some secret key'
85
+ # attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
86
+ # end
87
+ #
88
+ # @user = User.new
89
+ # @user.encrypted_email # nil
90
+ # @user.email? # false
91
+ # @user.email = 'test@example.com'
92
+ # @user.email? # true
93
+ # @user.encrypted_email # returns the encrypted version of 'test@example.com'
94
+ #
95
+ # @user.configuration = { :time_zone => 'UTC' }
96
+ # @user.encrypted_configuration # returns the encrypted version of configuration
97
+ #
98
+ # See README for more examples
99
+ def attr_encrypted(*attributes)
100
+ options = {
101
+ :prefix => 'encrypted_',
102
+ :suffix => '',
103
+ :if => true,
104
+ :unless => false,
105
+ :encode => false,
106
+ :default_encoding => 'm',
107
+ :marshal => false,
108
+ :marshaler => Marshal,
109
+ :dump_method => 'dump',
110
+ :load_method => 'load',
111
+ :encryptor => Encryptor,
112
+ :encrypt_method => 'encrypt',
113
+ :decrypt_method => 'decrypt',
114
+ :key_identifier => '',
115
+ :keys => {}
116
+ }
117
+ options.merge!(attr_encrypted_options)
118
+ options.merge!(attributes.extract_options!)
119
+
120
+ if options[:key].nil?
121
+ if options[:keys] && options[:keys][options[:key_identifier]]
122
+ options[:key] = options[:keys][options[:key_identifier]]
123
+ end
124
+ end
125
+
126
+ options[:encode] = options[:default_encoding] if options[:encode] == true
127
+
128
+ attributes.each do |attribute|
129
+ encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
130
+
131
+ instance_methods_as_symbols = instance_methods.collect { |method| method.to_sym }
132
+ attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
133
+ attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
134
+
135
+ define_method(attribute) do
136
+ instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name)))
137
+ end
138
+
139
+ define_method("#{attribute}=") do |value|
140
+ send("#{encrypted_attribute_name}=", encrypt(attribute, value))
141
+ instance_variable_set("@#{attribute}", value)
142
+ end
143
+
144
+ define_method("#{attribute}?") do
145
+ value = send(attribute)
146
+ value.respond_to?(:empty?) ? !value.empty? : !!value
147
+ end
148
+
149
+ encrypted_attributes[attribute.to_sym] = options.merge(:attribute => encrypted_attribute_name)
150
+ end
151
+ end
152
+ alias_method :attr_encryptor, :attr_encrypted
153
+
154
+ # Default options to use with calls to <tt>attr_encrypted</tt>
155
+ #
156
+ # It will inherit existing options from its superclass
157
+ def attr_encrypted_options
158
+ @attr_encrypted_options ||= superclass.attr_encrypted_options.dup
159
+ end
160
+
161
+ # Checks if an attribute is configured with <tt>attr_encrypted</tt>
162
+ #
163
+ # Example
164
+ #
165
+ # class User
166
+ # attr_accessor :name
167
+ # attr_encrypted :email
168
+ # end
169
+ #
170
+ # User.attr_encrypted?(:name) # false
171
+ # User.attr_encrypted?(:email) # true
172
+ def attr_encrypted?(attribute)
173
+ encrypted_attributes.has_key?(attribute.to_sym)
174
+ end
175
+
176
+ # Decrypts a value for the attribute specified
177
+ #
178
+ # Example
179
+ #
180
+ # class User
181
+ # attr_encrypted :email
182
+ # end
183
+ #
184
+ # email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
185
+ def decrypt(attribute, encrypted_value, options = {})
186
+ options = encrypted_attributes[attribute.to_sym].merge(options)
187
+ if options[:if] && !options[:unless] && !encrypted_value.nil? && !(encrypted_value.is_a?(String) && encrypted_value.empty?)
188
+ if encrypted_value =~ /::/
189
+ identifier, encrypted_value = encrypted_value.split("::", 2)
190
+ key = options[:keys][identifier]
191
+ else
192
+ key = options[:key]
193
+ end
194
+ encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
195
+ value = options[:encryptor].send(options[:decrypt_method], options.merge!(:value => encrypted_value, :key => key))
196
+ value = options[:marshaler].send(options[:load_method], value) if options[:marshal]
197
+ value
198
+ else
199
+ encrypted_value
200
+ end
201
+ end
202
+
203
+ # Encrypts a value for the attribute specified
204
+ #
205
+ # Example
206
+ #
207
+ # class User
208
+ # attr_encrypted :email
209
+ # end
210
+ #
211
+ # encrypted_email = User.encrypt(:email, 'test@example.com')
212
+ def encrypt(attribute, value, options = {})
213
+ options = encrypted_attributes[attribute.to_sym].merge(options)
214
+ if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
215
+ value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
216
+ encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(:value => value))
217
+ encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
218
+
219
+ if options[:key_identifier].present?
220
+ encrypted_value.insert(0, options[:key_identifier] + '::')
221
+ end
222
+
223
+ encrypted_value
224
+ else
225
+ value
226
+ end
227
+ end
228
+
229
+ # Contains a hash of encrypted attributes with virtual attribute names as keys
230
+ # and their corresponding options as values
231
+ #
232
+ # Example
233
+ #
234
+ # class User
235
+ # attr_encrypted :email, :key => 'my secret key'
236
+ # end
237
+ #
238
+ # User.encrypted_attributes # { :email => { :attribute => 'encrypted_email', :key => 'my secret key' } }
239
+ def encrypted_attributes
240
+ @encrypted_attributes ||= superclass.encrypted_attributes.dup
241
+ end
242
+
243
+ # Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
244
+ # if attribute was configured with attr_encrypted
245
+ #
246
+ # Example
247
+ #
248
+ # class User
249
+ # attr_encrypted :email, :key => 'my secret key'
250
+ # end
251
+ #
252
+ # User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
253
+ def method_missing(method, *arguments, &block)
254
+ if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
255
+ send($1, $3, *arguments)
256
+ else
257
+ super
258
+ end
259
+ end
260
+
261
+ module InstanceMethods
262
+ # Decrypts a value for the attribute specified using options evaluated in the current object's scope
263
+ #
264
+ # Example
265
+ #
266
+ # class User
267
+ # attr_accessor :secret_key
268
+ # attr_encrypted :email, :key => :secret_key
269
+ #
270
+ # def initialize(secret_key)
271
+ # self.secret_key = secret_key
272
+ # end
273
+ # end
274
+ #
275
+ # @user = User.new('some-secret-key')
276
+ # @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
277
+ def decrypt(attribute, encrypted_value)
278
+ self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
279
+ end
280
+
281
+ # Encrypts a value for the attribute specified using options evaluated in the current object's scope
282
+ #
283
+ # Example
284
+ #
285
+ # class User
286
+ # attr_accessor :secret_key
287
+ # attr_encrypted :email, :key => :secret_key
288
+ #
289
+ # def initialize(secret_key)
290
+ # self.secret_key = secret_key
291
+ # end
292
+ # end
293
+ #
294
+ # @user = User.new('some-secret-key')
295
+ # @user.encrypt(:email, 'test@example.com')
296
+ def encrypt(attribute, value)
297
+ self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
298
+ end
299
+
300
+ protected
301
+
302
+ # Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
303
+ def evaluated_attr_encrypted_options_for(attribute)
304
+ self.class.encrypted_attributes[attribute.to_sym].inject({}) { |hash, (option, value)| hash.merge!(option => evaluate_attr_encrypted_option(value)) }
305
+ end
306
+
307
+ # Evaluates symbol (method reference) or proc (responds to call) options
308
+ #
309
+ # If the option is not a symbol or proc then the original option is returned
310
+ def evaluate_attr_encrypted_option(option)
311
+ if option.is_a?(Symbol) && respond_to?(option)
312
+ send(option)
313
+ elsif option.respond_to?(:call)
314
+ option.call(self)
315
+ else
316
+ option
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ Object.extend AttrEncrypted
323
+
324
+ require 'attr_encrypted/adapters/active_record' if defined?(ActiveRecord::Base)
325
+ require 'attr_encrypted/adapters/data_mapper' if defined?(DataMapper)
326
+ require 'attr_encrypted/adapters/sequel' if defined?(Sequel)
327
+ require 'attr_encrypted/adapters/mongo_mapper' if defined?(MongoMapper)