lockbox 0.4.7 → 0.6.1

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: 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: []