lockbox 0.3.6 → 0.4.3

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: 9d220724fb1331a7a9fdda8fdd073de62c19f601b2d15173a5fe22046e51071d
4
- data.tar.gz: 9f144deb1211bd79d49f5eb2dccee29b25a2ea8869aadadad92ebda825012c00
3
+ metadata.gz: c072d4c6e5935ff9e176c0fc705c0e36228da4b1fc530a7eaf0249db57c71ddc
4
+ data.tar.gz: a0d293cb80ea7050deeccd039b5e0be01d51c09577181c1d9bf9df8a520764ac
5
5
  SHA512:
6
- metadata.gz: 31492e71cafb170205944089f047269302249a5ba03f23533d302b45f45fc770ee221eb56af50f47c39ac44d980f8886ff0aa3cd50ed9dcc018638859ac06ace
7
- data.tar.gz: 69faba8f29dca9c7b85c3e5d7905db625d96105b2ae74a61b0a3cd7094cbcea437caf909a731346ac53607f8624ec27b1b9d58bb44f80f294028c07ab813e938
6
+ metadata.gz: a46270931d8c21b25090be9d1825259e872eae678ba1d1df6ca7898f0e678f81edc2f590a4338502631686be3a7b2911e736c58d9704354918b707dbaafdba41
7
+ data.tar.gz: 7bd511b855d777da969ea03aa14d7ce336a9c96670f01ac3e20424bb6fe039d43f183e561a33acb5e320ac2b8230aae287d608c292c68c4a91b605dd2be0cdb9
@@ -1,3 +1,29 @@
1
+ ## 0.4.3 (2020-05-26)
2
+
3
+ - Improved error message for bad key length
4
+ - Fixed missing attribute error
5
+
6
+ ## 0.4.2 (2020-05-11)
7
+
8
+ - Added experimental support for migrating Active Storage files
9
+ - Fixed `metadata` support for Active Storage
10
+
11
+ ## 0.4.1 (2020-05-08)
12
+
13
+ - Added support for Action Text
14
+ - Added warning if unencrypted column exists and not migrating
15
+
16
+ ## 0.4.0 (2020-05-03)
17
+
18
+ - Load encrypted attributes when `attributes` called
19
+ - Added support for migrating and rotating relations
20
+ - Removed deprecated `attached_encrypted` method
21
+ - Removed legacy `attr_encrypted` encryptor
22
+
23
+ ## 0.3.7 (2020-04-20)
24
+
25
+ - Added Active Support notifications for Active Storage and Carrierwave
26
+
1
27
  ## 0.3.6 (2020-04-19)
2
28
 
3
29
  - Fixed content type detection 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
@@ -316,19 +426,35 @@ To serve encrypted files, use a controller action.
316
426
  ```ruby
317
427
  def license
318
428
  user = User.find(params[:id])
319
- send_data box.decrypt(user.license.read), type: user.license.mime_type
429
+ send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
320
430
  end
321
431
  ```
322
432
 
323
433
  ## Local Files
324
434
 
325
- Read the file as a binary string
435
+ Generate a key
436
+
437
+ ```ruby
438
+ key = Lockbox.generate_key
439
+ ```
440
+
441
+ Create a lockbox
326
442
 
327
443
  ```ruby
328
- message = File.binread("file.txt")
444
+ lockbox = Lockbox.new(key: key)
329
445
  ```
330
446
 
331
- Then follow the instructions for encrypting a string below.
447
+ Encrypt
448
+
449
+ ```ruby
450
+ ciphertext = lockbox.encrypt(File.binread("file.txt"))
451
+ ```
452
+
453
+ Decrypt
454
+
455
+ ```ruby
456
+ lockbox.decrypt(ciphertext)
457
+ ```
332
458
 
333
459
  ## Strings
334
460
 
@@ -362,7 +488,7 @@ Use `decrypt_str` get the value as UTF-8
362
488
 
363
489
  To make key rotation easy, you can pass previous versions of keys that can decrypt.
364
490
 
365
- ### Active Record
491
+ ### Active Record & Mongoid
366
492
 
367
493
  Update your model:
368
494
 
@@ -382,26 +508,6 @@ Lockbox.rotate(User, attributes: [:email])
382
508
 
383
509
  Once all records are rotated, you can remove `previous_versions` from the model.
384
510
 
385
- ### Mongoid
386
-
387
- Update your model:
388
-
389
- ```ruby
390
- class User
391
- encrypts :email, previous_versions: [{key: previous_key}]
392
- end
393
- ```
394
-
395
- Use `master_key` instead of `key` if passing the master key.
396
-
397
- To rotate existing records, use:
398
-
399
- ```ruby
400
- Lockbox.rotate(User, attributes: [:email])
401
- ```
402
-
403
- Once all records are rotated, you can remove `previous_versions` from the model.
404
-
405
511
  ### Active Storage
