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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +136 -30
- data/lib/lockbox.rb +9 -2
- data/lib/lockbox/active_storage_extensions.rb +15 -27
- data/lib/lockbox/carrier_wave_extensions.rb +31 -12
- data/lib/lockbox/encryptor.rb +0 -20
- data/lib/lockbox/key_generator.rb +1 -1
- data/lib/lockbox/log_subscriber.rb +21 -0
- data/lib/lockbox/migrator.rb +44 -12
- data/lib/lockbox/model.rb +30 -8
- data/lib/lockbox/railtie.rb +14 -5
- data/lib/lockbox/utils.rb +42 -22
- data/lib/lockbox/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c072d4c6e5935ff9e176c0fc705c0e36228da4b1fc530a7eaf0249db57c71ddc
|
4
|
+
data.tar.gz: a0d293cb80ea7050deeccd039b5e0be01d51c09577181c1d9bf9df8a520764ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a46270931d8c21b25090be9d1825259e872eae678ba1d1df6ca7898f0e678f81edc2f590a4338502631686be3a7b2911e736c58d9704354918b707dbaafdba41
|
7
|
+
data.tar.gz: 7bd511b855d777da969ea03aa14d7ce336a9c96670f01ac3e20424bb6fe039d43f183e561a33acb5e320ac2b8230aae287d608c292c68c4a91b605dd2be0cdb9
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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
|
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
|
-
|
435
|
+
Generate a key
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
key = Lockbox.generate_key
|
439
|
+
```
|
440
|
+
|
441
|
+
Create a lockbox
|
326
442
|
|
327
443
|
```ruby
|
328
|
-
|
444
|
+
lockbox = Lockbox.new(key: key)
|
329
445
|
```
|
330
446
|
|
331
|
-
|
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
|
data/lib/lockbox.rb
CHANGED
@@ -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
|
-
|
98
|
-
|
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
|
-
|
109
|
-
|
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(
|
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
|
data/lib/lockbox/encryptor.rb
CHANGED
@@ -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
|
data/lib/lockbox/migrator.rb
CHANGED
@@ -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
|
-
|
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
|
38
|
-
relation =
|
40
|
+
def perform_attachments(attachments:, restart:)
|
41
|
+
relation = base_relation
|
39
42
|
|
40
|
-
#
|
41
|
-
|
42
|
-
relation = relation.
|
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
|
-
|
49
|
-
|
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
|
data/lib/lockbox/model.rb
CHANGED
@@ -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.
|
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.
|
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
|
data/lib/lockbox/railtie.rb
CHANGED
@@ -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
|
-
|
17
|
-
if
|
18
|
-
|
19
|
-
|
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
@@ -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, "
|
40
|
-
raise Lockbox::Error, "
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
data/lib/lockbox/version.rb
CHANGED
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
|
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-
|
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.
|
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.
|
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
|