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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe55b94af6abc4b2cf562badea225c8aff715ae3be8cc40e81f645ae6c0a6a9b
4
- data.tar.gz: dd6c5dbd4d3280548212ca145f56b99909b5d4af07784bc095fc1df6afd156ff
3
+ metadata.gz: 2fb82c6baf0c56ae7f051c8363f7cb52554ebab8180b891d4aa25caf7505cec3
4
+ data.tar.gz: 64b6bda4259cc3fddd7e5635333b0b7c4a6680c4ff2da31bba735603ea50b010
5
5
  SHA512:
6
- metadata.gz: 3a50542c82cfbb3cab24e6e18f2aaec1862b0a76dfef762b416710a67b17ec9171be6620b48bd59c50ef8d02f29bd195e860579e96fcbb8f2667cd151d277a56
7
- data.tar.gz: 42ce91cccba7bbed944a6454e076f7f8db08d107aa96da5e9a69e862faa0c465e27e4441007b9964e0ce953ce5294bbd3120d55e6b91aea516a2597ab9375e9d
6
+ metadata.gz: e1953d9159c2cb1f1ec55436d925738777ea0b78afedcb32864280f7772d1d9e12a48f65c69385fd6bcdab166668049b3889f1e17e3b316c696705bcb98650dd
7
+ data.tar.gz: a3559c399a385949526137eed03b73c38baae424bed3b597c89f287222e6e18fd2c5665b2cbba6b20dad71bf680f0840ee2753a68befd3af40fdb522e45e4088
@@ -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. 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:
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).
@@ -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
@@ -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
- 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
@@ -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
- type = (try(:attribute_types) || {})[name.to_s]
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
- type = (try(:attribute_types) || {})[name.to_s]
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
@@ -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.1"
2
+ VERSION = "0.4.6"
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.1
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-05-08 00:00:00.000000000 Z
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