lockbox 1.4.1 → 2.0.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: b4771553bf23214b514e9a4a5c94ce665d234d34969f9a3941ea68bdc6729ed4
4
- data.tar.gz: a921894d4f3f43d5245a91941e06f79ea652f09c9c77ccf8dc7e442d1dbda0e5
3
+ metadata.gz: 15860ff6f0c444491002e98231bb32785c05d2f6f73be85e34ecb7a53ad43888
4
+ data.tar.gz: 026be73917780e5bea3fc7178d404d2d4a2320378f94dc98c7dca0b180e40a83
5
5
  SHA512:
6
- metadata.gz: 90fef82d743a0172a61728fd9f314eca597d43820e87aeb9cb77330e4ec824919d354b81cf4c6c9c2b1c8cedcebb8916425fed5e469b24ea11b6a2e8730feb6f
7
- data.tar.gz: fe874ec6aa3f563927f2ea1743d34a795468846ce74cc9e1a0467e41757721d40c8449d7093b158f63f92b8566ea578cf88e5f2b0a8caefc9facee2a8b731afa
6
+ metadata.gz: 5cef3979e21483caaa9a860d2e6301c29dbc6c58a44be765da92970981695eb813d09a6c0b43dbd3a53fe3b30dc48dc212436be63dcbeac303d566064095e36b
7
+ data.tar.gz: 9a7378720dcfb6a1a07687a42d957b3d4abfae349cbef0678a98e6b0f458d4726087c1ee69457624c13f8342ef67687232ccab25d6e2e339561e5603d98b9ea3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 2.0.0 (2024-10-26)
2
+
3
+ - Improved `attributes`, `attribute_names`, and `has_attribute?` when ciphertext attributes not loaded
4
+ - Removed deprecated `lockbox_encrypts` (use `has_encrypted` instead)
5
+ - Dropped support for Active Record < 7 and Ruby < 3.1
6
+ - Dropped support for Mongoid < 8
7
+
1
8
  ## 1.4.1 (2024-09-09)
2
9
 
3
10
  - Fixed error message for previews for Active Storage 7.1.4
