lockbox 0.5.0 → 0.6.4
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 +28 -0
- data/LICENSE.txt +1 -1
- data/README.md +70 -27
- data/lib/generators/lockbox/audits_generator.rb +11 -3
- data/lib/lockbox.rb +10 -1
- data/lib/lockbox/active_storage_extensions.rb +4 -0
- data/lib/lockbox/carrier_wave_extensions.rb +17 -2
- data/lib/lockbox/model.rb +114 -15
- data/lib/lockbox/railtie.rb +13 -1
- data/lib/lockbox/utils.rb +1 -2
- data/lib/lockbox/version.rb +1 -1
- metadata +5 -187
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06b9f5c13a4cbdab22a46664beaf453bffa923a7bbebd918adecea008ecbc797
|
4
|
+
data.tar.gz: 42c68577c2f4b8b4d1b068c01f314893a069b6fd361bd06d87a0de6531636c94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 138df2feafe849bfc5ba80818e1b3695eb107e1f37b0d6654467383bc1788d5803bc00cb753bd7d3a8d3f30773ef4ed3d05f0945a896529d8b685df55b0e10ae
|
7
|
+
data.tar.gz: 29ad6a9cb2248489caec35bdc56fdbcb6d1d251a0833ec1d597e1e955ac17ccc07626f59ce92683843658fc45311571f495d044f6219075abfcef8b2783dd6a8
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
## 0.6.4 (2021-04-05)
|
2
|
+
|
3
|
+
- Fixed in place changes in callbacks
|
4
|
+
- Fixed `[]` method for encrypted attributes
|
5
|
+
|
6
|
+
## 0.6.3 (2021-03-30)
|
7
|
+
|
8
|
+
- Fixed empty arrays and hashes
|
9
|
+
- Fixed content type for CarrierWave 2.2.1
|
10
|
+
|
11
|
+
## 0.6.2 (2021-02-08)
|
12
|
+
|
13
|
+
- Added `inet` type
|
14
|
+
- Fixed error when `lockbox` key in Rails credentials has a string value
|
15
|
+
- Fixed deprecation warning with Active Record 6.1
|
16
|
+
|
17
|
+
## 0.6.1 (2020-12-03)
|
18
|
+
|
19
|
+
- Added integration with Rails credentials
|
20
|
+
- Fixed in place changes for Active Record 6.1
|
21
|
+
- Fixed error with `content_type` method for CarrierWave < 2
|
22
|
+
|
23
|
+
## 0.6.0 (2020-12-03)
|
24
|
+
|
25
|
+
- Added `encrypted` flag to Active Storage metadata
|
26
|
+
- Added encrypted columns to `filter_attributes`
|
27
|
+
- Improved `inspect` method
|
28
|
+
|
1
29
|
## 0.5.0 (2020-11-22)
|
2
30
|
|
3
31
|
- Improved error messages for hybrid cryptography
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -27,7 +27,7 @@ Generate a key
|
|
27
27
|
Lockbox.generate_key
|
28
28
|
```
|
29
29
|
|
30
|
-
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production.
|
30
|
+
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production.
|
31
31
|
|
32
32
|
Set the following environment variable with your key (you can use this one in development)
|
33
33
|
|
@@ -35,10 +35,17 @@ 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+)
|
39
|
+
|
40
|
+
```yml
|
41
|
+
lockbox:
|
42
|
+
master_key: "0000000000000000000000000000000000000000000000000000000000000000"
|
43
|
+
```
|
44
|
+
|
38
45
|
or create `config/initializers/lockbox.rb` with something like
|
39
46
|
|
40
47
|
```ruby
|
41
|
-
Lockbox.master_key = Rails.application.credentials.
|
48
|
+
Lockbox.master_key = Rails.application.credentials.lockbox[:master_key]
|
42
49
|
```
|
43
50
|
|
44
51
|
Then follow the instructions below for the data you want to encrypt.
|
@@ -65,7 +72,7 @@ Then follow the instructions below for the data you want to encrypt.
|
|
65
72
|
Create a migration with:
|
66
73
|
|
67
74
|
```ruby
|
68
|
-
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.
|
75
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.1]
|
69
76
|
def change
|
70
77
|
add_column :users, :email_ciphertext, :text
|
71
78
|
end
|
@@ -114,6 +121,7 @@ class User < ApplicationRecord
|
|
114
121
|
encrypts :properties, type: :json
|
115
122
|
encrypts :settings, type: :hash
|
116
123
|
encrypts :messages, type: :array
|
124
|
+
encrypts :ip, type: :inet
|
117
125
|
end
|
118
126
|
```
|
119
127
|
|
@@ -197,6 +205,34 @@ class User < ApplicationRecord
|
|
197
205
|
end
|
198
206
|
```
|
199
207
|
|
208
|
+
#### Model Changes
|
209
|
+
|
210
|
+
If tracking changes to model attributes, be sure to remove or redact encrypted attributes.
|
211
|
+
|
212
|
+
PaperTrail
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class User < ApplicationRecord
|
216
|
+
# for an encrypted history (still tracks ciphertext changes)
|
217
|
+
has_paper_trail skip: [:email]
|
218
|
+
|
219
|
+
# for no history (add blind indexes as well)
|
220
|
+
has_paper_trail skip: [:email, :email_ciphertext]
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
Audited
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
class User < ApplicationRecord
|
228
|
+
# for an encrypted history (still tracks ciphertext changes)
|
229
|
+
audited except: [:email]
|
230
|
+
|
231
|
+
# for no history (add blind indexes as well)
|
232
|
+
audited except: [:email, :email_ciphertext]
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
200
236
|
#### Decryption
|
201
237
|
|
202
238
|
To decrypt data outside the model, use:
|
@@ -212,7 +248,7 @@ User.decrypt_email_ciphertext(user.email_ciphertext)
|
|
212
248
|
Create a migration with:
|
213
249
|
|
214
250
|
```ruby
|
215
|
-
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[6.
|
251
|
+
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[6.1]
|
216
252
|
def change
|
217
253
|
add_column :action_text_rich_texts, :body_ciphertext, :text
|
218
254
|
end
|
@@ -300,9 +336,7 @@ def license
|
|
300
336
|
end
|
301
337
|
```
|
302
338
|
|
303
|
-
#### Migrating Existing Files
|
304
|
-
|
305
|
-
**Note:** This feature is experimental. Please try it in a non-production environment and [share](https://github.com/ankane/lockbox/issues/44) how it goes.
|
339
|
+
#### Migrating Existing Files
|
306
340
|
|
307
341
|
Lockbox makes it easy to encrypt existing files without downtime.
|
308
342
|
|
@@ -343,7 +377,7 @@ Encryption is applied to all versions after processing.
|
|
343
377
|
You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration:
|
344
378
|
|
345
379
|
```ruby
|
346
|
-
class AddLicenseToUsers < ActiveRecord::Migration[6.
|
380
|
+
class AddLicenseToUsers < ActiveRecord::Migration[6.1]
|
347
381
|
def change
|
348
382
|
add_column :users, :license, :string
|
349
383
|
end
|
@@ -532,12 +566,10 @@ Update your model:
|
|
532
566
|
|
533
567
|
```ruby
|
534
568
|
class User < ApplicationRecord
|
535
|
-
encrypts :email, previous_versions: [{
|
569
|
+
encrypts :email, previous_versions: [{master_key: previous_key}]
|
536
570
|
end
|
537
571
|
```
|
538
572
|
|
539
|
-
Use `master_key` instead of `key` if passing the master key.
|
540
|
-
|
541
573
|
To rotate existing records, use:
|
542
574
|
|
543
575
|
```ruby
|
@@ -551,11 +583,9 @@ Once all records are rotated, you can remove `previous_versions` from the model.
|
|
551
583
|
Update your initializer:
|
552
584
|
|
553
585
|
```ruby
|
554
|
-
Lockbox.encrypts_action_text_body(previous_versions: [{
|
586
|
+
Lockbox.encrypts_action_text_body(previous_versions: [{master_key: previous_key}])
|
555
587
|
```
|
556
588
|
|
557
|
-
Use `master_key` instead of `key` if passing the master key.
|
558
|
-
|
559
589
|
To rotate existing records, use:
|
560
590
|
|
561
591
|
```ruby
|
@@ -570,12 +600,10 @@ Update your model:
|
|
570
600
|
|
571
601
|
```ruby
|
572
602
|
class User < ApplicationRecord
|
573
|
-
encrypts_attached :license, previous_versions: [{
|
603
|
+
encrypts_attached :license, previous_versions: [{master_key: previous_key}]
|
574
604
|
end
|
575
605
|
```
|
576
606
|
|
577
|
-
Use `master_key` instead of `key` if passing the master key.
|
578
|
-
|
579
607
|
To rotate existing files, use:
|
580
608
|
|
581
609
|
```ruby
|
@@ -592,12 +620,10 @@ Update your model:
|
|
592
620
|
|
593
621
|
```ruby
|
594
622
|
class LicenseUploader < CarrierWave::Uploader::Base
|
595
|
-
encrypt previous_versions: [{
|
623
|
+
encrypt previous_versions: [{master_key: previous_key}]
|
596
624
|
end
|
597
625
|
```
|
598
626
|
|
599
|
-
Use `master_key` instead of `key` if passing the master key.
|
600
|
-
|
601
627
|
To rotate existing files, use:
|
602
628
|
|
603
629
|
```ruby
|
@@ -672,7 +698,7 @@ This is the default algorithm. It’s:
|
|
672
698
|
|
673
699
|
Lockbox uses 256-bit keys.
|
674
700
|
|
675
|
-
**For users who do a lot of encryptions:** You should rotate an individual key after 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the key. Each database field and file uploader use a different key (derived from the master key) to extend this window.
|
701
|
+
**For users who do a lot of encryptions:** You should rotate an individual key after 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the authentication key. Each database field and file uploader use a different key (derived from the master key) to extend this window.
|
676
702
|
|
677
703
|
### XSalsa20
|
678
704
|
|
@@ -731,14 +757,14 @@ For Ubuntu 20.04 and 18.04, use:
|
|
731
757
|
|
732
758
|
```yml
|
733
759
|
- name: Install Libsodium
|
734
|
-
run: sudo apt-get install libsodium23
|
760
|
+
run: sudo apt-get update && sudo apt-get install libsodium23
|
735
761
|
```
|
736
762
|
|
737
763
|
For Ubuntu 16.04, use:
|
738
764
|
|
739
765
|
```yml
|
740
766
|
- name: Install Libsodium
|
741
|
-
run: sudo apt-get install libsodium18
|
767
|
+
run: sudo apt-get update && sudo apt-get install libsodium18
|
742
768
|
```
|
743
769
|
|
744
770
|
##### Travis CI
|
@@ -961,7 +987,7 @@ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
|
|
961
987
|
You can use `binary` columns for the ciphertext instead of `text` columns.
|
962
988
|
|
963
989
|
```ruby
|
964
|
-
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.
|
990
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.1]
|
965
991
|
def change
|
966
992
|
add_column :users, :email_ciphertext, :binary
|
967
993
|
end
|
@@ -1006,7 +1032,7 @@ end
|
|
1006
1032
|
Create a migration with:
|
1007
1033
|
|
1008
1034
|
```ruby
|
1009
|
-
class MigrateToLockbox < ActiveRecord::Migration[6.
|
1035
|
+
class MigrateToLockbox < ActiveRecord::Migration[6.1]
|
1010
1036
|
def change
|
1011
1037
|
add_column :users, :name_ciphertext, :text
|
1012
1038
|
add_column :users, :email_ciphertext, :text
|
@@ -1039,7 +1065,7 @@ end
|
|
1039
1065
|
Then remove the previous gem from your Gemfile and drop its columns.
|
1040
1066
|
|
1041
1067
|
```ruby
|
1042
|
-
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[6.
|
1068
|
+
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[6.1]
|
1043
1069
|
def change
|
1044
1070
|
remove_column :users, :encrypted_name, :text
|
1045
1071
|
remove_column :users, :encrypted_name_iv, :text
|
@@ -1051,12 +1077,29 @@ end
|
|
1051
1077
|
|
1052
1078
|
## Upgrading
|
1053
1079
|
|
1080
|
+
### 0.6.0
|
1081
|
+
|
1082
|
+
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:
|
1083
|
+
|
1084
|
+
```ruby
|
1085
|
+
User.with_attached_license.find_each do |user|
|
1086
|
+
next unless user.license.attached?
|
1087
|
+
|
1088
|
+
metadata = user.license.metadata
|
1089
|
+
unless metadata["encrypted"]
|
1090
|
+
user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
|
1091
|
+
end
|
1092
|
+
end
|
1093
|
+
```
|
1094
|
+
|
1054
1095
|
### 0.3.6
|
1055
1096
|
|
1056
1097
|
0.3.6 makes content type detection more reliable for Active Storage. You can check and update the content type of existing files with:
|
1057
1098
|
|
1058
1099
|
```ruby
|
1059
|
-
User.find_each do |user|
|
1100
|
+
User.with_attached_license.find_each do |user|
|
1101
|
+
next unless user.license.attached?
|
1102
|
+
|
1060
1103
|
license = user.license
|
1061
1104
|
content_type = Marcel::MimeType.for(license.download, name: license.filename.to_s)
|
1062
1105
|
if content_type != license.content_type
|
@@ -16,9 +16,7 @@ module Lockbox
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def data_type
|
19
|
-
|
20
|
-
# so database connection isn't needed
|
21
|
-
case ActiveRecord::Base.connection_config[:adapter].to_s
|
19
|
+
case adapter
|
22
20
|
when /postg/i # postgres, postgis
|
23
21
|
"jsonb"
|
24
22
|
when /mysql/i
|
@@ -27,6 +25,16 @@ module Lockbox
|
|
27
25
|
"text"
|
28
26
|
end
|
29
27
|
end
|
28
|
+
|
29
|
+
# use connection_config instead of connection.adapter
|
30
|
+
# so database connection isn't needed
|
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
|
37
|
+
end
|
30
38
|
end
|
31
39
|
end
|
32
40
|
end
|
data/lib/lockbox.rb
CHANGED
@@ -27,13 +27,22 @@ end
|
|
27
27
|
|
28
28
|
if defined?(ActiveSupport.on_load)
|
29
29
|
ActiveSupport.on_load(:active_record) do
|
30
|
+
# TODO raise error in 0.7.0
|
31
|
+
if ActiveRecord::VERSION::STRING.to_f <= 5.0
|
32
|
+
warn "Active Record version (#{ActiveRecord::VERSION::STRING}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
33
|
+
end
|
34
|
+
|
30
35
|
extend Lockbox::Model
|
31
36
|
extend Lockbox::Model::Attached
|
37
|
+
# alias_method is private in Ruby < 2.5
|
38
|
+
singleton_class.send(:alias_method, :encrypts, :lockbox_encrypts) if ActiveRecord::VERSION::MAJOR < 7
|
32
39
|
ActiveRecord::Calculations.prepend Lockbox::Calculations
|
33
40
|
end
|
34
41
|
|
35
42
|
ActiveSupport.on_load(:mongoid) do
|
36
43
|
Mongoid::Document::ClassMethods.include(Lockbox::Model)
|
44
|
+
# alias_method is private in Ruby < 2.5
|
45
|
+
Mongoid::Document::ClassMethods.send(:alias_method, :encrypts, :lockbox_encrypts)
|
37
46
|
end
|
38
47
|
end
|
39
48
|
|
@@ -101,7 +110,7 @@ module Lockbox
|
|
101
110
|
|
102
111
|
def self.encrypts_action_text_body(**options)
|
103
112
|
ActiveSupport.on_load(:action_text_rich_text) do
|
104
|
-
ActionText::RichText.
|
113
|
+
ActionText::RichText.lockbox_encrypts :body, **options
|
105
114
|
end
|
106
115
|
end
|
107
116
|
end
|
@@ -89,6 +89,10 @@ module Lockbox
|
|
89
89
|
module CreateOne
|
90
90
|
def initialize(name, record, attachable)
|
91
91
|
# this won't encrypt existing blobs
|
92
|
+
# ideally we'd check metadata for the encrypted flag
|
93
|
+
# and disallow unencrypted blobs
|
94
|
+
# since they'll raise an error on decryption
|
95
|
+
# but earlier versions of Lockbox won't have it
|
92
96
|
attachable = Lockbox::Utils.encrypt_attachable(record, name, attachable) if Lockbox::Utils.encrypted?(record, name) && !attachable.is_a?(ActiveStorage::Blob)
|
93
97
|
super(name, record, attachable)
|
94
98
|
end
|
@@ -32,9 +32,17 @@ module Lockbox
|
|
32
32
|
read.bytesize
|
33
33
|
end
|
34
34
|
|
35
|
-
# based on CarrierWave::SanitizedFile#mime_magic_content_type
|
36
35
|
def content_type
|
37
|
-
|
36
|
+
if Gem::Version.new(CarrierWave::VERSION) >= Gem::Version.new("2.2.1")
|
37
|
+
# based on CarrierWave::SanitizedFile#marcel_magic_content_type
|
38
|
+
Marcel::Magic.by_magic(read).try(:type) || "invalid/invalid"
|
39
|
+
elsif CarrierWave::VERSION.to_i >= 2
|
40
|
+
# based on CarrierWave::SanitizedFile#mime_magic_content_type
|
41
|
+
MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
|
42
|
+
else
|
43
|
+
# uses filename
|
44
|
+
super
|
45
|
+
end
|
38
46
|
end
|
39
47
|
|
40
48
|
# disable processing since already processed
|
@@ -97,4 +105,11 @@ module Lockbox
|
|
97
105
|
end
|
98
106
|
end
|
99
107
|
|
108
|
+
if CarrierWave::VERSION.to_i > 2
|
109
|
+
raise "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
110
|
+
elsif CarrierWave::VERSION.to_i < 1
|
111
|
+
# TODO raise error in 0.7.0
|
112
|
+
warn "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
113
|
+
end
|
114
|
+
|
100
115
|
CarrierWave::Uploader::Base.extend(Lockbox::CarrierWaveExtensions)
|
data/lib/lockbox/model.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Lockbox
|
2
2
|
module Model
|
3
|
-
def
|
3
|
+
def lockbox_encrypts(*attributes, **options)
|
4
4
|
# support objects
|
5
5
|
# case options[:type]
|
6
6
|
# when Date
|
@@ -22,7 +22,8 @@ module Lockbox
|
|
22
22
|
# end
|
23
23
|
|
24
24
|
custom_type = options[:type].respond_to?(:serialize) && options[:type].respond_to?(:deserialize)
|
25
|
-
|
25
|
+
valid_types = [nil, :string, :boolean, :date, :datetime, :time, :integer, :float, :binary, :json, :hash, :array, :inet]
|
26
|
+
raise ArgumentError, "Unknown type: #{options[:type]}" unless custom_type || valid_types.include?(options[:type])
|
26
27
|
|
27
28
|
activerecord = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
|
28
29
|
raise ArgumentError, "Type not supported yet with Mongoid" if options[:type] && !activerecord
|
@@ -55,6 +56,15 @@ module Lockbox
|
|
55
56
|
decrypt_method_name = "decrypt_#{encrypted_attribute}"
|
56
57
|
|
57
58
|
class_eval do
|
59
|
+
# Lockbox uses custom inspect
|
60
|
+
# but this could be useful for other gems
|
61
|
+
if activerecord && ActiveRecord::VERSION::MAJOR >= 6
|
62
|
+
# only add virtual attribute
|
63
|
+
# need to use regexp since strings do partial matching
|
64
|
+
# also, need to use += instead of <<
|
65
|
+
self.filter_attributes += [/\A#{Regexp.escape(options[:attribute])}\z/]
|
66
|
+
end
|
67
|
+
|
58
68
|
@lockbox_attributes ||= {}
|
59
69
|
|
60
70
|
if @lockbox_attributes.empty?
|
@@ -79,15 +89,40 @@ module Lockbox
|
|
79
89
|
super(options)
|
80
90
|
end
|
81
91
|
|
82
|
-
#
|
92
|
+
# maintain order
|
93
|
+
# replace ciphertext attributes w/ virtual attributes (filtered)
|
83
94
|
def inspect
|
84
|
-
|
85
|
-
|
86
|
-
|
95
|
+
lockbox_attributes = {}
|
96
|
+
lockbox_encrypted_attributes = {}
|
97
|
+
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
98
|
+
lockbox_attributes[lockbox_attribute[:attribute]] = true
|
99
|
+
lockbox_encrypted_attributes[lockbox_attribute[:encrypted_attribute]] = lockbox_attribute[:attribute]
|
100
|
+
end
|
101
|
+
|
102
|
+
inspection = []
|
103
|
+
# use serializable_hash like Devise
|
104
|
+
values = serializable_hash
|
105
|
+
self.class.attribute_names.each do |k|
|
106
|
+
next if !has_attribute?(k) || lockbox_attributes[k]
|
107
|
+
|
108
|
+
# check for lockbox attribute
|
109
|
+
if lockbox_encrypted_attributes[k]
|
110
|
+
# check if ciphertext attribute nil to avoid loading attribute
|
111
|
+
v = send(k).nil? ? "nil" : "[FILTERED]"
|
112
|
+
k = lockbox_encrypted_attributes[k]
|
113
|
+
elsif values.key?(k)
|
114
|
+
v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect
|
115
|
+
|
116
|
+
# fix for https://github.com/rails/rails/issues/40725
|
117
|
+
# TODO only apply to Active Record 6.0
|
118
|
+
if respond_to?(:inspection_filter, true) && v != "nil"
|
119
|
+
v = inspection_filter.filter_param(k, v)
|
120
|
+
end
|
121
|
+
else
|
122
|
+
next
|
87
123
|
end
|
88
124
|
|
89
|
-
|
90
|
-
inspection << "#{lockbox_attribute[:attribute]}: [FILTERED]" if has_attribute?(lockbox_attribute[:encrypted_attribute])
|
125
|
+
inspection << "#{k}: #{v}"
|
91
126
|
end
|
92
127
|
|
93
128
|
"#<#{self.class} #{inspection.join(", ")}>"
|
@@ -114,16 +149,38 @@ module Lockbox
|
|
114
149
|
# needed for in-place modifications
|
115
150
|
# assigned attributes are encrypted on assignment
|
116
151
|
# and then again here
|
117
|
-
|
152
|
+
def lockbox_sync_attributes
|
118
153
|
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
119
154
|
attribute = lockbox_attribute[:attribute]
|
120
155
|
|
121
|
-
if attribute_changed_in_place?(attribute)
|
156
|
+
if attribute_changed_in_place?(attribute) || (send("#{attribute}_changed?") && !send("#{lockbox_attribute[:encrypted_attribute]}_changed?"))
|
122
157
|
send("#{attribute}=", send(attribute))
|
123
158
|
end
|
124
159
|
end
|
125
160
|
end
|
126
161
|
|
162
|
+
# safety check
|
163
|
+
[:_create_record, :_update_record].each do |method_name|
|
164
|
+
unless private_method_defined?(method_name) || method_defined?(method_name)
|
165
|
+
raise Lockbox::Error, "Expected #{method_name} to be defined. Please report an issue."
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def _create_record(*)
|
170
|
+
lockbox_sync_attributes
|
171
|
+
super
|
172
|
+
end
|
173
|
+
|
174
|
+
def _update_record(*)
|
175
|
+
lockbox_sync_attributes
|
176
|
+
super
|
177
|
+
end
|
178
|
+
|
179
|
+
def [](attr_name)
|
180
|
+
send(attr_name) if self.class.lockbox_attributes.any? { |_, la| la[:attribute] == attr_name.to_s }
|
181
|
+
super
|
182
|
+
end
|
183
|
+
|
127
184
|
def update_columns(attributes)
|
128
185
|
return super unless attributes.is_a?(Hash)
|
129
186
|
|
@@ -159,8 +216,11 @@ module Lockbox
|
|
159
216
|
attributes_to_set.each do |k, v|
|
160
217
|
if respond_to?(:write_attribute_without_type_cast, true)
|
161
218
|
write_attribute_without_type_cast(k, v)
|
162
|
-
|
219
|
+
elsif respond_to?(:raw_write_attribute, true)
|
163
220
|
raw_write_attribute(k, v)
|
221
|
+
else
|
222
|
+
@attributes.write_cast_value(k, v)
|
223
|
+
clear_attribute_change(k)
|
164
224
|
end
|
165
225
|
end
|
166
226
|
|
@@ -213,6 +273,23 @@ module Lockbox
|
|
213
273
|
else
|
214
274
|
attribute name, :string
|
215
275
|
end
|
276
|
+
else
|
277
|
+
# hack for Active Record 6.1
|
278
|
+
# to set string type after serialize
|
279
|
+
# otherwise, type gets set to ActiveModel::Type::Value
|
280
|
+
# which always returns false for changed_in_place?
|
281
|
+
# earlier versions of Active Record take the previous code path
|
282
|
+
if ActiveRecord::VERSION::STRING.to_f >= 7.0 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
|
283
|
+
attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil)
|
284
|
+
if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
|
285
|
+
attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
|
286
|
+
end
|
287
|
+
elsif ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
|
288
|
+
attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
|
289
|
+
if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
|
290
|
+
attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
|
291
|
+
end
|
292
|
+
end
|
216
293
|
end
|
217
294
|
|
218
295
|
define_method("#{name}_was") do
|
@@ -324,7 +401,11 @@ module Lockbox
|
|
324
401
|
# check for this explicitly as a layer of safety
|
325
402
|
if message.nil? || ((message == {} || message == []) && activerecord && @attributes[name.to_s].value_before_type_cast.nil?)
|
326
403
|
ciphertext = send(encrypted_attribute)
|
327
|
-
|
404
|
+
|
405
|
+
# keep original message for empty hashes and arrays
|
406
|
+
unless ciphertext.nil?
|
407
|
+
message = self.class.send(decrypt_method_name, ciphertext, context: self)
|
408
|
+
end
|
328
409
|
|
329
410
|
if activerecord
|
330
411
|
# set previous attribute so changes populate correctly
|
@@ -336,8 +417,13 @@ module Lockbox
|
|
336
417
|
# decrypt method does type casting
|
337
418
|
if respond_to?(:write_attribute_without_type_cast, true)
|
338
419
|
write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
|
339
|
-
|
420
|
+
elsif respond_to?(:raw_write_attribute, true)
|
340
421
|
raw_write_attribute(name, message) if !@attributes.frozen?
|
422
|
+
else
|
423
|
+
if !@attributes.frozen?
|
424
|
+
@attributes.write_cast_value(name.to_s, message)
|
425
|
+
clear_attribute_change(name)
|
426
|
+
end
|
341
427
|
end
|
342
428
|
else
|
343
429
|
instance_variable_set("@#{name}", message)
|
@@ -352,7 +438,7 @@ module Lockbox
|
|
352
438
|
table = activerecord ? table_name : collection_name.to_s
|
353
439
|
|
354
440
|
unless message.nil?
|
355
|
-
# TODO use attribute type class in 0.
|
441
|
+
# TODO use attribute type class in 0.7.0
|
356
442
|
case options[:type]
|
357
443
|
when :boolean
|
358
444
|
message = ActiveRecord::Type::Boolean.new.serialize(message)
|
@@ -380,6 +466,14 @@ module Lockbox
|
|
380
466
|
message = ActiveRecord::Type::Float.new.serialize(message)
|
381
467
|
# double precision, big endian
|
382
468
|
message = [message].pack("G") unless message.nil?
|
469
|
+
when :inet
|
470
|
+
unless message.nil?
|
471
|
+
ip = message.is_a?(IPAddr) ? message : (IPAddr.new(message) rescue nil)
|
472
|
+
# same format as Postgres, with ipv4 padded to 16 bytes
|
473
|
+
# family, netmask, ip
|
474
|
+
# return nil for invalid IP like Active Record
|
475
|
+
message = ip ? [ip.ipv4? ? 0 : 1, ip.prefix, ip.hton].pack("CCa16") : nil
|
476
|
+
end
|
383
477
|
when :string, :binary
|
384
478
|
# do nothing
|
385
479
|
# encrypt will convert to binary
|
@@ -407,7 +501,7 @@ module Lockbox
|
|
407
501
|
end
|
408
502
|
|
409
503
|
unless message.nil?
|
410
|
-
# TODO use attribute type class in 0.
|
504
|
+
# TODO use attribute type class in 0.7.0
|
411
505
|
case options[:type]
|
412
506
|
when :boolean
|
413
507
|
message = message == "t"
|
@@ -426,6 +520,11 @@ module Lockbox
|
|
426
520
|
when :binary
|
427
521
|
# do nothing
|
428
522
|
# decrypt returns binary string
|
523
|
+
when :inet
|
524
|
+
family, prefix, addr = message.unpack("CCa16")
|
525
|
+
len = family == 0 ? 4 : 16
|
526
|
+
message = IPAddr.new_ntoh(addr.first(len))
|
527
|
+
message.prefix = prefix
|
429
528
|
else
|
430
529
|
# use original name for serialized attributes
|
431
530
|
type = (try(:attribute_types) || {})[original_name.to_s]
|
data/lib/lockbox/railtie.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
module Lockbox
|
2
2
|
class Railtie < Rails::Railtie
|
3
3
|
initializer "lockbox" do |app|
|
4
|
+
if defined?(Rails.application.credentials)
|
5
|
+
# needs to work when lockbox key has a string value
|
6
|
+
Lockbox.master_key ||= Rails.application.credentials.try(:lockbox).try(:fetch, :master_key, nil)
|
7
|
+
end
|
8
|
+
|
4
9
|
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
5
10
|
|
6
11
|
if defined?(ActiveStorage)
|
@@ -14,7 +19,14 @@ module Lockbox
|
|
14
19
|
ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
|
15
20
|
|
16
21
|
# use load hooks when possible
|
17
|
-
if ActiveStorage::VERSION::MAJOR >=
|
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
|
18
30
|
ActiveSupport.on_load(:active_storage_attachment) do
|
19
31
|
include Lockbox::ActiveStorageExtensions::Attachment
|
20
32
|
end
|
data/lib/lockbox/utils.rb
CHANGED
@@ -93,8 +93,7 @@ module Lockbox
|
|
93
93
|
end
|
94
94
|
|
95
95
|
# don't analyze encrypted data
|
96
|
-
metadata = {"analyzed" => true}
|
97
|
-
metadata["encrypted"] = true if options[:migrating]
|
96
|
+
metadata = {"analyzed" => true, "encrypted" => true}
|
98
97
|
attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
|
99
98
|
end
|
100
99
|
|
data/lib/lockbox/version.rb
CHANGED
metadata
CHANGED
@@ -1,199 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lockbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: bundler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: carrierwave
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: combustion
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.3'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '1.3'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rails
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: minitest
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '5'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '5'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rake
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rbnacl
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '6'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '6'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: sqlite3
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: pg
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
139
|
-
- !ruby/object:Gem::Dependency
|
140
|
-
name: mysql2
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
142
|
-
requirements:
|
143
|
-
- - ">="
|
144
|
-
- !ruby/object:Gem::Version
|
145
|
-
version: '0'
|
146
|
-
type: :development
|
147
|
-
prerelease: false
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
149
|
-
requirements:
|
150
|
-
- - ">="
|
151
|
-
- !ruby/object:Gem::Version
|
152
|
-
version: '0'
|
153
|
-
- !ruby/object:Gem::Dependency
|
154
|
-
name: shrine
|
155
|
-
requirement: !ruby/object:Gem::Requirement
|
156
|
-
requirements:
|
157
|
-
- - ">="
|
158
|
-
- !ruby/object:Gem::Version
|
159
|
-
version: '0'
|
160
|
-
type: :development
|
161
|
-
prerelease: false
|
162
|
-
version_requirements: !ruby/object:Gem::Requirement
|
163
|
-
requirements:
|
164
|
-
- - ">="
|
165
|
-
- !ruby/object:Gem::Version
|
166
|
-
version: '0'
|
167
|
-
- !ruby/object:Gem::Dependency
|
168
|
-
name: shrine-mongoid
|
169
|
-
requirement: !ruby/object:Gem::Requirement
|
170
|
-
requirements:
|
171
|
-
- - ">="
|
172
|
-
- !ruby/object:Gem::Version
|
173
|
-
version: '0'
|
174
|
-
type: :development
|
175
|
-
prerelease: false
|
176
|
-
version_requirements: !ruby/object:Gem::Requirement
|
177
|
-
requirements:
|
178
|
-
- - ">="
|
179
|
-
- !ruby/object:Gem::Version
|
180
|
-
version: '0'
|
181
|
-
- !ruby/object:Gem::Dependency
|
182
|
-
name: benchmark-ips
|
183
|
-
requirement: !ruby/object:Gem::Requirement
|
184
|
-
requirements:
|
185
|
-
- - ">="
|
186
|
-
- !ruby/object:Gem::Version
|
187
|
-
version: '0'
|
188
|
-
type: :development
|
189
|
-
prerelease: false
|
190
|
-
version_requirements: !ruby/object:Gem::Requirement
|
191
|
-
requirements:
|
192
|
-
- - ">="
|
193
|
-
- !ruby/object:Gem::Version
|
194
|
-
version: '0'
|
11
|
+
date: 2021-04-06 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
195
13
|
description:
|
196
|
-
email: andrew@
|
14
|
+
email: andrew@ankane.org
|
197
15
|
executables: []
|
198
16
|
extensions: []
|
199
17
|
extra_rdoc_files: []
|
@@ -240,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
240
58
|
- !ruby/object:Gem::Version
|
241
59
|
version: '0'
|
242
60
|
requirements: []
|
243
|
-
rubygems_version: 3.
|
61
|
+
rubygems_version: 3.2.3
|
244
62
|
signing_key:
|
245
63
|
specification_version: 4
|
246
64
|
summary: Modern encryption for Ruby and Rails
|