lockbox 1.3.0 → 1.4.1
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 +23 -0
- data/LICENSE.txt +1 -1
- data/README.md +10 -23
- data/lib/lockbox/active_storage_extensions.rb +13 -0
- data/lib/lockbox/carrier_wave_extensions.rb +1 -1
- data/lib/lockbox/migrator.rb +1 -1
- data/lib/lockbox/model.rb +111 -40
- data/lib/lockbox/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4771553bf23214b514e9a4a5c94ce665d234d34969f9a3941ea68bdc6729ed4
|
4
|
+
data.tar.gz: a921894d4f3f43d5245a91941e06f79ea652f09c9c77ccf8dc7e442d1dbda0e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90fef82d743a0172a61728fd9f314eca597d43820e87aeb9cb77330e4ec824919d354b81cf4c6c9c2b1c8cedcebb8916425fed5e469b24ea11b6a2e8730feb6f
|
7
|
+
data.tar.gz: fe874ec6aa3f563927f2ea1743d34a795468846ce74cc9e1a0467e41757721d40c8449d7093b158f63f92b8566ea578cf88e5f2b0a8caefc9facee2a8b731afa
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
## 1.4.1 (2024-09-09)
|
2
|
+
|
3
|
+
- Fixed error message for previews for Active Storage 7.1.4
|
4
|
+
|
5
|
+
## 1.4.0 (2024-08-09)
|
6
|
+
|
7
|
+
- Added support for Active Record 7.2
|
8
|
+
- Added support for Mongoid 9
|
9
|
+
- Fixed error when `decryption_key` option is a proc or symbol and returns `nil`
|
10
|
+
|
11
|
+
## 1.3.3 (2024-02-07)
|
12
|
+
|
13
|
+
- Added warning for encrypting store attributes
|
14
|
+
|
15
|
+
## 1.3.2 (2024-01-10)
|
16
|
+
|
17
|
+
- Fixed issue with serialized attributes
|
18
|
+
|
19
|
+
## 1.3.1 (2024-01-06)
|
20
|
+
|
21
|
+
- Fixed error with `array` and `hash` types and no default column serializer with Rails 7.1
|
22
|
+
- Fixed Action Text deserialization with Rails 7.1
|
23
|
+
|
1
24
|
## 1.3.0 (2023-07-02)
|
2
25
|
|
3
26
|
- Added support for CarrierWave 3
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
|
10
10
|
Learn [the principles behind it](https://ankane.org/modern-encryption-rails), [how to secure emails with Devise](https://ankane.org/securing-user-emails-lockbox), and [how to secure sensitive data in Rails](https://ankane.org/sensitive-data-rails).
|
11
11
|
|
12
|
-
[](https://github.com/ankane/lockbox/actions)
|
13
13
|
|
14
14
|
## Installation
|
15
15
|
|
@@ -72,7 +72,7 @@ Then follow the instructions below for the data you want to encrypt.
|
|
72
72
|
Create a migration with:
|
73
73
|
|
74
74
|
```ruby
|
75
|
-
class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.
|
75
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.2]
|
76
76
|
def change
|
77
77
|
add_column :users, :email_ciphertext, :text
|
78
78
|
end
|
@@ -140,6 +140,8 @@ class User < ApplicationRecord
|
|
140
140
|
end
|
141
141
|
```
|
142
142
|
|
143
|
+
For [Active Record Store](https://api.rubyonrails.org/classes/ActiveRecord/Store.html), encrypt the column rather than individual accessors.
|
144
|
+
|
143
145
|
For [StoreModel](https://github.com/DmitryTsepelev/store_model), use:
|
144
146
|
|
145
147
|
```ruby
|
@@ -192,7 +194,7 @@ class User < ApplicationRecord
|
|
192
194
|
has_encrypted :email
|
193
195
|
|
194
196
|
# remove this line after dropping email column
|
195
|
-
self.ignored_columns
|
197
|
+
self.ignored_columns += ["email"]
|
196
198
|
end
|
197
199
|
```
|
198
200
|
|
@@ -249,7 +251,7 @@ User.decrypt_email_ciphertext(user.email_ciphertext)
|
|
249
251
|
Create a migration with:
|
250
252
|
|
251
253
|
```ruby
|
252
|
-
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[7.
|
254
|
+
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[7.2]
|
253
255
|
def change
|
254
256
|
add_column :action_text_rich_texts, :body_ciphertext, :text
|
255
257
|
end
|
@@ -380,7 +382,7 @@ Encryption is applied to all versions after processing.
|
|
380
382
|
You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration:
|
381
383
|
|
382
384
|
```ruby
|
383
|
-
class AddLicenseToUsers < ActiveRecord::Migration[7.
|
385
|
+
class AddLicenseToUsers < ActiveRecord::Migration[7.2]
|
384
386
|
def change
|
385
387
|
add_column :users, :license, :string
|
386
388
|
end
|
@@ -908,7 +910,7 @@ end
|
|
908
910
|
You can use `binary` columns for the ciphertext instead of `text` columns.
|
909
911
|
|
910
912
|
```ruby
|
911
|
-
class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.
|
913
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.2]
|
912
914
|
def change
|
913
915
|
add_column :users, :email_ciphertext, :binary
|
914
916
|
end
|
@@ -959,7 +961,7 @@ end
|
|
959
961
|
Create a migration with:
|
960
962
|
|
961
963
|
```ruby
|
962
|
-
class MigrateToLockbox < ActiveRecord::Migration[7.
|
964
|
+
class MigrateToLockbox < ActiveRecord::Migration[7.2]
|
963
965
|
def change
|
964
966
|
add_column :users, :name_ciphertext, :text
|
965
967
|
add_column :users, :email_ciphertext, :text
|
@@ -992,7 +994,7 @@ end
|
|
992
994
|
Then remove the previous gem from your Gemfile and drop its columns.
|
993
995
|
|
994
996
|
```ruby
|
995
|
-
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[7.
|
997
|
+
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[7.2]
|
996
998
|
def change
|
997
999
|
remove_column :users, :encrypted_name, :text
|
998
1000
|
remove_column :users, :encrypted_name_iv, :text
|
@@ -1014,21 +1016,6 @@ class User < ApplicationRecord
|
|
1014
1016
|
end
|
1015
1017
|
```
|
1016
1018
|
|
1017
|
-
### 0.6.0
|
1018
|
-
|
1019
|
-
0.6.0 adds `encrypted: true` to Active Storage metadata for new files. This field is informational, but if you prefer to add it to existing files, use:
|
1020
|
-
|
1021
|
-
```ruby
|
1022
|
-
User.with_attached_license.find_each do |user|
|
1023
|
-
next unless user.license.attached?
|
1024
|
-
|
1025
|
-
metadata = user.license.metadata
|
1026
|
-
unless metadata["encrypted"]
|
1027
|
-
user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
|
1028
|
-
end
|
1029
|
-
end
|
1030
|
-
```
|
1031
|
-
|
1032
1019
|
## History
|
1033
1020
|
|
1034
1021
|
View the [changelog](https://github.com/ankane/lockbox/blob/master/CHANGELOG.md)
|
@@ -124,6 +124,13 @@ module Lockbox
|
|
124
124
|
super
|
125
125
|
end
|
126
126
|
|
127
|
+
if ActiveStorage::VERSION::STRING.to_f == 7.1 && ActiveStorage.version >= "7.1.4"
|
128
|
+
def transform_variants_later
|
129
|
+
blob.instance_variable_set(:@lockbox_encrypted, true) if Utils.encrypted_options(record, name)
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
127
134
|
if ActiveStorage::VERSION::MAJOR >= 6
|
128
135
|
def open(**options)
|
129
136
|
blob.open(**options) do |file|
|
@@ -150,6 +157,12 @@ module Lockbox
|
|
150
157
|
end
|
151
158
|
|
152
159
|
module Blob
|
160
|
+
if ActiveStorage::VERSION::STRING.to_f == 7.1 && ActiveStorage.version >= "7.1.4"
|
161
|
+
def preview_image_needed_before_processing_variants?
|
162
|
+
!instance_variable_defined?(:@lockbox_encrypted) && super
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
153
166
|
private
|
154
167
|
|
155
168
|
def extract_content_type(io)
|
data/lib/lockbox/migrator.rb
CHANGED
@@ -2,7 +2,7 @@ module Lockbox
|
|
2
2
|
class Migrator
|
3
3
|
def initialize(relation, batch_size:)
|
4
4
|
@relation = relation
|
5
|
-
@transaction = @relation.respond_to?(:transaction)
|
5
|
+
@transaction = @relation.respond_to?(:transaction) && !mongoid_relation?(base_relation)
|
6
6
|
@batch_size = batch_size
|
7
7
|
end
|
8
8
|
|
data/lib/lockbox/model.rb
CHANGED
@@ -137,13 +137,16 @@ module Lockbox
|
|
137
137
|
# essentially a no-op if already loaded
|
138
138
|
# an exception is thrown if decryption fails
|
139
139
|
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
140
|
-
# don't try to decrypt if no decryption key given
|
141
|
-
next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
|
142
|
-
|
143
140
|
# it is possible that the encrypted attribute is not loaded, eg.
|
144
141
|
# if the record was fetched partially (`User.select(:id).first`).
|
145
142
|
# accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
|
146
|
-
|
143
|
+
if has_attribute?(lockbox_attribute[:encrypted_attribute])
|
144
|
+
begin
|
145
|
+
send(lockbox_attribute[:attribute])
|
146
|
+
rescue ArgumentError => e
|
147
|
+
raise e if e.message != "No decryption key set"
|
148
|
+
end
|
149
|
+
end
|
147
150
|
end
|
148
151
|
super
|
149
152
|
end
|
@@ -230,6 +233,20 @@ module Lockbox
|
|
230
233
|
end
|
231
234
|
|
232
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
|
248
|
+
end
|
249
|
+
|
233
250
|
def self.insert_all(attributes, **options)
|
234
251
|
super(lockbox_map_attributes(attributes), **options)
|
235
252
|
end
|
@@ -248,30 +265,37 @@ module Lockbox
|
|
248
265
|
return records unless records.is_a?(Array)
|
249
266
|
|
250
267
|
records.map do |attributes|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
attribute_aliases[n] || n
|
255
|
-
end
|
268
|
+
lockbox_map_record_attributes(attributes, check_readonly: false)
|
269
|
+
end
|
270
|
+
end
|
256
271
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
272
|
+
# private
|
273
|
+
def self.lockbox_map_record_attributes(attributes, check_readonly: false)
|
274
|
+
return attributes unless attributes.is_a?(Hash)
|
275
|
+
|
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
|
281
|
+
|
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]}"
|
271
289
|
end
|
272
290
|
|
273
|
-
attributes
|
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
|
274
296
|
end
|
297
|
+
|
298
|
+
attributes
|
275
299
|
end
|
276
300
|
end
|
277
301
|
else
|
@@ -289,8 +313,18 @@ module Lockbox
|
|
289
313
|
@lockbox_attributes[original_name] = options
|
290
314
|
|
291
315
|
if activerecord
|
316
|
+
# warn on store attributes
|
317
|
+
if stored_attributes.any? { |k, v| v.include?(name) }
|
318
|
+
warn "[lockbox] WARNING: encrypting store accessors is not supported. Encrypt the column instead."
|
319
|
+
end
|
320
|
+
|
292
321
|
# warn on default attributes
|
293
|
-
if
|
322
|
+
if ActiveRecord::VERSION::STRING.to_f >= 7.2
|
323
|
+
# TODO improve
|
324
|
+
if pending_attribute_modifications.any? { |v| v.is_a?(ActiveModel::AttributeRegistration::ClassMethods::PendingDefault) && v.name == name.to_s }
|
325
|
+
warn "[lockbox] WARNING: attributes with `:default` option are not supported. Use `after_initialize` instead."
|
326
|
+
end
|
327
|
+
elsif attributes_to_define_after_schema_loads.key?(name.to_s)
|
294
328
|
opt = attributes_to_define_after_schema_loads[name.to_s][1]
|
295
329
|
|
296
330
|
has_default =
|
@@ -324,13 +358,43 @@ module Lockbox
|
|
324
358
|
attribute name, attribute_type
|
325
359
|
|
326
360
|
if ActiveRecord::VERSION::STRING.to_f >= 7.1
|
327
|
-
|
328
|
-
|
329
|
-
|
361
|
+
case options[:type]
|
362
|
+
when :json
|
363
|
+
serialize name, coder: JSON
|
364
|
+
when :hash
|
365
|
+
serialize name, type: Hash, coder: default_column_serializer || YAML
|
366
|
+
when :array
|
367
|
+
serialize name, type: Array, coder: default_column_serializer || YAML
|
368
|
+
end
|
330
369
|
else
|
331
|
-
|
332
|
-
|
333
|
-
|
370
|
+
case options[:type]
|
371
|
+
when :json
|
372
|
+
serialize name, JSON
|
373
|
+
when :hash
|
374
|
+
serialize name, Hash
|
375
|
+
when :array
|
376
|
+
serialize name, Array
|
377
|
+
end
|
378
|
+
end
|
379
|
+
elsif ActiveRecord::VERSION::STRING.to_f >= 7.2
|
380
|
+
decorate_attributes([name]) do |attr_name, cast_type|
|
381
|
+
if cast_type.instance_of?(ActiveRecord::Type::Value)
|
382
|
+
original_type = pending_attribute_modifications.find { |v| v.is_a?(ActiveModel::AttributeRegistration::ClassMethods::PendingType) && v.name == original_name.to_s && !v.type.nil? }&.type
|
383
|
+
if original_type
|
384
|
+
original_type
|
385
|
+
elsif options[:migrating]
|
386
|
+
cast_type
|
387
|
+
else
|
388
|
+
ActiveRecord::Type::String.new
|
389
|
+
end
|
390
|
+
elsif cast_type.is_a?(ActiveRecord::Type::Serialized) && cast_type.subtype.instance_of?(ActiveModel::Type::Value)
|
391
|
+
# hack to set string type after serialize
|
392
|
+
# otherwise, type gets set to ActiveModel::Type::Value
|
393
|
+
# which always returns false for changed_in_place?
|
394
|
+
ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, cast_type.coder)
|
395
|
+
else
|
396
|
+
cast_type
|
397
|
+
end
|
334
398
|
end
|
335
399
|
elsif !attributes_to_define_after_schema_loads.key?(name.to_s)
|
336
400
|
# when migrating it's best to specify the type directly
|
@@ -345,8 +409,7 @@ module Lockbox
|
|
345
409
|
attribute name, :string
|
346
410
|
end
|
347
411
|
else
|
348
|
-
# hack for Active Record 6.1
|
349
|
-
# to set string type after serialize
|
412
|
+
# hack for Active Record 6.1+ to set string type after serialize
|
350
413
|
# otherwise, type gets set to ActiveModel::Type::Value
|
351
414
|
# which always returns false for changed_in_place?
|
352
415
|
# earlier versions of Active Record take the previous code path
|
@@ -432,12 +495,12 @@ module Lockbox
|
|
432
495
|
# decrypt first for dirty tracking
|
433
496
|
# don't raise error if can't decrypt previous
|
434
497
|
# don't try to decrypt if no decryption key given
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
498
|
+
begin
|
499
|
+
send(name)
|
500
|
+
rescue Lockbox::DecryptionError
|
501
|
+
warn "[lockbox] Decrypting previous value failed"
|
502
|
+
rescue ArgumentError => e
|
503
|
+
raise e if e.message != "No decryption key set"
|
441
504
|
end
|
442
505
|
|
443
506
|
send("lockbox_direct_#{name}=", message)
|
@@ -499,6 +562,9 @@ module Lockbox
|
|
499
562
|
clear_attribute_change(name)
|
500
563
|
end
|
501
564
|
end
|
565
|
+
|
566
|
+
# ensure same object is returned as next call
|
567
|
+
message = super()
|
502
568
|
else
|
503
569
|
instance_variable_set("@#{name}", message)
|
504
570
|
end
|
@@ -615,6 +681,10 @@ module Lockbox
|
|
615
681
|
else
|
616
682
|
# use original name for serialized attributes if no type specified
|
617
683
|
type = (try(:attribute_types) || {})[(options[:type] ? name : original_name).to_s]
|
684
|
+
# for Action Text
|
685
|
+
if activerecord && type.is_a?(ActiveRecord::Type::Serialized) && defined?(ActionText::Content) && type.coder == ActionText::Content
|
686
|
+
message.force_encoding(Encoding::UTF_8)
|
687
|
+
end
|
618
688
|
message = type.deserialize(message) if type
|
619
689
|
message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
|
620
690
|
end
|
@@ -647,7 +717,8 @@ module Lockbox
|
|
647
717
|
end
|
648
718
|
|
649
719
|
def lockbox_encrypts(*attributes, **options)
|
650
|
-
ActiveSupport::
|
720
|
+
deprecator = ActiveSupport::VERSION::STRING.to_f >= 7.2 ? ActiveSupport.deprecator : ActiveSupport::Deprecation
|
721
|
+
deprecator.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`")
|
651
722
|
has_encrypted(*attributes, **options)
|
652
723
|
end
|
653
724
|
|
data/lib/lockbox/version.rb
CHANGED
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
|
+
version: 1.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: andrew@ankane.org
|
@@ -58,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
58
|
- !ruby/object:Gem::Version
|
59
59
|
version: '0'
|
60
60
|
requirements: []
|
61
|
-
rubygems_version: 3.
|
61
|
+
rubygems_version: 3.5.16
|
62
62
|
signing_key:
|
63
63
|
specification_version: 4
|
64
64
|
summary: Modern encryption for Ruby and Rails
|