lockbox 0.4.1 → 0.4.2

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