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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb1e70486f88d6aad134fe0a13d74e750505888415cff633138fab97887b8d0a
4
- data.tar.gz: e40ed2533aa32adc6b5c3048ab3e4588a652c0335d2eb7c408c3ea90e833f8c5
3
+ metadata.gz: 38b7a0b301adce103c1acebfa412c7a0657f687416fe237d19b02756b7019285
4
+ data.tar.gz: 17a67fabef8fdab72e2821750e95caf99db70f8d35d4fc6e75033d05e93e4c4b
5
5
  SHA512:
6
- metadata.gz: '02051826a17857790dbf6045c4a5a3948d0843fb27a0b4799599393b8c600a43891f6c40e84c7d7dc08be2ff2fe272eaa71c2f8220b29ee6e78cb0f4e8513e53'
7
- data.tar.gz: e997bace782a9affd36a92b80de749b4aaa708caf6f8323023f0b651c8d98bfc08f6911c9946a3baa0a807d4c8020b119459523c7afd6ba66adf038c28973b96
6
+ metadata.gz: 5fae42738060a5ab6a0f8d3dca97703e025dd1a131d474ea807fefa251d88e0ff1098c09993420f55480cb3469c26db5abfe21d93f53ddb23b920aad162bc6f6
7
+ data.tar.gz: a24973afcb9f2ec6aad8a5219f23493fcc821d760e6f3fc602bf97c98e60ef05de33461f95cef188452c474bd54b7be3694c050eaea52b50548f5d08f325b56d
@@ -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. Add a new column for the ciphertext, then add to your model:
154
+ Lockbox makes it easy to encrypt an existing column without downtime.
155
+
156
+ Add a new column for the ciphertext, then add to your model:
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).
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -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
- if options
87
+ # only trust the metadata when migrating
88
+ # as earlier versions of Lockbox won't have it
89
+ # and it's not a good practice to trust modifiable data
90
+ encrypted = options && (!options[:migrating] || blob.metadata["encrypted"])
91
+ if encrypted
98
92
  result = Utils.decrypt_result(record, name, options, result)
99
93
  end
100
94
 
@@ -105,7 +99,11 @@ module Lockbox
105
99
  def open(**options)
106
100
  blob.open(**options) do |file|
107
101
  options = Utils.encrypted_options(record, name)
108
- if options
102
+ # only trust the metadata when migrating
103
+ # as earlier versions of Lockbox won't have it
104
+ # and it's not a good practice to trust modifiable data
105
+ encrypted = options && (!options[:migrating] || blob.metadata["encrypted"])
106
+ if encrypted
109
107
  result = Utils.decrypt_result(record, name, options, file.read)
110
108
  file.rewind
111
109
  # truncate may not be available on all platforms
@@ -120,16 +118,6 @@ module Lockbox
120
118
  end
121
119
  end
122
120
  end
123
-
124
- def mark_analyzed
125
- if Utils.encrypted_options(record, name)
126
- blob.update!(metadata: blob.metadata.merge(analyzed: true))
127
- end
128
- end
129
-
130
- included do
131
- after_save :mark_analyzed
132
- end
133
121
  end
134
122
 
135
123
  module Blob
@@ -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(with_notification("encrypt_file") { lockbox.encrypt_io(file) })
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
- with_notification("decrypt_file") { lockbox.decrypt(r) } if r
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 with_notification(type)
61
+ def lockbox_notify(type)
62
62
  if defined?(ActiveSupport::Notifications)
63
63
  name = lockbox_name
64
64
 
@@ -13,7 +13,7 @@ module Lockbox
13
13
  end
14
14
 
15
15
  def encrypt(message, **options)
16
- message = check_string(message, "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, "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, name)
69
+ def check_string(str)
70
70
  str = str.read if str.respond_to?(:read)
71
- raise TypeError, "can't convert #{name} to string" unless str.respond_to?(:to_str)
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
@@ -24,26 +24,49 @@ module Lockbox
24
24
 
25
25
  # TODO add attributes option
26
26
  def migrate(restart:)
27
- fields = model.lockbox_attributes.select { |k, v| v[:migrating] }
27
+ fields = model.respond_to?(:lockbox_attributes) ? model.lockbox_attributes.select { |k, v| v[:migrating] } : {}
28
28
 
29
29
  # need blind indexes for building relation
30
30
  blind_indexes = model.respond_to?(:blind_indexes) ? model.blind_indexes.select { |k, v| v[:migrating] } : {}
31
31
 
