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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +118 -26
- data/SECURITY.md +3 -0
- data/lib/lockbox.rb +8 -2
- data/lib/lockbox/active_storage_extensions.rb +12 -24
- data/lib/lockbox/calculations.rb +36 -0
- data/lib/lockbox/carrier_wave_extensions.rb +3 -3
- data/lib/lockbox/encryptor.rb +0 -20
- data/lib/lockbox/key_generator.rb +1 -1
- data/lib/lockbox/migrator.rb +44 -12
- data/lib/lockbox/model.rb +31 -9
- data/lib/lockbox/railtie.rb +14 -5
- data/lib/lockbox/utils.rb +26 -11
- data/lib/lockbox/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6b1676e34ca9367ec4cfd3c655d263d23082243196abe8f0a174318383f7d4b1
|
4
|
+
data.tar.gz: cdda6ac4e9dccc4ab48a67e3628212fdde369a99639cc93e569d0d623f5ab8f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e747bb720312daedcb2db5458d919cb50e554b03ee04d219a3b1c2f40ca917815a066dfd767b075046f29e387d5d48a734c193a3a2c48927b5abf6e0daaa099
|
7
|
+
data.tar.gz: 2a456f05d61ecc75dfdc76aca15df194b58f91d701a28d12d3b27cc40f93f301244e4f60d506f7deae7a1b6a53afab087205aaced37a54af75f81810934ed8af
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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).
|
data/SECURITY.md
ADDED
data/lib/lockbox.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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(
|
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
|
-
|
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
|
61
|
+
def lockbox_notify(type)
|
62
62
|
if defined?(ActiveSupport::Notifications)
|
63
63
|
name = lockbox_name
|
64
64
|
|
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
|
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
|
|
@@ -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.
|
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
@@ -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.
|
8
|
-
|
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, "
|
40
|
-
raise Lockbox::Error, "
|
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
|
-
|
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
|
-
|
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
|
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.
|
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-
|
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
|