lockbox 0.4.7 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f8c447dd90537203a1a3038c347c10de0e48f5b29795b382a2a77019e6e5764
4
- data.tar.gz: 9133e9eb0c2132b7c77c39f8c24a3c27ea9b3cbb1d3d82f7f069b2db9992198f
3
+ metadata.gz: 723ecf0e8367d053e2e80afddfa954559901f1b8c44d3a7cdb3ad562b3f5135a
4
+ data.tar.gz: 6c512616214fa1fdca743539769e1d2bd5f91135337a904b89ba58273dc9dbc5
5
5
  SHA512:
6
- metadata.gz: 4396ee4ead0de0592e7a3574b563f98d58f0402198dd8662cbdc374cc5f8a39e629f6397414b498456c665e8a635518db3a532056ecd42154206fde4ab938e5c
7
- data.tar.gz: 6057ea6f43db261580a0ee0ae13f1303251d2ee8ef2b1bcdba8399b6a858dca1fdf6c84e25f5da85a059a6d260be7094969e2118ff11a8c1d2565617c48f74cd
6
+ metadata.gz: 440177ac7cbe84f4e20eeedffe3045060debab17f178631bcba0b631e2e4c656bce1c0977f3af9dc84ce06788cf370626fed9053d3a4503632bc3daa0b6ab43d
7
+ data.tar.gz: ab23683aa75ff078b88cdfb65f29564691f5785b49549fa1d9286baf11a9959f7d49f0a21a7c84d10dd7d181c8affd05ca739769cdbe68193e4fb211f9a932af
@@ -1,3 +1,35 @@
1
+ ## 0.6.1 (2020-12-03)
2
+
3
+ - Added integration with Rails credentials
4
+ - Fixed in place changes for Active Record 6.1
5
+ - Fixed error with `content_type` method for CarrierWave < 2
6
+
7
+ ## 0.6.0 (2020-12-03)
8
+
9
+ - Added `encrypted` flag to Active Storage metadata
10
+ - Added encrypted columns to `filter_attributes`
11
+ - Improved `inspect` method
12
+
13
+ ## 0.5.0 (2020-11-22)
14
+
15
+ - Improved error messages for hybrid cryptography
16
+ - Changed warning to error when no attributes specified
17
+ - Fixed issue with `pluck` when migrating
18
+ - Fixed error with `key_table` and `key_attribute` options with `previous_versions`
19
+
20
+ ## 0.4.9 (2020-10-01)
21
+
22
+ - Added `key_table` and `key_attribute` options to `previous_versions`
23
+ - Added `encrypted_attribute` option
24
+ - Added support for encrypting empty string
25
+ - Improved `inspect` for models with encrypted attributes
26
+
27
+ ## 0.4.8 (2020-08-30)
28
+
29
+ - Added `key_table` and `key_attribute` options
30
+ - Added warning when no attributes specified
31
+ - Fixed error when Active Support partially loaded
32
+
1
33
  ## 0.4.7 (2020-08-18)
2
34
 
3
35
  - Added `lockbox_options` method to encrypted CarrierWave uploaders
data/README.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # Lockbox
2
2
 
3
- :package: Modern encryption for Rails
3
+ :package: Modern encryption for Ruby and Rails
4
4
 
5
- - Uses state-of-the-art algorithms
6
5
  - Works with database fields, files, and strings
6
+ - Maximizes compatibility with existing code and libraries
7
7
  - Makes migrating existing data and key rotation easy
8
-
9
- Lockbox aims to make encryption as friendly and intuitive as possible. Encrypted fields and files behave just like unencrypted ones for maximum compatibility with 3rd party libraries and existing code.
8
+ - Has zero dependencies and many integrations
10
9
 
