lockbox 0.6.2 → 1.1.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 +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
|