data/README.md CHANGED
@@ -35,7 +35,7 @@ Set the following environment variable with your key (you can use this one in de
35
35
  LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
36
36
  ```
37
37
 
38
- or add it to your credentials for each environment (`rails credentials:edit --environment <env>` for Rails 6+)
38
+ or add it to your credentials for each environment (`rails credentials:edit --environment <env>`)
39
39
 
40
40
  ```yml
41
41
  lockbox:
@@ -1004,18 +1004,6 @@ class RemovePreviousEncryptedColumns < ActiveRecord::Migration[7.2]
1004
1004
  end
1005
1005
  ```
1006
1006
 
1007
- ## Upgrading
1008
-
1009
- ### 1.0.0
1010
-
1011
- `encrypts` is now deprecated in favor of `has_encrypted` to avoid conflicting with Active Record encryption.
1012
-
1013
- ```ruby
1014
- class User < ApplicationRecord
1015
- has_encrypted :email
1016
- end
1017
- ```
1018
-
1019
1007
  ## History
1020
1008
 
1021
1009
  View the [changelog](https://github.com/ankane/lockbox/blob/master/CHANGELOG.md)
@@ -29,11 +29,7 @@ module Lockbox
29
29
  # use connection_config instead of connection.adapter
30
30
  # so database connection isn't needed
31
31
  def adapter
32
- if ActiveRecord::VERSION::STRING.to_f >= 6.1
33
- ActiveRecord::Base.connection_db_config.adapter.to_s
34
- else
35
- ActiveRecord::Base.connection_config[:adapter].to_s
36
- end
32
+ ActiveRecord::Base.connection_db_config.adapter.to_s
37
33
  end
38
34
  end
39
35
  end
@@ -34,13 +34,6 @@ module Lockbox
34
34
  end
35
35
 
36
36
  module AttachedOne
37
- if ActiveStorage::VERSION::MAJOR < 6
38
- def attach(attachable)
39
- attachable = encrypt_attachable(attachable) if encrypted?
40
- super(attachable)
41
- end
42
- end
43
-
44
37
  def rotate_encryption!
45
38
  raise "Not encrypted" unless encrypted?
46
39
 
@@ -51,19 +44,6 @@ module Lockbox
51
44
  end
52
45
 
53
46
  module AttachedMany
54
- if ActiveStorage::VERSION::MAJOR < 6
55
- def attach(*attachables)
56
- if encrypted?
57
- attachables =
58
- attachables.flatten.collect do |attachable|
59
- encrypt_attachable(attachable)
60
- end
61
- end
62
-
63
- super(attachables)
64
- end
65
- end
66
-
67
47
  def rotate_encryption!
68
48
  raise "Not encrypted" unless encrypted?
69
49
 
@@ -131,27 +111,25 @@ module Lockbox
131
111
  end
132
112
  end
133
113
 
134
- if ActiveStorage::VERSION::MAJOR >= 6
135
- def open(**options)
136
- blob.open(**options) do |file|
137
- options = Utils.encrypted_options(record, name)
138
- # only trust the metadata when migrating
139
- # as earlier versions of Lockbox won't have it
140
- # and it's not a good practice to trust modifiable data
141
- encrypted = options && (!options[:migrating] || blob.metadata["encrypted"])
142
- if encrypted
143
- result = Utils.decrypt_result(record, name, options, file.read)
144
- file.rewind
145
- # truncate may not be available on all platforms
146
- # according to the Ruby docs
147
- # may need to create a new temp file instead
148
- file.truncate(0)
149
- file.write(result)
150
- file.rewind
151
- end
152
-
153
- yield file
114
+ def open(**options)
115
+ blob.open(**options) do |file|
116
+ options = Utils.encrypted_options(record, name)
117
+ # only trust the metadata when migrating
118
+ # as earlier versions of Lockbox won't have it
119
+ # and it's not a good practice to trust modifiable data
120
+ encrypted = options && (!options[:migrating] || blob.metadata["encrypted"])
121
+ if encrypted
122
+ result = Utils.decrypt_result(record, name, options, file.read)
123
+ file.rewind
124
+ # truncate may not be available on all platforms
125
+ # according to the Ruby docs
126
+ # may need to create a new temp file instead
127
+ file.truncate(0)
128
+ file.write(result)
129
+ file.rewind
154
130
  end
131
+
132
+ yield file
155
133
  end
156
134
  end
157
135
  end
data/lib/lockbox/model.rb CHANGED
@@ -60,7 +60,7 @@ module Lockbox
60
60
  class_eval do
61
61
  # Lockbox uses custom inspect
62
62
  # but this could be useful for other gems
63
- if activerecord && ActiveRecord::VERSION::MAJOR >= 6
63
+ if activerecord
64
64
  # only add virtual attribute
65
65
  # need to use regexp since strings do partial matching
66
66
  # also, need to use += instead of <<
@@ -114,12 +114,6 @@ module Lockbox
114
114
  k = lockbox_encrypted_attributes[k]
115
115
  elsif values.key?(k)
116
116
  v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect
117
-
118
- # fix for https://github.com/rails/rails/issues/40725
119
- # TODO only apply to Active Record 6.0
120
- if respond_to?(:inspection_filter, true) && v != "nil"
121
- v = inspection_filter.filter_param(k, v)
122
- end
123
117
  else
124
118
  next
125
119
  end
@@ -148,7 +142,40 @@ module Lockbox
148
142
  end
149
143
  end
150
144
  end
151
- super
145
+
146
+ # remove attributes that do not have a ciphertext attribute
147
+ attributes = super
148
+ self.class.lockbox_attributes.each do |k, lockbox_attribute|
149
+ if !attributes.include?(lockbox_attribute[:encrypted_attribute].to_s)
150
+ attributes.delete(k.to_s)
151
+ attributes.delete(lockbox_attribute[:attribute])
152
+ end
153
+ end
154
+ attributes
155
+ end
156
+
157
+ # remove attribute names that do not have a ciphertext attribute
158
+ def attribute_names
159
+ # hash preserves key order
160
+ names_set = super.to_h { |v| [v, true] }
161
+ self.class.lockbox_attributes.each do |k, lockbox_attribute|
162
+ if !names_set.include?(lockbox_attribute[:encrypted_attribute].to_s)
163
+ names_set.delete(k.to_s)
164
+ names_set.delete(lockbox_attribute[:attribute])
165
+ end
166
+ end
167
+ names_set.keys
168
+ end
169
+
170
+ # check the ciphertext attribute for encrypted attributes
171
+ def has_attribute?(attr_name)
172
+ attr_name = attr_name.to_s
173
+ _, lockbox_attribute = self.class.lockbox_attributes.find { |_, la| la[:attribute] == attr_name }
174
+ if lockbox_attribute
175
+ super(lockbox_attribute[:encrypted_attribute])
176
+ else
177
+ super
178
+ end
152
179
  end
153
180
 
154
181
  # needed for in-place modifications
@@ -232,71 +259,69 @@ module Lockbox
232
259
  result
233
260
  end
234
261
 
235
- if ActiveRecord::VERSION::MAJOR >= 6
236
- if ActiveRecord::VERSION::STRING.to_f >= 7.2
237
- def self.insert(attributes, **options)
238
- super(lockbox_map_record_attributes(attributes), **options)
239
- end
240
-
241
- def self.insert!(attributes, **options)
242
- super(lockbox_map_record_attributes(attributes), **options)
243
- end
244
-
245
- def self.upsert(attributes, **options)
246
- super(lockbox_map_record_attributes(attributes, check_readonly: true), **options)
247
- end
262
+ if ActiveRecord::VERSION::STRING.to_f >= 7.2
263
+ def self.insert(attributes, **options)
264
+ super(lockbox_map_record_attributes(attributes), **options)
248
265
  end
249
266
 
250
- def self.insert_all(attributes, **options)
251
- super(lockbox_map_attributes(attributes), **options)
267
+ def self.insert!(attributes, **options)
268
+ super(lockbox_map_record_attributes(attributes), **options)
252
269
  end
253
270
 
254
- def self.insert_all!(attributes, **options)
255
- super(lockbox_map_attributes(attributes), **options)
271
+ def self.upsert(attributes, **options)
272
+ super(lockbox_map_record_attributes(attributes, check_readonly: true), **options)
256
273
  end
274
+ end
257
275
 
258
- def self.upsert_all(attributes, **options)
259
- super(lockbox_map_attributes(attributes, check_readonly: true), **options)
260
- end
276
+ def self.insert_all(attributes, **options)
277
+ super(lockbox_map_attributes(attributes), **options)
278
+ end
261
279
 
262
- # private
263
- # does not try to handle :returning option for simplicity
264
- def self.lockbox_map_attributes(records, check_readonly: false)
265
- return records unless records.is_a?(Array)
280
+ def self.insert_all!(attributes, **options)
281
+ super(lockbox_map_attributes(attributes), **options)
282
+ end
266
283
 
267
- records.map do |attributes|
268
- lockbox_map_record_attributes(attributes, check_readonly: false)
269
- end
270
- end
284
+ def self.upsert_all(attributes, **options)
285
+ super(lockbox_map_attributes(attributes, check_readonly: true), **options)
286
+ end
271
287
 
272
- # private
273
- def self.lockbox_map_record_attributes(attributes, check_readonly: false)
274
- return attributes unless attributes.is_a?(Hash)
288
+ # private
289
+ # does not try to handle :returning option for simplicity
290
+ def self.lockbox_map_attributes(records, check_readonly: false)
291
+ return records unless records.is_a?(Array)
275
292
 
276
- # transform keys like Active Record
277
- attributes = attributes.transform_keys do |key|
278
- n = key.to_s
279
- attribute_aliases[n] || n
280
- end
293
+ records.map do |attributes|
294
+ lockbox_map_record_attributes(attributes, check_readonly: false)
295
+ end
296
+ end
281
297
 
282
- lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
283
- lockbox_attributes.each do |key, lockbox_attribute|
284
- attribute = key.to_s
285
- # check read only
286
- # users should mark both plaintext and ciphertext columns
287
- if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s)
288
- warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}"
289
- end
298
+ # private
299
+ def self.lockbox_map_record_attributes(attributes, check_readonly: false)
300
+ return attributes unless attributes.is_a?(Hash)
290
301
 
291
- message = attributes[attribute]
292
- attributes.delete(attribute) unless lockbox_attribute[:migrating]
293
- encrypted_attribute = lockbox_attribute[:encrypted_attribute]
294
- ciphertext = send("generate_#{encrypted_attribute}", message)
295
- attributes[encrypted_attribute] = ciphertext
302
+ # transform keys like Active Record
303
+ attributes = attributes.transform_keys do |key|
304
+ n = key.to_s
305
+ attribute_aliases[n] || n
306
+ end
307
+
308
+ lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
309
+ lockbox_attributes.each do |key, lockbox_attribute|
310
+ attribute = key.to_s
311
+ # check read only
312
+ # users should mark both plaintext and ciphertext columns
313
+ if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s)
314
+ warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}"
296
315
  end
297
316
 
298
- attributes
317
+ message = attributes[attribute]
318
+ attributes.delete(attribute) unless lockbox_attribute[:migrating]
319
+ encrypted_attribute = lockbox_attribute[:encrypted_attribute]
320
+ ciphertext = send("generate_#{encrypted_attribute}", message)
321
+ attributes[encrypted_attribute] = ciphertext
299
322
  end
323
+
324
+ attributes
300
325
  end
301
326
  else
302
327
  def reload
@@ -327,13 +352,8 @@ module Lockbox
327
352
  elsif attributes_to_define_after_schema_loads.key?(name.to_s)
328
353
  opt = attributes_to_define_after_schema_loads[name.to_s][1]
329
354
 
330
- has_default =
331
- if ActiveRecord::VERSION::MAJOR >= 7
332
- # not ideal, since NO_DEFAULT_PROVIDED is private
333
- opt != ActiveRecord::Attributes::ClassMethods.const_get(:NO_DEFAULT_PROVIDED)
334
- else
335
- opt.is_a?(Hash) && opt.key?(:default)
336
- end
355
+ # not ideal, since NO_DEFAULT_PROVIDED is private
356
+ has_default = opt != ActiveRecord::Attributes::ClassMethods.const_get(:NO_DEFAULT_PROVIDED)
337
357
 
338
358
  if has_default
339
359
  warn "[lockbox] WARNING: attributes with `:default` option are not supported. Use `after_initialize` instead."
@@ -408,21 +428,14 @@ module Lockbox
408
428
  else
409
429
  attribute name, :string
410
430
  end
411
- else
431
+ elsif attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
412
432
  # hack for Active Record 6.1+ to set string type after serialize
413
433
  # otherwise, type gets set to ActiveModel::Type::Value
414
434
  # which always returns false for changed_in_place?
415
435
  # earlier versions of Active Record take the previous code path
416
- if ActiveRecord::VERSION::STRING.to_f >= 7.0 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
417
- attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil)
418
- if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
419
- attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
420
- end
421
- elsif ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
422
- attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
423
- if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
424
- attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
425
- end
436
+ attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil)
437
+ if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
438
+ attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
426
439
  end
427
440
  end
428
441
 
@@ -581,7 +594,6 @@ module Lockbox
581
594
  case options[:type]
582
595
  when :boolean
583
596
  message = ActiveRecord::Type::Boolean.new.serialize(message)
584
- message = nil if message == "" # for Active Record < 5.2
585
597
  message = message ? "t" : "f" unless message.nil?
586
598
  when :date
587
599
  message = ActiveRecord::Type::Date.new.serialize(message)
@@ -589,7 +601,6 @@ module Lockbox
589
601
  message = message.strftime("%Y-%m-%d") unless message.nil?
590
602
  when :datetime
591
603
  message = ActiveRecord::Type::DateTime.new.serialize(message)
592
- message = nil unless message.respond_to?(:iso8601) # for Active Record < 5.2
593
604
  message = message.iso8601(9) unless message.nil?
594
605
  when :time
595
606
  message = ActiveRecord::Type::Time.new.serialize(message)
@@ -606,14 +617,7 @@ module Lockbox
606
617
  # double precision, big endian
607
618
  message = [message].pack("G") unless message.nil?
608
619
  when :decimal
609
- message =
610
- if ActiveRecord::VERSION::MAJOR >= 6
611
- ActiveRecord::Type::Decimal.new.serialize(message)
612
- else
613
- # issue with serialize in Active Record < 6
614
- # https://github.com/rails/rails/commit/a741208f80dd33420a56486bd9ed2b0b9862234a
615
- ActiveRecord::Type::Decimal.new.cast(message)
616
- end
620
+ message = ActiveRecord::Type::Decimal.new.serialize(message)
617
621
  # Postgres stores 4 decimal digits in 2 bytes
618
622
  # plus 3 to 8 bytes of overhead
619
623
  # but use string for simplicity
@@ -716,12 +720,6 @@ module Lockbox
716
720
  end
717
721
  end
718
722
 
719
- def lockbox_encrypts(*attributes, **options)
720
- deprecator = ActiveSupport::VERSION::STRING.to_f >= 7.2 ? ActiveSupport.deprecator : ActiveSupport::Deprecation
721
- deprecator.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`")
722
- has_encrypted(*attributes, **options)
723
- end
724
-
725
723
  module Attached
726
724
  def encrypts_attached(*attributes, **options)
727
725
  attributes.each do |name|
@@ -12,32 +12,15 @@ module Lockbox
12
12
  require "lockbox/active_storage_extensions"
13
13
 
14
14
  ActiveStorage::Attached.prepend(Lockbox::ActiveStorageExtensions::Attached)
15
- if ActiveStorage::VERSION::MAJOR >= 6
16
- ActiveStorage::Attached::Changes::CreateOne.prepend(Lockbox::ActiveStorageExtensions::CreateOne)
17
- end
15
+ ActiveStorage::Attached::Changes::CreateOne.prepend(Lockbox::ActiveStorageExtensions::CreateOne)
18
16
  ActiveStorage::Attached::One.prepend(Lockbox::ActiveStorageExtensions::AttachedOne)
19
17
  ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
20
18
 
21
- # use load hooks when possible
22
- if ActiveStorage::VERSION::MAJOR >= 7
23
- ActiveSupport.on_load(:active_storage_attachment) do
24
- prepend Lockbox::ActiveStorageExtensions::Attachment
25
- end
26
- ActiveSupport.on_load(:active_storage_blob) do
27
- prepend Lockbox::ActiveStorageExtensions::Blob
28
- end
29
- elsif ActiveStorage::VERSION::MAJOR >= 6
30
- ActiveSupport.on_load(:active_storage_attachment) do
31
- include Lockbox::ActiveStorageExtensions::Attachment
32
- end
33
- ActiveSupport.on_load(:active_storage_blob) do
34
- prepend Lockbox::ActiveStorageExtensions::Blob
35
- end
36
- else
37
- app.config.to_prepare do
38
- ActiveStorage::Attachment.include(Lockbox::ActiveStorageExtensions::Attachment)
39
- ActiveStorage::Blob.prepend(Lockbox::ActiveStorageExtensions::Blob)
40
- end
19
+ ActiveSupport.on_load(:active_storage_attachment) do
20
+ prepend Lockbox::ActiveStorageExtensions::Attachment
21
+ end
22
+ ActiveSupport.on_load(:active_storage_blob) do
23
+ prepend Lockbox::ActiveStorageExtensions::Blob
41
24
  end
42
25
  end
43
26
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "1.4.1"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/lockbox.rb CHANGED
@@ -99,8 +99,10 @@ end
99
99
  if defined?(ActiveSupport.on_load)
100
100
  ActiveSupport.on_load(:active_record) do
101
101
  ar_version = ActiveRecord::VERSION::STRING.to_f
102
- if ar_version < 5.2
103
- if ar_version >= 5
102
+ if ar_version < 7
103
+ if ar_version >= 5.2
104
+ raise Lockbox::Error, "Active Record #{ActiveRecord::VERSION::STRING} requires Lockbox < 2"
105
+ elsif ar_version >= 5
104
106
  raise Lockbox::Error, "Active Record #{ActiveRecord::VERSION::STRING} requires Lockbox < 0.7"
105
107
  else
106
108
  raise Lockbox::Error, "Active Record #{ActiveRecord::VERSION::STRING} not supported"
@@ -109,12 +111,19 @@ if defined?(ActiveSupport.on_load)
109
111
 
110
112
  extend Lockbox::Model
111
113
  extend Lockbox::Model::Attached
112
- singleton_class.alias_method(:encrypts, :lockbox_encrypts) if ActiveRecord::VERSION::MAJOR < 7
113
114
  ActiveRecord::Relation.prepend Lockbox::Calculations
114
115
  end
115
116
 
116
117
  ActiveSupport.on_load(:mongoid) do
118
+ mongoid_version = Mongoid::VERSION.to_i
119
+ if mongoid_version < 8
120
+ if mongoid_version >= 6
121
+ raise Lockbox::Error, "Mongoid #{Mongoid::VERSION} requires Lockbox < 2"
122
+ else
123
+ raise Lockbox::Error, "Mongoid #{Mongoid::VERSION} not supported"
124
+ end
125
+ end
126
+
117
127
  Mongoid::Document::ClassMethods.include(Lockbox::Model)
118
- Mongoid::Document::ClassMethods.alias_method(:encrypts, :lockbox_encrypts)
119
128
  end
120
129
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lockbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-09 00:00:00.000000000 Z
11
+ date: 2024-10-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: andrew@ankane.org
@@ -51,7 +51,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '2.6'
54
+ version: '3.1'
55
55
  required_rubygems_version: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="