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 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
- ...