attr_encrypted 3.0.0 → 4.0.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.
- 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
|
|