attr_encrypted 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +14 -16
- data/CHANGELOG.md +60 -14
- data/README.md +27 -9
- data/Rakefile +3 -0
- data/attr_encrypted.gemspec +6 -13
- data/checksum/attr_encrypted-3.0.0.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.0.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.0.1.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.1.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.0.2.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.2.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.0.3.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.0.3.gem.sha512 +1 -0
- data/checksum/attr_encrypted-3.1.0.gem.sha256 +1 -0
- data/checksum/attr_encrypted-3.1.0.gem.sha512 +1 -0
- data/lib/attr_encrypted/adapters/active_record.rb +58 -30
- data/lib/attr_encrypted/adapters/data_mapper.rb +3 -1
- data/lib/attr_encrypted/adapters/sequel.rb +3 -1
- data/lib/attr_encrypted/version.rb +3 -1
- data/lib/attr_encrypted.rb +160 -129
- data/test/active_record_test.rb +130 -104
- data/test/attr_encrypted_test.rb +113 -16
- data/test/compatibility_test.rb +21 -21
- data/test/data_mapper_test.rb +2 -0
- data/test/legacy_active_record_test.rb +9 -9
- data/test/legacy_attr_encrypted_test.rb +8 -6
- data/test/legacy_compatibility_test.rb +15 -15
- data/test/legacy_data_mapper_test.rb +2 -0
- data/test/legacy_sequel_test.rb +2 -0
- data/test/run.sh +15 -7
- data/test/sequel_test.rb +2 -0
- data/test/test_helper.rb +11 -5
- metadata +27 -50
- checksums.yaml.gz.sig +0 -0
- data/certs/saghaulor.pem +0 -21
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -0
data/lib/attr_encrypted.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'encryptor'
|
2
4
|
|
3
5
|
# Adds attr_accessors that encrypt and decrypt an object's attributes
|
@@ -8,7 +10,7 @@ module AttrEncrypted
|
|
8
10
|
base.class_eval do
|
9
11
|
include InstanceMethods
|
10
12
|
attr_writer :attr_encrypted_options
|
11
|
-
@attr_encrypted_options, @
|
13
|
+
@attr_encrypted_options, @attr_encrypted_encrypted_attributes = {}, {}
|
12
14
|
end
|
13
15
|
end
|
14
16
|
|
@@ -16,90 +18,93 @@ module AttrEncrypted
|
|
16
18
|
#
|
17
19
|
# Options (any other options you specify are passed to the Encryptor's encrypt and decrypt methods)
|
18
20
|
#
|
19
|
-
# attribute:
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
# prefix:
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
# suffix:
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# key:
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
# encode:
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
# encode_iv:
|
58
|
-
|
59
|
-
# encode_salt:
|
60
|
-
#
|
61
|
-
# default_encoding:
|
62
|
-
#
|
63
|
-
# marshal:
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# marshaler:
|
69
|
-
#
|
70
|
-
#
|
71
|
-
# dump_method:
|
72
|
-
#
|
73
|
-
#
|
74
|
-
# load_method:
|
75
|
-
#
|
76
|
-
#
|
77
|
-
# encryptor:
|
78
|
-
#
|
79
|
-
#
|
80
|
-
# encrypt_method:
|
81
|
-
#
|
82
|
-
#
|
83
|
-
# decrypt_method:
|
84
|
-
#
|
85
|
-
#
|
86
|
-
# if:
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
# unless:
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
# mode:
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
21
|
+
# attribute: The name of the referenced encrypted attribute. For example
|
22
|
+
# <tt>attr_accessor :email, attribute: :ee</tt> would generate an
|
23
|
+
# attribute named 'ee' to store the encrypted email. This is useful when defining
|
24
|
+
# one attribute to encrypt at a time or when the :prefix and :suffix options
|
25
|
+
# aren't enough.
|
26
|
+
# Defaults to nil.
|
27
|
+
#
|
28
|
+
# prefix: A prefix used to generate the name of the referenced encrypted attributes.
|
29
|
+
# For example <tt>attr_accessor :email, prefix: 'crypted_'</tt> would
|
30
|
+
# generate attributes named 'crypted_email' to store the encrypted
|
31
|
+
# email and password.
|
32
|
+
# Defaults to 'encrypted_'.
|
33
|
+
#
|
34
|
+
# suffix: A suffix used to generate the name of the referenced encrypted attributes.
|
35
|
+
# For example <tt>attr_accessor :email, prefix: '', suffix: '_encrypted'</tt>
|
36
|
+
# would generate attributes named 'email_encrypted' to store the
|
37
|
+
# encrypted email.
|
38
|
+
# Defaults to ''.
|
39
|
+
#
|
40
|
+
# key: The encryption key. This option may not be required if
|
41
|
+
# you're using a custom encryptor. If you pass a symbol
|
42
|
+
# representing an instance method then the :key option
|
43
|
+
# will be replaced with the result of the method before
|
44
|
+
# being passed to the encryptor. Objects that respond
|
45
|
+
# to :call are evaluated as well (including procs).
|
46
|
+
# Any other key types will be passed directly to the encryptor.
|
47
|
+
# Defaults to nil.
|
48
|
+
#
|
49
|
+
# encode: If set to true, attributes will be encoded as well as
|
50
|
+
# encrypted. This is useful if you're planning on storing
|
51
|
+
# the encrypted attributes in a database. The default
|
52
|
+
# encoding is 'm' (base64), however this can be overwritten
|
53
|
+
# by setting the :encode option to some other encoding
|
54
|
+
# string instead of just 'true'. See
|
55
|
+
# http://www.ruby-doc.org/core/classes/Array.html#M002245
|
56
|
+
# for more encoding directives.
|
57
|
+
# Defaults to false unless you're using it with ActiveRecord, DataMapper, or Sequel.
|
58
|
+
#
|
59
|
+
# encode_iv: Defaults to true.
|
60
|
+
|
61
|
+
# encode_salt: Defaults to true.
|
62
|
+
#
|
63
|
+
# default_encoding: Defaults to 'm' (base64).
|
64
|
+
#
|
65
|
+
# marshal: If set to true, attributes will be marshaled as well
|
66
|
+
# as encrypted. This is useful if you're planning on
|
67
|
+
# encrypting something other than a string.
|
68
|
+
# Defaults to false.
|
69
|
+
#
|
70
|
+
# marshaler: The object to use for marshaling.
|
71
|
+
# Defaults to Marshal.
|
72
|
+
#
|
73
|
+
# dump_method: The dump method name to call on the <tt>:marshaler</tt> object to.
|
74
|
+
# Defaults to 'dump'.
|
75
|
+
#
|
76
|
+
# load_method: The load method name to call on the <tt>:marshaler</tt> object.
|
77
|
+
# Defaults to 'load'.
|
78
|
+
#
|
79
|
+
# encryptor: The object to use for encrypting.
|
80
|
+
# Defaults to Encryptor.
|
81
|
+
#
|
82
|
+
# encrypt_method: The encrypt method name to call on the <tt>:encryptor</tt> object.
|
83
|
+
# Defaults to 'encrypt'.
|
84
|
+
#
|
85
|
+
# decrypt_method: The decrypt method name to call on the <tt>:encryptor</tt> object.
|
86
|
+
# Defaults to 'decrypt'.
|
87
|
+
#
|
88
|
+
# if: Attributes are only encrypted if this option evaluates
|
89
|
+
# to true. If you pass a symbol representing an instance
|
90
|
+
# method then the result of the method will be evaluated.
|
91
|
+
# Any objects that respond to <tt>:call</tt> are evaluated as well.
|
92
|
+
# Defaults to true.
|
93
|
+
#
|
94
|
+
# unless: Attributes are only encrypted if this option evaluates
|
95
|
+
# to false. If you pass a symbol representing an instance
|
96
|
+
# method then the result of the method will be evaluated.
|
97
|
+
# Any objects that respond to <tt>:call</tt> are evaluated as well.
|
98
|
+
# Defaults to false.
|
99
|
+
#
|
100
|
+
# mode: Selects encryption mode for attribute: choose <tt>:single_iv_and_salt</tt> for compatibility
|
101
|
+
# with the old attr_encrypted API: the IV is derived from the encryption key by the underlying Encryptor class; salt is not used.
|
102
|
+
# The <tt>:per_attribute_iv_and_salt</tt> mode uses a per-attribute IV and salt. The salt is used to derive a unique key per attribute.
|
103
|
+
# A <tt>:per_attribute_iv</default> mode derives a unique IV per attribute; salt is not used.
|
104
|
+
# Defaults to <tt>:per_attribute_iv</tt>.
|
105
|
+
#
|
106
|
+
# allow_empty_value: Attributes which have nil or empty string values will not be encrypted unless this option
|
107
|
+
# has a truthy value.
|
103
108
|
#
|
104
109
|
# You can specify your own default options
|
105
110
|
#
|
@@ -140,23 +145,26 @@ module AttrEncrypted
|
|
140
145
|
encrypted_attribute_name = (options[:attribute] ? options[:attribute] : [options[:prefix], attribute, options[:suffix]].join).to_sym
|
141
146
|
|
142
147
|
instance_methods_as_symbols = attribute_instance_methods_as_symbols
|
143
|
-
attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
|
144
|
-
attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
|
145
148
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
+
if attribute_instance_methods_as_symbols_available?
|
150
|
+
attr_reader encrypted_attribute_name unless instance_methods_as_symbols.include?(encrypted_attribute_name)
|
151
|
+
attr_writer encrypted_attribute_name unless instance_methods_as_symbols.include?(:"#{encrypted_attribute_name}=")
|
152
|
+
|
153
|
+
iv_name = "#{encrypted_attribute_name}_iv".to_sym
|
154
|
+
attr_reader iv_name unless instance_methods_as_symbols.include?(iv_name)
|
155
|
+
attr_writer iv_name unless instance_methods_as_symbols.include?(:"#{iv_name}=")
|
149
156
|
|
150
|
-
|
151
|
-
|
152
|
-
|
157
|
+
salt_name = "#{encrypted_attribute_name}_salt".to_sym
|
158
|
+
attr_reader salt_name unless instance_methods_as_symbols.include?(salt_name)
|
159
|
+
attr_writer salt_name unless instance_methods_as_symbols.include?(:"#{salt_name}=")
|
160
|
+
end
|
153
161
|
|
154
162
|
define_method(attribute) do
|
155
|
-
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}",
|
163
|
+
instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", attr_encrypted_decrypt(attribute, send(encrypted_attribute_name)))
|
156
164
|
end
|
157
165
|
|
158
166
|
define_method("#{attribute}=") do |value|
|
159
|
-
send("#{encrypted_attribute_name}=",
|
167
|
+
send("#{encrypted_attribute_name}=", attr_encrypted_encrypt(attribute, value))
|
160
168
|
instance_variable_set("@#{attribute}", value)
|
161
169
|
end
|
162
170
|
|
@@ -165,7 +173,7 @@ module AttrEncrypted
|
|
165
173
|
value.respond_to?(:empty?) ? !value.empty? : !!value
|
166
174
|
end
|
167
175
|
|
168
|
-
|
176
|
+
self.attr_encrypted_encrypted_attributes[attribute.to_sym] = options.merge(attribute: encrypted_attribute_name)
|
169
177
|
end
|
170
178
|
end
|
171
179
|
|
@@ -197,6 +205,7 @@ module AttrEncrypted
|
|
197
205
|
decrypt_method: 'decrypt',
|
198
206
|
mode: :per_attribute_iv,
|
199
207
|
algorithm: 'aes-256-gcm',
|
208
|
+
allow_empty_value: false,
|
200
209
|
}
|
201
210
|
end
|
202
211
|
|
@@ -214,7 +223,7 @@ module AttrEncrypted
|
|
214
223
|
# User.attr_encrypted?(:name) # false
|
215
224
|
# User.attr_encrypted?(:email) # true
|
216
225
|
def attr_encrypted?(attribute)
|
217
|
-
|
226
|
+
attr_encrypted_encrypted_attributes.has_key?(attribute.to_sym)
|
218
227
|
end
|
219
228
|
|
220
229
|
# Decrypts a value for the attribute specified
|
@@ -225,10 +234,10 @@ module AttrEncrypted
|
|
225
234
|
# attr_encrypted :email
|
226
235
|
# end
|
227
236
|
#
|
228
|
-
# email = User.
|
229
|
-
def
|
230
|
-
options =
|
231
|
-
if options[:if] && !options[:unless] &&
|
237
|
+
# email = User.attr_encrypted_decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
238
|
+
def attr_encrypted_decrypt(attribute, encrypted_value, options = {})
|
239
|
+
options = attr_encrypted_encrypted_attributes[attribute.to_sym].merge(options)
|
240
|
+
if options[:if] && !options[:unless] && not_empty?(encrypted_value)
|
232
241
|
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
|
233
242
|
value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
|
234
243
|
if options[:marshal]
|
@@ -251,10 +260,10 @@ module AttrEncrypted
|
|
251
260
|
# attr_encrypted :email
|
252
261
|
# end
|
253
262
|
#
|
254
|
-
# encrypted_email = User.
|
255
|
-
def
|
256
|
-
options =
|
257
|
-
if options[:if] && !options[:unless] &&
|
263
|
+
# encrypted_email = User.attr_encrypted_encrypt(:email, 'test@example.com')
|
264
|
+
def attr_encrypted_encrypt(attribute, value, options = {})
|
265
|
+
options = attr_encrypted_encrypted_attributes[attribute.to_sym].merge(options)
|
266
|
+
if options[:if] && !options[:unless] && (options[:allow_empty_value] || not_empty?(value))
|
258
267
|
value = options[:marshal] ? options[:marshaler].send(options[:dump_method], value) : value.to_s
|
259
268
|
encrypted_value = options[:encryptor].send(options[:encrypt_method], options.merge!(value: value))
|
260
269
|
encrypted_value = [encrypted_value].pack(options[:encode]) if options[:encode]
|
@@ -264,6 +273,10 @@ module AttrEncrypted
|
|
264
273
|
end
|
265
274
|
end
|
266
275
|
|
276
|
+
def not_empty?(value)
|
277
|
+
!value.nil? && !(value.is_a?(String) && value.empty?)
|
278
|
+
end
|
279
|
+
|
267
280
|
# Contains a hash of encrypted attributes with virtual attribute names as keys
|
268
281
|
# and their corresponding options as values
|
269
282
|
#
|
@@ -273,9 +286,9 @@ module AttrEncrypted
|
|
273
286
|
# attr_encrypted :email, key: 'my secret key'
|
274
287
|
# end
|
275
288
|
#
|
276
|
-
# User.
|
277
|
-
def
|
278
|
-
@
|
289
|
+
# User.attr_encrypted_encrypted_attributes # { email: { attribute: 'encrypted_email', key: 'my secret key' } }
|
290
|
+
def attr_encrypted_encrypted_attributes
|
291
|
+
@attr_encrypted_encrypted_attributes ||= superclass.attr_encrypted_encrypted_attributes.dup
|
279
292
|
end
|
280
293
|
|
281
294
|
# Forwards calls to :encrypt_#{attribute} or :decrypt_#{attribute} to the corresponding encrypt or decrypt method
|
@@ -290,7 +303,7 @@ module AttrEncrypted
|
|
290
303
|
# User.encrypt_email('SOME_ENCRYPTED_EMAIL_STRING')
|
291
304
|
def method_missing(method, *arguments, &block)
|
292
305
|
if method.to_s =~ /^((en|de)crypt)_(.+)$/ && attr_encrypted?($3)
|
293
|
-
send($1, $3, *arguments)
|
306
|
+
send("attr_encrypted_#{$1}", $3, *arguments)
|
294
307
|
else
|
295
308
|
super
|
296
309
|
end
|
@@ -312,9 +325,10 @@ module AttrEncrypted
|
|
312
325
|
#
|
313
326
|
# @user = User.new('some-secret-key')
|
314
327
|
# @user.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
315
|
-
def
|
316
|
-
|
317
|
-
self.class.
|
328
|
+
def attr_encrypted_decrypt(attribute, encrypted_value)
|
329
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :decrypting
|
330
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
|
331
|
+
self.class.attr_encrypted_decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
|
318
332
|
end
|
319
333
|
|
320
334
|
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
|
@@ -331,17 +345,22 @@ module AttrEncrypted
|
|
331
345
|
# end
|
332
346
|
#
|
333
347
|
# @user = User.new('some-secret-key')
|
334
|
-
# @user.
|
335
|
-
def
|
336
|
-
|
337
|
-
self.class.
|
348
|
+
# @user.attr_encrypted_encrypt(:email, 'test@example.com')
|
349
|
+
def attr_encrypted_encrypt(attribute, value)
|
350
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:operation] = :encrypting
|
351
|
+
attr_encrypted_encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(value)
|
352
|
+
self.class.attr_encrypted_encrypt(attribute, value, evaluated_attr_encrypted_options_for(attribute))
|
338
353
|
end
|
339
354
|
|
340
355
|
# Copies the class level hash of encrypted attributes with virtual attribute names as keys
|
341
356
|
# and their corresponding options as values to the instance
|
342
357
|
#
|
343
|
-
def
|
344
|
-
@
|
358
|
+
def attr_encrypted_encrypted_attributes
|
359
|
+
@attr_encrypted_encrypted_attributes ||= begin
|
360
|
+
duplicated= {}
|
361
|
+
self.class.attr_encrypted_encrypted_attributes.map { |key, value| duplicated[key] = value.dup }
|
362
|
+
duplicated
|
363
|
+
end
|
345
364
|
end
|
346
365
|
|
347
366
|
protected
|
@@ -349,20 +368,28 @@ module AttrEncrypted
|
|
349
368
|
# Returns attr_encrypted options evaluated in the current object's scope for the attribute specified
|
350
369
|
def evaluated_attr_encrypted_options_for(attribute)
|
351
370
|
evaluated_options = Hash.new
|
352
|
-
|
353
|
-
|
354
|
-
|
371
|
+
attributes = attr_encrypted_encrypted_attributes[attribute.to_sym]
|
372
|
+
attribute_option_value = attributes[:attribute]
|
373
|
+
|
374
|
+
[:if, :unless, :value_present, :allow_empty_value].each do |option|
|
375
|
+
evaluated_options[option] = evaluate_attr_encrypted_option(attributes[option])
|
355
376
|
end
|
356
377
|
|
357
378
|
evaluated_options[:attribute] = attribute_option_value
|
358
379
|
|
359
380
|
evaluated_options.tap do |options|
|
360
|
-
unless options[:
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
381
|
+
if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value]
|
382
|
+
(attributes.keys - evaluated_options.keys).each do |option|
|
383
|
+
options[option] = evaluate_attr_encrypted_option(attributes[option])
|
384
|
+
end
|
385
|
+
|
386
|
+
unless options[:mode] == :single_iv_and_salt
|
387
|
+
load_iv_for_attribute(attribute, options)
|
388
|
+
end
|
389
|
+
|
390
|
+
if options[:mode] == :per_attribute_iv_and_salt
|
391
|
+
load_salt_for_attribute(attribute, options)
|
392
|
+
end
|
366
393
|
end
|
367
394
|
end
|
368
395
|
end
|
@@ -371,7 +398,7 @@ module AttrEncrypted
|
|
371
398
|
#
|
372
399
|
# If the option is not a symbol or proc then the original option is returned
|
373
400
|
def evaluate_attr_encrypted_option(option)
|
374
|
-
if option.is_a?(Symbol) && respond_to?(option)
|
401
|
+
if option.is_a?(Symbol) && respond_to?(option, true)
|
375
402
|
send(option)
|
376
403
|
elsif option.respond_to?(:call)
|
377
404
|
option.call(self)
|
@@ -408,7 +435,7 @@ module AttrEncrypted
|
|
408
435
|
encrypted_attribute_name = options[:attribute]
|
409
436
|
encode_salt = options[:encode_salt]
|
410
437
|
salt = options[:salt] || send("#{encrypted_attribute_name}_salt")
|
411
|
-
if
|
438
|
+
if options[:operation] == :encrypting
|
412
439
|
salt = SecureRandom.random_bytes
|
413
440
|
salt = prefix_and_encode_salt(salt, encode_salt) if encode_salt
|
414
441
|
send("#{encrypted_attribute_name}_salt=", salt)
|
@@ -436,6 +463,10 @@ module AttrEncrypted
|
|
436
463
|
instance_methods.collect { |method| method.to_sym }
|
437
464
|
end
|
438
465
|
|
466
|
+
def attribute_instance_methods_as_symbols_available?
|
467
|
+
true
|
468
|
+
end
|
469
|
+
|
439
470
|
end
|
440
471
|
|
441
472
|
|