lockbox 0.4.0 → 0.4.5
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 +25 -0
- data/README.md +113 -3
- data/SECURITY.md +3 -0
- data/lib/lockbox.rb +8 -0
- 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 +5 -4
- data/lib/lockbox/key_generator.rb +1 -1
- data/lib/lockbox/migrator.rb +44 -9
- data/lib/lockbox/model.rb +29 -6
- 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: 38b7a0b301adce103c1acebfa412c7a0657f687416fe237d19b02756b7019285
|
4
|
+
data.tar.gz: 17a67fabef8fdab72e2821750e95caf99db70f8d35d4fc6e75033d05e93e4c4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5fae42738060a5ab6a0f8d3dca97703e025dd1a131d474ea807fefa251d88e0ff1098c09993420f55480cb3469c26db5abfe21d93f53ddb23b920aad162bc6f6
|
7
|
+
data.tar.gz: a24973afcb9f2ec6aad8a5219f23493fcc821d760e6f3fc602bf97c98e60ef05de33461f95cef188452c474bd54b7be3694c050eaea52b50548f5d08f325b56d
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
## 0.4.5 (2020-06-26)
|
2
|
+
|
3
|
+
- Improved error message for non-string values
|
4
|
+
- Fixed error with migrating Action Text
|
5
|
+
- Fixed error with migrating serialized attributes
|
6
|
+
|
7
|
+
## 0.4.4 (2020-06-23)
|
8
|
+
|
9
|
+
- Added support for `pluck`
|
10
|
+
|
11
|
+
## 0.4.3 (2020-05-26)
|
12
|
+
|
13
|
+
- Improved error message for bad key length
|
14
|
+
- Fixed missing attribute error
|
15
|
+
|
16
|
+
## 0.4.2 (2020-05-11)
|
17
|
+
|
18
|
+
- Added experimental support for migrating Active Storage files
|
19
|
+
- Fixed `metadata` support for Active Storage
|
20
|
+
|
21
|
+
## 0.4.1 (2020-05-08)
|
22
|
+
|
23
|
+
- Added support for Action Text
|
24
|
+
- Added warning if unencrypted column exists and not migrating
|
25
|
+
|
1
26
|
## 0.4.0 (2020-05-03)
|
2
27
|
|
3
28
|
- Load encrypted attributes when `attributes` called
|
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:
|
@@ -241,6 +276,34 @@ def license
|
|
241
276
|
end
|
242
277
|
```
|
243
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
|
+
|
244
307
|
## CarrierWave
|
245
308
|
|
246
309
|
Add to your uploader:
|
@@ -280,6 +343,51 @@ def license
|
|
280
343
|
end
|
281
344
|
```
|
282
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
|
+
|
283
391
|
## Shrine
|
284
392
|
|
285
393
|
Generate a key
|
@@ -415,7 +523,7 @@ Use `master_key` instead of `key` if passing the master key.
|
|
415
523
|
To rotate existing files, use:
|
416
524
|
|
417
525
|
```ruby
|
418
|
-
User.find_each do |user|
|
526
|
+
User.with_attached_license.find_each do |user|
|
419
527
|
user.license.rotate_encryption!
|
420
528
|
end
|
421
529
|
```
|
@@ -539,7 +647,7 @@ Heroku [comes with libsodium](https://devcenter.heroku.com/articles/stack-packag
|
|
539
647
|
|
540
648
|
##### Ubuntu
|
541
649
|
|
542
|
-
For Ubuntu 18.04, use:
|
650
|
+
For Ubuntu 20.04 and 18.04, use:
|
543
651
|
|
544
652
|
```sh
|
545
653
|
sudo apt-get install libsodium23
|
@@ -857,3 +965,5 @@ cd lockbox
|
|
857
965
|
bundle install
|
858
966
|
bundle exec rake test
|
859
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,6 +26,7 @@ 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
|
@@ -93,4 +95,10 @@ module Lockbox
|
|
93
95
|
def self.new(**options)
|
94
96
|
Encryptor.new(**options)
|
95
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
|
96
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
@@ -13,7 +13,7 @@ module Lockbox
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def encrypt(message, **options)
|
16
|
-
message = check_string(message
|
16
|
+
message = check_string(message)
|
17
17
|
ciphertext = @boxes.first.encrypt(message, **options)
|
18
18
|
ciphertext = Base64.strict_encode64(ciphertext) if @encode
|
19
19
|
ciphertext
|
@@ -21,7 +21,7 @@ module Lockbox
|
|
21
21
|
|
22
22
|
def decrypt(ciphertext, **options)
|
23
23
|
ciphertext = Base64.decode64(ciphertext) if @encode
|
24
|
-
ciphertext = check_string(ciphertext
|
24
|
+
ciphertext = check_string(ciphertext)
|
25
25
|
|
26
26
|
# ensure binary
|
27
27
|
if ciphertext.encoding != Encoding::BINARY
|
@@ -66,9 +66,10 @@ module Lockbox
|
|
66
66
|
|
67
67
|
private
|
68
68
|
|
69
|
-
def check_string(str
|
69
|
+
def check_string(str)
|
70
70
|
str = str.read if str.respond_to?(:read)
|
71
|
-
|
71
|
+
# Ruby uses "no implicit conversion of Object into String"
|
72
|
+
raise TypeError, "can't convert #{str.class.name} to String" unless str.respond_to?(:to_str)
|
72
73
|
str.to_str
|
73
74
|
end
|
74
75
|
|
@@ -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,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
|
-
|
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
|
+
# eager load attachments
|
44
|
+
attachments.each_key do |k|
|
45
|
+
relation = relation.send("with_attached_#{k}")
|
43
46
|
end
|
44
47
|
|
45
|
-
|
46
|
-
|
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
|
data/lib/lockbox/model.rb
CHANGED
@@ -87,7 +87,10 @@ module Lockbox
|
|
87
87
|
# essentially a no-op if already loaded
|
88
88
|
# an exception is thrown if decryption fails
|
89
89
|
self.class.lockbox_attributes.each do |_, lockbox_attribute|
|
90
|
-
|
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])
|
91
94
|
end
|
92
95
|
super
|
93
96
|
end
|
@@ -143,6 +146,10 @@ module Lockbox
|
|
143
146
|
# however, we can try to use the original type if its already defined
|
144
147
|
if attributes_to_define_after_schema_loads.key?(original_name.to_s)
|
145
148
|
attribute name, attributes_to_define_after_schema_loads[original_name.to_s].first
|
149
|
+
elsif options[:migrating]
|
150
|
+
# we use the original attribute for serialization in the encrypt and decrypt methods
|
151
|
+
# so we can use a generic value here
|
152
|
+
attribute name, ActiveRecord::Type::Value.new
|
146
153
|
else
|
147
154
|
attribute name, :string
|
148
155
|
end
|
@@ -223,6 +230,20 @@ module Lockbox
|
|
223
230
|
|
224
231
|
send("lockbox_direct_#{name}=", message)
|
225
232
|
|
233
|
+
# warn every time, as this should be addressed
|
234
|
+
# maybe throw an error in the future
|
235
|
+
if !options[:migrating]
|
236
|
+
if activerecord
|
237
|
+
if self.class.columns_hash.key?(name.to_s)
|
238
|
+
warn "[lockbox] WARNING: Unencrypted column with same name: #{name}. Set `ignored_columns` or remove it to protect the data."
|
239
|
+
end
|
240
|
+
else
|
241
|
+
if self.class.fields.key?(name.to_s)
|
242
|
+
warn "[lockbox] WARNING: Unencrypted field with same name: #{name}. Remove it to protect the data."
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
226
247
|
super(message)
|
227
248
|
end
|
228
249
|
|
@@ -253,7 +274,7 @@ module Lockbox
|
|
253
274
|
# cache
|
254
275
|
# decrypt method does type casting
|
255
276
|
if respond_to?(:write_attribute_without_type_cast, true)
|
256
|
-
write_attribute_without_type_cast(name, message) if !@attributes.frozen?
|
277
|
+
write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
|
257
278
|
else
|
258
279
|
raw_write_attribute(name, message) if !@attributes.frozen?
|
259
280
|
end
|
@@ -270,7 +291,7 @@ module Lockbox
|
|
270
291
|
table = activerecord ? table_name : collection_name.to_s
|
271
292
|
|
272
293
|
unless message.nil?
|
273
|
-
# TODO use attribute type class in 0.
|
294
|
+
# TODO use attribute type class in 0.5.0
|
274
295
|
case options[:type]
|
275
296
|
when :boolean
|
276
297
|
message = ActiveRecord::Type::Boolean.new.serialize(message)
|
@@ -302,7 +323,8 @@ module Lockbox
|
|
302
323
|
# do nothing
|
303
324
|
# encrypt will convert to binary
|
304
325
|
else
|
305
|
-
|
326
|
+
# use original name for serialized attributes
|
327
|
+
type = (try(:attribute_types) || {})[original_name.to_s]
|
306
328
|
message = type.serialize(message) if type
|
307
329
|
end
|
308
330
|
end
|
@@ -324,7 +346,7 @@ module Lockbox
|
|
324
346
|
end
|
325
347
|
|
326
348
|
unless message.nil?
|
327
|
-
# TODO use attribute type class in 0.
|
349
|
+
# TODO use attribute type class in 0.5.0
|
328
350
|
case options[:type]
|
329
351
|
when :boolean
|
330
352
|
message = message == "t"
|
@@ -344,7 +366,8 @@ module Lockbox
|
|
344
366
|
# do nothing
|
345
367
|
# decrypt returns binary string
|
346
368
|
else
|
347
|
-
|
369
|
+
# use original name for serialized attributes
|
370
|
+
type = (try(:attribute_types) || {})[original_name.to_s]
|
348
371
|
message = type.deserialize(message) if type
|
349
372
|
message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
|
350
373
|
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.
|
4
|
+
version: 0.4.5
|
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-26 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
|