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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ba37bc916c02e18555640f29e47483898a96e04b75639b49ce9a0003fbaa443
4
- data.tar.gz: 750a53ca3201e51b6dc0221305d4c021d64716a163a2e0cfc98c11ec8d1b6af8
3
+ metadata.gz: 1a8a7995008cdd49c48d95e0a968e45666276fb095de76954ef949800080d40b
4
+ data.tar.gz: da7d7796776c325ce1871a2755f6cc012c060f16293cbe28a31f2547c92ec0ad
5
5
  SHA512:
6
- metadata.gz: b4e23752c311bf6b161e6817ab204ba0b5936594db5c2509c3f5ef6e354c169097a1c799d7b5147daa0d68982f072d1d22363019c6881566403517f97a5f2c45
7
- data.tar.gz: 51ab913facdc34aea3e3ef263dab2fa5c3852aa052de80804ec29019c2517df9244eefb8c15e6f2c1ca0059bfd4f9cc020ce00fff543874f6461faeec1e78443
6
+ metadata.gz: d97e45d14fbc7f452eef5e09c2b2d8d3b2c2f5d4b8a42645c0138849b21ef61194f7cf2e36158f4e920b7cc5709cef63a9769310f97cdd40c3f78790415100d0
7
+ data.tar.gz: f04c5264be1ec1e69c406593bdc143f10f846776a34744d38a368ba0e0d2662c874315b9387c5e7c53a47621cbabb2da146114008d1b5963dcc3e8b845a86ff2
data/CHANGELOG.md CHANGED
@@ -1,4 +1,48 @@
1
- ## 0.6.2 (2020-02-08)
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
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018-2021 Andrew Kane
3
+ Copyright (c) 2018-2022 Andrew Kane
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 'lockbox'
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[6.0]
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
- encrypts :email
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
- encrypts :email, :phone, :city
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
- encrypts :born_on, type: :date
115
- encrypts :signed_at, type: :datetime
116
- encrypts :opens_at, type: :time
117
- encrypts :active, type: :boolean
118
- encrypts :salary, type: :integer
119
- encrypts :latitude, type: :float
120
- encrypts :video, type: :binary
121
- encrypts :properties, type: :json
122
- encrypts :settings, type: :hash
123
- encrypts :messages, type: :array
124
- encrypts :ip, type: :inet
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
- encrypts :properties, :settings, :configuration
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
- encrypts :configuration, type: Configuration.to_type
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
- encrypts :email, migrating: true
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
- encrypts :email
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[6.0]
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
- encrypts :email
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
- #### Migrating Existing Files [experimental]
339
+ Use `filename` to specify a filename or `disposition: "inline"` to show inline.
340
340
 
341
- **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.
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[6.0]
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
- encrypts :email, previous_versions: [{key: previous_key}]
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: [{key: previous_key}])
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: [{key: previous_key}]
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: [{key: previous_key}]
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 add to your Gemfile:
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 'rbnacl'
726
+ gem "rbnacl"
725
727
  ```
726
728
 
727
- Then add to your model:
729
+ And add to your model:
728
730
 
729
731
 
730
732
  ```ruby
731
733
  class User < ApplicationRecord
732
- encrypts :email, algorithm: "xsalsa20"
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 = {algorithm: "xsalsa20"}
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
- encrypts :email, algorithm: "hybrid", encryption_key: encryption_key, decryption_key: decryption_key
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
- encrypts :email, key_table: "original_table"
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
- encrypts :email, key_attribute: "original_column"
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
- encrypts :email, key: ENV["USER_EMAIL_ENCRYPTION_KEY"]
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
- encrypts :email, key: -> { code }
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
- encrypts :email, key: :some_method
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
- encrypts :email, key: -> { some_method }
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
- encrypts :email, key: :kms_key
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[6.0]
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
- encrypts :email, encode: false
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[6.0]
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 `encrypts` to your model with the `migrating` option:
989
+ And add `has_encrypted` to your model with the `migrating` option:
1054
990
 
1055
991
  ```ruby
1056
992
  class User < ApplicationRecord
1057
- encrypts :name, :email, migrating: true
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
- encrypts :name, :email
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[6.0]
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.6.0
1026
+ ### 1.0.0
1091
1027
 
1092
- 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:
1028
+ `encrypts` is now deprecated in favor of `has_encrypted` to avoid conflicting with Active Record encryption.
1093
1029
 
1094
1030
  ```ruby
1095
- User.with_attached_license.find_each do |user|
1096
- next unless user.license.attached?
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.3.6
1036
+ ### 0.6.0
1106
1037
 
1107
- 0.3.6 makes content type detection more reliable for Active Storage. You can check and update the content type of existing files with:
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
- license = user.license
1114
- content_type = Marcel::MimeType.for(license.download, name: license.filename.to_s)
1115
- if content_type != license.content_type
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)
@@ -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 { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select { |la, _i| la && !la[:migrating] }
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.to_i >= 2
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 version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
109
+ raise Lockbox::Error, "CarrierWave #{CarrierWave::VERSION} not supported in this version of Lockbox"
107
110
  elsif CarrierWave::VERSION.to_i < 1
108
- # TODO raise error in 0.7.0
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 encrypts(*attributes, **options)
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
- before_save do
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
- else
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 >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
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
- if ActiveRecord::VERSION::STRING >= "5.1"
277
- define_method("#{name}_in_database") do
278
- send(name) # writes attribute when not already set
279
- super()
280
- end
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
- define_method("#{name}?") do
324
- send("#{encrypted_attribute}?")
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
- message = self.class.send(decrypt_method_name, ciphertext, context: self)
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
- else
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|
@@ -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 >= 6
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
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.6.2"
2
+ VERSION = "1.1.1"
3
3
  end
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.encrypts :body, **options
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: 0.6.2
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: 2021-02-08 00:00:00.000000000 Z
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.4'
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.2.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