lockbox 0.6.2 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -1
- data/LICENSE.txt +1 -1
- data/README.md +69 -183
- data/lib/lockbox/calculations.rb +6 -1
- data/lib/lockbox/carrier_wave_extensions.rb +6 -4
- data/lib/lockbox/model.rb +125 -17
- data/lib/lockbox/railtie.rb +8 -1
- data/lib/lockbox/version.rb +1 -1
- data/lib/lockbox.rb +34 -27
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a8a7995008cdd49c48d95e0a968e45666276fb095de76954ef949800080d40b
|
4
|
+
data.tar.gz: da7d7796776c325ce1871a2755f6cc012c060f16293cbe28a31f2547c92ec0ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d97e45d14fbc7f452eef5e09c2b2d8d3b2c2f5d4b8a42645c0138849b21ef61194f7cf2e36158f4e920b7cc5709cef63a9769310f97cdd40c3f78790415100d0
|
7
|
+
data.tar.gz: f04c5264be1ec1e69c406593bdc143f10f846776a34744d38a368ba0e0d2662c874315b9387c5e7c53a47621cbabb2da146114008d1b5963dcc3e8b845a86ff2
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,48 @@
|
|
1
|
-
##
|
1
|
+
## 1.1.1 (2022-12-08)
|
2
|
+
|
3
|
+
- Fixed error when `StringIO` not loaded
|
4
|
+
|
5
|
+
## 1.1.0 (2022-10-09)
|
6
|
+
|
7
|
+
- Added support for `insert`, `insert_all`, `insert_all!`, `upsert`, and `upsert_all`
|
8
|
+
|
9
|
+
## 1.0.0 (2022-06-11)
|
10
|
+
|
11
|
+
- Deprecated `encrypts` in favor of `has_encrypted` to avoid conflicting with Active Record encryption
|
12
|
+
- Deprecated `lockbox_encrypts` in favor of `has_encrypted`
|
13
|
+
- Fixed error with `pluck`
|
14
|
+
- Restored warning for attributes with `default` option
|
15
|
+
- Dropped support for Active Record < 5.2 and Ruby < 2.6
|
16
|
+
|
17
|
+
## 0.6.8 (2022-01-25)
|
18
|
+
|
19
|
+
- Fixed issue with `encrypts` loading model schema early
|
20
|
+
- Removed warning for attributes with `default` option
|
21
|
+
|
22
|
+
## 0.6.7 (2022-01-25)
|
23
|
+
|
24
|
+
- Added warning for attributes with `default` option
|
25
|
+
- Removed warning for Active Record 5.0 (still supported)
|
26
|
+
|
27
|
+
## 0.6.6 (2021-09-27)
|
28
|
+
|
29
|
+
- Fixed `attribute?` method for `boolean` and `integer` types
|
30
|
+
|
31
|
+
## 0.6.5 (2021-07-07)
|
32
|
+
|
33
|
+
- Fixed issue with `pluck` extension not loading in some cases
|
34
|
+
|
35
|
+
## 0.6.4 (2021-04-05)
|
36
|
+
|
37
|
+
- Fixed in place changes in callbacks
|
38
|
+
- Fixed `[]` method for encrypted attributes
|
39
|
+
|
40
|
+
## 0.6.3 (2021-03-30)
|
41
|
+
|
42
|
+
- Fixed empty arrays and hashes
|
43
|
+
- Fixed content type for CarrierWave 2.2.1
|
44
|
+
|
45
|
+
## 0.6.2 (2021-02-08)
|
2
46
|
|
3
47
|
- Added `inet` type
|
4
48
|
- Fixed error when `lockbox` key in Rails credentials has a string value
|
@@ -7,6 +51,7 @@
|
|
7
51
|
## 0.6.1 (2020-12-03)
|
8
52
|
|
9
53
|
- Added integration with Rails credentials
|
54
|
+
- Added warning for unsupported versions of Active Record
|
10
55
|
- Fixed in place changes for Active Record 6.1
|
11
56
|
- Fixed error with `content_type` method for CarrierWave < 2
|
12
57
|
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -16,7 +16,7 @@ Learn [the principles behind it](https://ankane.org/modern-encryption-rails), [h
|
|
16
16
|
Add this line to your application’s Gemfile:
|
17
17
|
|
18
18
|
```ruby
|
19
|
-
gem
|
19
|
+
gem "lockbox"
|
20
20
|
```
|
21
21
|
|
22
22
|
## Key Generation
|
@@ -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[
|
75
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.0]
|
76
76
|
def change
|
77
77
|
add_column :users, :email_ciphertext, :text
|
78
78
|
end
|
@@ -83,7 +83,7 @@ Add to your model:
|
|
83
83
|
|
84
84
|
```ruby
|
85
85
|
class User < ApplicationRecord
|
86
|
-
|
86
|
+
has_encrypted :email
|
87
87
|
end
|
88
88
|
```
|
89
89
|
|
@@ -101,7 +101,7 @@ You can specify multiple fields in single line.
|
|
101
101
|
|
102
102
|
```ruby
|
103
103
|
class User < ApplicationRecord
|
104
|
-
|
104
|
+
has_encrypted :email, :phone, :city
|
105
105
|
end
|
106
106
|
```
|
107
107
|
|
@@ -111,17 +111,17 @@ Fields are strings by default. Specify the type of a field with:
|
|
111
111
|
|
112
112
|
```ruby
|
113
113
|
class User < ApplicationRecord
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
114
|
+
has_encrypted :birthday, type: :date
|
115
|
+
has_encrypted :signed_at, type: :datetime
|
116
|
+
has_encrypted :opens_at, type: :time
|
117
|
+
has_encrypted :active, type: :boolean
|
118
|
+
has_encrypted :salary, type: :integer
|
119
|
+
has_encrypted :latitude, type: :float
|
120
|
+
has_encrypted :video, type: :binary
|
121
|
+
has_encrypted :properties, type: :json
|
122
|
+
has_encrypted :settings, type: :hash
|
123
|
+
has_encrypted :messages, type: :array
|
124
|
+
has_encrypted :ip, type: :inet
|
125
125
|
end
|
126
126
|
```
|
127
127
|
|
@@ -135,7 +135,7 @@ class User < ApplicationRecord
|
|
135
135
|
store :settings, accessors: [:color, :homepage]
|
136
136
|
attribute :configuration, CustomType.new
|
137
137
|
|
138
|
-
|
138
|
+
has_encrypted :properties, :settings, :configuration
|
139
139
|
end
|
140
140
|
```
|
141
141
|
|
@@ -143,7 +143,7 @@ For [StoreModel](https://github.com/DmitryTsepelev/store_model), use:
|
|
143
143
|
|
144
144
|
```ruby
|
145
145
|
class User < ApplicationRecord
|
146
|
-
|
146
|
+
has_encrypted :configuration, type: Configuration.to_type
|
147
147
|
|
148
148
|
after_initialize do
|
149
149
|
self.configuration ||= {}
|
@@ -174,7 +174,7 @@ Add a new column for the ciphertext, then add to your model:
|
|
174
174
|
|
175
175
|
```ruby
|
176
176
|
class User < ApplicationRecord
|
177
|
-
|
177
|
+
has_encrypted :email, migrating: true
|
178
178
|
end
|
179
179
|
```
|
180
180
|
|
@@ -188,7 +188,7 @@ Then update the model to the desired state:
|
|
188
188
|
|
189
189
|
```ruby
|
190
190
|
class User < ApplicationRecord
|
191
|
-
|
191
|
+
has_encrypted :email
|
192
192
|
|
193
193
|
# remove this line after dropping email column
|
194
194
|
self.ignored_columns = ["email"]
|
@@ -248,7 +248,7 @@ User.decrypt_email_ciphertext(user.email_ciphertext)
|
|
248
248
|
Create a migration with:
|
249
249
|
|
250
250
|
```ruby
|
251
|
-
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[
|
251
|
+
class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[7.0]
|
252
252
|
def change
|
253
253
|
add_column :action_text_rich_texts, :body_ciphertext, :text
|
254
254
|
end
|
@@ -287,7 +287,7 @@ Add to your model:
|
|
287
287
|
class User
|
288
288
|
field :email_ciphertext, type: String
|
289
289
|
|
290
|
-
|
290
|
+
has_encrypted :email
|
291
291
|
end
|
292
292
|
```
|
293
293
|
|
@@ -336,9 +336,9 @@ def license
|
|
336
336
|
end
|
337
337
|
```
|
338
338
|
|
339
|
-
|
339
|
+
Use `filename` to specify a filename or `disposition: "inline"` to show inline.
|
340
340
|
|
341
|
-
|
341
|
+
#### Migrating Existing Files
|
342
342
|
|
343
343
|
Lockbox makes it easy to encrypt existing files without downtime.
|
344
344
|
|
@@ -379,7 +379,7 @@ Encryption is applied to all versions after processing.
|
|
379
379
|
You can mount the uploader [as normal](https://github.com/carrierwaveuploader/carrierwave#activerecord). With Active Record, this involves creating a migration:
|
380
380
|
|
381
381
|
```ruby
|
382
|
-
class AddLicenseToUsers < ActiveRecord::Migration[
|
382
|
+
class AddLicenseToUsers < ActiveRecord::Migration[7.0]
|
383
383
|
def change
|
384
384
|
add_column :users, :license, :string
|
385
385
|
end
|
@@ -403,6 +403,8 @@ def license
|
|
403
403
|
end
|
404
404
|
```
|
405
405
|
|
406
|
+
Use `filename` to specify a filename or `disposition: "inline"` to show inline.
|
407
|
+
|
406
408
|
#### Migrating Existing Files
|
407
409
|
|
408
410
|
Encrypt existing files without downtime. Create a new encrypted uploader:
|
@@ -478,6 +480,8 @@ def license
|
|
478
480
|
end
|
479
481
|
```
|
480
482
|
|
483
|
+
Use `filename` to specify a filename or `disposition: "inline"` to show inline.
|
484
|
+
|
481
485
|
#### Non-Models
|
482
486
|
|
483
487
|
Generate a key
|
@@ -568,12 +572,10 @@ Update your model:
|
|
568
572
|
|
569
573
|
```ruby
|
570
574
|
class User < ApplicationRecord
|
571
|
-
|
575
|
+
has_encrypted :email, previous_versions: [{master_key: previous_key}]
|
572
576
|
end
|
573
577
|
```
|
574
578
|
|
575
|
-
Use `master_key` instead of `key` if passing the master key.
|
576
|
-
|
577
579
|
To rotate existing records, use:
|
578
580
|
|
579
581
|
```ruby
|
@@ -587,11 +589,9 @@ Once all records are rotated, you can remove `previous_versions` from the model.
|
|
587
589
|
Update your initializer:
|
588
590
|
|
589
591
|
```ruby
|
590
|
-
Lockbox.encrypts_action_text_body(previous_versions: [{
|
592
|
+
Lockbox.encrypts_action_text_body(previous_versions: [{master_key: previous_key}])
|
591
593
|
```
|
592
594
|
|
593
|
-
Use `master_key` instead of `key` if passing the master key.
|
594
|
-
|
595
595
|
To rotate existing records, use:
|
596
596
|
|
597
597
|
```ruby
|
@@ -606,12 +606,10 @@ Update your model:
|
|
606
606
|
|
607
607
|
```ruby
|
608
608
|
class User < ApplicationRecord
|
609
|
-
encrypts_attached :license, previous_versions: [{
|
609
|
+
encrypts_attached :license, previous_versions: [{master_key: previous_key}]
|
610
610
|
end
|
611
611
|
```
|
612
612
|
|
613
|
-
Use `master_key` instead of `key` if passing the master key.
|
614
|
-
|
615
613
|
To rotate existing files, use:
|
616
614
|
|
617
615
|
```ruby
|
@@ -628,12 +626,10 @@ Update your model:
|
|
628
626
|
|
629
627
|
```ruby
|
630
628
|
class LicenseUploader < CarrierWave::Uploader::Base
|
631
|
-
encrypt previous_versions: [{
|
629
|
+
encrypt previous_versions: [{master_key: previous_key}]
|
632
630
|
end
|
633
631
|
```
|
634
632
|
|
635
|
-
Use `master_key` instead of `key` if passing the master key.
|
636
|
-
|
637
633
|
To rotate existing files, use:
|
638
634
|
|
639
635
|
```ruby
|
@@ -708,105 +704,45 @@ This is the default algorithm. It’s:
|
|
708
704
|
|
709
705
|
Lockbox uses 256-bit keys.
|
710
706
|
|
711
|
-
**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.
|
707
|
+
**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.
|
712
708
|
|
713
709
|
### XSalsa20
|
714
710
|
|
715
|
-
You can also use XSalsa20, which uses an extended nonce so you don’t have to worry about nonce collisions. First, [install Libsodium](https://github.com/crypto-rb/rbnacl/wiki/Installing-libsodium). For Homebrew, use:
|
711
|
+
You can also use XSalsa20, which uses an extended nonce so you don’t have to worry about nonce collisions. First, [install Libsodium](https://github.com/crypto-rb/rbnacl/wiki/Installing-libsodium). It comes preinstalled on [Heroku](https://devcenter.heroku.com/articles/stack-packages). For Homebrew, use:
|
716
712
|
|
717
713
|
```sh
|
718
714
|
brew install libsodium
|
719
715
|
```
|
720
716
|
|
721
|
-
And
|
717
|
+
And for Ubuntu, use:
|
718
|
+
|
719
|
+
```sh
|
720
|
+
sudo apt-get install libsodium23
|
721
|
+
```
|
722
|
+
|
723
|
+
Then add to your Gemfile:
|
722
724
|
|
723
725
|
```ruby
|
724
|
-
gem
|
726
|
+
gem "rbnacl"
|
725
727
|
```
|
726
728
|
|
727
|
-
|
729
|
+
And add to your model:
|
728
730
|
|
729
731
|
|
730
732
|
```ruby
|
731
733
|
class User < ApplicationRecord
|
732
|
-
|
734
|
+
has_encrypted :email, algorithm: "xsalsa20"
|
733
735
|
end
|
734
736
|
```
|
735
737
|
|
736
738
|
Make it the default with:
|
737
739
|
|
738
740
|
```ruby
|
739
|
-
Lockbox.default_options =
|
741
|
+
Lockbox.default_options[:algorithm] = "xsalsa20"
|
740
742
|
```
|
741
743
|
|
742
744
|
You can also pass an algorithm to `previous_versions` for key rotation.
|
743
745
|
|
744
|
-
#### XSalsa20 Deployment
|
745
|
-
|
746
|
-
##### Heroku
|
747
|
-
|
748
|
-
Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packages) preinstalled.
|
749
|
-
|
750
|
-
##### Ubuntu
|
751
|
-
|
752
|
-
For Ubuntu 20.04 and 18.04, use:
|
753
|
-
|
754
|
-
```sh
|
755
|
-
sudo apt-get install libsodium23
|
756
|
-
```
|
757
|
-
|
758
|
-
For Ubuntu 16.04, use:
|
759
|
-
|
760
|
-
```sh
|
761
|
-
sudo apt-get install libsodium18
|
762
|
-
```
|
763
|
-
|
764
|
-
##### GitHub Actions
|
765
|
-
|
766
|
-
For Ubuntu 20.04 and 18.04, use:
|
767
|
-
|
768
|
-
```yml
|
769
|
-
- name: Install Libsodium
|
770
|
-
run: sudo apt-get update && sudo apt-get install libsodium23
|
771
|
-
```
|
772
|
-
|
773
|
-
For Ubuntu 16.04, use:
|
774
|
-
|
775
|
-
```yml
|
776
|
-
- name: Install Libsodium
|
777
|
-
run: sudo apt-get update && sudo apt-get install libsodium18
|
778
|
-
```
|
779
|
-
|
780
|
-
##### Travis CI
|
781
|
-
|
782
|
-
On Bionic, add to `.travis.yml`:
|
783
|
-
|
784
|
-
```yml
|
785
|
-
addons:
|
786
|
-
apt:
|
787
|
-
packages:
|
788
|
-
- libsodium23
|
789
|
-
```
|
790
|
-
|
791
|
-
On Xenial, add to `.travis.yml`:
|
792
|
-
|
793
|
-
```yml
|
794
|
-
addons:
|
795
|
-
apt:
|
796
|
-
packages:
|
797
|
-
- libsodium18
|
798
|
-
```
|
799
|
-
|
800
|
-
##### CircleCI
|
801
|
-
|
802
|
-
Add a step to `.circleci/config.yml`:
|
803
|
-
|
804
|
-
```yml
|
805
|
-
- run:
|
806
|
-
name: install Libsodium
|
807
|
-
command: sudo apt-get install -y libsodium18
|
808
|
-
```
|
809
|
-
|
810
746
|
## Hybrid Cryptography
|
811
747
|
|
812
748
|
[Hybrid cryptography](https://en.wikipedia.org/wiki/Hybrid_cryptosystem) allows servers to encrypt data without being able to decrypt it.
|
@@ -823,7 +759,7 @@ Store the keys with your other secrets. Then use:
|
|
823
759
|
|
824
760
|
```ruby
|
825
761
|
class User < ApplicationRecord
|
826
|
-
|
762
|
+
has_encrypted :email, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
|
827
763
|
end
|
828
764
|
```
|
829
765
|
|
@@ -853,7 +789,7 @@ To rename a table with encrypted columns/uploaders, use:
|
|
853
789
|
|
854
790
|
```ruby
|
855
791
|
class User < ApplicationRecord
|
856
|
-
|
792
|
+
has_encrypted :email, key_table: "original_table"
|
857
793
|
end
|
858
794
|
```
|
859
795
|
|
@@ -861,7 +797,7 @@ To rename an encrypted column itself, use:
|
|
861
797
|
|
862
798
|
```ruby
|
863
799
|
class User < ApplicationRecord
|
864
|
-
|
800
|
+
has_encrypted :email, key_attribute: "original_column"
|
865
801
|
end
|
866
802
|
```
|
867
803
|
|
@@ -871,7 +807,7 @@ To set a key for an individual field/uploader, use a string:
|
|
871
807
|
|
872
808
|
```ruby
|
873
809
|
class User < ApplicationRecord
|
874
|
-
|
810
|
+
has_encrypted :email, key: ENV["USER_EMAIL_ENCRYPTION_KEY"]
|
875
811
|
end
|
876
812
|
```
|
877
813
|
|
@@ -879,7 +815,7 @@ Or a proc:
|
|
879
815
|
|
880
816
|
```ruby
|
881
817
|
class User < ApplicationRecord
|
882
|
-
|
818
|
+
has_encrypted :email, key: -> { code }
|
883
819
|
end
|
884
820
|
```
|
885
821
|
|
@@ -889,7 +825,7 @@ To use a different key for each record, use a symbol:
|
|
889
825
|
|
890
826
|
```ruby
|
891
827
|
class User < ApplicationRecord
|
892
|
-
|
828
|
+
has_encrypted :email, key: :some_method
|
893
829
|
end
|
894
830
|
```
|
895
831
|
|
@@ -897,7 +833,7 @@ Or a proc:
|
|
897
833
|
|
898
834
|
```ruby
|
899
835
|
class User < ApplicationRecord
|
900
|
-
|
836
|
+
has_encrypted :email, key: -> { some_method }
|
901
837
|
end
|
902
838
|
```
|
903
839
|
|
@@ -909,7 +845,7 @@ For Active Record and Mongoid, use:
|
|
909
845
|
|
910
846
|
```ruby
|
911
847
|
class User < ApplicationRecord
|
912
|
-
|
848
|
+
has_encrypted :email, key: :kms_key
|
913
849
|
end
|
914
850
|
```
|
915
851
|
|
@@ -997,7 +933,7 @@ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
|
|
997
933
|
You can use `binary` columns for the ciphertext instead of `text` columns.
|
998
934
|
|
999
935
|
```ruby
|
1000
|
-
class AddEmailCiphertextToUsers < ActiveRecord::Migration[
|
936
|
+
class AddEmailCiphertextToUsers < ActiveRecord::Migration[7.0]
|
1001
937
|
def change
|
1002
938
|
add_column :users, :email_ciphertext, :binary
|
1003
939
|
end
|
@@ -1008,7 +944,7 @@ Disable Base64 encoding to save space.
|
|
1008
944
|
|
1009
945
|
```ruby
|
1010
946
|
class User < ApplicationRecord
|
1011
|
-
|
947
|
+
has_encrypted :email, encode: false
|
1012
948
|
end
|
1013
949
|
```
|
1014
950
|
|
@@ -1042,7 +978,7 @@ end
|
|
1042
978
|
Create a migration with:
|
1043
979
|
|
1044
980
|
```ruby
|
1045
|
-
class MigrateToLockbox < ActiveRecord::Migration[
|
981
|
+
class MigrateToLockbox < ActiveRecord::Migration[7.0]
|
1046
982
|
def change
|
1047
983
|
add_column :users, :name_ciphertext, :text
|
1048
984
|
add_column :users, :email_ciphertext, :text
|
@@ -1050,11 +986,11 @@ class MigrateToLockbox < ActiveRecord::Migration[6.0]
|
|
1050
986
|
end
|
1051
987
|
```
|
1052
988
|
|
1053
|
-
And add `
|
989
|
+
And add `has_encrypted` to your model with the `migrating` option:
|
1054
990
|
|
1055
991
|
```ruby
|
1056
992
|
class User < ApplicationRecord
|
1057
|
-
|
993
|
+
has_encrypted :name, :email, migrating: true
|
1058
994
|
end
|
1059
995
|
```
|
1060
996
|
|
@@ -1068,14 +1004,14 @@ Once all records are migrated, remove the `migrating` option and the previous mo
|
|
1068
1004
|
|
1069
1005
|
```ruby
|
1070
1006
|
class User < ApplicationRecord
|
1071
|
-
|
1007
|
+
has_encrypted :name, :email
|
1072
1008
|
end
|
1073
1009
|
```
|
1074
1010
|
|
1075
1011
|
Then remove the previous gem from your Gemfile and drop its columns.
|
1076
1012
|
|
1077
1013
|
```ruby
|
1078
|
-
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[
|
1014
|
+
class RemovePreviousEncryptedColumns < ActiveRecord::Migration[7.0]
|
1079
1015
|
def change
|
1080
1016
|
remove_column :users, :encrypted_name, :text
|
1081
1017
|
remove_column :users, :encrypted_name_iv, :text
|
@@ -1087,81 +1023,31 @@ end
|
|
1087
1023
|
|
1088
1024
|
## Upgrading
|
1089
1025
|
|
1090
|
-
### 0.
|
1026
|
+
### 1.0.0
|
1091
1027
|
|
1092
|
-
|
1028
|
+
`encrypts` is now deprecated in favor of `has_encrypted` to avoid conflicting with Active Record encryption.
|
1093
1029
|
|
1094
1030
|
```ruby
|
1095
|
-
User
|
1096
|
-
|
1097
|
-
|
1098
|
-
metadata = user.license.metadata
|
1099
|
-
unless metadata["encrypted"]
|
1100
|
-
user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
|
1101
|
-
end
|
1031
|
+
class User < ApplicationRecord
|
1032
|
+
has_encrypted :email
|
1102
1033
|
end
|
1103
1034
|
```
|
1104
1035
|
|
1105
|
-
### 0.
|
1036
|
+
### 0.6.0
|
1106
1037
|
|
1107
|
-
0.
|
1038
|
+
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:
|
1108
1039
|
|
1109
1040
|
```ruby
|
1110
1041
|
User.with_attached_license.find_each do |user|
|
1111
1042
|
next unless user.license.attached?
|
1112
1043
|
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
license.update!(content_type: content_type)
|
1044
|
+
metadata = user.license.metadata
|
1045
|
+
unless metadata["encrypted"]
|
1046
|
+
user.license.blob.update!(metadata: metadata.merge("encrypted" => true))
|
1117
1047
|
end
|
1118
1048
|
end
|
1119
1049
|
```
|
1120
1050
|
|
1121
|
-
### 0.2.0
|
1122
|
-
|
1123
|
-
0.2.0 brings a number of improvements. Here are a few to be aware of:
|
1124
|
-
|
1125
|
-
- Added `encrypts` method for database fields
|
1126
|
-
- Added support for XSalsa20
|
1127
|
-
- `attached_encrypted` is deprecated in favor of `encrypts_attached`.
|
1128
|
-
|
1129
|
-
#### Optional
|
1130
|
-
|
1131
|
-
To switch to a master key, generate a key:
|
1132
|
-
|
1133
|
-
```ruby
|
1134
|
-
Lockbox.generate_key
|
1135
|
-
```
|
1136
|
-
|
1137
|
-
And set `ENV["LOCKBOX_MASTER_KEY"]` or `Lockbox.master_key`.
|
1138
|
-
|
1139
|
-
Update your model:
|
1140
|
-
|
1141
|
-
```ruby
|
1142
|
-
class User < ApplicationRecord
|
1143
|
-
encrypts_attached :license, previous_versions: [{key: key}]
|
1144
|
-
end
|
1145
|
-
```
|
1146
|
-
|
1147
|
-
New uploads will be encrypted with the new key.
|
1148
|
-
|
1149
|
-
You can rotate existing records with:
|
1150
|
-
|
1151
|
-
```ruby
|
1152
|
-
User.unscoped.find_each do |user|
|
1153
|
-
user.license.rotate_encryption!
|
1154
|
-
end
|
1155
|
-
```
|
1156
|
-
|
1157
|
-
Once that’s complete, update your model:
|
1158
|
-
|
1159
|
-
```ruby
|
1160
|
-
class User < ApplicationRecord
|
1161
|
-
encrypts_attached :license
|
1162
|
-
end
|
1163
|
-
```
|
1164
|
-
|
1165
1051
|
## History
|
1166
1052
|
|
1167
1053
|
View the [changelog](https://github.com/ankane/lockbox/blob/master/CHANGELOG.md)
|
data/lib/lockbox/calculations.rb
CHANGED
@@ -3,7 +3,12 @@ module Lockbox
|
|
3
3
|
def pluck(*column_names)
|
4
4
|
return super unless model.respond_to?(:lockbox_attributes)
|
5
5
|
|
6
|
-
lockbox_columns = column_names.map.with_index
|
6
|
+
lockbox_columns = column_names.map.with_index do |c, i|
|
7
|
+
next unless c.respond_to?(:to_sym)
|
8
|
+
[model.lockbox_attributes[c.to_sym], i]
|
9
|
+
end.select do |la, _i|
|
10
|
+
la && !la[:migrating]
|
11
|
+
end
|
7
12
|
|
8
13
|
return super unless lockbox_columns.any?
|
9
14
|
|
@@ -33,7 +33,10 @@ module Lockbox
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def content_type
|
36
|
-
if CarrierWave::VERSION
|
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
|
37
40
|
# based on CarrierWave::SanitizedFile#mime_magic_content_type
|
38
41
|
MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
|
39
42
|
else
|
@@ -103,10 +106,9 @@ module Lockbox
|
|
103
106
|
end
|
104
107
|
|
105
108
|
if CarrierWave::VERSION.to_i > 2
|
106
|
-
raise "CarrierWave
|
109
|
+
raise Lockbox::Error, "CarrierWave #{CarrierWave::VERSION} not supported in this version of Lockbox"
|
107
110
|
elsif CarrierWave::VERSION.to_i < 1
|
108
|
-
|
109
|
-
warn "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
|
111
|
+
raise Lockbox::Error, "CarrierWave #{CarrierWave::VERSION} not supported"
|
110
112
|
end
|
111
113
|
|
112
114
|
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 has_encrypted(*attributes, **options)
|
4
4
|
# support objects
|
5
5
|
# case options[:type]
|
6
6
|
# when Date
|
@@ -149,16 +149,38 @@ module Lockbox
|
|
149
149
|
# needed for in-place modifications
|
150
150
|
# assigned attributes are encrypted on assignment
|
151
151
|
# and then again here
|
152
|
-
|
152
|
+
def lockbox_sync_attributes
|
153
153
|
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
154
154
|
attribute = lockbox_attribute[:attribute]
|
155
155
|
|
156
|
-
if attribute_changed_in_place?(attribute)
|
156
|
+
if attribute_changed_in_place?(attribute) || (send("#{attribute}_changed?") && !send("#{lockbox_attribute[:encrypted_attribute]}_changed?"))
|
157
157
|
send("#{attribute}=", send(attribute))
|
158
158
|
end
|
159
159
|
end
|
160
160
|
end
|
161
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
|
+
|
162
184
|
def update_columns(attributes)
|
163
185
|
return super unless attributes.is_a?(Hash)
|
164
186
|
|
@@ -194,13 +216,62 @@ module Lockbox
|
|
194
216
|
attributes_to_set.each do |k, v|
|
195
217
|
if respond_to?(:write_attribute_without_type_cast, true)
|
196
218
|
write_attribute_without_type_cast(k, v)
|
197
|
-
|
219
|
+
elsif respond_to?(:raw_write_attribute, true)
|
198
220
|
raw_write_attribute(k, v)
|
221
|
+
else
|
222
|
+
@attributes.write_cast_value(k, v)
|
223
|
+
clear_attribute_change(k)
|
199
224
|
end
|
200
225
|
end
|
201
226
|
|
202
227
|
result
|
203
228
|
end
|
229
|
+
|
230
|
+
if ActiveRecord::VERSION::MAJOR >= 6
|
231
|
+
def self.insert_all(attributes, **options)
|
232
|
+
super(lockbox_map_attributes(attributes), **options)
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.insert_all!(attributes, **options)
|
236
|
+
super(lockbox_map_attributes(attributes), **options)
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.upsert_all(attributes, **options)
|
240
|
+
super(lockbox_map_attributes(attributes, check_readonly: true), **options)
|
241
|
+
end
|
242
|
+
|
243
|
+
# private
|
244
|
+
# does not try to handle :returning option for simplicity
|
245
|
+
def self.lockbox_map_attributes(records, check_readonly: false)
|
246
|
+
return records unless records.is_a?(Array)
|
247
|
+
|
248
|
+
records.map do |attributes|
|
249
|
+
# transform keys like Active Record
|
250
|
+
attributes = attributes.transform_keys do |key|
|
251
|
+
n = key.to_s
|
252
|
+
attribute_aliases[n] || n
|
253
|
+
end
|
254
|
+
|
255
|
+
lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
|
256
|
+
lockbox_attributes.each do |key, lockbox_attribute|
|
257
|
+
attribute = key.to_s
|
258
|
+
# check read only
|
259
|
+
# users should mark both plaintext and ciphertext columns
|
260
|
+
if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s)
|
261
|
+
warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}"
|
262
|
+
end
|
263
|
+
|
264
|
+
message = attributes[attribute]
|
265
|
+
attributes.delete(attribute) unless lockbox_attribute[:migrating]
|
266
|
+
encrypted_attribute = lockbox_attribute[:encrypted_attribute]
|
267
|
+
ciphertext = send("generate_#{encrypted_attribute}", message)
|
268
|
+
attributes[encrypted_attribute] = ciphertext
|
269
|
+
end
|
270
|
+
|
271
|
+
attributes
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
204
275
|
else
|
205
276
|
def reload
|
206
277
|
self.class.lockbox_attributes.each do |_, v|
|
@@ -216,6 +287,23 @@ module Lockbox
|
|
216
287
|
@lockbox_attributes[original_name] = options
|
217
288
|
|
218
289
|
if activerecord
|
290
|
+
# warn on default attributes
|
291
|
+
if attributes_to_define_after_schema_loads.key?(name.to_s)
|
292
|
+
opt = attributes_to_define_after_schema_loads[name.to_s][1]
|
293
|
+
|
294
|
+
has_default =
|
295
|
+
if ActiveRecord::VERSION::MAJOR >= 7
|
296
|
+
# not ideal, since NO_DEFAULT_PROVIDED is private
|
297
|
+
opt != ActiveRecord::Attributes::ClassMethods.const_get(:NO_DEFAULT_PROVIDED)
|
298
|
+
else
|
299
|
+
opt.is_a?(Hash) && opt.key?(:default)
|
300
|
+
end
|
301
|
+
|
302
|
+
if has_default
|
303
|
+
warn "[lockbox] WARNING: attributes with `:default` option are not supported. Use `after_initialize` instead."
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
219
307
|
# preference:
|
220
308
|
# 1. type option
|
221
309
|
# 2. existing virtual attribute
|
@@ -254,7 +342,12 @@ module Lockbox
|
|
254
342
|
# otherwise, type gets set to ActiveModel::Type::Value
|
255
343
|
# which always returns false for changed_in_place?
|
256
344
|
# earlier versions of Active Record take the previous code path
|
257
|
-
if ActiveRecord::VERSION::STRING.to_f >=
|
345
|
+
if ActiveRecord::VERSION::STRING.to_f >= 7.0 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
|
346
|
+
attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil)
|
347
|
+
if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
|
348
|
+
attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
|
349
|
+
end
|
350
|
+
elsif ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
|
258
351
|
attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
|
259
352
|
if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
|
260
353
|
attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
|
@@ -273,11 +366,14 @@ module Lockbox
|
|
273
366
|
send("restore_#{encrypted_attribute}!")
|
274
367
|
end
|
275
368
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
369
|
+
define_method("#{name}_in_database") do
|
370
|
+
send(name) # writes attribute when not already set
|
371
|
+
super()
|
372
|
+
end
|
373
|
+
|
374
|
+
define_method("#{name}?") do
|
375
|
+
# uses public_send, so we don't need to preload attribute
|
376
|
+
query_attribute(name)
|
281
377
|
end
|
282
378
|
else
|
283
379
|
# keep this module dead simple
|
@@ -318,10 +414,10 @@ module Lockbox
|
|
318
414
|
send("reset_#{encrypted_attribute}_to_default!")
|
319
415
|
send(name)
|
320
416
|
end
|
321
|
-
end
|
322
417
|
|
323
|
-
|
324
|
-
|
418
|
+
define_method("#{name}?") do
|
419
|
+
send("#{encrypted_attribute}?")
|
420
|
+
end
|
325
421
|
end
|
326
422
|
|
327
423
|
define_method("#{name}=") do |message|
|
@@ -371,7 +467,11 @@ module Lockbox
|
|
371
467
|
# check for this explicitly as a layer of safety
|
372
468
|
if message.nil? || ((message == {} || message == []) && activerecord && @attributes[name.to_s].value_before_type_cast.nil?)
|
373
469
|
ciphertext = send(encrypted_attribute)
|
374
|
-
|
470
|
+
|
471
|
+
# keep original message for empty hashes and arrays
|
472
|
+
unless ciphertext.nil?
|
473
|
+
message = self.class.send(decrypt_method_name, ciphertext, context: self)
|
474
|
+
end
|
375
475
|
|
376
476
|
if activerecord
|
377
477
|
# set previous attribute so changes populate correctly
|
@@ -383,8 +483,13 @@ module Lockbox
|
|
383
483
|
# decrypt method does type casting
|
384
484
|
if respond_to?(:write_attribute_without_type_cast, true)
|
385
485
|
write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
|
386
|
-
|
486
|
+
elsif respond_to?(:raw_write_attribute, true)
|
387
487
|
raw_write_attribute(name, message) if !@attributes.frozen?
|
488
|
+
else
|
489
|
+
if !@attributes.frozen?
|
490
|
+
@attributes.write_cast_value(name.to_s, message)
|
491
|
+
clear_attribute_change(name)
|
492
|
+
end
|
388
493
|
end
|
389
494
|
else
|
390
495
|
instance_variable_set("@#{name}", message)
|
@@ -399,7 +504,6 @@ module Lockbox
|
|
399
504
|
table = activerecord ? table_name : collection_name.to_s
|
400
505
|
|
401
506
|
unless message.nil?
|
402
|
-
# TODO use attribute type class in 0.7.0
|
403
507
|
case options[:type]
|
404
508
|
when :boolean
|
405
509
|
message = ActiveRecord::Type::Boolean.new.serialize(message)
|
@@ -462,7 +566,6 @@ module Lockbox
|
|
462
566
|
end
|
463
567
|
|
464
568
|
unless message.nil?
|
465
|
-
# TODO use attribute type class in 0.7.0
|
466
569
|
case options[:type]
|
467
570
|
when :boolean
|
468
571
|
message = message == "t"
|
@@ -520,6 +623,11 @@ module Lockbox
|
|
520
623
|
end
|
521
624
|
end
|
522
625
|
|
626
|
+
def lockbox_encrypts(*attributes, **options)
|
627
|
+
ActiveSupport::Deprecation.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`")
|
628
|
+
has_encrypted(*attributes, **options)
|
629
|
+
end
|
630
|
+
|
523
631
|
module Attached
|
524
632
|
def encrypts_attached(*attributes, **options)
|
525
633
|
attributes.each do |name|
|
data/lib/lockbox/railtie.rb
CHANGED
@@ -19,7 +19,14 @@ module Lockbox
|
|
19
19
|
ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
|
20
20
|
|
21
21
|
# use load hooks when possible
|
22
|
-
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
|
23
30
|
ActiveSupport.on_load(:active_storage_attachment) do
|
24
31
|
include Lockbox::ActiveStorageExtensions::Attachment
|
25
32
|
end
|
data/lib/lockbox/version.rb
CHANGED
data/lib/lockbox.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require "base64"
|
3
3
|
require "openssl"
|
4
4
|
require "securerandom"
|
5
|
+
require "stringio"
|
5
6
|
|
6
7
|
# modules
|
7
8
|
require "lockbox/aes_gcm"
|
@@ -16,32 +17,6 @@ require "lockbox/padding"
|
|
16
17
|
require "lockbox/utils"
|
17
18
|
require "lockbox/version"
|
18
19
|
|
19
|
-
# integrations
|
20
|
-
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
21
|
-
require "lockbox/railtie" if defined?(Rails)
|
22
|
-
|
23
|
-
if defined?(ActiveSupport::LogSubscriber)
|
24
|
-
require "lockbox/log_subscriber"
|
25
|
-
Lockbox::LogSubscriber.attach_to :lockbox
|
26
|
-
end
|
27
|
-
|
28
|
-
if defined?(ActiveSupport.on_load)
|
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
|
-
|
35
|
-
extend Lockbox::Model
|
36
|
-
extend Lockbox::Model::Attached
|
37
|
-
ActiveRecord::Calculations.prepend Lockbox::Calculations
|
38
|
-
end
|
39
|
-
|
40
|
-
ActiveSupport.on_load(:mongoid) do
|
41
|
-
Mongoid::Document::ClassMethods.include(Lockbox::Model)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
20
|
module Lockbox
|
46
21
|
class Error < StandardError; end
|
47
22
|
class DecryptionError < Error; end
|
@@ -106,7 +81,39 @@ module Lockbox
|
|
106
81
|
|
107
82
|
def self.encrypts_action_text_body(**options)
|
108
83
|
ActiveSupport.on_load(:action_text_rich_text) do
|
109
|
-
ActionText::RichText.
|
84
|
+
ActionText::RichText.has_encrypted :body, **options
|
110
85
|
end
|
111
86
|
end
|
112
87
|
end
|
88
|
+
|
89
|
+
# integrations
|
90
|
+
require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
|
91
|
+
require "lockbox/railtie" if defined?(Rails)
|
92
|
+
|
93
|
+
if defined?(ActiveSupport::LogSubscriber)
|
94
|
+
require "lockbox/log_subscriber"
|
95
|
+
Lockbox::LogSubscriber.attach_to :lockbox
|
96
|
+
end
|
97
|
+
|
98
|
+
if defined?(ActiveSupport.on_load)
|
99
|
+
ActiveSupport.on_load(:active_record) do
|
100
|
+
ar_version = ActiveRecord::VERSION::STRING.to_f
|
101
|
+
if ar_version < 5.2
|
102
|
+
if ar_version >= 5
|
103
|
+
raise Lockbox::Error, "Active Record #{ActiveRecord::VERSION::STRING} requires Lockbox < 0.7"
|
104
|
+
else
|
105
|
+
raise Lockbox::Error, "Active Record #{ActiveRecord::VERSION::STRING} not supported"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
extend Lockbox::Model
|
110
|
+
extend Lockbox::Model::Attached
|
111
|
+
singleton_class.alias_method(:encrypts, :lockbox_encrypts) if ActiveRecord::VERSION::MAJOR < 7
|
112
|
+
ActiveRecord::Relation.prepend Lockbox::Calculations
|
113
|
+
end
|
114
|
+
|
115
|
+
ActiveSupport.on_load(:mongoid) do
|
116
|
+
Mongoid::Document::ClassMethods.include(Lockbox::Model)
|
117
|
+
Mongoid::Document::ClassMethods.alias_method(:encrypts, :lockbox_encrypts)
|
118
|
+
end
|
119
|
+
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:
|
4
|
+
version: 1.1.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: 2022-12-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: andrew@ankane.org
|
@@ -51,14 +51,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '2.
|
54
|
+
version: '2.6'
|
55
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
56
|
requirements:
|
57
57
|
- - ">="
|
58
58
|
- !ruby/object:Gem::Version
|
59
59
|
version: '0'
|
60
60
|
requirements: []
|
61
|
-
rubygems_version: 3.
|
61
|
+
rubygems_version: 3.3.7
|
62
62
|
signing_key:
|
63
63
|
specification_version: 4
|
64
64
|
summary: Modern encryption for Ruby and Rails
|