attr_encrypted-magicless 1.3.42
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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +6 -0
- data/Gemfile +9 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +336 -0
- data/Rakefile +24 -0
- data/attr_encrypted-magicless.gemspec +38 -0
- data/lib/attr_encrypted-magicless.rb +1 -0
- data/lib/attr_encrypted.rb +384 -0
- data/lib/attr_encrypted/adapters/active_record.rb +49 -0
- data/lib/attr_encrypted/version.rb +3 -0
- data/test/active_record_test.rb +230 -0
- data/test/attr_encrypted_test.rb +339 -0
- data/test/compatibility_test.rb +106 -0
- data/test/legacy_active_record_test.rb +94 -0
- data/test/legacy_attr_encrypted_test.rb +306 -0
- data/test/legacy_compatibility_test.rb +87 -0
- data/test/test_helper.rb +38 -0
- metadata +245 -0
@@ -0,0 +1 @@
|
|
1
|
+
require 'attr_encrypted'
|
@@ -0,0 +1,384 @@
|
|
1
|
+
require 'attr_encrypted/version'
|
2
|
+
require 'encryptor'
|
3
|
+
require 'active_support/core_ext/object/blank'
|
4
|
+
require 'active_support/core_ext/class/attribute'
|
5
|
+
require 'active_support/core_ext/array/extract_options'
|
6
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
7
|
+
require 'active_support/dependencies/autoload'
|
8
|
+
require 'active_support/concern'
|
9
|
+
|
10
|
+
# Adds attr_accessors that encrypt and decrypt an object's attributes
|
11
|
+
module AttrEncrypted
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
extend ActiveSupport::Autoload
|
14
|
+
|
15
|
+
autoload_under "adapters" do
|
16
|
+
autoload :ActiveRecord
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# :singleton-method: attr_encrypted_options
|
21
|
+
# Default options to use with calls to <tt>attr_encrypted</tt>
|
22
|
+
#
|
23
|
+
# It will inherit existing options from its superclass
|
24
|
+
|
25
|
+
##
|
26
|
+
# :singleton-method: encrypted_attributes
|
27
|
+
# Contains a hash of encrypted attributes with virtual attribute names as keys
|
28
|
+
# and their corresponding options as values
|
29
|
+
#
|
30
|
+
# Example
|
31
|
+
#
|
32
|
+
# class User
|
33
|
+
# attr_encrypted :email, :key => 'my secret key'
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# User.encrypted_attributes # { :email => { :attribute => 'encrypted_email', :key => 'my secret key' } }
|
37
|
+
|
38
|
+
|
39
|
+
included do
|
40
|
+
class_attribute :attr_encrypted_options, instance_accessor: false
|
41
|
+
self.attr_encrypted_options ||= {}
|
42
|
+
|
43
|
+
class_attribute :encrypted_attributes, instance_accessor: false
|
44
|
+
self.encrypted_attributes ||= {}
|
45
|
+
|
46
|
+
if defined?(::ActiveRecord) && self <= ::ActiveRecord::Base
|
47
|
+
include AttrEncrypted::ActiveRecord
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module ClassMethods
|
52
|
+
DEFAULT_ATTR_ENCRYPTED_OPTIONS = {
|
53
|
+
:prefix => 'encrypted_',
|
54
|
+
:suffix => '',
|
55
|
+
:if => true,
|
56
|
+
:unless => false,
|
57
|
+
:encode => false,
|
58
|
+
:default_encoding => 'm',
|
59
|
+
:marshal => false,
|
60
|
+
:marshaler => Marshal,
|
61
|
+
:dump_method => 'dump',
|
62
|
+
:load_method => 'load',
|
63
|
+
:encryptor => Encryptor,
|
64
|
+
:encrypt_method => 'encrypt',
|
65
|
+
:decrypt_method => 'decrypt',
|
66
|
+
:mode => :single_iv_and_salt
|
67
|
+
}
|
68
|
+
|
69
|
+
private_constant :DEFAULT_ATTR_ENCRYPTED_OPTIONS
|
70
|
+
|
71
|
+
# Generates attr_accessors that encrypt and decrypt attributes transparently
|
72
|
+
#
|
73
|
+
# Options (any other options you specify are passed to the encryptor's encrypt and decrypt methods)
|
74
|
+
#
|
75
|
+
# :attribute => The name of the referenced encrypted attribute. For example
|
76
|
+
# <tt>attr_accessor :email, :attribute => :ee</tt> would generate an
|
77
|
+
# attribute named 'ee' to store the encrypted email. This is useful when defining
|
78
|
+
# one attribute to encrypt at a time or when the :prefix and :suffix options
|
79
|
+
# aren't enough. Defaults to nil.
|
80
|
+
#
|
81
|
+
# :prefix => A prefix used to generate the name of the referenced encrypted attributes.
|
82
|
+
# For example <tt>attr_accessor :email, :password, :prefix => 'crypted_'</tt> would
|
83
|
+
# generate attributes named 'crypted_email' and 'crypted_password' to store the
|
84
|
+
# encrypted email and password. Defaults to 'encrypted_'.
|
85
|
+
#
|
86
|
+
# :suffix => A suffix used to generate the name of the referenced encrypted attributes.
|
87
|
+
# For example <tt>attr_accessor :email, :password, :prefix => '', :suffix => '_encrypted'</tt>
|
88
|
+
# would generate attributes named 'email_encrypted' and 'password_encrypted' to store the
|
89
|
+
# encrypted email. Defaults to ''.
|
90
|
+
#
|
91
|
+
# :key => The encryption key. This option may not be required if you're using a custom encryptor. If you pass
|
92
|
+
# a symbol representing an instance method then the :key option will be replaced with the result of the
|
93
|
+
# method before being passed to the encryptor. Objects that respond to :call are evaluated as well (including procs).
|
94
|
+
# Any other key types will be passed directly to the encryptor.
|
95
|
+
#
|
96
|
+
# :encode => If set to true, attributes will be encoded as well as encrypted. This is useful if you're
|
97
|
+
# planning on storing the encrypted attributes in a database. The default encoding is 'm' (base64),
|
98
|
+
# however this can be overwritten by setting the :encode option to some other encoding string instead of
|
99
|
+
# just 'true'. See http://www.ruby-doc.org/core/classes/Array.html#M002245 for more encoding directives.
|
100
|
+
# Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
|
101
|
+
#
|
102
|
+
# :default_encoding => Defaults to 'm' (base64).
|
103
|
+
#
|
104
|
+
# :marshal => If set to true, attributes will be marshaled as well as encrypted. This is useful if you're planning
|
105
|
+
# on encrypting something other than a string. Defaults to false unless you're using it with ActiveRecord
|
106
|
+
# or DataMapper.
|
107
|
+
#
|
108
|
+
# :marshaler => The object to use for marshaling. Defaults to Marshal.
|
109
|
+
#
|
110
|
+
# :dump_method => The dump method name to call on the <tt>:marshaler</tt> object to. Defaults to 'dump'.
|
111
|
+
#
|
112
|
+
# :load_method => The load method name to call on the <tt>:marshaler</tt> object. Defaults to 'load'.
|
113
|
+
#
|
114
|
+
# :encryptor => The object to use for encrypting. Defaults to Encryptor.
|
115
|
+
#
|
116
|
+
# :encrypt_method => The encrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'encrypt'.
|
117
|
+
#
|
118
|
+
# :decrypt_method => The decrypt method name to call on the <tt>:encryptor</tt> object. Defaults to 'decrypt'.
|
119
|
+
#
|
120
|
+
# :if => Attributes are only encrypted if this option evaluates to true. If you pass a symbol representing an instance
|
121
|
+
# method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
|
122
|
+
# Defaults to true.
|
123
|
+
#
|
124
|
+
# :unless => Attributes are only encrypted if this option evaluates to false. If you pass a symbol representing an instance
|
125
|
+
# method then the result of the method will be evaluated. Any objects that respond to <tt>:call</tt> are evaluated as well.
|
126
|
+
# Defaults to false.
|
127
|
+
#
|
128
|
+
# :mode => Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
|
129
|
+
# with the old attr_encrypted API: the default IV and salt of the underlying encryptor object
|
130
|
+
# is used; <tt>:per_attribute_iv_and_salt</tt> uses a per-attribute IV and salt attribute and
|
131
|
+
# is the recommended mode for new deployments.
|
132
|
+
# Defaults to <tt>:single_iv_and_salt</tt>.
|
133
|
+
#
|
134
|
+
# You can specify your own default options
|
135
|
+
#
|
136
|
+
# class User
|
137
|
+
# # now all attributes will be encoded and marshaled by default
|
138
|
+
# attr_encrypted_options.merge!(:encode => true, :marshal => true, :some_other_option => true)
|
139
|
+
# attr_encrypted :configuration, :key => 'my secret key'
|
140
|
+
# end
|
141
|
+
#
|
142
|
+
#
|
143
|
+
# Example
|
144
|
+
#
|
145
|
+
# class User
|
146
|
+
# attr_encrypted :email, :credit_card, :key => 'some secret key'
|
147
|
+
# attr_encrypted :configuration, :key => 'some other secret key', :marshal => true
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# @user = User.new
|
151
|
+
# @user.encrypted_email # nil
|
152
|
+
# @user.email? # false
|
153
|
+
# @user.email = 'test@example.com'
|
154
|
+
# @user.email? # true
|
155
|
+
# @user.encrypted_email # returns the encrypted version of 'test@example.com'
|
156
|
+
#
|
157
|
+
# @user.configuration = { :time_zone => 'UTC' }
|
158
|
+
# @user.encrypted_configuration # returns the encrypted version of configuration
|
159
|
+
#
|
160
|
+
# See README for more examples
|
161
|
+
def attr_encrypted(*attributes)
|
162
|
+
options = DEFAULT_ATTR_ENCRYPTED_OPTIONS.merge(attr_encrypted_options).merge!(attributes.extract_options!)
|
163
|
+
|
164
|
+
options[:encode] = options[:default_encoding] if options[:encode].is_a?(TrueClass)
|
165
|
+
|
166
|
+
attributes.each do |attribute|
|
167
|
+
encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join.to_sym)
|
168
|
+
iv_name = :"#{encrypted_attribute_name}_iv"
|
169
|
+
salt_name = :"#{encrypted_attribute_name}_salt"
|
170
|
+
ivar_name = :"@#{attribute}"
|
171
|
+
|
172
|
+
attr_reader encrypted_attribute_name unless attribute_method_already_implemented?(encrypted_attribute_name)
|
173
|
+
attr_writer encrypted_attribute_name unless attribute_method_already_implemented?(:"#{encrypted_attribute_name}=")
|
174
|
+
|
175
|
+
if options[:mode] == :per_attribute_iv_and_salt
|
176
|
+
attr_reader iv_name unless attribute_method_already_implemented?(iv_name)
|
177
|
+
attr_writer iv_name unless attribute_method_already_implemented?(:"#{iv_name}=")
|
178
|
+
|
179
|
+
attr_reader salt_name unless attribute_method_already_implemented?(salt_name)
|
180
|
+
attr_writer salt_name unless attribute_method_already_implemented?(:"#{salt_name}=")
|
181
|
+
end
|
182
|
+
|
183
|
+
define_method(attribute) do
|
184
|
+
instance_variable_get(ivar_name) || instance_variable_set(ivar_name, decrypt(attribute, send(encrypted_attribute_name)))
|
185
|
+
end
|
186
|
+
|
187
|
+
define_method(:"#{attribute}=") do |value|
|
188
|
+
send(:"#{encrypted_attribute_name}=", encrypt(attribute, value))
|
189
|
+
instance_variable_set(ivar_name, value)
|
190
|
+
end
|
191
|
+
|
192
|
+
define_method(:"#{attribute}?") do
|
193
|
+
send(attribute).present?
|
194
|
+
end
|
195
|
+
|
196
|
+
encrypted_attributes[attribute.to_sym] = options.merge(:attribute => encrypted_attribute_name)
|
197
|
+
|
198
|
+
yield(attribute) if block_given?
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
alias_method :attr_encryptor, :attr_encrypted
|
203
|
+
|
204
|
+
protected :attr_encryptor, :attr_encrypted
|
205
|
+
|
206
|
+
# Checks if an attribute is configured with <tt>attr_encrypted</tt>
|
207
|
+
#
|
208
|
+
# Example
|
209
|
+
#
|
210
|
+
# class User
|
211
|
+
# attr_accessor :name
|
212
|
+
# attr_encrypted :email
|
213
|
+
# end
|
214
|
+
#
|
215
|
+
# User.attr_encrypted?(:name) # false
|
216
|
+
# User.attr_encrypted?(:email) # true
|
217
|
+
def attr_encrypted?(attribute)
|
218
|
+
encrypted_attributes.include?(attribute.to_sym)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Decrypts a value for the attribute specified
|
222
|
+
#
|
223
|
+
# Example
|
224
|
+
#
|
225
|
+
# class User
|
226
|
+
# attr_encrypted :email
|
227
|
+
# end
|
228
|
+
#
|
229
|
+
# email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
230
|
+
def decrypt(attribute, encrypted_value, options = {})
|
231
|
+
options = encrypted_attributes[attribute.to_sym].merge(options)
|
232
|
+
if options[:if] && !options[:unless] && !encrypted_value.nil? && !(encrypted_value.is_a?(String) && encrypted_value.empty?)
|
233
|
+
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
|
234
|
+
value = options[:encryptor].send(options[:decrypt_method], options.merge!(:value => encrypted_value))
|
235
|
+
if options[:marshal]
|
236
|
+
value = options[:marshaler].send(options[:load_method], value)
|
237
|
+
elsif defined?(Encoding)
|
238
|
+
encoding = Encoding.default_internal || Encoding.default_external
|
239
|
+
value = value.force_encoding(encoding.name)
|
240
|
+
end
|
241
|
+
value
|
242
|
+
else
|
243
|
+
encrypted_value
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Encrypts a value for the attribute specified
|
248
|
+
#
|
249
|
+
# Example
|
250
|
+
#
|
251
|
+
# class User
|
252
|
+
# attr_encrypted :email
|
253
|
+
# end
|
254
|
+
#
|
255
|
+
# encrypted_email = User.encrypt(:email, 'test@example.com')
|
256
|
+
def encrypt(attribute, value, options = {})
|
257
|
+
options = encrypted_attributes[attribute.to_sym].merge(options)
|
258
|
+
if options[:if] && !options[:unless] && !value.nil? && !(value.is_a?(String) && value.empty?)
|
259
|
+
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
|
260
|
+
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(:value => value))
|
261
|
+
encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
|
262
|
+
encrypted_value
|
263
|
+
else
|
264
|
+
value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
protected
|
269
|
+
|
270
|
+
def attribute_method_already_implemented?(method_name)
|
271
|
+
method_defined?(method_name) || private_method_defined?(method_name)
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
|
277
|
+
# if attribute was configured with attr_encrypted
|
278
|
+
#
|
279
|
+
# Example
|
280
|
+
#
|
281
|
+
# class User
|
282
|
+
# attr_encrypted :email, :key => 'my secret key'
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
|
286
|
+
def method_missing(method, *arguments, &block)
|
287
|
+
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
|
288
|
+
send($1, $3, *arguments)
|
289
|
+
else
|
290
|
+
super
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def inherited(subclass)
|
295
|
+
super
|
296
|
+
subclass.attr_encrypted_options = attr_encrypted_options.dup
|
297
|
+
subclass.encrypted_attributes = encrypted_attributes.dup
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Decrypts a value for the attribute specified using options evaluated in the current object's scope
|
302
|
+
#
|
303
|
+
# Example
|
304
|
+
#
|
305
|
+
# class User
|
306
|
+
# attr_accessor :secret_key
|
307
|
+
# attr_encrypted :email, :key => :secret_key
|
308
|
+
#
|
309
|
+
# def initialize(secret_key)
|
310
|
+
# self.secret_key = secret_key
|
311
|
+
# end
|
312
|
+
# end
|
313
|
+
#
|
314
|
+
# @user = User.new('some-secret-key')
|
315
|
+
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
316
|
+
def decrypt(attribute, encrypted_value)
|
317
|
+
self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
|
318
|
+
end
|
319
|
+
|
320
|
+
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
|
321
|
+
#
|
322
|
+
# Example
|
323
|
+
#
|
324
|
+
# class User
|
325
|
+
# attr_accessor :secret_key
|
326
|
+
# attr_encrypted :email, :key => :secret_key
|
327
|
+
#
|
328
|
+
# def initialize(secret_key)
|
329
|
+
# self.secret_key = secret_key
|
330
|
+
# end
|
331
|
+
# end
|
332
|
+
#
|
333
|
+
# @user = User.new('some-secret-key')
|
334
|
+
# @user.encrypt(:email, 'test@example.com')
|
335
|
+
def encrypt(attribute, value)
|
336
|
+
self.class.encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
|
337
|
+
end
|
338
|
+
|
339
|
+
protected
|
340
|
+
|
341
|
+
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
|
342
|
+
def evaluated_attr_encrypted_options_for(attribute)
|
343
|
+
if evaluate_attr_encrypted_option(self.class.encrypted_attributes[attribute.to_sym][:mode]) == :per_attribute_iv_and_salt
|
344
|
+
load_iv_for_attribute(attribute, self.class.encrypted_attributes[attribute.to_sym][:algorithm])
|
345
|
+
load_salt_for_attribute(attribute)
|
346
|
+
end
|
347
|
+
|
348
|
+
self.class.encrypted_attributes[attribute.to_sym].inject({}) { |hash, (option, value)| hash[option] = evaluate_attr_encrypted_option(value); hash }
|
349
|
+
end
|
350
|
+
|
351
|
+
# Evaluates symbol (method reference) or proc (responds to call) options
|
352
|
+
#
|
353
|
+
# If the option is not a symbol or proc then the original option is returned
|
354
|
+
def evaluate_attr_encrypted_option(option)
|
355
|
+
if option.is_a?(Symbol) && respond_to?(option)
|
356
|
+
send(option)
|
357
|
+
elsif option.respond_to?(:call)
|
358
|
+
option.call(self)
|
359
|
+
else
|
360
|
+
option
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def load_iv_for_attribute(attribute, algorithm)
|
365
|
+
encrypted_attribute_name = self.class.encrypted_attributes[attribute.to_sym][:attribute]
|
366
|
+
iv = send(:"#{encrypted_attribute_name}_iv")
|
367
|
+
if iv.nil?
|
368
|
+
begin
|
369
|
+
algorithm ||= "aes-256-cbc"
|
370
|
+
algo = OpenSSL::Cipher::Cipher.new(algorithm)
|
371
|
+
iv = [algo.random_iv].pack("m")
|
372
|
+
send(:"#{encrypted_attribute_name}_iv=", iv)
|
373
|
+
rescue RuntimeError
|
374
|
+
end
|
375
|
+
end
|
376
|
+
self.class.encrypted_attributes[attribute.to_sym][:iv] = iv.unpack("m").first if iv.present?
|
377
|
+
end
|
378
|
+
|
379
|
+
def load_salt_for_attribute(attribute)
|
380
|
+
encrypted_attribute_name = self.class.encrypted_attributes[attribute.to_sym][:attribute]
|
381
|
+
salt = send(:"#{encrypted_attribute_name}_salt") || send(:"#{encrypted_attribute_name}_salt=", Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15])
|
382
|
+
self.class.encrypted_attributes[attribute.to_sym][:salt] = salt
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'active_support/core_ext/hash/slice'
|
2
|
+
require 'active_support/core_ext/hash/keys'
|
3
|
+
require 'active_support/core_ext/module/aliasing'
|
4
|
+
|
5
|
+
module AttrEncrypted
|
6
|
+
module ActiveRecord
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
alias_method_chain :reload, :attr_encrypted
|
11
|
+
alias_method_chain :assign_attributes, :attr_encrypted
|
12
|
+
alias_method :attributes=, :assign_attributes_with_attr_encrypted
|
13
|
+
attr_encrypted_options[:encode] = true
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
protected
|
18
|
+
|
19
|
+
# <tt>attr_encrypted</tt> method
|
20
|
+
def attr_encrypted(*)
|
21
|
+
super do |attribute_name|
|
22
|
+
alias_method :"#{attribute_name}_before_type_cast", attribute_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def attribute_method_already_implemented?(method_name)
|
27
|
+
super || attribute_method?(method_name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# https://github.com/attr-encrypted/attr_encrypted/issues/68
|
32
|
+
def reload_with_attr_encrypted(*args, &block)
|
33
|
+
reload_without_attr_encrypted(*args, &block).tap do
|
34
|
+
self.class.encrypted_attributes.each_key do |attribute_name|
|
35
|
+
instance_variable_set(:"@#{attribute_name}", nil)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def assign_attributes_with_attr_encrypted(new_attributes)
|
41
|
+
return if new_attributes.blank?
|
42
|
+
new_attributes = new_attributes.to_options
|
43
|
+
encrypted_part = new_attributes.extract!(*self.class.encrypted_attributes.keys)
|
44
|
+
|
45
|
+
assign_attributes_without_attr_encrypted(new_attributes)
|
46
|
+
assign_attributes_without_attr_encrypted(encrypted_part)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
|
4
|
+
|
5
|
+
def create_tables
|
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.binary :salt
|
13
|
+
t.string :encrypted_email_salt
|
14
|
+
t.string :encrypted_credentials_salt
|
15
|
+
t.string :encrypted_email_iv
|
16
|
+
t.string :encrypted_credentials_iv
|
17
|
+
end
|
18
|
+
create_table :accounts do |t|
|
19
|
+
t.string :encrypted_password
|
20
|
+
t.string :encrypted_password_iv
|
21
|
+
t.string :encrypted_password_salt
|
22
|
+
end
|
23
|
+
create_table :users do |t|
|
24
|
+
t.string :login
|
25
|
+
t.string :encrypted_password
|
26
|
+
t.boolean :is_admin
|
27
|
+
end
|
28
|
+
create_table :prime_ministers do |t|
|
29
|
+
t.string :encrypted_name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# The table needs to exist before defining the class
|
36
|
+
create_tables
|
37
|
+
|
38
|
+
ActiveRecord::MissingAttributeError = ActiveModel::MissingAttributeError unless defined?(ActiveRecord::MissingAttributeError)
|
39
|
+
|
40
|
+
module Rack
|
41
|
+
module Test
|
42
|
+
class UploadedFile; end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
require 'action_controller/metal/strong_parameters'
|
47
|
+
|
48
|
+
class Person < ActiveRecord::Base
|
49
|
+
self.attr_encrypted_options[:mode] = :per_attribute_iv_and_salt
|
50
|
+
attr_encrypted :email, :key => SECRET_KEY
|
51
|
+
attr_encrypted :credentials, :key => Proc.new { |user| Encryptor.encrypt(:value => user.salt, :key => SECRET_KEY) }, :marshal => true
|
52
|
+
|
53
|
+
after_initialize :initialize_salt_and_credentials
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def initialize_salt_and_credentials
|
58
|
+
self.salt ||= Digest::SHA256.hexdigest((Time.now.to_i * rand(1000)).to_s)[0..15]
|
59
|
+
self.credentials ||= { :username => 'example', :password => 'test' }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class PersonWithValidation < Person
|
64
|
+
validates_presence_of :email
|
65
|
+
end
|
66
|
+
|
67
|
+
class PersonWithProcMode < Person
|
68
|
+
attr_encrypted :email, :key => SECRET_KEY, :mode => Proc.new { :per_attribute_iv_and_salt }
|
69
|
+
attr_encrypted :credentials, :key => SECRET_KEY, :mode => Proc.new { :single_iv_and_salt }
|
70
|
+
end
|
71
|
+
|
72
|
+
class Account < ActiveRecord::Base
|
73
|
+
attr_accessor :key
|
74
|
+
attr_encrypted :password, :key => Proc.new {|account| account.key}
|
75
|
+
end
|
76
|
+
|
77
|
+
class PersonWithSerialization < ActiveRecord::Base
|
78
|
+
self.table_name = 'people'
|
79
|
+
attr_encrypted :email, :key => 'a secret key'
|
80
|
+
serialize :password
|
81
|
+
end
|
82
|
+
|
83
|
+
class UserWithProtectedAttribute < ActiveRecord::Base
|
84
|
+
self.table_name = 'users'
|
85
|
+
attr_encrypted :password, :key => 'a secret key'
|
86
|
+
end
|
87
|
+
|
88
|
+
class PersonUsingAlias < ActiveRecord::Base
|
89
|
+
self.table_name = 'people'
|
90
|
+
attr_encryptor :email, :key => 'a secret key'
|
91
|
+
end
|
92
|
+
|
93
|
+
class PrimeMinister < ActiveRecord::Base
|
94
|
+
attr_encrypted :name, :marshal => true, :key => 'SECRET_KEY'
|
95
|
+
end
|
96
|
+
|
97
|
+
class ActiveRecordTest < Minitest::Test
|
98
|
+
|
99
|
+
def setup
|
100
|
+
ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
|
101
|
+
create_tables
|
102
|
+
Account.create!(:key => SECRET_KEY, :password => "password")
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_should_encrypt_email
|
106
|
+
@person = Person.create :email => 'test@example.com'
|
107
|
+
refute_nil @person.encrypted_email
|
108
|
+
refute_equal @person.email, @person.encrypted_email
|
109
|
+
assert_equal @person.email, Person.first.email
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_should_marshal_and_encrypt_credentials
|
113
|
+
@person = Person.create
|
114
|
+
refute_nil @person.encrypted_credentials
|
115
|
+
refute_equal @person.credentials, @person.encrypted_credentials
|
116
|
+
assert_equal @person.credentials, Person.first.credentials
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_should_encode_by_default
|
120
|
+
assert Person.attr_encrypted_options[:encode]
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_should_validate_presence_of_email
|
124
|
+
@person = PersonWithValidation.new
|
125
|
+
assert !@person.valid?
|
126
|
+
assert !@person.errors[:email].empty? || @person.errors.on(:email)
|
127
|
+
end
|
128
|
+
|
129
|
+
def test_should_encrypt_decrypt_with_iv
|
130
|
+
@person = Person.create :email => 'test@example.com'
|
131
|
+
@person2 = Person.find(@person.id)
|
132
|
+
refute_nil @person2.encrypted_email_iv
|
133
|
+
assert_equal 'test@example.com', @person2.email
|
134
|
+
end
|
135
|
+
|
136
|
+
def test_should_ensure_attributes_can_be_deserialized
|
137
|
+
@person = PersonWithSerialization.new :email => 'test@example.com', :password => %w(an array of strings)
|
138
|
+
@person.save
|
139
|
+
assert_equal @person.password, %w(an array of strings)
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_should_create_an_account_regardless_of_arguments_order
|
143
|
+
Account.create!(:key => SECRET_KEY, :password => "password")
|
144
|
+
Account.create!(:password => "password" , :key => SECRET_KEY)
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_should_set_attributes_regardless_of_arguments_order
|
148
|
+
Account.new.attributes = { :password => "password" , :key => SECRET_KEY }
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_should_preserve_hash_key_type
|
152
|
+
hash = { :foo => 'bar' }
|
153
|
+
account = Account.create!(:key => hash)
|
154
|
+
assert_equal account.key, hash
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_should_assign_attributes
|
158
|
+
@user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false
|
159
|
+
@user.attributes = ActionController::Parameters.new(:login => 'modified', :is_admin => true).permit(:login)
|
160
|
+
assert_equal 'modified', @user.login
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_should_not_assign_protected_attributes
|
164
|
+
@user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false
|
165
|
+
@user.attributes = ActionController::Parameters.new(:login => 'modified', :is_admin => true).permit(:login)
|
166
|
+
assert !@user.is_admin?
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_should_raise_exception_if_not_permitted
|
170
|
+
@user = UserWithProtectedAttribute.new :login => 'login', :is_admin => false
|
171
|
+
assert_raises ActiveModel::ForbiddenAttributesError do
|
172
|
+
@user.attributes = ActionController::Parameters.new(:login => 'modified', :is_admin => true)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_should_raise_exception_on_init_if_not_permitted
|
177
|
+
assert_raises ActiveModel::ForbiddenAttributesError do
|
178
|
+
@user = UserWithProtectedAttribute.new ActionController::Parameters.new(:login => 'modified', :is_admin => true)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_should_allow_assignment_of_nil_attributes
|
183
|
+
@person = Person.new
|
184
|
+
assert_nil(@person.attributes = nil)
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_should_allow_proc_based_mode
|
188
|
+
@person = PersonWithProcMode.create :email => 'test@example.com', :credentials => 'password123'
|
189
|
+
|
190
|
+
# Email is :per_attribute_iv_and_salt
|
191
|
+
assert_equal @person.class.encrypted_attributes[:email][:mode].class, Proc
|
192
|
+
assert_equal @person.class.encrypted_attributes[:email][:mode].call, :per_attribute_iv_and_salt
|
193
|
+
refute_nil @person.encrypted_email_salt
|
194
|
+
refute_nil @person.encrypted_email_iv
|
195
|
+
|
196
|
+
# Credentials is :single_iv_and_salt
|
197
|
+
assert_equal @person.class.encrypted_attributes[:credentials][:mode].class, Proc
|
198
|
+
assert_equal @person.class.encrypted_attributes[:credentials][:mode].call, :single_iv_and_salt
|
199
|
+
assert_nil @person.encrypted_credentials_salt
|
200
|
+
assert_nil @person.encrypted_credentials_iv
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_should_allow_assign_attributes_with_nil
|
204
|
+
@person = Person.new
|
205
|
+
assert_nil(@person.assign_attributes nil)
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_that_alias_encrypts_column
|
209
|
+
user = PersonUsingAlias.new
|
210
|
+
user.email = 'test@example.com'
|
211
|
+
user.save
|
212
|
+
|
213
|
+
refute_nil user.encrypted_email
|
214
|
+
refute_equal user.email, user.encrypted_email
|
215
|
+
assert_equal user.email, PersonUsingAlias.first.email
|
216
|
+
end
|
217
|
+
|
218
|
+
# See https://github.com/attr-encrypted/attr_encrypted/issues/68
|
219
|
+
def test_should_invalidate_virtual_attributes_on_reload
|
220
|
+
pm = PrimeMinister.new(:name => 'Winston Churchill')
|
221
|
+
pm.save!
|
222
|
+
assert_equal 'Winston Churchill', pm.name
|
223
|
+
pm.name = 'Neville Chamberlain'
|
224
|
+
assert_equal 'Neville Chamberlain', pm.name
|
225
|
+
|
226
|
+
result = pm.reload
|
227
|
+
assert_equal pm, result
|
228
|
+
assert_equal 'Winston Churchill', pm.name
|
229
|
+
end
|
230
|
+
end
|