lockbox 0.4.2 → 0.4.7

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: cb4f05b7519d563c465c963893ded5f21063f666bdb41d4bfc69f23ea6b82374
4
- data.tar.gz: 0f13b613b16b15ca6514de06fc57ec62d98b94a06a01eb6d8e77fdce9d23343b
3
+ metadata.gz: 3f8c447dd90537203a1a3038c347c10de0e48f5b29795b382a2a77019e6e5764
4
+ data.tar.gz: 9133e9eb0c2132b7c77c39f8c24a3c27ea9b3cbb1d3d82f7f069b2db9992198f
5
5
  SHA512:
6
- metadata.gz: bdb2f4cbf5977bd1e40c9055c6e166c819b8e13ccbdd2d870c80ee907608205f72f65faa9fe56e882fcf9eab155b506537f29bd7593c3fcf31e3a0f94740e0ec
7
- data.tar.gz: ef966eff17c633ee5b590742c33a107bd58da4d262fbaf1bea458c2e5146dc16993f75b6d783118f3ff7f7fd827913423c71558bf1e8d60df66e3de35627e1a1
6
+ metadata.gz: 4396ee4ead0de0592e7a3574b563f98d58f0402198dd8662cbdc374cc5f8a39e629f6397414b498456c665e8a635518db3a532056ecd42154206fde4ab938e5c
7
+ data.tar.gz: 6057ea6f43db261580a0ee0ae13f1303251d2ee8ef2b1bcdba8399b6a858dca1fdf6c84e25f5da85a059a6d260be7094969e2118ff11a8c1d2565617c48f74cd
@@ -1,3 +1,27 @@
1
+ ## 0.4.7 (2020-08-18)
2
+
3
+ - Added `lockbox_options` method to encrypted CarrierWave uploaders
4
+ - Improved attribute loading when no decryption key specified
5
+
6
+ ## 0.4.6 (2020-07-02)
7
+
8
+ - Added support for `update_column` and `update_columns`
9
+
10
+ ## 0.4.5 (2020-06-26)
11
+
12
+ - Improved error message for non-string values
13
+ - Fixed error with migrating Action Text
14
+ - Fixed error with migrating serialized attributes
15
+
16
+ ## 0.4.4 (2020-06-23)
17
+
18
+ - Added support for `pluck`
19
+
20
+ ## 0.4.3 (2020-05-26)
21
+
22
+ - Improved error message for bad key length
23
+ - Fixed missing attribute error
24
+
1
25
  ## 0.4.2 (2020-05-11)
2
26
 
3
27
  - Added experimental support for migrating Active Storage files
data/README.md CHANGED
@@ -190,6 +190,8 @@ end
190
190
 
191
191
  ## Action Text
192
192
 
193
+ **Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
194
+
193
195
  Create a migration with:
194
196
 
195
197
  ```ruby
@@ -264,8 +266,9 @@ end
264
266
 
265
267
  There are a few limitations to be aware of:
266
268
 
267
- - Metadata like image width and height are not extracted when encrypted
268
- - Direct uploads cannot be encrypted
269
+ - Variants and previews aren’t supported when encrypted
270
+ - Metadata like image width and height aren’t extracted when encrypted
271
+ - Direct uploads can’t be encrypted with application-level encryption like Lockbox, but can use server-side encryption
269
272
 
270
273
  To serve encrypted files, use a controller action.
271
274
 
@@ -278,7 +281,7 @@ end
278
281
 
279
282
  #### Migrating Existing Files [experimental]
280
283
 
281
- **Note:** This feature is experimental. Please try it in a non-production environment and let us know how it goes.
284
+ **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
285
 
283
286
  Lockbox makes it easy to encrypt existing files without downtime.
284
287
 
@@ -508,6 +511,24 @@ Lockbox.rotate(User, attributes: [:email])
508
511
 
509
512
  Once all records are rotated, you can remove `previous_versions` from the model.
510
513
 
514
+ ### Action Text
515
+
516
+ Update your initializer:
517
+
518
+ ```ruby
519
+ Lockbox.encrypts_action_text_body(previous_versions: [{key: previous_key}])
520
+ ```
521
+
522
+ Use `master_key` instead of `key` if passing the master key.
523
+
524
+ To rotate existing records, use:
525
+
526
+ ```ruby
527
+ Lockbox.rotate(ActionText::RichText, attributes: [:body])
528
+ ```
529
+
530
+ Once all records are rotated, you can remove `previous_versions` from the initializer.
531
+
511
532
  ### Active Storage
512
533
 
513
534
  Update your model:
@@ -550,6 +571,14 @@ User.find_each do |user|
550
571
  end
551
572
  ```
552
573
 