11
10
  Learn [the principles behind it](https://ankane.org/modern-encryption-rails), [how to secure emails with Devise](https://ankane.org/securing-user-emails-lockbox), and [how to secure sensitive data in Rails](https://ankane.org/sensitive-data-rails).
12
11
 
13
- [![Build Status](https://travis-ci.org/ankane/lockbox.svg?branch=master)](https://travis-ci.org/ankane/lockbox)
12
+ [![Build Status](https://github.com/ankane/lockbox/workflows/build/badge.svg?branch=master)](https://github.com/ankane/lockbox/actions)
14
13
 
15
14
  ## Installation
16
15
 
@@ -36,10 +35,17 @@ Set the following environment variable with your key (you can use this one in de
36
35
  LOCKBOX_MASTER_KEY=0000000000000000000000000000000000000000000000000000000000000000
37
36
  ```
38
37
 
38
+ or add it to your credentials for each environment (`rails credentials:edit --environment <env>` for Rails 6+)
39
+
40
+ ```yml
41
+ lockbox:
42
+ master_key: "0000000000000000000000000000000000000000000000000000000000000000"
43
+ ```
44
+
39
45
  or create `config/initializers/lockbox.rb` with something like
40
46
 
41
47
  ```ruby
42
- Lockbox.master_key = Rails.application.credentials.lockbox_master_key
48
+ Lockbox.master_key = Rails.application.credentials.lockbox[:master_key]
43
49
  ```
44
50
 
45
51
  Then follow the instructions below for the data you want to encrypt.
@@ -89,6 +95,16 @@ User.create!(email: "hi@example.org")
89
95
 
90
96
  If you need to query encrypted fields, check out [Blind Index](https://github.com/ankane/blind_index).
91
97
 
98
+ #### Multiple Fields
99
+
100
+ You can specify multiple fields in single line.
101
+
102
+ ```ruby
103
+ class User < ApplicationRecord
104
+ encrypts :email, :phone, :city
105
+ end
106
+ ```
107
+
92
108
  #### Types
93
109
 
94
110
  Fields are strings by default. Specify the type of a field with:
@@ -188,6 +204,42 @@ class User < ApplicationRecord
188
204
  end
189
205
  ```
190
206
 
207
+ #### Model Changes
208
+
209
+ If tracking changes to model attributes, be sure to remove or redact encrypted attributes.
210
+
211
+ PaperTrail
212
+
213
+ ```ruby
214
+ class User < ApplicationRecord
215
+ # for an encrypted history (still tracks ciphertext changes)
216
+ has_paper_trail skip: [:email]
217
+
218
+ # for no history (add blind indexes as well)
219
+ has_paper_trail skip: [:email, :email_ciphertext]
220
+ end
221
+ ```
222
+
223
+ Audited
224
+
225
+ ```ruby
226
+ class User < ApplicationRecord
227
+ # for an encrypted history (still tracks ciphertext changes)
228
+ audited except: [:email]
229
+
230
+ # for no history (add blind indexes as well)
231
+ audited except: [:email, :email_ciphertext]
232
+ end
233
+ ```
234
+
235
+ #### Decryption
236
+
237
+ To decrypt data outside the model, use:
238
+
239
+ ```ruby
240
+ User.decrypt_email_ciphertext(user.email_ciphertext)
241
+ ```
242
+
191
243
  ## Action Text
192
244
 
193
245
  **Note:** Action Text uses direct uploads for files, which cannot be encrypted with application-level encryption like Lockbox. This only encrypts the database field.
@@ -222,6 +274,10 @@ Lockbox.encrypts_action_text_body
222
274
 
223
275
  And drop the unencrypted column.
224
276
 
277
+ #### Options
278
+
279
+ You can pass any Lockbox options to the `encrypts_action_text_body` method.
280
+
225
281
  ## Mongoid
226
282
 
227
283
  Add to your model:
@@ -393,44 +449,58 @@ Finally, delete the unencrypted files and drop the column for the original uploa
393
449
 
394
450
  ## Shrine
395
451
 
396
- Generate a key
452
+ #### Models
453
+
454
+ Include the attachment as normal:
397
455
 
398
456
  ```ruby
399
- key = Lockbox.generate_key
457
+ class User < ApplicationRecord
458
+ include LicenseUploader::Attachment(:license)
459
+ end
400
460
  ```
401
461
 
402
- Create a lockbox
462
+ And encrypt in a controller (or background job, etc) with:
403
463
 
404
464
  ```ruby
405
- lockbox = Lockbox.new(key: key)
465
+ license = params.require(:user).fetch(:license)
466
+ lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
467
+ user.license = lockbox.encrypt_io(license)
406
468
  ```
407
469
 
408
- Encrypt files before passing them to Shrine
470
+ To serve encrypted files, use a controller action.
409
471
 
410
472
  ```ruby
411
- LicenseUploader.upload(lockbox.encrypt_io(file), :store)
473
+ def license
474
+ user = User.find(params[:id])
475
+ lockbox = Lockbox.new(key: Lockbox.attribute_key(table: "users", attribute: "license"))
476
+ send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
477
+ end
412
478
  ```
413
479
 
414
- And decrypt them after reading
480
+ #### Non-Models
481
+
482
+ Generate a key
415
483
 
416
484
  ```ruby
417
- lockbox.decrypt(uploaded_file.read)
485
+ key = Lockbox.generate_key
418
486
  ```
419
487
 
420
- For models, encrypt with:
488
+ Create a lockbox
421
489
 
422
490
  ```ruby
423
- license = params.require(:user).fetch(:license)
424
- user.license = lockbox.encrypt_io(license)
491
+ lockbox = Lockbox.new(key: key)
425
492
  ```
426
493
 
427
- To serve encrypted files, use a controller action.
494
+ Encrypt files before passing them to Shrine
428
495
 
429
496
  ```ruby
430
- def license
431
- user = User.find(params[:id])
432
- send_data lockbox.decrypt(user.license.read), type: user.license.mime_type
433
- end
497
+ LicenseUploader.upload(lockbox.encrypt_io(file), :store)
498
+ ```
499
+
500
+ And decrypt them after reading
501
+
502
+ ```ruby
503
+ lockbox.decrypt(uploaded_file.read)
434
504
  ```
435
505
 
436
506
  ## Local Files
@@ -635,6 +705,8 @@ This is the default algorithm. It’s:
635
705
  - an IETF standard
636
706
  - fast thanks to a [dedicated instruction set](https://en.wikipedia.org/wiki/AES_instruction_set)
637
707
 
708
+ Lockbox uses 256-bit keys.
709
+
638
710
  **For users who do a lot of encryptions:** You should rotate an individual key after 2 billion encryptions to minimize the chance of a [nonce collision](https://www.cryptologie.net/article/402/is-symmetric-security-solved/), which will expose the key. Each database field and file uploader use a different key (derived from the master key) to extend this window.
639
711
 
640
712
  ### XSalsa20
@@ -688,6 +760,22 @@ For Ubuntu 16.04, use:
688
760
  sudo apt-get install libsodium18
689
761
  ```
690
762
 
763
+ ##### GitHub Actions
764
+
765
+ For Ubuntu 20.04 and 18.04, use:
766
+
767
+ ```yml
768
+ - name: Install Libsodium
769
+ run: sudo apt-get update && sudo apt-get install libsodium23
770
+ ```
771
+
772
+ For Ubuntu 16.04, use:
773
+
774
+ ```yml
775
+ - name: Install Libsodium
776
+ run: sudo apt-get update && sudo apt-get install libsodium18
777
+ ```
778
+
691
779
  ##### Travis CI
692
780
 
693
781
  On Bionic, add to `.travis.yml`:
@@ -715,8 +803,7 @@ Add a step to `.circleci/config.yml`:
715
803
  ```yml
716
804
  - run:
717
805
  name: install Libsodium
718
- command: |
719
- sudo apt-get install -y libsodium18
806
+ command: sudo apt-get install -y libsodium18
720
807
  ```
721
808
 
722
809
  ## Hybrid Cryptography
@@ -743,15 +830,43 @@ Make sure `decryption_key` is `nil` on servers that shouldn’t decrypt.
743
830
 
744
831
  This uses X25519 for key exchange and XSalsa20 for encryption.
745
832
 
746
- ## Key Separation
833
+ ## Key Configuration
834
+
835
+ Lockbox supports a few different ways to set keys for database fields and files.
836
+
837
+ 1. Master key
838
+ 2. Per field/uploader
839
+ 3. Per record
747
840
 
748
- The master key is used to generate unique keys for each column. This technique comes from [CipherSweet](https://ciphersweet.paragonie.com/internals/key-hierarchy). The table name and column name are both used in this process. If you need to rename a table with encrypted columns, or an encrypted column itself, get the key:
841
+ ### Master Key
842
+
843
+ By default, the master key is used to generate unique keys for each field/uploader. This technique comes from [CipherSweet](https://ciphersweet.paragonie.com/internals/key-hierarchy). The table name and column/uploader name are both used in this process.
844
+
845
+ You can get an individual key with:
749
846
 
750
847
  ```ruby
751
848
  Lockbox.attribute_key(table: "users", attribute: "email_ciphertext")
752
849
  ```
753
850
 
754
- And set it directly before renaming:
851
+ To rename a table with encrypted columns/uploaders, use:
852
+
853
+ ```ruby
854
+ class User < ApplicationRecord
855
+ encrypts :email, key_table: "original_table"
856
+ end
857
+ ```
858
+
859
+ To rename an encrypted column itself, use:
860
+
861
+ ```ruby
862
+ class User < ApplicationRecord
863
+ encrypts :email, key_attribute: "original_column"
864
+ end
865
+ ```
866
+
867
+ ### Per Field/Uploader
868
+
869
+ To set a key for an individual field/uploader, use a string:
755
870
 
756
871
  ```ruby
757
872
  class User < ApplicationRecord
@@ -759,6 +874,32 @@ class User < ApplicationRecord
759
874
  end
760
875
  ```
761
876
 
877
+ Or a proc:
878
+
879
+ ```ruby
880
+ class User < ApplicationRecord
881
+ encrypts :email, key: -> { code }
882
+ end
883
+ ```
884
+
885
+ ### Per Record
886
+
887
+ To use a different key for each record, use a symbol:
888
+
889
+ ```ruby
890
+ class User < ApplicationRecord
891
+ encrypts :email, key: :some_method
892
+ end
893
+ ```
894
+
895
+ Or a proc:
896
+
897
+ ```ruby
898
+ class User < ApplicationRecord
899
+ encrypts :email, key: -> { some_method }
900
+ end
901
+ ```
902
+
762
903
  ## Key Management
763
904
 
764
905
  You can use a key management service to manage your keys with [KMS Encrypted](https://github.com/ankane/kms_encrypted).
@@ -870,12 +1011,6 @@ class User < ApplicationRecord
870
1011
  end
871
1012
  ```
872
1013
 
873
- or set it globally:
874
-
875
- ```ruby
876
- Lockbox.default_options = {encode: false}
877
- ```
878
-
879
1014
  ## Compatibility
880
1015
 
881
1016
  It’s easy to read encrypted data in another language if needed.
@@ -4,6 +4,7 @@ require "openssl"
4
4
  require "securerandom"
5
5
 
6
6
  # modules
7
+ require "lockbox/aes_gcm"
7
8
  require "lockbox/box"
8
9
  require "lockbox/calculations"
9
10
  require "lockbox/encryptor"
@@ -19,11 +20,18 @@ require "lockbox/version"
19
20
  require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
20
21
  require "lockbox/railtie" if defined?(Rails)
21
22
 
22
- if defined?(ActiveSupport)
23
+ if defined?(ActiveSupport::LogSubscriber)
23
24
  require "lockbox/log_subscriber"
24
25
  Lockbox::LogSubscriber.attach_to :lockbox
26
+ end
25
27
 
28
+ if defined?(ActiveSupport.on_load)
26
29
  ActiveSupport.on_load(:active_record) do
30
+ # TODO raise error in 0.7.0
31
+ if ActiveRecord::VERSION::STRING.to_f <= 5.0
32
+ warn "Active Record version (#{ActiveRecord::VERSION::STRING}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
33
+ end
34
+
27
35
  extend Lockbox::Model
28
36
  extend Lockbox::Model::Attached
29
37
  ActiveRecord::Calculations.prepend Lockbox::Calculations
@@ -89,6 +89,10 @@ module Lockbox
89
89
  module CreateOne
90
90
  def initialize(name, record, attachable)
91
91
  # this won't encrypt existing blobs
92
+ # ideally we'd check metadata for the encrypted flag
93
+ # and disallow unencrypted blobs
94
+ # since they'll raise an error on decryption
95
+ # but earlier versions of Lockbox won't have it
92
96
  attachable = Lockbox::Utils.encrypt_attachable(record, name, attachable) if Lockbox::Utils.encrypted?(record, name) && !attachable.is_a?(ActiveStorage::Blob)
93
97
  super(name, record, attachable)
94
98
  end
@@ -18,9 +18,10 @@ module Lockbox
18
18
  # In encryption mode, it must be set after calling #encrypt and setting #key= and #iv=
19
19
  cipher.auth_data = associated_data || ""
20
20
 
21
- ciphertext = cipher.update(message) + cipher.final
21
+ ciphertext = String.new
22
+ ciphertext << cipher.update(message) unless message.empty?
23
+ ciphertext << cipher.final
22
24
  ciphertext << cipher.auth_tag
23
-
24
25
  ciphertext
25
26
  end
26
27
 
@@ -29,7 +30,6 @@ module Lockbox
29
30
 
30
31
  fail_decryption if nonce.to_s.bytesize != nonce_bytes
31
32
  fail_decryption if auth_tag.to_s.bytesize != auth_tag_bytes
32
- fail_decryption if ciphertext.to_s.bytesize == 0
33
33
 
34
34
  cipher = OpenSSL::Cipher.new("aes-256-gcm")
35
35
  # do not change order of operations
@@ -43,7 +43,10 @@ module Lockbox
43
43
  cipher.auth_data = associated_data || ""
44
44
 
45
45
  begin
46
- cipher.update(ciphertext) + cipher.final
46
+ message = String.new
47
+ message << cipher.update(ciphertext) unless ciphertext.to_s.empty?
48
+ message << cipher.final
49
+ message
47
50
  rescue OpenSSL::Cipher::CipherError
48
51
  fail_decryption
49
52
  end
@@ -1,7 +1,7 @@
1
1
  module Lockbox
2
2
  class Box
3
3
  def initialize(key: nil, algorithm: nil, encryption_key: nil, decryption_key: nil, padding: false)
4
- raise ArgumentError, "Cannot pass both key and public/private key" if key && (encryption_key || decryption_key)
4
+ raise ArgumentError, "Cannot pass both key and encryption/decryption key" if key && (encryption_key || decryption_key)
5
5
 
6
6
  key = Lockbox::Utils.decode_key(key) if key
7
7
  encryption_key = Lockbox::Utils.decode_key(encryption_key, size: 64) if encryption_key
@@ -12,7 +12,6 @@ module Lockbox
12
12
  case algorithm
13
13
  when "aes-gcm"
14
14
  raise ArgumentError, "Missing key" unless key
15
- require "lockbox/aes_gcm"
16
15
  @box = AES_GCM.new(key)
17
16
  when "xchacha20"
18
17
  raise ArgumentError, "Missing key" unless key
@@ -39,7 +38,7 @@ module Lockbox
39
38
  message = Lockbox.pad(message, size: @padding) if @padding
40
39
  case @algorithm
41
40
  when "hybrid"
42
- raise ArgumentError, "No public key set" unless @encryption_box
41
+ raise ArgumentError, "No encryption key set" unless defined?(@encryption_box)
43
42
  raise ArgumentError, "Associated data not supported with this algorithm" if associated_data
44
43
  nonce = generate_nonce(@encryption_box)
45
44
  ciphertext = @encryption_box.encrypt(nonce, message)
@@ -58,7 +57,7 @@ module Lockbox
58
57
  message =
59
58
  case @algorithm
60
59
  when "hybrid"
61
- raise ArgumentError, "No private key set" unless @decryption_box
60
+ raise ArgumentError, "No decryption key set" unless defined?(@decryption_box)
62
61
  raise ArgumentError, "Associated data not supported with this algorithm" if associated_data
63
62
  nonce, ciphertext = extract_nonce(@decryption_box, ciphertext)
64
63
  @decryption_box.decrypt(nonce, ciphertext)
@@ -3,7 +3,8 @@ module Lockbox
3
3
  def pluck(*column_names)
4
4
  return super unless model.respond_to?(:lockbox_attributes)
5
5
 
6
- lockbox_columns = column_names.map.with_index { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select(&:first)
6
+ lockbox_columns = column_names.map.with_index { |c, i| [model.lockbox_attributes[c.to_sym], i] }.select { |la, _i| la && !la[:migrating] }
7
+
7
8
  return super unless lockbox_columns.any?
8
9
 
9
10
  # replace column with ciphertext column
@@ -32,9 +32,14 @@ module Lockbox
32
32
  read.bytesize
33
33
  end
34
34
 
35
- # based on CarrierWave::SanitizedFile#mime_magic_content_type
36
35
  def content_type
37
- MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
36
+ if CarrierWave::VERSION.to_i >= 2
37
+ # based on CarrierWave::SanitizedFile#mime_magic_content_type
38
+ MimeMagic.by_magic(read).try(:type) || "invalid/invalid"
39
+ else
40
+ # uses filename
41
+ super
42
+ end
38
43
  end
39
44
 
40
45
  # disable processing since already processed
@@ -97,4 +102,11 @@ module Lockbox
97
102
  end
98
103
  end
99
104
 
105
+ if CarrierWave::VERSION.to_i > 2
106
+ raise "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
107
+ elsif CarrierWave::VERSION.to_i < 1
108
+ # TODO raise error in 0.7.0
109
+ warn "CarrierWave version (#{CarrierWave::VERSION}) not supported in this version of Lockbox (#{Lockbox::VERSION})"
110
+ end
111
+
100
112
  CarrierWave::Uploader::Base.extend(Lockbox::CarrierWaveExtensions)
@@ -27,11 +27,19 @@ module Lockbox
27
27
  activerecord = defined?(ActiveRecord::Base) && self < ActiveRecord::Base
28
28
  raise ArgumentError, "Type not supported yet with Mongoid" if options[:type] && !activerecord
29
29
 
30
+ raise ArgumentError, "No attributes specified" if attributes.empty?
31
+
32
+ raise ArgumentError, "Cannot use key_attribute with multiple attributes" if options[:key_attribute] && attributes.size > 1
33
+
34
+ original_options = options.dup
35
+
30
36
  attributes.each do |name|
31
- # add default options
32
- encrypted_attribute = "#{name}_ciphertext"
37
+ # per attribute options
38
+ # TODO use a different name
39
+ options = original_options.dup
33
40
 
34
- options = options.dup
41
+ # add default options
42
+ encrypted_attribute = options.delete(:encrypted_attribute) || "#{name}_ciphertext"
35
43
 
36
44
  # migrating
37
45
  original_name = name.to_sym
@@ -47,6 +55,15 @@ module Lockbox
47
55
  decrypt_method_name = "decrypt_#{encrypted_attribute}"
48
56
 
49
57
  class_eval do
58
+ # Lockbox uses custom inspect
59
+ # but this could be useful for other gems
60
+ if activerecord && ActiveRecord::VERSION::MAJOR >= 6
61
+ # only add virtual attribute
62
+ # need to use regexp since strings do partial matching
63
+ # also, need to use += instead of <<
64
+ self.filter_attributes += [/\A#{Regexp.escape(options[:attribute])}\z/]
65
+ end
66
+
50
67
  @lockbox_attributes ||= {}
51
68
 
52
69
  if @lockbox_attributes.empty?
@@ -71,12 +88,42 @@ module Lockbox
71
88
  super(options)
72
89
  end
73
90
 
74
- # use same approach as devise
91
+ # maintain order
92
+ # replace ciphertext attributes w/ virtual attributes (filtered)
75
93
  def inspect
76
- inspection =
77
- serializable_hash.map do |k,v|
78
- "#{k}: #{respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : v.inspect}"
94
+ lockbox_attributes = {}
95
+ lockbox_encrypted_attributes = {}
96
+ self.class.lockbox_attributes.each do |_, lockbox_attribute|
97
+ lockbox_attributes[lockbox_attribute[:attribute]] = true
98
+ lockbox_encrypted_attributes[lockbox_attribute[:encrypted_attribute]] = lockbox_attribute[:attribute]
99
+ end
100
+
101
+ inspection = []
102
+ # use serializable_hash like Devise
103
+ values = serializable_hash
104
+ self.class.attribute_names.each do |k|
105
+ next if !has_attribute?(k) || lockbox_attributes[k]
106
+
107
+ # check for lockbox attribute
108
+ if lockbox_encrypted_attributes[k]
109
+ # check if ciphertext attribute nil to avoid loading attribute
110
+ v = send(k).nil? ? "nil" : "[FILTERED]"
111
+ k = lockbox_encrypted_attributes[k]
112
+ elsif values.key?(k)
113
+ v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect
114
+
115
+ # fix for https://github.com/rails/rails/issues/40725
116
+ # TODO only apply to Active Record 6.0
117
+ if respond_to?(:inspection_filter, true) && v != "nil"
118
+ v = inspection_filter.filter_param(k, v)
119
+ end
120
+ else
121
+ next
79
122
  end
123
+
124
+ inspection << "#{k}: #{v}"
125
+ end
126
+
80
127
  "#<#{self.class} #{inspection.join(", ")}>"
81
128
  end
82
129
 
@@ -164,6 +211,7 @@ module Lockbox
164
211
  end
165
212
 
166
213
  raise "Duplicate encrypted attribute: #{original_name}" if lockbox_attributes[original_name]
214
+ raise "Multiple encrypted attributes use the same column: #{encrypted_attribute}" if lockbox_attributes.any? { |_, v| v[:encrypted_attribute] == encrypted_attribute }
167
215
  @lockbox_attributes[original_name] = options
168
216
 
169
217
  if activerecord
@@ -199,6 +247,18 @@ module Lockbox
199
247
  else
200
248
  attribute name, :string
201
249
  end
250
+ else
251
+ # hack for Active Record 6.1
252
+ # to set string type after serialize
253
+ # otherwise, type gets set to ActiveModel::Type::Value
254
+ # which always returns false for changed_in_place?
255
+ # earlier versions of Active Record take the previous code path
256
+ if ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc)
257
+ attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call
258
+ if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil?
259
+ attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder)
260
+ end
261
+ end
202
262
  end
203
263
 
204
264
  define_method("#{name}_was") do
@@ -338,7 +398,7 @@ module Lockbox
338
398
  table = activerecord ? table_name : collection_name.to_s
339
399
 
340
400
  unless message.nil?
341
- # TODO use attribute type class in 0.5.0
401
+ # TODO use attribute type class in 0.7.0
342
402
  case options[:type]
343
403
  when :boolean
344
404
  message = ActiveRecord::Type::Boolean.new.serialize(message)
@@ -393,7 +453,7 @@ module Lockbox
393
453
  end
394
454
 
395
455
  unless message.nil?
396
- # TODO use attribute type class in 0.5.0
456
+ # TODO use attribute type class in 0.7.0
397
457
  case options[:type]
398
458
  when :boolean
399
459
  message = message == "t"
@@ -1,6 +1,10 @@
1
1
  module Lockbox
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "lockbox" do |app|
4
+ if defined?(Rails.application.credentials)
5
+ Lockbox.master_key ||= Rails.application.credentials.dig(:lockbox, :master_key)
6
+ end
7
+
4
8
  require "lockbox/carrier_wave_extensions" if defined?(CarrierWave)
5
9
 
6
10
  if defined?(ActiveStorage)
@@ -1,6 +1,7 @@
1
1
  module Lockbox
2
2
  class Utils
3
3
  def self.build_box(context, options, table, attribute)
4
+ # dup options (with except) since keys are sometimes changed or deleted
4
5
  options = options.except(:attribute, :encrypted_attribute, :migrating, :attached, :type)
5
6
  options[:encode] = false unless options.key?(:encode)
6
7
  options.each do |k, v|
@@ -16,14 +17,32 @@ module Lockbox
16
17
  end
17
18
 
18
19
  unless options[:key] || options[:encryption_key] || options[:decryption_key]
19
- options[:key] = Lockbox.attribute_key(table: table, attribute: attribute, master_key: options.delete(:master_key), encode: false)
20
+ options[:key] =
21
+ Lockbox.attribute_key(
22
+ table: options.delete(:key_table) || table,
23
+ attribute: options.delete(:key_attribute) || attribute,
24
+ master_key: options.delete(:master_key),
25
+ encode: false
26
+ )
20
27
  end
21
28
 
22
29
  if options[:previous_versions].is_a?(Array)
23
- options[:previous_versions] = options[:previous_versions].dup
30
+ # dup previous versions array (with map) since elements are updated
31
+ # dup each version (with dup) since keys are sometimes deleted
32
+ options[:previous_versions] = options[:previous_versions].map(&:dup)
24
33
  options[:previous_versions].each_with_index do |version, i|
25
- if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && version[: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))
34
+ if !(version[:key] || version[:encryption_key] || version[:decryption_key]) && (version[:master_key] || version[:key_table] || version[:key_attribute])
35
+ # could also use key_table and key_attribute from options
36
+ # when specified, but keep simple for now
37
+ # also, this change isn't backward compatible
38
+ key =
39
+ Lockbox.attribute_key(
40
+ table: version.delete(:key_table) || table,
41
+ attribute: version.delete(:key_attribute) || attribute,
42
+ master_key: version.delete(:master_key),
43
+ encode: false
44
+ )
45
+ options[:previous_versions][i] = version.merge(key: key)
27
46
  end
28
47
  end
29
48
  end
@@ -40,7 +59,7 @@ module Lockbox
40
59
  key = [key].pack("H*")
41
60
  end
42
61
 
43
- raise Lockbox::Error, "#{name} must be 32 bytes (64 hex digits)" if key.bytesize != size
62
+ raise Lockbox::Error, "#{name} must be #{size} bytes (#{size * 2} hex digits)" if key.bytesize != size
44
63
  raise Lockbox::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
45
64
 
46
65
  key
@@ -70,13 +89,11 @@ module Lockbox
70
89
  attachable = attachable.dup
71
90
  attachable[:io] = box.encrypt_io(io)
72
91
  else
73
- # TODO raise ArgumentError
74
- raise NotImplementedError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
92
+ raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
75
93
  end
76
94
 
77
95
  # don't analyze encrypted data
78
- metadata = {"analyzed" => true}
79
- metadata["encrypted"] = true if options[:migrating]
96
+ metadata = {"analyzed" => true, "encrypted" => true}
80
97
  attachable[:metadata] = (attachable[:metadata] || {}).merge(metadata)
81
98
  end
82
99
 
@@ -1,3 +1,3 @@
1
1
  module Lockbox
2
- VERSION = "0.4.7"
2
+ VERSION = "0.6.1"
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.7
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-19 00:00:00.000000000 Z
11
+ date: 2020-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -150,6 +150,34 @@ dependencies:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: shrine
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: shrine-mongoid
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: benchmark-ips
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -164,7 +192,7 @@ dependencies:
164
192
  - - ">="
165
193
  - !ruby/object:Gem::Version
166
194
  version: '0'
167
- description:
195
+ description:
168
196
  email: andrew@chartkick.com
169
197
  executables: []
170
198
  extensions: []
@@ -197,7 +225,7 @@ homepage: https://github.com/ankane/lockbox
197
225
  licenses:
198
226
  - MIT
199
227
  metadata: {}
200
- post_install_message:
228
+ post_install_message:
201
229
  rdoc_options: []
202
230
  require_paths:
203
231
  - lib
@@ -212,8 +240,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
240
  - !ruby/object:Gem::Version
213
241
  version: '0'
214
242
  requirements: []
215
- rubygems_version: 3.1.2
216
- signing_key:
243
+ rubygems_version: 3.1.4
244
+ signing_key:
217
245
  specification_version: 4
218
- summary: Modern encryption for Rails
246
+ summary: Modern encryption for Ruby and Rails
219
247
  test_files: []