powerhome-attr_encrypted 1.0.1 → 1.1.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +1 -1
- data/lib/attr_encrypted/version.rb +2 -2
- data/lib/attr_encrypted.rb +36 -2
- data/test/key_rotation_test.rb +165 -0
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f397f23e70ea3033291a979ecb6d49e5caa6bb98feda217d82c5c6112703b964
|
4
|
+
data.tar.gz: ffcf85615feed2510c191e9a594dac1f0774e7a147f1fda44babc54f452fed3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 024ccff97003a17ba51f242f088e5c6ee70b6ebcdcaa0dfef4ce2d2fd5caeea396124b0934d8b8c333e8e87a4c1b7457ecbe648d22b48e95759e25b492dba257
|
7
|
+
data.tar.gz: 5b2289e1c763c18b56bc41399e6da35e3dc85ab5430b442f0ac5a10e633826dfe65dc541fb6e2666927e1c5287c2c30cef450b9f8765ea56426c3ef62342fb86
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# attr_encrypted #
|
2
2
|
|
3
|
+
## 1.1.0 ##
|
4
|
+
* Added: Ability to rotate cipher key and iv (https://github.com/powerhome/attr_encrypted/pull/4)
|
5
|
+
|
6
|
+
## 1.0.1 ##
|
7
|
+
* Added: Support for ActiveRecord 5.2
|
8
|
+
|
9
|
+
## 1.0.0 ##
|
10
|
+
* Forked from upstream (https://github.com/attr-encrypted/attr_encrypted) and rebraned
|
11
|
+
|
3
12
|
## 3.1.0 ##
|
4
13
|
* Added: Abitilty to encrypt empty values. (@tamird)
|
5
14
|
* Added: MIT license
|
data/README.md
CHANGED
@@ -4,8 +4,8 @@ module AttrEncrypted
|
|
4
4
|
# Contains information about this gem's version
|
5
5
|
module Version
|
6
6
|
MAJOR = 1
|
7
|
-
MINOR =
|
8
|
-
PATCH =
|
7
|
+
MINOR = 1
|
8
|
+
PATCH = 0
|
9
9
|
|
10
10
|
# Returns a version string by joining <tt>MAJOR</tt>, <tt>MINOR</tt>, and <tt>PATCH</tt> with <tt>'.'</tt>
|
11
11
|
#
|
data/lib/attr_encrypted.rb
CHANGED
@@ -236,7 +236,7 @@ module AttrEncrypted
|
|
236
236
|
#
|
237
237
|
# email = User.decrypt(:email, 'SOME_ENCRYPTED_EMAIL_STRING')
|
238
238
|
def decrypt(attribute, encrypted_value, options = {})
|
239
|
-
options = encrypted_attributes[attribute.to_sym].merge(options)
|
239
|
+
options = encrypted_attributes[attribute.to_sym].merge(options).compact
|
240
240
|
if options[:if] && !options[:unless] && not_empty?(encrypted_value)
|
241
241
|
encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode]
|
242
242
|
value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value))
|
@@ -328,7 +328,41 @@ module AttrEncrypted
|
|
328
328
|
def decrypt(attribute, encrypted_value)
|
329
329
|
encrypted_attributes[attribute.to_sym][:operation] = :decrypting
|
330
330
|
encrypted_attributes[attribute.to_sym][:value_present] = self.class.not_empty?(encrypted_value)
|
331
|
-
|
331
|
+
begin
|
332
|
+
self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
|
333
|
+
rescue OpenSSL::Cipher::CipherError => e
|
334
|
+
# When decryption fails with `key:` and the attribute is
|
335
|
+
# configured to attempt a key rotation, let's try to decrypt
|
336
|
+
# with the `old_key:` then rotate the value using the
|
337
|
+
# `rotation_handler:`
|
338
|
+
options = evaluated_attr_encrypted_options_for(attribute)
|
339
|
+
raise e if nil == options[:old_key] || nil == options[:rotation_handler]
|
340
|
+
|
341
|
+
# but even this may fail if the column's data is encrypted with
|
342
|
+
# neither of these keys, or is corrupted in some way. We need to
|
343
|
+
# catch this scenario and optionally give the host application
|
344
|
+
# the ability to handle unrecoverable data
|
345
|
+
begin
|
346
|
+
value = self.class.decrypt(
|
347
|
+
attribute,
|
348
|
+
encrypted_value,
|
349
|
+
options.merge(
|
350
|
+
key: options[:old_key],
|
351
|
+
iv: options[:old_iv]
|
352
|
+
)
|
353
|
+
)
|
354
|
+
|
355
|
+
handler = options[:rotation_handler]
|
356
|
+
handler.new(self, attribute, value, encrypted_value, options).call
|
357
|
+
|
358
|
+
value
|
359
|
+
rescue OpenSSL::Cipher::CipherError => e
|
360
|
+
raise e unless options[:rotation_error_handler].present?
|
361
|
+
|
362
|
+
error_handler = options[:rotation_error_handler]
|
363
|
+
error_handler.new(self, attribute, e, encrypted_value, options).call
|
364
|
+
end
|
365
|
+
end
|
332
366
|
end
|
333
367
|
|
334
368
|
# Encrypts a value for the attribute specified using options evaluated in the current object's scope
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
require_relative 'test_helper'
|
5
|
+
|
6
|
+
class KeyRotationTest < Minitest::Test
|
7
|
+
class FakeRotatable
|
8
|
+
extend AttrEncrypted
|
9
|
+
|
10
|
+
self.attr_encrypted_options[:mode] = :single_iv_and_salt
|
11
|
+
|
12
|
+
def initialize(value: nil)
|
13
|
+
self.value = value
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor(
|
17
|
+
:value,
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@old_config = generate_attr_encrypted_config
|
23
|
+
@new_config = generate_attr_encrypted_config
|
24
|
+
|
25
|
+
FakeRotatable.class_eval do
|
26
|
+
attr_encrypted(
|
27
|
+
:value,
|
28
|
+
*@old_config,
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown
|
34
|
+
Object.send(:remove_const, :FakeRotatable) if Object.const_defined?("FakeRotatable")
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_decrypt_error_while_not_rotataing_keys_raises_cipher_error
|
38
|
+
original = instance_with_attr_encrypted_config(@old_config)
|
39
|
+
original.value = "cleartext-value"
|
40
|
+
|
41
|
+
with_wrong_key = instance_with_attr_encrypted_config(@new_config)
|
42
|
+
with_wrong_key.encrypted_value = original.encrypted_value
|
43
|
+
|
44
|
+
assert_raises(OpenSSL::Cipher::CipherError, "expected OpenSSL::Cipher::CipherError was not raised") do
|
45
|
+
with_wrong_key.value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_decrypt_error_when_rotating_keys_retries_with_the_old_key
|
50
|
+
original = instance_with_attr_encrypted_config(@old_config)
|
51
|
+
original.value = "cleartext-value"
|
52
|
+
rotation_handler_instance = Minitest::Mock.new
|
53
|
+
rotation_handler_instance.expect(:call, true)
|
54
|
+
|
55
|
+
rotating = setup_key_rotation(
|
56
|
+
from_config: @old_config,
|
57
|
+
to_config: @new_config,
|
58
|
+
rotation_handler: mock_rotation_handler_class(instance: rotation_handler_instance)
|
59
|
+
)
|
60
|
+
|
61
|
+
assert "cleartext_value", rotating.value
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_rotation_invokes_the_rotation_handler
|
65
|
+
original = instance_with_attr_encrypted_config(@old_config)
|
66
|
+
original.value = "cleartext-value"
|
67
|
+
rotation_handler_instance = Minitest::Mock.new
|
68
|
+
rotation_handler_instance.expect(:call, true)
|
69
|
+
|
70
|
+
rotating = setup_key_rotation(
|
71
|
+
from_config: @old_config,
|
72
|
+
to_config: @new_config,
|
73
|
+
rotation_handler: mock_rotation_handler_class(instance: rotation_handler_instance)
|
74
|
+
)
|
75
|
+
rotating.value
|
76
|
+
|
77
|
+
rotation_handler_instance.verify
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_rotation_invokes_the_rotation_error_handler_when_rotation_fails
|
81
|
+
rotation_handler_instance = Minitest::Mock.new
|
82
|
+
rotation_handler_instance.expect(:call, true)
|
83
|
+
rotation_error_handler_instance = Minitest::Mock.new
|
84
|
+
rotation_error_handler_instance.expect(:call, true)
|
85
|
+
wrong_key = generate_key
|
86
|
+
original = instance_with_attr_encrypted_config(@old_config)
|
87
|
+
original.value = "cleartext-value"
|
88
|
+
rotating = setup_key_rotation(
|
89
|
+
from_config: @old_config.merge(key: wrong_key),
|
90
|
+
to_config: @new_config,
|
91
|
+
rotation_handler: mock_rotation_handler_class(instance: rotation_handler_instance),
|
92
|
+
rotation_error_handler: mock_rotation_error_handler_class(instance: rotation_error_handler_instance)
|
93
|
+
)
|
94
|
+
rotating.encrypted_value = original.encrypted_value
|
95
|
+
|
96
|
+
rotating.value
|
97
|
+
|
98
|
+
rotation_error_handler_instance.verify
|
99
|
+
end
|
100
|
+
|
101
|
+
def instance_with_attr_encrypted_config(key: generate_key, iv: generate_iv)
|
102
|
+
FakeRotatable.class_eval do
|
103
|
+
attr_encrypted(
|
104
|
+
:value,
|
105
|
+
key: key,
|
106
|
+
iv: iv,
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
FakeRotatable.new
|
111
|
+
end
|
112
|
+
|
113
|
+
def setup_key_rotation(from_config: @old_config, to_config: @new_config, rotation_handler: proc {}, rotation_error_handler: proc {})
|
114
|
+
original = instance_with_attr_encrypted_config(from_config)
|
115
|
+
original.value = "cleartext-value"
|
116
|
+
FakeRotatable.class_eval do
|
117
|
+
attr_encrypted(
|
118
|
+
:value,
|
119
|
+
key: to_config.fetch(:key),
|
120
|
+
iv: to_config.fetch(:iv),
|
121
|
+
old_key: from_config.fetch(:key),
|
122
|
+
old_iv: from_config.fetch(:iv),
|
123
|
+
rotation_handler: rotation_handler,
|
124
|
+
rotation_error_handler: rotation_error_handler,
|
125
|
+
)
|
126
|
+
end
|
127
|
+
rotating = FakeRotatable.new
|
128
|
+
rotating.encrypted_value = original.encrypted_value
|
129
|
+
|
130
|
+
rotating
|
131
|
+
end
|
132
|
+
|
133
|
+
def generate_attr_encrypted_config(key: generate_key, iv: generate_iv)
|
134
|
+
{
|
135
|
+
key: key,
|
136
|
+
iv: iv,
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def generate_key
|
141
|
+
SecureRandom.random_bytes(32)
|
142
|
+
end
|
143
|
+
|
144
|
+
def generate_iv
|
145
|
+
SecureRandom.random_bytes(12)
|
146
|
+
end
|
147
|
+
|
148
|
+
def mock_rotation_handler_class(instance:)
|
149
|
+
mock_rotation_handler_class = Minitest::Mock.new
|
150
|
+
mock_rotation_handler_class.expect(:new, instance, [FakeRotatable, :value, "cleartext-value", String, Hash])
|
151
|
+
mock_rotation_handler_class.expect(:!, false) # For presence checks
|
152
|
+
3.times { mock_rotation_handler_class.expect(:is_a?, false, [Symbol]) }
|
153
|
+
|
154
|
+
mock_rotation_handler_class
|
155
|
+
end
|
156
|
+
|
157
|
+
def mock_rotation_error_handler_class(instance:)
|
158
|
+
mock_rotation_handler_class = Minitest::Mock.new
|
159
|
+
mock_rotation_handler_class.expect(:new, instance, [FakeRotatable, :value, StandardError, String, Hash])
|
160
|
+
mock_rotation_handler_class.expect(:!, false) # For presence checks
|
161
|
+
3.times { mock_rotation_handler_class.expect(:is_a?, false, [Symbol]) }
|
162
|
+
|
163
|
+
mock_rotation_handler_class
|
164
|
+
end
|
165
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: powerhome-attr_encrypted
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wade Winningham
|
8
8
|
- Ben Langfeld
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2021-09-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: encryptor
|
@@ -235,6 +235,7 @@ files:
|
|
235
235
|
- test/attr_encrypted_test.rb
|
236
236
|
- test/compatibility_test.rb
|
237
237
|
- test/data_mapper_test.rb
|
238
|
+
- test/key_rotation_test.rb
|
238
239
|
- test/legacy_active_record_test.rb
|
239
240
|
- test/legacy_attr_encrypted_test.rb
|
240
241
|
- test/legacy_compatibility_test.rb
|
@@ -274,8 +275,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
274
275
|
- !ruby/object:Gem::Version
|
275
276
|
version: '0'
|
276
277
|
requirements: []
|
277
|
-
rubygems_version: 3.1.
|
278
|
-
signing_key:
|
278
|
+
rubygems_version: 3.1.4
|
279
|
+
signing_key:
|
279
280
|
specification_version: 4
|
280
281
|
summary: Power's version of the attr_encrypted gem
|
281
282
|
test_files:
|
@@ -283,6 +284,7 @@ test_files:
|
|
283
284
|
- test/attr_encrypted_test.rb
|
284
285
|
- test/compatibility_test.rb
|
285
286
|
- test/data_mapper_test.rb
|
287
|
+
- test/key_rotation_test.rb
|
286
288
|
- test/legacy_active_record_test.rb
|
287
289
|
- test/legacy_attr_encrypted_test.rb
|
288
290
|
- test/legacy_compatibility_test.rb
|
@@ -291,4 +293,3 @@ test_files:
|
|
291
293
|
- test/run.sh
|
292
294
|
- test/sequel_test.rb
|
293
295
|
- test/test_helper.rb
|
294
|
-
...
|