574
+ For multiple files, use:
575
+
576
+ ```ruby
577
+ User.find_each do |user|
578
+ user.licenses.map(&:rotate_encryption!)
579
+ end
580
+ ```
581
+
553
582
  Once all files are rotated, you can remove `previous_versions` from the model.
554
583
 
555
584
  ### Local Files & Strings
@@ -734,12 +763,32 @@ end
734
763
 
735
764
  You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
736
765
 
766
+ For Active Record and Mongoid, use:
767
+
737
768
  ```ruby
738
769
  class User < ApplicationRecord
739
770
  encrypts :email, key: :kms_key
740
771
  end
741
772
  ```
742
773
 
774
+ For Action Text, use:
775
+
776
+ ```ruby
777
+ ActiveSupport.on_load(:action_text_rich_text) do
778
+ ActionText::RichText.has_kms_key
779
+ end
780
+
781
+ Lockbox.encrypts_action_text_body(key: :kms_key)
782
+ ```
783
+
784
+ For Active Storage, use:
785
+
786
+ ```ruby
787
+ class User < ApplicationRecord
788
+ encrypts_attached :license, key: :kms_key
789
+ end
790
+ ```
791
+
743
792
  For CarrierWave, use:
744
793
 
745
794
  ```ruby
@@ -772,7 +821,7 @@ lockbox.encrypt("clear").bytesize # 44
772
821
  lockbox.encrypt("consider").bytesize # 44
773
822
  ```
774
823
 