32
- perform(fields: fields, blind_indexes: blind_indexes, restart: restart)
32
+ attachments = model.respond_to?(:lockbox_attachments) ? model.lockbox_attachments.select { |k, v| v[:migrating] } : {}
33
+
34
+ perform(fields: fields, blind_indexes: blind_indexes, restart: restart) if fields.any? || blind_indexes.any?
35
+ perform_attachments(attachments: attachments, restart: restart) if attachments.any?
33
36
  end
34
37
 
35
38
  private
36
39
 
37
- def perform(fields:, blind_indexes: [], restart: true, rotate: false)
38
- relation = @relation
40
+ def perform_attachments(attachments:, restart:)
41
+ relation = base_relation
39
42
 
40
- # unscope if passed a model
41
- unless ar_relation?(relation) || mongoid_relation?(relation)
42
- relation = relation.unscoped
43
+ # eager load attachments
44
+ attachments.each_key do |k|
45
+ relation = relation.send("with_attached_#{k}")
43
46
  end
44
47
 
45
- # convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
46
- relation = relation.all
48
+ each_batch(relation) do |records|
49
+ records.each do |record|
50
+ attachments.each_key do |k|
51
+ attachment = record.send(k)
52
+ if attachment.attached?
53
+ if attachment.is_a?(ActiveStorage::Attached::One)
54
+ unless attachment.metadata["encrypted"]
55
+ attachment.rotate_encryption!
56
+ end
57
+ else
58
+ unless attachment.all? { |a| a.metadata["encrypted"] }
59
+ attachment.rotate_encryption!
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def perform(fields:, blind_indexes: [], restart: true, rotate: false)
69
+ relation = base_relation
47
70
 
48
71
  unless restart
49
72
  attributes = fields.map { |_, v| v[:encrypted_attribute] }
@@ -138,6 +161,18 @@ module Lockbox
138
161
  end
139
162
  end
140
163
 
164
+ def base_relation
165
+ relation = @relation
166
+
167
+ # unscope if passed a model
168
+ unless ar_relation?(relation) || mongoid_relation?(relation)
169
+ relation = relation.unscoped
170
+ end
171
+
172
+ # convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
173
+ relation.all
174
+ end
175
+
141
176
  def ar_relation?(relation)
142
177
  defined?(ActiveRecord::Relation) && relation.is_a?(ActiveRecord::Relation)
143
178
  end
@@ -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
- send(lockbox_attribute[: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])
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.4.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
- type = (try(:attribute_types) || {})[name.to_s]
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.4.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
- type = (try(:attribute_types) || {})[name.to_s]
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
@@ -5,18 +5,27 @@ module Lockbox
5
5
 
6
6
  if defined?(ActiveStorage)
7
7
  require "lockbox/active_storage_extensions"
8
+
8
9
  ActiveStorage::Attached.prepend(Lockbox::ActiveStorageExtensions::Attached)
9
10
  if ActiveStorage::VERSION::MAJOR >= 6
10
11
  ActiveStorage::Attached::Changes::CreateOne.prepend(Lockbox::ActiveStorageExtensions::CreateOne)
11
12
  end
12
13
  ActiveStorage::Attached::One.prepend(Lockbox::ActiveStorageExtensions::AttachedOne)
13
14
  ActiveStorage::Attached::Many.prepend(Lockbox::ActiveStorageExtensions::AttachedMany)
14
- end
15
15
 
16
- app.config.to_prepare do
17
- if defined?(ActiveStorage)
18
- ActiveStorage::Attachment.include(Lockbox::ActiveStorageExtensions::Attachment)
19
- ActiveStorage::Blob.prepend(Lockbox::ActiveStorageExtensions::Blob)
16
+ # use load hooks when possible
17
+ if ActiveStorage::VERSION::MAJOR >= 6
18
+ ActiveSupport.on_load(:active_storage_attachment) do
19
+ include Lockbox::ActiveStorageExtensions::Attachment
20
+ end
21
+ ActiveSupport.on_load(:active_storage_blob) do
22
+ prepend Lockbox::ActiveStorageExtensions::Blob
23
+ end
24
+ else
25
+ app.config.to_prepare do
26
+ ActiveStorage::Attachment.include(Lockbox::ActiveStorageExtensions::Attachment)
27
+ ActiveStorage::Blob.prepend(Lockbox::ActiveStorageExtensions::Blob)
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -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.is_a?(Proc)
8
- options[k] = context.instance_exec(&v) if v.respond_to?(:call)
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, "Key must use binary encoding" if key.encoding != Encoding::BINARY
40
- raise Lockbox::Error, "Key must be 32 bytes" if key.bytesize != size
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
- io: box.encrypt_io(io),
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
- raise NotImplementedError, "Not supported"
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
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lockbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
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-05-04 00:00:00.000000000 Z
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