lockbox 0.5.0 → 0.6.4

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