406
512
 
407
513
  Update your model:
@@ -417,7 +523,7 @@ Use `master_key` instead of `key` if passing the master key.
417
523
  To rotate existing files, use:
418
524
 
419
525
  ```ruby
420
- User.find_each do |user|
526
+ User.with_attached_license.find_each do |user|
421
527
  user.license.rotate_encryption!
422
528
  end
423
529
  ```
@@ -446,9 +552,9 @@ end
446
552
 
447
553
  Once all files are rotated, you can remove `previous_versions` from the model.
448
554
 
449
- ### Strings
555
+ ### Local Files & Strings
450
556
 
451
- For strings, use:
557
+ For local files and strings, use:
452
558
 
453
559
  ```ruby
454
560
  Lockbox.new(key: key, previous_versions: [{key: previous_key}])
@@ -541,7 +647,7 @@ Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packag
541
647
 
542
648
  ##### Ubuntu
543
649
 
544
- For Ubuntu 18.04, use:
650
+ For Ubuntu 20.04 and 18.04, use:
545
651
 
546
652
  ```sh
547
653
  sudo apt-get install libsodium23
@@ -19,6 +19,9 @@ require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
19
19
  require "lockbox/railtie" if defined?(Rails)
20
20
 
21
21
  if defined?(ActiveSupport)
22
+ require "lockbox/log_subscriber"
23
+ Lockbox::LogSubscriber.attach_to :lockbox
24
+
22
25
  ActiveSupport.on_load(:active_record) do
23
26
  extend Lockbox::Model
24
27
  extend Lockbox::Model::Attached
@@ -26,8 +29,6 @@ if defined?(ActiveSupport)
26
29
 
27
30
  ActiveSupport.on_load(:mongoid) do
28
31
  Mongoid::Document::ClassMethods.include(Lockbox::Model)
29
- # TODO remove in 0.4.0
30
- Mongoid::Document::ClassMethods.include(Lockbox::Model::Attached)
31
32
  end
32
33
  end
33
34
 
@@ -92,4 +93,10 @@ module Lockbox
92
93
  def self.new(**options)
93
94
  Encryptor.new(**options)
94
95
  end
96
+
97
+ def self.encrypts_action_text_body(**options)
98
+ ActiveSupport.on_load(:action_text_rich_text) do
99
+ ActionText::RichText.encrypts :body, **options
100
+ end
101
+ end
95
102
  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,14 +80,16 @@ 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
98
- result = Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
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
92
+ result = Utils.decrypt_result(record, name, options, result)
99
93
  end
100
94
 
101
95
  result
@@ -105,14 +99,18 @@ 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
109
- result = file.read
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
107
+ result = Utils.decrypt_result(record, name, options, file.read)
110
108
  file.rewind
111
109
  # truncate may not be available on all platforms
112
110
  # according to the Ruby docs
113
111
  # may need to create a new temp file instead
114
112
  file.truncate(0)
115
- file.write(Utils.build_box(record, options, record.class.table_name, name).decrypt(result))
113
+ file.write(result)
116
114
  file.rewind
117
115
  end
118
116
 
@@ -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
@@ -5,13 +5,13 @@ module Lockbox
5
5
  before :cache, :encrypt
6
6
 
7
7
  def encrypt(file)
