lockbox 0.4.1 → 0.4.6
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 +24 -0
- data/README.md +80 -3
- data/SECURITY.md +3 -0
- data/lib/lockbox.rb +2 -1
- 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 +56 -4
- 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: 2fb82c6baf0c56ae7f051c8363f7cb52554ebab8180b891d4aa25caf7505cec3
|
4
|
+
data.tar.gz: 64b6bda4259cc3fddd7e5635333b0b7c4a6680c4ff2da31bba735603ea50b010
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1953d9159c2cb1f1ec55436d925738777ea0b78afedcb32864280f7772d1d9e12a48f65c69385fd6bcdab166668049b3889f1e17e3b316c696705bcb98650dd
|
7
|
+
data.tar.gz: a3559c399a385949526137eed03b73c38baae424bed3b597c89f287222e6e18fd2c5665b2cbba6b20dad71bf680f0840ee2753a68befd3af40fdb522e45e4088
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
## 0.4.6 (2020-07-02)
|
2
|
+
|
3
|
+
- Added support for `update_column` and `update_columns`
|
4
|
+
|
5
|
+
## 0.4.5 (2020-06-26)
|
6
|
+
|
7
|
+
- Improved error message for non-string values
|
8
|
+
- Fixed error with migrating Action Text
|
9
|
+
- Fixed error with migrating serialized attributes
|
10
|
+
|
11
|
+
## 0.4.4 (2020-06-23)
|
12
|
+
|
13
|
+
- Added support for `pluck`
|
14
|
+
|
15
|
+
## 0.4.3 (2020-05-26)
|
16
|
+
|
17
|
+
- Improved error message for bad key length
|
18
|
+
- Fixed missing attribute error
|
19
|
+
|
20
|
+
## 0.4.2 (2020-05-11)
|
21
|
+
|
22
|
+
- Added experimental support for migrating Active Storage files
|
23
|
+
- Fixed `metadata` support for Active Storage
|
24
|
+
|
1
25
|
## 0.4.1 (2020-05-08)
|
2
26
|
|
3
27
|
- 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
|
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 [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
|
+
|
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
|
@@ -890,3 +965,5 @@ cd lockbox
|
|
890
965
|
bundle install
|
891
966
|
bundle exec rake test
|
892
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
|
@@ -95,7 +97,6 @@ module Lockbox
|
|
95
97
|
end
|
96
98
|
|
97
99
|
def self.encrypts_action_text_body(**options)
|
98
|
-
# runs every reload
|
99
100
|
ActiveSupport.on_load(:action_text_rich_text) do
|
100
101
|
ActionText::RichText.encrypts :body, **options
|
101
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,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
|
@@ -104,6 +107,49 @@ module Lockbox
|
|
104
107
|
end
|
105
108
|
end
|
106
109
|
end
|
110
|
+
|
111
|
+
def update_columns(attributes)
|
112
|
+
return super unless attributes.is_a?(Hash)
|
113
|
+
|
114
|
+
# transform keys like Active Record
|
115
|
+
attributes = attributes.transform_keys do |key|
|
116
|
+
n = key.to_s
|
117
|
+
self.class.attribute_aliases[n] || n
|
118
|
+
end
|
119
|
+
|
120
|
+
lockbox_attributes = self.class.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
|
121
|
+
return super unless lockbox_attributes.any?
|
122
|
+
|
123
|
+
attributes_to_set = {}
|
124
|
+
|
125
|
+
lockbox_attributes.each do |key, lockbox_attribute|
|
126
|
+
attribute = key.to_s
|
127
|
+
# check read only
|
128
|
+
verify_readonly_attribute(attribute)
|
129
|
+
|
130
|
+
message = attributes[attribute]
|
131
|
+
attributes.delete(attribute) unless lockbox_attribute[:migrating]
|
132
|
+
encrypted_attribute = lockbox_attribute[:encrypted_attribute]
|
133
|
+
ciphertext = self.class.send("generate_#{encrypted_attribute}", message, context: self)
|
134
|
+
attributes[encrypted_attribute] = ciphertext
|
135
|
+
attributes_to_set[attribute] = message
|
136
|
+
attributes_to_set[lockbox_attribute[:attribute]] = message if lockbox_attribute[:migrating]
|
137
|
+
end
|
138
|
+
|
139
|
+
result = super(attributes)
|
140
|
+
|
141
|
+
# same logic as Active Record
|
142
|
+
# (although this happens before saving)
|
143
|
+
attributes_to_set.each do |k, v|
|
144
|
+
if respond_to?(:write_attribute_without_type_cast, true)
|
145
|
+
write_attribute_without_type_cast(k, v)
|
146
|
+
else
|
147
|
+
raw_write_attribute(k, v)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
result
|
152
|
+
end
|
107
153
|
else
|
108
154
|
def reload
|
109
155
|
self.class.lockbox_attributes.each do |_, v|
|
@@ -143,6 +189,10 @@ module Lockbox
|
|
143
189
|
# however, we can try to use the original type if its already defined
|
144
190
|
if attributes_to_define_after_schema_loads.key?(original_name.to_s)
|
145
191
|
attribute name, attributes_to_define_after_schema_loads[original_name.to_s].first
|
192
|
+
elsif options[:migrating]
|
193
|
+
# we use the original attribute for serialization in the encrypt and decrypt methods
|
194
|
+
# so we can use a generic value here
|
195
|
+
attribute name, ActiveRecord::Type::Value.new
|
146
196
|
else
|
147
197
|
attribute name, :string
|
148
198
|
end
|
@@ -267,7 +317,7 @@ module Lockbox
|
|
267
317
|
# cache
|
268
318
|
# decrypt method does type casting
|
269
319
|
if respond_to?(:write_attribute_without_type_cast, true)
|
270
|
-
write_attribute_without_type_cast(name, message) if !@attributes.frozen?
|
320
|
+
write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
|
271
321
|
else
|
272
322
|
raw_write_attribute(name, message) if !@attributes.frozen?
|
273
323
|
end
|
@@ -316,7 +366,8 @@ module Lockbox
|
|
316
366
|
# do nothing
|
317
367
|
# encrypt will convert to binary
|
318
368
|
else
|
319
|
-
|
369
|
+
# use original name for serialized attributes
|
370
|
+
type = (try(:attribute_types) || {})[original_name.to_s]
|
320
371
|
message = type.serialize(message) if type
|
321
372
|
end
|
322
373
|
end
|
@@ -358,7 +409,8 @@ module Lockbox
|
|
358
409
|
# do nothing
|
359
410
|
# decrypt returns binary string
|
360
411
|
else
|
361
|
-
|
412
|
+
# use original name for serialized attributes
|
413
|
+
type = (try(:attribute_types) || {})[original_name.to_s]
|
362
414
|
message = type.deserialize(message) if type
|
363
415
|
message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
|
364
416
|
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.6
|
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-07-03 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
|