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,55 @@
1
+ module AttrEncrypted
2
+ module Adapters
3
+ module ActiveRecord
4
+ def self.extended(base) # :nodoc:
5
+ base.class_eval do
6
+ attr_encrypted_options[:encode] = true
7
+ class << self; alias_method_chain :method_missing, :attr_encrypted; end
8
+ end
9
+ end
10
+
11
+ protected
12
+
13
+ # Ensures the attribute methods for db fields have been defined before calling the original
14
+ # <tt>attr_encrypted</tt> method
15
+ def attr_encrypted(*attrs)
16
+ define_attribute_methods rescue nil
17
+ super
18
+ attrs.reject { |attr| attr.is_a?(Hash) }.each { |attr| alias_method "#{attr}_before_type_cast", attr }
19
+ end
20
+
21
+ # Allows you to use dynamic methods like <tt>find_by_email</tt> or <tt>scoped_by_email</tt> for
22
+ # encrypted attributes
23
+ #
24
+ # NOTE: This only works when the <tt>:key</tt> option is specified as a string (see the README)
25
+ #
26
+ # This is useful for encrypting fields like email addresses. Your user's email addresses
27
+ # are encrypted in the database, but you can still look up a user by email for logging in
28
+ #
29
+ # Example
30
+ #
31
+ # class User < ActiveRecord::Base
32
+ # attr_encrypted :email, :key => 'secret key'
33
+ # end
34
+ #
35
+ # User.find_by_email_and_password('test@example.com', 'testing')
36
+ # # results in a call to
37
+ # User.find_by_encrypted_email_and_password('the_encrypted_version_of_test@example.com', 'testing')
38
+ def method_missing_with_attr_encrypted(method, *args, &block)
39
+ if match = /^(find|scoped)_(all_by|by)_([_a-zA-Z]\w*)$/.match(method.to_s)
40
+ attribute_names = match.captures.last.split('_and_')
41
+ attribute_names.each_with_index do |attribute, index|
42
+ if attr_encrypted?(attribute)
43
+ args[index] = send("encrypt_#{attribute}", args[index])
44
+ attribute_names[index] = encrypted_attributes[attribute.to_sym][:attribute]
45
+ end
46
+ end
47
+ method = "#{match.captures[0]}_#{match.captures[1]}_#{attribute_names.join('_and_')}".to_sym
48
+ end
49
+ method_missing_without_attr_encrypted(method, *args, &block)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ ActiveRecord::Base.extend AttrEncrypted::Adapters::ActiveRecord
@@ -0,0 +1,19 @@
1
+ module AttrEncrypted
2
+ module Adapters
3
+ module DataMapper
4
+ def self.extended(base) # :nodoc:
5
+ class << base
6
+ alias_method :included_without_attr_encrypted, :included
7
+ alias_method :included, :included_with_attr_encrypted
8
+ end
9
+ end
10
+
11
+ def included_with_attr_encrypted(base)
12
+ included_without_attr_encrypted(base)
13
+ base.attr_encrypted_options[:encode] = true
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ DataMapper::Resource.extend AttrEncrypted::Adapters::DataMapper
@@ -0,0 +1,13 @@
1
+ module AttrEncrypted
2
+ module Adapters
3
+ module MongoMapper
4
+ def self.extended(base) # :nodoc:
5
+ base.class_eval do
6
+ attr_encrypted_options[:encode] = true
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ #MongoMapper::Docuemnt.extend AttrEncrypted::Adapters::MongoMapper
@@ -0,0 +1,11 @@
1
+ module AttrEncrypted
2
+ module Adapters
3
+ module Sequel
4
+ def self.extended(base) # :nodoc:
5
+ base.attr_encrypted_options[:encode] = true
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ Sequel::Model.extend AttrEncrypted::Adapters::Sequel
@@ -0,0 +1,17 @@
1
+ module AttrEncrypted
2
+ # Contains information about this gem's version
3
+ module Version
4
+ MAJOR = 1
5
+ MINOR = 2
6
+ PATCH = 0
7
+
8
+ # Returns a version string by joining <tt>MAJOR</tt>, <tt>MINOR</tt>, and <tt>PATCH</tt> with <tt>'.'</tt>
9
+ #
10
+ # Example
11
+ #
12
+ # Version.string # '1.0.2'
13
+ def self.string
14
+ [MAJOR, MINOR, PATCH].join('.')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,108 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
4
+
5
+ def create_people_table
6
+ silence_stream(STDOUT) do
7
+ ActiveRecord::Schema.define(:version => 1) do
8
+ create_table :people do |t|
9
+ t.string :encrypted_email
10
+ t.string :password
11
+ t.string :encrypted_credentials
12
+ t.string :salt
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ # The table needs to exist before defining the class
19
+ create_people_table
20
+
21
+ ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)
22
+
23
+ class Person < ActiveRecord::Base
24
+ attr_encrypted :email, :key => 'a secret key'
25
+ attr_encrypted :credentials, :key => Proc.new { |user| Encryptor.encrypt(:value => user.salt, :key => 'some private key') }, :marshal => true
26
+
27
+ ActiveSupport::Deprecation.silenced = true
28
+ def after_initialize; end
29
+ ActiveSupport::Deprecation.silenced = false
30
+
31
+ after_initialize :initialize_salt_and_credentials
32
+
33
+ protected
34
+
35
+ def initialize_salt_and_credentials
36
+ self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(5)).to_s)
37
+ self.credentials ||= { :username => 'example', :password => 'test' }
38
+ rescue ActiveRecord::MissingAttributeError
39
+ end
40
+ end
41
+
42
+ class PersonWithValidation < Person
43
+ validates_presence_of :email
44
+ validates_uniqueness_of :encrypted_email
45
+ end
46
+
47
+ class ActiveRecordTest < Test::Unit::TestCase
48
+
49
+ def setup
50
+ ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
51
+ create_people_table
52
+ end
53
+
54
+ def test_should_encrypt_email
55
+ @person = Person.create :email => 'test@example.com'
56
+ assert_not_nil @person.encrypted_email
57
+ assert_not_equal @person.email, @person.encrypted_email
58
+ assert_equal @person.email, Person.find(:first).email
59
+ end
60
+
61
+ def test_should_marshal_and_encrypt_credentials
62
+ @person = Person.create
63
+ assert_not_nil @person.encrypted_credentials
64
+ assert_not_equal @person.credentials, @person.encrypted_credentials
65
+ assert_equal @person.credentials, Person.find(:first).credentials
66
+ end
67
+
68
+ def test_should_find_by_email
69
+ @person = Person.create(:email => 'test@example.com')
70
+ assert_equal @person, Person.find_by_email('test@example.com')
71
+ end
72
+
73
+ def test_should_find_by_email_and_password
74
+ Person.create(:email => 'test@example.com', :password => 'invalid')
75
+ @person = Person.create(:email => 'test@example.com', :password => 'test')
76
+ assert_equal @person, Person.find_by_email_and_password('test@example.com', 'test')
77
+ end
78
+
79
+ def test_should_scope_by_email
80
+ @person = Person.create(:email => 'test@example.com')
81
+ assert_equal @person, Person.scoped_by_email('test@example.com').find(:first) rescue NoMethodError
82
+ end
83
+
84
+ def test_should_scope_by_email_and_password
85
+ Person.create(:email => 'test@example.com', :password => 'invalid')
86
+ @person = Person.create(:email => 'test@example.com', :password => 'test')
87
+ assert_equal @person, Person.scoped_by_email_and_password('test@example.com', 'test').find(:first) rescue NoMethodError
88
+ end
89
+
90
+ def test_should_encode_by_default
91
+ assert Person.attr_encrypted_options[:encode]
92
+ end
93
+
94
+ def test_should_validate_presence_of_email
95
+ @person = PersonWithValidation.new
96
+ assert !@person.valid?
97
+ assert !@person.errors[:email].empty? || @person.errors.on(:email)
98
+ end
99
+
100
+ def test_should_validate_uniqueness_of_email
101
+ @person = PersonWithValidation.new :email => 'test@example.com'
102
+ assert @person.save
103
+ @person2 = PersonWithValidation.new :email => @person.email
104
+ assert !@person2.valid?
105
+ assert !@person2.errors[:encrypted_email].empty? || @person2.errors.on(:encrypted_email)
106
+ end
107
+
108
+ end
@@ -0,0 +1,313 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class SillyEncryptor
4
+ def self.silly_encrypt(options)
5
+ (options[:value] + options[:some_arg]).reverse
6
+ end
7
+
8
+ def self.silly_decrypt(options)
9
+ options[:value].reverse.gsub(/#{options[:some_arg]}$/, '')
10
+ end
11
+ end
12
+
13
+ class User
14
+ self.attr_encrypted_options[:inheritance_check] = 'yup'
15
+
16
+ proc_key = Proc.new { |user| user.class.to_s }
17
+
18
+ attr_encrypted :email, :without_encoding, :key => 'secret key'
19
+ attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test', :key => proc_key
20
+ attr_encrypted :ssn, :key => :salt, :attribute => 'ssn_encrypted'
21
+ attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test', :key => proc_key
22
+ attr_encrypted :with_encoding, :key => 'secret key', :encode => true
23
+ attr_encrypted :with_custom_encoding, :key => 'secret key', :encode => 'm'
24
+ attr_encrypted :with_marshaling, :key => 'secret key', :marshal => true
25
+ attr_encrypted :with_true_if, :key => 'secret key', :if => true
26
+ attr_encrypted :with_false_if, :key => 'secret key', :if => false
27
+ attr_encrypted :with_true_unless, :key => 'secret key', :unless => true
28
+ attr_encrypted :with_false_unless, :key => 'secret key', :unless => false
29
+ attr_encrypted :with_if_changed, :key => 'secret key', :if => :should_encrypt
30
+ attr_encrypted :with_key_identifier, :keys => {
31
+ '2011' => 'current secret key',
32
+ '2010' => 'old secret key'
33
+ }, :key_identifier => '2011'
34
+
35
+ attr_encryptor :aliased, :key => 'secret_key'
36
+
37
+ attr_accessor :salt
38
+ attr_accessor :should_encrypt
39
+
40
+ def initialize
41
+ self.salt = Time.now.to_i.to_s
42
+ self.should_encrypt = true
43
+ end
44
+ end
45
+
46
+ class Admin < User
47
+ attr_encrypted :testing
48
+ end
49
+
50
+ class SomeOtherClass
51
+ def self.call(object)
52
+ object.class
53
+ end
54
+ end
55
+
56
+ class AttrEncryptedTest < Test::Unit::TestCase
57
+
58
+ def test_should_store_email_in_encrypted_attributes
59
+ assert User.encrypted_attributes.include?(:email)
60
+ end
61
+
62
+ def test_should_not_store_salt_in_encrypted_attributes
63
+ assert !User.encrypted_attributes.include?(:salt)
64
+ end
65
+
66
+ def test_attr_encrypted_should_return_true_for_email
67
+ assert User.attr_encrypted?('email')
68
+ end
69
+
70
+ def test_attr_encrypted_should_not_use_the_same_attribute_name_for_two_attributes_in_the_same_line
71
+ assert_not_equal User.encrypted_attributes[:email][:attribute], User.encrypted_attributes[:without_encoding][:attribute]
72
+ end
73
+
74
+ def test_attr_encrypted_should_return_false_for_salt
75
+ assert !User.attr_encrypted?('salt')
76
+ end
77
+
78
+ def test_should_generate_an_encrypted_attribute
79
+ assert User.new.respond_to?(:encrypted_email)
80
+ end
81
+
82
+ def test_should_generate_an_encrypted_attribute_with_a_prefix_and_suffix
83
+ assert User.new.respond_to?(:crypted_password_test)
84
+ end
85
+
86
+ def test_should_generate_an_encrypted_attribute_with_the_attribute_option
87
+ assert User.new.respond_to?(:ssn_encrypted)
88
+ end
89
+
90
+ def test_should_not_encrypt_nil_value
91
+ assert_nil User.encrypt_email(nil)
92
+ end
93
+
94
+ def test_should_not_encrypt_empty_string
95
+ assert_equal '', User.encrypt_email('')
96
+ end
97
+
98
+ def test_should_encrypt_email
99
+ assert_not_nil User.encrypt_email('test@example.com')
100
+ assert_not_equal 'test@example.com', User.encrypt_email('test@example.com')
101
+ end
102
+
103
+ def test_should_encrypt_email_when_modifying_the_attr_writer
104
+ @user = User.new
105
+ assert_nil @user.encrypted_email
106
+ @user.email = 'test@example.com'
107
+ assert_not_nil @user.encrypted_email
108
+ assert_equal User.encrypt_email('test@example.com'), @user.encrypted_email
109
+ end
110
+
111
+ def test_should_not_decrypt_nil_value
112
+ assert_nil User.decrypt_email(nil)
113
+ end
114
+
115
+ def test_should_not_decrypt_empty_string
116
+ assert_equal '', User.decrypt_email('')
117
+ end
118
+
119
+ def test_should_decrypt_email
120
+ encrypted_email = User.encrypt_email('test@example.com')
121
+ assert_not_equal 'test@test.com', encrypted_email
122
+ assert_equal 'test@example.com', User.decrypt_email(encrypted_email)
123
+ end
124
+
125
+ def test_should_decrypt_email_when_reading
126
+ @user = User.new
127
+ assert_nil @user.email
128
+ @user.encrypted_email = User.encrypt_email('test@example.com')
129
+ assert_equal 'test@example.com', @user.email
130
+ end
131
+
132
+ def test_should_encrypt_with_encoding
133
+ assert_equal User.encrypt_with_encoding('test'), [User.encrypt_without_encoding('test')].pack('m')
134
+ end
135
+
136
+ def test_should_decrypt_with_encoding
137
+ encrypted = User.encrypt_with_encoding('test')
138
+ assert_equal 'test', User.decrypt_with_encoding(encrypted)
139
+ assert_equal User.decrypt_with_encoding(encrypted), User.decrypt_without_encoding(encrypted.unpack('m').first)
140
+ end
141
+
142
+ def test_should_encrypt_with_custom_encoding
143
+ assert_equal User.encrypt_with_encoding('test'), [User.encrypt_without_encoding('test')].pack('m')
144
+ end
145
+
146
+ def test_should_decrypt_with_custom_encoding
147
+ encrypted = User.encrypt_with_encoding('test')
148
+ assert_equal 'test', User.decrypt_with_encoding(encrypted)
149
+ assert_equal User.decrypt_with_encoding(encrypted), User.decrypt_without_encoding(encrypted.unpack('m').first)
150
+ end
151
+
152
+ def test_should_encrypt_with_marshaling
153
+ @user = User.new
154
+ @user.with_marshaling = [1, 2, 3]
155
+ assert_not_nil @user.encrypted_with_marshaling
156
+ assert_equal User.encrypt_with_marshaling([1, 2, 3]), @user.encrypted_with_marshaling
157
+ end
158
+
159
+ def test_should_decrypt_with_marshaling
160
+ encrypted = User.encrypt_with_marshaling([1, 2, 3])
161
+ @user = User.new
162
+ assert_nil @user.with_marshaling
163
+ @user.encrypted_with_marshaling = encrypted
164
+ assert_equal [1, 2, 3], @user.with_marshaling
165
+ end
166
+
167
+ def test_should_use_custom_encryptor_and_crypt_method_names_and_arguments
168
+ assert_equal SillyEncryptor.silly_encrypt(:value => 'testing', :some_arg => 'test'), User.encrypt_credit_card('testing')
169
+ end
170
+
171
+ def test_should_evaluate_a_key_passed_as_a_symbol
172
+ @user = User.new
173
+ assert_nil @user.ssn_encrypted
174
+ @user.ssn = 'testing'
175
+ assert_not_nil @user.ssn_encrypted
176
+ assert_equal Encryptor.encrypt(:value => 'testing', :key => @user.salt), @user.ssn_encrypted
177
+ end
178
+
179
+ def test_should_evaluate_a_key_passed_as_a_proc
180
+ @user = User.new
181
+ assert_nil @user.crypted_password_test
182
+ @user.password = 'testing'
183
+ assert_not_nil @user.crypted_password_test
184
+ assert_equal Encryptor.encrypt(:value => 'testing', :key => 'User'), @user.crypted_password_test
185
+ end
186
+
187
+ def test_should_use_options_found_in_the_attr_encrypted_options_attribute
188
+ @user = User.new
189
+ assert_nil @user.crypted_password_test
190
+ @user.password = 'testing'
191
+ assert_not_nil @user.crypted_password_test
192
+ assert_equal Encryptor.encrypt(:value => 'testing', :key => 'User'), @user.crypted_password_test
193
+ end
194
+
195
+ def test_should_inherit_encrypted_attributes
196
+ assert_equal [User.encrypted_attributes.keys, :testing].flatten.collect { |key| key.to_s }.sort, Admin.encrypted_attributes.keys.collect { |key| key.to_s }.sort
197
+ end
198
+
199
+ def test_should_inherit_attr_encrypted_options
200
+ assert !User.attr_encrypted_options.empty?
201
+ assert_equal User.attr_encrypted_options, Admin.attr_encrypted_options
202
+ end
203
+
204
+ def test_should_not_inherit_unrelated_attributes
205
+ assert SomeOtherClass.attr_encrypted_options.empty?
206
+ assert SomeOtherClass.encrypted_attributes.empty?
207
+ end
208
+
209
+ def test_should_evaluate_a_symbol_option
210
+ assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, :class)
211
+ end
212
+
213
+ def test_should_evaluate_a_proc_option
214
+ assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, proc { |object| object.class })
215
+ end
216
+
217
+ def test_should_evaluate_a_lambda_option
218
+ assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, lambda { |object| object.class })
219
+ end
220
+
221
+ def test_should_evaluate_a_method_option
222
+ assert_equal Object, Object.new.send(:evaluate_attr_encrypted_option, SomeOtherClass.method(:call))
223
+ end
224
+
225
+ def test_should_return_a_string_option
226
+ assert_equal 'Object', Object.new.send(:evaluate_attr_encrypted_option, 'Object')
227
+ end
228
+
229
+ def test_should_encrypt_with_true_if
230
+ @user = User.new
231
+ assert_nil @user.encrypted_with_true_if
232
+ @user.with_true_if = 'testing'
233
+ assert_not_nil @user.encrypted_with_true_if
234
+ assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key'), @user.encrypted_with_true_if
235
+ end
236
+
237
+ def test_should_not_encrypt_with_false_if
238
+ @user = User.new
239
+ assert_nil @user.encrypted_with_false_if
240
+ @user.with_false_if = 'testing'
241
+ assert_not_nil @user.encrypted_with_false_if
242
+ assert_equal 'testing', @user.encrypted_with_false_if
243
+ end
244
+
245
+ def test_should_encrypt_with_false_unless
246
+ @user = User.new
247
+ assert_nil @user.encrypted_with_false_unless
248
+ @user.with_false_unless = 'testing'
249
+ assert_not_nil @user.encrypted_with_false_unless
250
+ assert_equal Encryptor.encrypt(:value => 'testing', :key => 'secret key'), @user.encrypted_with_false_unless
251
+ end
252
+
253
+ def test_should_not_encrypt_with_true_unless
254
+ @user = User.new
255
+ assert_nil @user.encrypted_with_true_unless
256
+ @user.with_true_unless = 'testing'
257
+ assert_not_nil @user.encrypted_with_true_unless
258
+ assert_equal 'testing', @user.encrypted_with_true_unless
259
+ end
260
+
261
+ def test_should_work_with_aliased_attr_encryptor
262
+ assert User.encrypted_attributes.include?(:aliased)
263
+ end
264
+
265
+ def test_should_always_reset_options
266
+ @user = User.new
267
+ @user.with_if_changed = "encrypt_stuff"
268
+ @user.stubs(:instance_variable_get).returns(nil)
269
+ @user.stubs(:instance_variable_set).raises("BadStuff")
270
+ assert_raise RuntimeError do
271
+ @user.with_if_changed
272
+ end
273
+
274
+ @user = User.new
275
+ @user.should_encrypt = false
276
+ @user.with_if_changed = "not_encrypted_stuff"
277
+ assert_equal "not_encrypted_stuff", @user.with_if_changed
278
+ assert_equal "not_encrypted_stuff", @user.encrypted_with_if_changed
279
+ end
280
+
281
+ def test_should_cast_values_as_strings_before_encrypting
282
+ string_encrypted_email = User.encrypt_email('3')
283
+ assert_equal string_encrypted_email, User.encrypt_email(3)
284
+ assert_equal '3', User.decrypt_email(string_encrypted_email)
285
+ end
286
+
287
+ def test_should_create_query_accessor
288
+ @user = User.new
289
+ assert !@user.email?
290
+ @user.email = ''
291
+ assert !@user.email?
292
+ @user.email = 'test@example.com'
293
+ assert @user.email?
294
+ end
295
+
296
+ def test_should_prefix_encryption_key_identifier_with_a_separator
297
+ string_encrypted_with_key_identifier = User.encrypt_with_key_identifier('blah')
298
+ assert_match /^2011::/, string_encrypted_with_key_identifier
299
+ end
300
+
301
+ def test_should_decrypt_with_appropriate_encryption_key
302
+ encrypted_2011 = '2011::' << Encryptor.encrypt(:value => 'testing', :key => 'current secret key')
303
+ assert_equal 'testing', User.decrypt_with_key_identifier(encrypted_2011)
304
+ end
305
+
306
+ def test_should_accept_many_keys_with_a_key_identifier
307
+ @user = User.new
308
+ assert_nil @user.with_key_identifier
309
+ @user.with_key_identifier = 'testing'
310
+ assert_not_nil @user.encrypted_with_key_identifier
311
+ assert_equal "2011::#{Encryptor.encrypt(:value => 'testing', :key => 'current secret key')}", @user.encrypted_with_key_identifier
312
+ end
313
+ end