8
- @file = CarrierWave::SanitizedFile.new(lockbox.encrypt_io(file))
8
+ @file = CarrierWave::SanitizedFile.new(with_notification("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
- lockbox.decrypt(r) if r
14
+ with_notification("decrypt_file") { lockbox.decrypt(r) } if r
15
15
  end
16
16
 
17
17
  def size
@@ -40,20 +40,39 @@ module Lockbox
40
40
  define_method :lockbox do
41
41
  @lockbox ||= begin
42
42
  table = model ? model.class.table_name : "_uploader"
43
- attribute =
44
- if mounted_as
45
- mounted_as.to_s
46
- else
47
- uploader = self
48
- while uploader.parent_version
49
- uploader = uploader.parent_version
50
- end
51
- uploader.class.name.sub(/Uploader\z/, "").underscore
52
- end
43
+ attribute = lockbox_name
53
44
 
54
45
  Utils.build_box(self, options, table, attribute)
55
46
  end
56
47
  end
48
+
49
+ def lockbox_name
50
+ if mounted_as
51
+ mounted_as.to_s
52
+ else
53
+ uploader = self
54
+ while uploader.parent_version
55
+ uploader = uploader.parent_version
56
+ end
57
+ uploader.class.name.sub(/Uploader\z/, "").underscore
58
+ end
59
+ end
60
+
61
+ def with_notification(type)
62
+ if defined?(ActiveSupport::Notifications)
63
+ name = lockbox_name
64
+
65
+ # get version
66
+ version, _ = parent_version && parent_version.versions.find { |k, v| v == self }
67
+ name = "#{name} #{version} version" if version
68
+
69
+ ActiveSupport::Notifications.instrument("#{type}.lockbox", {name: name}) do
70
+ yield
71
+ end
72
+ else
73
+ yield
74
+ end
75
+ end
57
76
  end
58
77
  end
59
78
  end
@@ -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
@@ -0,0 +1,21 @@
1
+ module Lockbox
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def encrypt_file(event)
4
+ return unless logger.debug?
5
+
6
+ payload = event.payload
7
+ name = "Encrypt File (#{event.duration.round(1)}ms)"
8
+
9
+ debug " #{color(name, YELLOW, true)} Encrypted #{payload[:name]}"
10
+ end
11
+
12
+ def decrypt_file(event)
13
+ return unless logger.debug?
14
+
15
+ payload = event.payload
16
+ name = "Decrypt File (#{event.duration.round(1)}ms)"
17
+
18
+ debug " #{color(name, YELLOW, true)} Decrypted #{payload[:name]}"
19
+ end
20
+ end
21
+ end
@@ -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
 
@@ -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
@@ -31,13 +31,13 @@ module Lockbox
31
31
  record.class.respond_to?(:lockbox_attachments) ? record.class.lockbox_attachments[name.to_sym] : nil
32
32
  end
33
33
 
34
- def self.decode_key(key, size: 32)
34
+ def self.decode_key(key, size: 32, name: "Key")
35
35
  if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{#{size * 2}}\z/i
36
36
  key = [key].pack("H*")
37
37
  end
38
38
 
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
39
+ raise Lockbox::Error, "#{name} must be 32 bytes (64 hex digits)" if key.bytesize != size
40
+ raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
41
41
 
42
42
  key
43
43
  end
@@ -47,27 +47,33 @@ module Lockbox
47
47
  end
48
48
 
49
49
  def self.encrypt_attachable(record, name, attachable)
50
- options = encrypted_options(record, name)
51
- box = build_box(record, options, record.class.table_name, name)
52
50
  io = nil
53
51
 
54
- case attachable
55
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
56
- io = attachable
57
- attachable = {
58
- io: box.encrypt_io(io),
59
- filename: attachable.original_filename,
60
- content_type: attachable.content_type
61
- }
62
- when Hash
63
- io = attachable[:io]
64
- attachable = {
65
- io: box.encrypt_io(io),
66
- filename: attachable[:filename],
67
- content_type: attachable[:content_type]
68
- }
69
- else
70
- raise NotImplementedError, "Not supported"
52
+ ActiveSupport::Notifications.instrument("encrypt_file.lockbox", {name: name}) do
53
+ options = encrypted_options(record, name)
54
+ box = build_box(record, options, record.class.table_name, name)
55
+
56
+ case attachable
57
+ when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
58
+ io = attachable
59
+ attachable = {
60
+ io: box.encrypt_io(io),
61
+ filename: attachable.original_filename,
62
+ content_type: attachable.content_type
63
+ }
64
+ when Hash
65
+ io = attachable[:io]
66
+ attachable = attachable.dup
67
+ attachable[:io] = box.encrypt_io(io)
68
+ else
69
+ # TODO raise ArgumentError
70
+ raise NotImplementedError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
71
+ end
72
+
73
+ # don't analyze encrypted data
74
+ metadata = {"analyzed" => true}
75
+ metadata["encrypted"] = true if options[:migrating]
76
+ attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
71
77
  end
72
78
 
73
79
  # set content type based on unencrypted data
@@ -76,5 +82,19 @@ module Lockbox
76
82
 
77
83
  attachable
78
84
  end
85
+
86
+ def self.decrypt_result(record, name, options, result)
87
+ ActiveSupport::Notifications.instrument("decrypt_file.lockbox", {name: name}) do
88
+ Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
89
+ end
90
+ end
91
+
92
+ def self.rebuild_attachable(attachment)
93
+ {
94
+ io: StringIO.new(attachment.download),
95
+ filename: attachment.filename,
96
+ content_type: attachment.content_type
97
+ }
98
+ end
79
99
  end
80
100
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.3.6"
2
+ VERSION = "0.4.3"
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.6
4
+ version: 0.4.3
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-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: combustion
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 1.1.2
47
+ version: '1.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 1.1.2
54
+ version: '1.3'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rails
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +184,7 @@ files:
184
184
  - lib/lockbox/encryptor.rb
185
185
  - lib/lockbox/io.rb
186
186
  - lib/lockbox/key_generator.rb
187
+ - lib/lockbox/log_subscriber.rb
187
188
  - lib/lockbox/migrator.rb
188
189
  - lib/lockbox/model.rb
189
190
  - lib/lockbox/padding.rb