lockbox 0.3.7 → 0.4.4

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: 72291291d3b8ac532d4c309bde141febf41d40b5228ce4d4b2541248702fe08d
4
- data.tar.gz: d184e486e64cf5f0ecc882cf453fa76b18ce69b03f34d93335da7b1f70c12a01
3
+ metadata.gz: 6b1676e34ca9367ec4cfd3c655d263d23082243196abe8f0a174318383f7d4b1
4
+ data.tar.gz: cdda6ac4e9dccc4ab48a67e3628212fdde369a99639cc93e569d0d623f5ab8f3
5
5
  SHA512:
6
- metadata.gz: 4badf3561effc1d381ae5ddfcdd3ce64a5bfb70e586c140df4795a392d36e5440a3d3930921f14732b349c2f93246a6f4ddfbe62ef508c2a2ad503b5f52d28f8
7
- data.tar.gz: 12e48eab11c6f1aefc7a1ea8656e8d1e0cdceebe54147e573e1beb13f635149d99a5bfea0402595a8a6d53403bb498b2f404bb3a70a684908bc87decbd39c365
6
+ metadata.gz: 9e747bb720312daedcb2db5458d919cb50e554b03ee04d219a3b1c2f40ca917815a066dfd767b075046f29e387d5d48a734c193a3a2c48927b5abf6e0daaa099
7
+ data.tar.gz: 2a456f05d61ecc75dfdc76aca15df194b58f91d701a28d12d3b27cc40f93f301244e4f60d506f7deae7a1b6a53afab087205aaced37a54af75f81810934ed8af
@@ -1,3 +1,29 @@
1
+ ## 0.4.4 (2020-06-23)
2
+
3
+ - Added support for `pluck`
4
+
5
+ ## 0.4.3 (2020-05-26)
6
+
7
+ - Improved error message for bad key length
8
+ - Fixed missing attribute error
9
+
10
+ ## 0.4.2 (2020-05-11)
11
+
12
+ - Added experimental support for migrating Active Storage files
13
+ - Fixed `metadata` support for Active Storage
14
+
15
+ ## 0.4.1 (2020-05-08)
16
+
17
+ - Added support for Action Text
18
+ - Added warning if unencrypted column exists and not migrating
19
+
20
+ ## 0.4.0 (2020-05-03)
21
+
22
+ - Load encrypted attributes when `attributes` called
23
+ - Added support for migrating and rotating relations
24
+ - Removed deprecated `attached_encrypted` method
25
+ - Removed legacy `attr_encrypted` encryptor
26
+
1
27
  ## 0.3.7 (2020-04-20)
2
28
 
3
29
  - Added Active Support notifications for Active Storage and Carrierwave
data/README.md CHANGED
@@ -47,6 +47,7 @@ Then follow the instructions below for the data you want to encrypt.
47
47
  #### Database Fields
48
48
 