775
- The block size for padding is 16 bytes by default. If we have a status larger than 15 bytes, it will have a different length than the others.
824
+ The block size for padding is 16 bytes by default. Lockbox uses [ISO/IEC 7816-4](https://en.wikipedia.org/wiki/Padding_(cryptography)#ISO/IEC_7816-4) padding, which uses at least one byte, so if we have a status larger than 15 bytes, it will have a different length than the others.
776
825
 
777
826
  ```ruby
778
827
  box.encrypt("length15status!").bytesize # 44
@@ -785,9 +834,25 @@ Change the block size with:
785
834
  Lockbox.new(padding: 32) # bytes
786
835
  ```
787
836
 
837
+ ## Associated Data
838
+
839
+ You can pass extra context during encryption to make sure encrypted data isn’t moved to a different context.
840
+
841
+ ```ruby
842
+ lockbox = Lockbox.new(key: key)
843
+ ciphertext = lockbox.encrypt(message, associated_data: "somecontext")
844
+ ```
845
+
846
+ Without the same context, decryption will fail.
847
+
848
+ ```ruby
849
+ lockbox.decrypt(ciphertext, associated_data: "somecontext") # success
850
+ lockbox.decrypt(ciphertext, associated_data: "othercontext") # fails
851
+ ```
852
+
788
853
  ## Binary Columns
789
854
 
790
- You can use `binary` columns for the ciphertext instead of `text` columns to save space.
855
+ You can use `binary` columns for the ciphertext instead of `text` columns.
791
856
 
792
857
  ```ruby
793
858
  class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
@@ -797,7 +862,7 @@ class AddEmailCiphertextToUsers < ActiveRecord::Migration[6.0]
797
862
  end
798
863
  ```
799
864
 
800
- You should disable Base64 encoding if you do this.
865
+ Disable Base64 encoding to save space.
801
866
 
802
867
  ```ruby
803
868
  class User < ApplicationRecord
@@ -965,3 +1030,5 @@ cd lockbox
965
1030
  bundle install
966
1031
  bundle exec rake test
967
1032
  ```
1033
+
1034
+ 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
@@ -1,7 +1,22 @@
1
- # ideally encrypt and decrypt would happen at the blob/service level
2
- # however, there isn't really a great place to define encryption settings there
3
- # instead, we encrypt and decrypt at the attachment level,
4
- # and we define encryption settings at the model level
1
+ # Ideally encryption and decryption would happen at the blob/service level.
2
+ # However, Active Storage < 6.1 only supports a single service (per environment).
3
+ # This means all attachments need to be encrypted or none of them,
4
+ # which is often not practical.
5
+ #
6
+ # Active Storage 6.1 adds support for multiple services, which changes this.
7
+ # We could have a Lockbox service:
8
+ #
9
+ # lockbox:
10
+ # service: Lockbox
11
+ # backend: local # delegate to another service, like mirror service
12
+ # key: ... # Lockbox options
13
+ #
14
+ # However, the checksum is computed *and stored on the blob*
15
+ # before the file is passed to the service.
16
+ # We don't want the MD5 checksum of the plaintext stored in the database.
17
+ #
18
+ # Instead, we encrypt and decrypt at the attachment level,
19
+ # and we define encryption settings at the model level.
5
20
  module Lockbox
6
21
  module ActiveStorageExtensions
7
22
  module Attached
@@ -95,6 +110,16 @@ module Lockbox
95
110
  result
96
111
  end
97
112
 
113
+ def variant(*args)
114
+ raise Lockbox::Error, "Variant not supported for encrypted files" if Utils.encrypted_options(record, name)
115
+ super
116
+ end
117
+
118
+ def preview(*args)
119
+ raise Lockbox::Error, "Preview not supported for encrypted files" if Utils.encrypted_options(record, name)
120
+ super
121
+ end
122
+
98
123
  if ActiveStorage::VERSION::MAJOR >= 6
99
124
  def open(**options)
100
125
  blob.open(**options) do |file|
@@ -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
@@ -2,18 +2,32 @@ module Lockbox
2
2
  module CarrierWaveExtensions
3
3
  def encrypt(**options)
4
4
  class_eval do
5
+ # uses same hook as process (before cache)
6
+ # processing can be disabled, so better to keep separate
5
7
  before :cache, :encrypt
6
8
 
9
+ define_singleton_method :lockbox_options do
10
+ options
11
+ end
12
+
7
13
  def encrypt(file)
8
- @file = CarrierWave::SanitizedFile.new(with_notification("encrypt_file") { lockbox.encrypt_io(file) })
14
+ # safety check
15
+ # see CarrierWave::Uploader::Cache#cache!
16
+ raise Lockbox::Error, "Expected files to be equal. Please report an issue." unless file && @file && file == @file
17
+
18
+ # processors in CarrierWave move updated file to current_path
19
+ # however, this causes versions to use the processed file
20
+ # we only want to change the file for the current version
21
+ @file = CarrierWave::SanitizedFile.new(lockbox_notify("encrypt_file") { lockbox.encrypt_io(file) })
9
22
  end
10
23
 
11
24
  # TODO safe to memoize?
12
25
  def read
13
26
  r = super
14
- with_notification("decrypt_file") { lockbox.decrypt(r) } if r
27
+ lockbox_notify("decrypt_file") { lockbox.decrypt(r) } if r
15
28
  end
16
29
 
30
+ # use size of plaintext since read and content type use plaintext
17
31
  def size
18
32
  read.bytesize
19
33
  end
@@ -23,6 +37,7 @@ module Lockbox
23
37
  MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
24
38
  end
25
39
 
40
+ # disable processing since already processed
26
41
  def rotate_encryption!
27
42
  io = Lockbox::IO.new(read)
28
43
  io.original_filename = file.filename
@@ -46,6 +61,8 @@ module Lockbox
46
61
  end
47
62
  end
48
63
 
64
+ # for mounted uploaders, use mounted name
65
+ # for others, use uploader name
49
66
  def lockbox_name
50
67
  if mounted_as
51
68
  mounted_as.to_s
@@ -58,7 +75,9 @@ module Lockbox
58
75
  end
59
76
  end
60
77
 
61
- def with_notification(type)
78
+ # Active Support notifications so it's easier
79
+ # to see when files are encrypted and decrypted
80
+ def lockbox_notify(type)
62
81
  if defined?(ActiveSupport::Notifications)
63
82
  name = lockbox_name
64
83
 
@@ -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
@@ -116,6 +116,13 @@ module Lockbox
116
116
  end
117
117
  end
118
118
 
119
+ # there's a small chance for this process to read data,
120
+ # another process to update the data, and
121
+ # this process to write the now stale data
122
+ # this time window can be reduced with smaller batch sizes
123
+ # locking individual records could eliminate this
124
+ # one option is: relation.in_batches { |batch| batch.lock }
125
+ # which runs SELECT ... FOR UPDATE in Postgres
119
126
  def migrate_records(records, fields:, blind_indexes:, restart:, rotate:)
120
127
  # do computation outside of transaction
121
128
  # especially expensive blind index computation
@@ -87,7 +87,13 @@ 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
+ # don't try to decrypt if no decryption key given
91
+ next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
92
+
93
+ # it is possible that the encrypted attribute is not loaded, eg.
94
+ # if the record was fetched partially (`User.select(:id).first`).
95
+ # accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
96
+ send(lockbox_attribute[:attribute]) if has_attribute?(lockbox_attribute[:encrypted_attribute])
91
97
  end
92
98
  super
93
99
  end
@@ -104,6 +110,49 @@ module Lockbox
104
110
  end
105
111
  end
106
112
  end
113
+
114
+ def update_columns(attributes)
115
+ return super unless attributes.is_a?(Hash)
116
+
117
+ # transform keys like Active Record
118
+ attributes = attributes.transform_keys do |key|
119
+ n = key.to_s
120
+ self.class.attribute_aliases[n] || n
121
+ end
122
+
123
+ lockbox_attributes = self.class.lockbox_attributes.slice(*attributes.keys.map(&:to_sym))
124
+ return super unless lockbox_attributes.any?
125
+
126
+ attributes_to_set = {}
127
+
128
+ lockbox_attributes.each do |key, lockbox_attribute|
129
+ attribute = key.to_s
130
+ # check read only
131
+ verify_readonly_attribute(attribute)
132
+
133
+ message = attributes[attribute]
134
+ attributes.delete(attribute) unless lockbox_attribute[:migrating]
135
+ encrypted_attribute = lockbox_attribute[:encrypted_attribute]
136
+ ciphertext = self.class.send("generate_#{encrypted_attribute}", message, context: self)
137
+ attributes[encrypted_attribute] = ciphertext
138
+ attributes_to_set[attribute] = message
139
+ attributes_to_set[lockbox_attribute[:attribute]] = message if lockbox_attribute[:migrating]
140
+ end
141
+
142
+ result = super(attributes)
143
+
144
+ # same logic as Active Record
145
+ # (although this happens before saving)
146
+ attributes_to_set.each do |k, v|
147
+ if respond_to?(:write_attribute_without_type_cast, true)
148
+ write_attribute_without_type_cast(k, v)
149
+ else
150
+ raw_write_attribute(k, v)
151
+ end
152
+ end
153
+
154
+ result
155
+ end
107
156
  else
108
157
  def reload
109
158
  self.class.lockbox_attributes.each do |_, v|
@@ -143,6 +192,10 @@ module Lockbox
143
192
  # however, we can try to use the original type if its already defined
144
193
  if attributes_to_define_after_schema_loads.key?(original_name.to_s)
145
194
  attribute name, attributes_to_define_after_schema_loads[original_name.to_s].first
195
+ elsif options[:migrating]
196
+ # we use the original attribute for serialization in the encrypt and decrypt methods
197
+ # so we can use a generic value here
198
+ attribute name, ActiveRecord::Type::Value.new
146
199
  else
147
200
  attribute name, :string
148
201
  end
@@ -213,12 +266,13 @@ module Lockbox
213
266
  define_method("#{name}=") do |message|
214
267
  # decrypt first for dirty tracking
215
268
  # don't raise error if can't decrypt previous
216
- begin
217
- send(name)
218
- rescue Lockbox::DecryptionError
219
- # this is expected for hybrid cryptography
220
- warn "[lockbox] Decrypting previous value failed" unless options[:algorithm] == "hybrid"
221
- nil
269
+ # don't try to decrypt if no decryption key given
270
+ unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
271
+ begin
272
+ send(name)
273
+ rescue Lockbox::DecryptionError
274
+ warn "[lockbox] Decrypting previous value failed"
275
+ end
222
276
  end
223
277
 
224
278
  send("lockbox_direct_#{name}=", message)
@@ -267,7 +321,7 @@ module Lockbox
267
321
  # cache
268
322
  # decrypt method does type casting
269
323
  if respond_to?(:write_attribute_without_type_cast, true)
270
- write_attribute_without_type_cast(name, message) if !@attributes.frozen?
324
+ write_attribute_without_type_cast(name.to_s, message) if !@attributes.frozen?
271
325
  else
272
326
  raw_write_attribute(name, message) if !@attributes.frozen?
273
327
  end
@@ -316,7 +370,8 @@ module Lockbox
316
370
  # do nothing
317
371
  # encrypt will convert to binary
318
372
  else
319
- type = (try(:attribute_types) || {})[name.to_s]
373
+ # use original name for serialized attributes
374
+ type = (try(:attribute_types) || {})[original_name.to_s]
320
375
  message = type.serialize(message) if type
321
376
  end
322
377
  end
@@ -358,7 +413,8 @@ module Lockbox
358
413
  # do nothing
359
414
  # decrypt returns binary string
360
415
  else
361
- type = (try(:attribute_types) || {})[name.to_s]
416
+ # use original name for serialized attributes
417
+ type = (try(:attribute_types) || {})[original_name.to_s]
362
418
  message = type.deserialize(message) if type
363
419
  message.force_encoding(Encoding::UTF_8) if !type || type.is_a?(ActiveModel::Type::String)
364
420
  end
@@ -4,22 +4,26 @@ 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
13
17
 
14
18
  unless options[:key] || options[:encryption_key] || options[:decryption_key]
15
- options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key))
19
+ options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key), encode: false)
16
20
  end
17
21
 
18
22
  if options[:previous_versions].is_a?(Array)
19
23
  options[:previous_versions] = options[:previous_versions].dup
20
24
  options[:previous_versions].each_with_index do |version, i|
21
25
  if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[:master_key]
22
- options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key)))
26
+ options[:previous_versions][i] = version.merge(key: Lockbox.attribute_key(table: table, attribute: attribute, master_key: version.delete(:master_key), encode: false))
23
27
  end
24
28
  end
25
29
  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
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.7"
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.2
4
+ version: 0.4.7
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-11 00:00:00.000000000 Z
11
+ date: 2020-08-19 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