powerhome-attr_encrypted 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 225b9354cf7318310fca332845cd11736b9a41916188ceb47b0631c6248894a4
4
- data.tar.gz: 7de216dc4eacec3cb6893c9dd11f8bf2f6598caaebadef6b46133d31bd384754
3
+ metadata.gz: f397f23e70ea3033291a979ecb6d49e5caa6bb98feda217d82c5c6112703b964
4
+ data.tar.gz: ffcf85615feed2510c191e9a594dac1f0774e7a147f1fda44babc54f452fed3a
5
5
  SHA512:
6
- metadata.gz: 25ba6cab8717767ce41e28651c571921a5e352ea1b7f1a272facd0d1bff9d502a802ebba0a21fba617b1adfba76f4e2cc7c359988065a0f6c2b76cad37f0f40a
7
- data.tar.gz: 334094f5193fdc3a96362a2b8779a6afb331a8c4fa7c15577d74c33b7b57001722a431f12e1d91cd4734f7ff7bde6efcb4f90be783835878a6e2f08e7fce5836
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
@@ -15,7 +15,7 @@ It works with ANY class, however, you get a few extra features when you're using
15
15
  Add attr_encrypted to your gemfile:
16
16
 
17
17
  ```ruby
18
- gem "attr_encrypted", "~> 3.1.0"
18
+ gem "attr_encrypted", "~> 1.1.0"
19
19
  ```
20
20
 
21
21
  Then install the gem:
@@ -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 = 0
8
- PATCH = 1
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
  #
@@ -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
- self.class.decrypt(attribute, encrypted_value, evaluated_attr_encrypted_options_for(attribute))
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.1
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: 2020-06-17 00:00:00.000000000 Z
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.2
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
- ...