49
49
  - [Active Record](#active-record)
50
+ - [Action Text](#action-text)
50
51
  - [Mongoid](#mongoid)
51
52
 
52
53
  #### Files
@@ -150,7 +151,9 @@ Be sure to include the `inspect` at the end or it won’t be encoded properly in
150
151
 
151
152
  #### Migrating Existing Data
152
153
 
153
- Lockbox makes it easy to encrypt an existing column. Add a new column for the ciphertext, then add to your model:
154
+ Lockbox makes it easy to encrypt an existing column without downtime.
155
+
156
+ Add a new column for the ciphertext, then add to your model:
154
157
 
155
158
  ```ruby
156
159
  class User < ApplicationRecord
@@ -185,6 +188,38 @@ class User < ApplicationRecord
185
188
  end
186
189
  ```
187
190
 
191
+ ## Action Text
192
+
193
+ Create a migration with:
194
+
195
+ ```ruby
196
+ class AddBodyCiphertextToRichTexts < ActiveRecord::Migration[6.0]
197
+ def change
198
+ add_column :action_text_rich_texts, :body_ciphertext, :text
199
+ end
200
+ end
201
+ ```
202
+
203
+ Create `config/initializers/lockbox.rb` with:
204
+
205
+ ```ruby
206
+ Lockbox.encrypts_action_text_body(migrating: true)
207
+ ```
208
+
209
+ Migrate existing data:
210
+
211
+ ```ruby
212
+ Lockbox.migrate(ActionText::RichText)
213
+ ```
214
+
215
+ Update the initializer:
216
+
217
+ ```ruby
218
+ Lockbox.encrypts_action_text_body
219
+ ```
220
+
221
+ And drop the unencrypted column.
222
+
188
223
  ## Mongoid
189
224
 
190
225
  Add to your model:
@@ -205,6 +240,8 @@ User.create!(email: "hi@example.org")
205
240
 
206
241
  If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
207
242
 
243
+ You can [migrate existing data](#migrating-existing-data) similarly to Active Record.
244
+
208
245
  ## Active Storage
209
246
 
210
247
  Add to your model:
@@ -239,6 +276,34 @@ def license
239
276
  end
240
277
  ```
241
278
 
279
+ #### Migrating Existing Files [experimental]
280
+
281
+ **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.
282
+
283
+ Lockbox makes it easy to encrypt existing files without downtime.
284
+
285
+ Add to your model:
286
+
287
+ ```ruby
288
+ class User < ApplicationRecord
289
+ encrypts_attached :license, migrating: true
290
+ end
291
+ ```
292
+
293
+ Migrate existing files:
294
+
295
+ ```ruby
296
+ Lockbox.migrate(User)
297
+ ```
298
+
299
+ Then update the model to the desired state:
300
+
301
+ ```ruby
302
+ class User < ApplicationRecord
303
+ encrypts_attached :license
304
+ end
305
+ ```
306
+
242
307
  ## CarrierWave
243
308
 
244
309
  Add to your uploader:
@@ -278,6 +343,51 @@ def license
278
343
  end
279
344
  ```
280
345
 
346
+ #### Migrating Existing Files
347
+
348
+ Encrypt existing files without downtime. Create a new encrypted uploader:
349
+
350
+ ```ruby
351
+ class LicenseV2Uploader < CarrierWave::Uploader::Base
352
+ encrypt key: Lockbox.attribute_key(table: "users", attribute: "license")
353
+ end
354
+ ```
355
+
356
+ Add a new column for the uploader, then add to your model:
357
+
358
+ ```ruby
359
+ class User < ApplicationRecord
360
+ mount_uploader :license_v2, LicenseV2Uploader
361
+
362
+ before_save :migrate_license, if: :license_changed?
363
+
364
+ def migrate_license
365
+ self.license_v2 = license
366
+ end
367
+ end
368
+ ```
369
+
370
+ Migrate existing files:
371
+
372
+ ```ruby
373
+ User.find_each do |user|
374
+ if user.license? && !user.license_v2?
375
+ user.migrate_license
376
+ user.save!
377
+ end
378
+ end
379
+ ```
380
+
381
+ Then update the model to the desired state:
382
+
383
+ ```ruby
384
+ class User < ApplicationRecord
385
+ mount_uploader :license, LicenseV2Uploader, mount_on: :license_v2
386
+ end
387
+ ```
388
+
389
+ Finally, delete the unencrypted files and drop the column for the original uploader. You can also remove the `key` option from the uploader.
390
+
281
391
  ## Shrine
282
392
 
283
393
  Generate a key
@@ -378,7 +488,7 @@ Use `decrypt_str` get the value as UTF-8
378
488
 
379
489
  To make key rotation easy, you can pass previous versions of keys that can decrypt.
380
490
 
381
- ### Active Record
491
+ ### Active Record & Mongoid
382
492
 
383
493
  Update your model:
384
494
 
@@ -398,26 +508,6 @@ Lockbox.rotate(User, attributes: [:email])
398
508
 
399
509
  Once all records are rotated, you can remove `previous_versions` from the model.
400
510
 
401
- ### Mongoid
402
-
403
- Update your model:
404
-
405
- ```ruby
406
- class User
407
- encrypts :email, previous_versions: [{key: previous_key}]
408
- end
409
- ```
410
-
411
- Use `master_key` instead of `key` if passing the master key.
412
-
413
- To rotate existing records, use:
414
-
415
- ```ruby
416
- Lockbox.rotate(User, attributes: [:email])
417
- ```
418
-
419
- Once all records are rotated, you can remove `previous_versions` from the model.
420
-
421
511
  ### Active Storage
422
512
 
423
513
  Update your model:
@@ -433,7 +523,7 @@ Use `master_key` instead of `key` if passing the master key.
433
523
  To rotate existing files, use:
434
524
 
435
525
  ```ruby
436
- User.find_each do |user|
526
+ User.with_attached_license.find_each do |user|
437
527
  user.license.rotate_encryption!
438
528
  end
439
529
  ```
@@ -462,9 +552,9 @@ end
462
552
 
463
553
  Once all files are rotated, you can remove `previous_versions` from the model.
464
554
 
465
- ### Strings
555
+ ### Local Files & Strings
466
556
 
467
- For strings, use:
557
+ For local files and strings, use:
468
558
 
469
559
  ```ruby
470
560
  Lockbox.new(key: key, previous_versions: [{key: previous_key}])
@@ -557,7 +647,7 @@ Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packag
557
647
 
558
648
  ##### Ubuntu
559
649
 
560
- For Ubuntu 18.04, use:
650
+ For Ubuntu 20.04 and 18.04, use:
561
651
 
562
652
  ```sh
563
653
  sudo apt-get install libsodium23
@@ -875,3 +965,5 @@ cd lockbox
875
965
  bundle install
876
966
  bundle exec rake test
877
967
  ```
968
+
969
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -5,6 +5,7 @@ require "securerandom"
5
5
 
6
6
  # modules
7
7
  require "lockbox/box"
8
+ require "lockbox/calculations"
8
9
  require "lockbox/encryptor"
9
10
  require "lockbox/key_generator"
10
11
  require "lockbox/io"
@@ -25,12 +26,11 @@ if defined?(ActiveSupport)
25
26
  ActiveSupport.on_load(:active_record) do
26
27
  extend Lockbox::Model
27
28
  extend Lockbox::Model::Attached
29
+ ActiveRecord::Calculations.prepend Lockbox::Calculations
28
30
  end
29
31
 
30
32
  ActiveSupport.on_load(:mongoid) do
31
33
  Mongoid::Document::ClassMethods.include(Lockbox::Model)
32
- # TODO remove in 0.4.0
33
- Mongoid::Document::ClassMethods.include(Lockbox::Model::Attached)
34
34
  end
35
35
  end
36
36
 
@@ -95,4 +95,10 @@ module Lockbox
95
95
  def self.new(**options)
96
96
  Encryptor.new(**options)
97
97
  end
98
+
99
+ def self.encrypts_action_text_body(**options)
100
+ ActiveSupport.on_load(:action_text_rich_text) do
101
+ ActionText::RichText.encrypts :body, **options
102
+ end
103
+ end
98
104
  end
@@ -16,14 +16,6 @@ module Lockbox
16
16
  def encrypt_attachable(attachable)
17
17
  Utils.encrypt_attachable(record, name, attachable)
18
18
  end
19
-
20
- def rebuild_attachable(attachment)
21
- {
22
- io: StringIO.new(attachment.download),
23
- filename: attachment.filename,
24
- content_type: attachment.content_type
25
- }
26
- end
27
19
  end
28
20
 
29
21
  module AttachedOne
@@ -37,7 +29,7 @@ module Lockbox
37
29
  def rotate_encryption!
38
30
  raise "Not encrypted" unless encrypted?
39
31
 
40
- attach(rebuild_attachable(self)) if attached?
32
+ attach(Utils.rebuild_attachable(self)) if attached?
41
33
 
42
34
  true
43
35
  end
@@ -65,7 +57,7 @@ module Lockbox
65
57
 
66
58
  attachables =
67
59
  previous_attachments.map do |attachment|
68
- rebuild_attachable(attachment)
60
+ Utils.rebuild_attachable(attachment)
69
61
  end
70
62
 
71
63
  ActiveStorage::Attachment.transaction do
@@ -88,13 +80,15 @@ module Lockbox
88
80
  end
89
81
 
90
82
  module Attachment
91
- extend ActiveSupport::Concern
92
-
93
83
  def download
94
84
  result = super
95
85
 
96
86
  options = Utils.encrypted_options(record, name)
97
- if options
87
+ # only trust the metadata when migrating
88
+ # as earlier versions of Lockbox won't have it
89
+ # and it's not a good practice to trust modifiable data
90
+ encrypted = options && (!options[:migrating] || blob.metadata["encrypted"])
91
+ if encrypted
98
92
  result = Utils.decrypt_result(record, name, options, result)
99
93
  end
100
94
 
@@ -105,7 +99,11 @@ module Lockbox
105
99
  def open(**options)
106
100
  blob.open(**options) do |file|
107
101
  options = Utils.encrypted_options(record, name)
108
- if options
102
+ # only trust the metadata when migrating
103
+ # as earlier versions of Lockbox won't have it
104
+ # and it's not a good practice to trust modifiable data
105
+ encrypted = options && (!options[:migrating] || blob.metadata["encrypted"])
106
+ if encrypted
109
107
  result = Utils.decrypt_result(record, name, options, file.read)
110
108
  file.rewind
111
109
  # truncate may not be available on all platforms
@@ -120,16 +118,6 @@ module Lockbox
120
118
  end
121
119
  end
122
120
  end
123
-
124
- def mark_analyzed
125
- if Utils.encrypted_options(record, name)
126
- blob.update!(metadata: blob.metadata.merge(analyzed: true))
127
- end
128
- end
129
-
130
- included do
131
- after_save :mark_analyzed
132
- end
133
121
  end
134
122
 
135
123
  module Blob
@@ -0,0 +1,36 @@
1
+ module Lockbox
2
+ module Calculations
3
+ def pluck(*column_names)
4
+ return super unless model.respond_to?(:lockbox_attributes)
5
+
6
+ lockbox_columns = column_names.map.with_index { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select(&:first)
7
+ return super unless lockbox_columns.any?
8
+
9
+ # replace column with ciphertext column
10
+ lockbox_columns.each do |la, i|
11
+ column_names[i] = la[:encrypted_attribute]
12
+ end
13
+
14
+ # pluck
15
+ result = super(*column_names)
16
+
17
+ # decrypt result
18
+ # handle pluck to single columns and multiple
19
+ #
20
+ # we can't pass context to decrypt method
21
+ # so this won't work if any options are a symbol or proc
22
+ if column_names.size == 1
23
+ la = lockbox_columns.first.first
24
+ result.map! { |v| model.send("decrypt_#{la[:encrypted_attribute]}", v) }
25
+ else
26
+ lockbox_columns.each do |la, i|
27
+ result.each do |v|
28
+ v[i] = model.send("decrypt_#{la[:encrypted_attribute]}", v[i])
29
+ end
30
+ end
31
+ end
32
+
33
+ result
34
+ end
35
+ end
36
+ end
@@ -5,13 +5,13 @@ module Lockbox
5
5
  before :cache, :encrypt
6
6
 
7
7
  def encrypt(file)
8
- @file = CarrierWave::SanitizedFile.new(with_notification("encrypt_file") { lockbox.encrypt_io(file) })
8
+ @file = CarrierWave::SanitizedFile.new(lockbox_notify("encrypt_file") { lockbox.encrypt_io(file) })
9
9
  end
10
10
 
11
11
  # TODO safe to memoize?
12
12
  def read
13
13
  r = super
14
- with_notification("decrypt_file") { lockbox.decrypt(r) } if r
14
+ lockbox_notify("decrypt_file") { lockbox.decrypt(r) } if r
15
15
  end
16
16
 
17
17
  def size
@@ -58,7 +58,7 @@ module Lockbox
58
58
  end
59
59
  end
60
60
 
61
- def with_notification(type)
61
+ def lockbox_notify(type)
62
62
  if defined?(ActiveSupport::Notifications)
63
63
  name = lockbox_name
64
64
 
@@ -82,25 +82,5 @@ module Lockbox
82
82
  target.content_type = source.content_type if source.respond_to?(:content_type)
83
83
  target.set_encoding(source.external_encoding) if source.respond_to?(:external_encoding)
84
84
  end
85
-
86
- # TODO remove in 0.4.0
87
- # legacy for attr_encrypted
88
- def self.encrypt(options)
89
- box(options).encrypt(options[:value])
90
- end
91
-
92
- # TODO remove in 0.4.0
93
- # legacy for attr_encrypted
94
- def self.decrypt(options)
95
- box(options).decrypt(options[:value])
96
- end
97
-
98
- # TODO remove in 0.4.0
99
- # legacy for attr_encrypted
100
- def self.box(options)
101
- options = options.slice(:key, :encryption_key, :decryption_key, :algorithm, :previous_versions)
102
- options[:algorithm] = "aes-gcm" if options[:algorithm] == "aes-256-gcm"
103
- Lockbox.new(options)
104
- end
105
85
  end
106
86
  end
@@ -11,7 +11,7 @@ module Lockbox
11
11
  raise ArgumentError, "Missing attribute for key generation" if attribute.to_s.empty?
12
12
 
13
13
  c = "\xB4"*32
14
- hkdf(Lockbox::Utils.decode_key(@master_key), salt: table.to_s, info: "#{c}#{attribute}", length: 32, hash: "sha384")
14
+ hkdf(Lockbox::Utils.decode_key(@master_key, name: "Master key"), salt: table.to_s, info: "#{c}#{attribute}", length: 32, hash: "sha384")
15
15
  end
16
16
 
17
17
  private
@@ -24,29 +24,49 @@ module Lockbox
24
24
 
25
25
  # TODO add attributes option
26
26
  def migrate(restart:)
27
- fields = model.lockbox_attributes.select { |k, v| v[:migrating] }
27
+ fields = model.respond_to?(:lockbox_attributes) ? model.lockbox_attributes.select { |k, v| v[:migrating] } : {}
28
28
 
29
29
  # need blind indexes for building relation
30
30
  blind_indexes = model.respond_to?(:blind_indexes) ? model.blind_indexes.select { |k, v| v[:migrating] } : {}
31
31
 
32
- perform(fields: fields, blind_indexes: blind_indexes, restart: restart)
32
+ attachments = model.respond_to?(:lockbox_attachments) ? model.lockbox_attachments.select { |k, v| v[:migrating] } : {}
33
+
34
+ perform(fields: fields, blind_indexes: blind_indexes, restart: restart) if fields.any? || blind_indexes.any?
35
+ perform_attachments(attachments: attachments, restart: restart) if attachments.any?
33
36
  end
34
37
 
35
38
  private
36
39
 
37
- def perform(fields:, blind_indexes: [], restart: true, rotate: false)
38
- relation = @relation
40
+ def perform_attachments(attachments:, restart:)
41
+ relation = base_relation
39
42
 
40
- # unscope if passed a model
41
- unless ar_relation?(relation) || mongoid_relation?(relation)
42
- relation = relation.unscoped
43
- else
44
- # TODO remove in 0.4.0
45
- relation = relation.unscoped
43
+ # eager load attachments
44
+ attachments.each_key do |k|
45
+ relation = relation.send("with_attached_#{k}")
46
46
  end
47
47
 
48
- # convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
49
- relation = relation.all
48
+ each_batch(relation) do |records|
49
+ records.each do |record|
50
+ attachments.each_key do |k|
51
+ attachment = record.send(k)
52
+ if attachment.attached?
53
+ if attachment.is_a?(ActiveStorage::Attached::One)
54
+ unless attachment.metadata["encrypted"]
55
+ attachment.rotate_encryption!
56
+ end
57
+ else
58
+ unless attachment.all? { |a| a.metadata["encrypted"] }
59
+ attachment.rotate_encryption!
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def perform(fields:, blind_indexes: [], restart: true, rotate: false)
69
+ relation = base_relation
50
70
 
51
71
  unless restart
52
72
  attributes = fields.map { |_, v| v[:encrypted_attribute] }
@@ -141,6 +161,18 @@ module Lockbox
141
161
  end
142
162
  end
143
163
 
164
+ def base_relation
165
+ relation = @relation
166
+
167
+ # unscope if passed a model
168
+ unless ar_relation?(relation) || mongoid_relation?(relation)
169
+ relation = relation.unscoped
170
+ end
171
+
172
+ # convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
173
+ relation.all
174
+ end
175
+
144
176
  def ar_relation?(relation)
145
177
  defined?(ActiveRecord::Relation) && relation.is_a?(ActiveRecord::Relation)
146
178
  end
@@ -81,6 +81,20 @@ module Lockbox
81
81
  end
82
82
 
83
83
  if activerecord
84
+ # TODO wrap in module?
85
+ def attributes
86
+ # load attributes
87
+ # essentially a no-op if already loaded
88
+ # an exception is thrown if decryption fails
89
+ self.class.lockbox_attributes.each do |_, lockbox_attribute|
90
+ # it is possible that the encrypted attribute is not loaded, eg.
91
+ # if the record was fetched partially (`User.select(:id).first`).
92
+ # accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
93
+ send(lockbox_attribute[:attribute]) if has_attribute?(lockbox_attribute[:encrypted_attribute])
94
+ end
95
+ super
96
+ end
97
+
84
98
  # needed for in-place modifications
85
99
  # assigned attributes are encrypted on assignment
86
100
  # and then again here
@@ -212,6 +226,20 @@ module Lockbox
212
226
 
213
227
  send("lockbox_direct_#{name}=", message)
214
228
 
229
+ # warn every time, as this should be addressed
230
+ # maybe throw an error in the future
231
+ if !options[:migrating]
232
+ if activerecord
233
+ if self.class.columns_hash.key?(name.to_s)
234
+ warn "[lockbox] WARNING: Unencrypted column with same name: #{name}. Set `ignored_columns` or remove it to protect the data."
235
+ end
236
+ else
237
+ if self.class.fields.key?(name.to_s)
238
+ warn "[lockbox] WARNING: Unencrypted field with same name: #{name}. Remove it to protect the data."
239
+ end
240
+ end
241
+ end
242
+
215
243
  super(message)
216
244
  end
217
245
 
@@ -242,7 +270,7 @@ module Lockbox
242
270
  # cache
243
271
  # decrypt method does type casting
244
272
  if respond_to?(:write_attribute_without_type_cast, true)
245
- write_attribute_without_type_cast(name, message) if !@attributes.frozen?
273
+ write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
246
274
  else
247
275
  raw_write_attribute(name, message) if !@attributes.frozen?
248
276
  end
@@ -259,7 +287,7 @@ module Lockbox
259
287
  table = activerecord ? table_name : collection_name.to_s
260
288
 
261
289
  unless message.nil?
262
- # TODO use attribute type class in 0.4.0
290
+ # TODO use attribute type class in 0.5.0
263
291
  case options[:type]
264
292
  when :boolean
265
293
  message = ActiveRecord::Type::Boolean.new.serialize(message)
@@ -313,7 +341,7 @@ module Lockbox
313
341
  end
314
342
 
315
343
  unless message.nil?
316
- # TODO use attribute type class in 0.4.0
344
+ # TODO use attribute type class in 0.5.0
317
345
  case options[:type]
318
346
  when :boolean
319
347
  message = message == "t"
@@ -391,12 +419,6 @@ module Lockbox
391
419
  end
392
420
  end
393
421
  end
394
-
395
- # TODO remove in future version
396
- def attached_encrypted(attribute, **options)
397
- warn "[lockbox] DEPRECATION WARNING: Use encrypts_attached instead"
398
- encrypts_attached(attribute, **options)
399
- end
400
422
  end
401
423
  end
402
424
  end
@@ -5,18 +5,27 @@ module Lockbox
5
5
 
6
6
  if defined?(ActiveStorage)
7
7
  require "lockbox/active_storage_extensions"
8
+
8
9
  ActiveStorage::Attached.prepend(Lockbox::ActiveStorageExtensions::Attached)
9
10
  if ActiveStorage::VERSION::MAJOR >= 6
10
11
  ActiveStorage::Attached::Changes::CreateOne.prepend(Lockbox::ActiveStorageExtensions::CreateOne)
11
12
  end
12
13
  ActiveStorage::Attached::One.prepend(Lockbox::ActiveStorageExtensions::AttachedOne)
13
14
  ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
14
- end
15
15
 
16
- app.config.to_prepare do
17
- if defined?(ActiveStorage)
18
- ActiveStorage::Attachment.include(Lockbox::ActiveStorageExtensions::Attachment)
19
- ActiveStorage::Blob.prepend(Lockbox::ActiveStorageExtensions::Blob)
16
+ # use load hooks when possible
17
+ if ActiveStorage::VERSION::MAJOR >= 6
18
+ ActiveSupport.on_load(:active_storage_attachment) do
19
+ include Lockbox::ActiveStorageExtensions::Attachment
20
+ end
21
+ ActiveSupport.on_load(:active_storage_blob) do
22
+ prepend Lockbox::ActiveStorageExtensions::Blob
23
+ end
24
+ else
25
+ app.config.to_prepare do
26
+ ActiveStorage::Attachment.include(Lockbox::ActiveStorageExtensions::Attachment)
27
+ ActiveStorage::Blob.prepend(Lockbox::ActiveStorageExtensions::Blob)
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -4,9 +4,13 @@ module Lockbox
4
4
  options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type)
5
5
  options[:encode] = false unless options.key?(:encode)
6
6
  options.each do |k, v|
7
- if v.is_a?(Proc)
8
- options[k] = context.instance_exec(&v) if v.respond_to?(:call)
7
+ if v.respond_to?(:call)
8
+ # context not present for pluck
9
+ # still possible to use if not dependent on context
10
+ options[k] = context ? context.instance_exec(&v) : v.call
9
11
  elsif v.is_a?(Symbol)
12
+ # context not present for pluck
13
+ raise Error, "Not available since :#{k} depends on record" unless context
10
14
  options[k] = context.send(v)
11
15
  end
12
16
  end
@@ -31,13 +35,13 @@ module Lockbox
31
35
  record.class.respond_to?(:lockbox_attachments) ? record.class.lockbox_attachments[name.to_sym] : nil
32
36
  end
33
37
 
34
- def self.decode_key(key, size: 32)
38
+ def self.decode_key(key, size: 32, name: "Key")
35
39
  if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{#{size * 2}}\z/i
36
40
  key = [key].pack("H*")
37
41
  end
38
42
 
39
- raise Lockbox::Error, "Key must use binary encoding" if key.encoding != Encoding::BINARY
40
- raise Lockbox::Error, "Key must be 32 bytes" if key.bytesize != size
43
+ raise Lockbox::Error, "#{name} must be 32 bytes (64 hex digits)" if key.bytesize != size
44
+ raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
41
45
 
42
46
  key
43
47
  end
@@ -63,14 +67,17 @@ module Lockbox
63
67
  }
64
68
  when Hash
65
69
  io = attachable[:io]
66
- attachable = {
67
- io: box.encrypt_io(io),
68
- filename: attachable[:filename],
69
- content_type: attachable[:content_type]
70
- }
70
+ attachable = attachable.dup
71
+ attachable[:io] = box.encrypt_io(io)
71
72
  else
72
- raise NotImplementedError, "Not supported"
73
+ # TODO raise ArgumentError
74
+ raise NotImplementedError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
73
75
  end
76
+
77
+ # don't analyze encrypted data
78
+ metadata = {"analyzed" => true}
79
+ metadata["encrypted"] = true if options[:migrating]
80
+ attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
74
81
  end
75
82
 
76
83
  # set content type based on unencrypted data
@@ -85,5 +92,13 @@ module Lockbox
85
92
  Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
86
93
  end
87
94
  end
95
+
96
+ def self.rebuild_attachable(attachment)
97
+ {
98
+ io: StringIO.new(attachment.download),
99
+ filename: attachment.filename,
100
+ content_type: attachment.content_type
101
+ }
102
+ end
88
103
  end
89
104
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.3.7"
2
+ VERSION = "0.4.4"
3
3
  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.3.7
4
+ version: 0.4.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-04-20 00:00:00.000000000 Z
11
+ date: 2020-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -173,6 +173,7 @@ files:
173
173
  - CHANGELOG.md
174
174
  - LICENSE.txt
175
175
  - README.md
176
+ - SECURITY.md
176
177
  - lib/generators/lockbox/audits_generator.rb
177
178
  - lib/generators/lockbox/templates/migration.rb.tt
178
179
  - lib/generators/lockbox/templates/model.rb.tt
@@ -180,6 +181,7 @@ files:
180
181
  - lib/lockbox/active_storage_extensions.rb
181
182
  - lib/lockbox/aes_gcm.rb
182
183
  - lib/lockbox/box.rb
184
+ - lib/lockbox/calculations.rb
183
185
  - lib/lockbox/carrier_wave_extensions.rb
184
186
  - lib/lockbox/encryptor.rb
185
187
  - lib/lockbox/io.rb