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 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