lockbox 0.4.1 → 0.4.2

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: fe55b94af6abc4b2cf562badea225c8aff715ae3be8cc40e81f645ae6c0a6a9b
4
- data.tar.gz: dd6c5dbd4d3280548212ca145f56b99909b5d4af07784bc095fc1df6afd156ff
3
+ metadata.gz: cb4f05b7519d563c465c963893ded5f21063f666bdb41d4bfc69f23ea6b82374
4
+ data.tar.gz: 0f13b613b16b15ca6514de06fc57ec62d98b94a06a01eb6d8e77fdce9d23343b
5
5
  SHA512:
6
- metadata.gz: 3a50542c82cfbb3cab24e6e18f2aaec1862b0a76dfef762b416710a67b17ec9171be6620b48bd59c50ef8d02f29bd195e860579e96fcbb8f2667cd151d277a56
7
- data.tar.gz: 42ce91cccba7bbed944a6454e076f7f8db08d107aa96da5e9a69e862faa0c465e27e4441007b9964e0ce953ce5294bbd3120d55e6b91aea516a2597ab9375e9d
6
+ metadata.gz: bdb2f4cbf5977bd1e40c9055c6e166c819b8e13ccbdd2d870c80ee907608205f72f65faa9fe56e882fcf9eab155b506537f29bd7593c3fcf31e3a0f94740e0ec
7
+ data.tar.gz: ef966eff17c633ee5b590742c33a107bd58da4d262fbaf1bea458c2e5146dc16993f75b6d783118f3ff7f7fd827913423c71558bf1e8d60df66e3de35627e1a1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.4.2 (2020-05-11)
2
+
3
+ - Added experimental support for migrating Active Storage files
4
+ - Fixed `metadata` support for Active Storage
5
+
1
6
  ## 0.4.1 (2020-05-08)
2
7
 
3
8
  - Added support for Action Text
data/README.md CHANGED
@@ -151,7 +151,9 @@ Be sure to include the `inspect` at the end or it won’t be encoded properly in
151
151
 
152
152
  #### Migrating Existing Data
153
153
 
154
- 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:
155
157
 
156
158
  ```ruby
157
159
  class User < ApplicationRecord
@@ -274,6 +276,34 @@ def license
274
276
  end
275
277
  ```
276
278
 
279
+ #### Migrating Existing Files [experimental]
280
+
281
+ **Note:** This feature is experimental. Please try it in a non-production environment and let us know 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
+
277
307
  ## CarrierWave
278
308
 
279
309
  Add to your uploader:
@@ -313,6 +343,51 @@ def license
313
343
  end
314
344
  ```
315
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
+
316
391
  ## Shrine
317
392
 
318
393
  Generate a key
@@ -448,7 +523,7 @@ Use `master_key` instead of `key` if passing the master key.
448
523
  To rotate existing files, use:
449
524
 
450
525
  ```ruby
451
- User.find_each do |user|
526
+ User.with_attached_license.find_each do |user|
452
527
  user.license.rotate_encryption!
453
528
  end
454
529
  ```
@@ -572,7 +647,7 @@ Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packag
572
647
 
573
648
  ##### Ubuntu
574
649
 
575
- For Ubuntu 18.04, use:
650
+ For Ubuntu 20.04 and 18.04, use:
576
651
 
577
652
  ```sh
578
653
  sudo apt-get install libsodium23
data/lib/lockbox.rb CHANGED
@@ -95,7 +95,6 @@ module Lockbox
95
95
  end
96
96
 
97
97
  def self.encrypts_action_text_body(**options)
98
- # runs every reload
99
98
  ActiveSupport.on_load(:action_text_rich_text) do
100
99
  ActionText::RichText.encrypts :body, **options
101
100
  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
@@ -24,26 +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
+ # eager load attachments
44
+ attachments.each_key do |k|
45
+ relation = relation.send("with_attached_#{k}")
43
46
  end
44
47
 
45
- # convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
46
- 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
47
70
 
48
71
  unless restart
49
72
  attributes = fields.map { |_, v| v[:encrypted_attribute] }
@@ -138,6 +161,18 @@ module Lockbox
138
161
  end
139
162
  end
140
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
+
141
176
  def ar_relation?(relation)
142
177
  defined?(ActiveRecord::Relation) && relation.is_a?(ActiveRecord::Relation)
143
178
  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
data/lib/lockbox/utils.rb CHANGED
@@ -63,14 +63,17 @@ module Lockbox
63
63
  }
64
64
  when Hash
65
65
  io = attachable[:io]
66
- attachable = {
67
- io: box.encrypt_io(io),
68
- filename: attachable[:filename],
69
- content_type: attachable[:content_type]
70
- }
66
+ attachable = attachable.dup
67
+ attachable[:io] = box.encrypt_io(io)
71
68
  else
72
- raise NotImplementedError, "Not supported"
69
+ # TODO raise ArgumentError
70
+ raise NotImplementedError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
73
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)
74
77
  end
75
78
 
76
79
  # set content type based on unencrypted data
@@ -85,5 +88,13 @@ module Lockbox
85
88
  Utils.build_box(record, options, record.class.table_name, name).decrypt(result)
86
89
  end
87
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
88
99
  end
89
100
  end
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.2"
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.4.1
4
+ version: 0.4.2
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-05-08 00:00:00.000000000 Z
11
+ date: 2020-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler